diff --git a/bin/composer b/bin/composer new file mode 100755 index 00000000..607a36e5 --- /dev/null +++ b/bin/composer @@ -0,0 +1,6 @@ +#!/bin/bash + +php \ + -dmemory_limit=-1 \ + /bin/composer \ + $@ diff --git a/bin/doctum b/bin/doctum index 569d076b..0a7abfea 100755 --- a/bin/doctum +++ b/bin/doctum @@ -1,15 +1,15 @@ #!/bin/bash cwd=$(dirname $0) pushd ${cwd}/../src/ -rm -rf cache/store/ +rm -rf ../docs/build/ cache/store/ php -dmemory_limit=-1 \ vendor/bin/doctum.php \ update \ doctum.config.php \ -v popd diff --git a/src/app/Domain.php b/src/app/Domain.php index 667eaa55..f93f186e 100644 --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -1,445 +1,497 @@ isPublic()) { return $this; } // See if this domain is already owned by another user. $wallet = $this->wallet(); if ($wallet) { \Log::error( "Domain {$this->namespace} is already assigned to {$wallet->owner->email}" ); return $this; } $wallet_id = $user->wallets()->first()->id; foreach ($package->skus as $sku) { for ($i = $sku->pivot->qty; $i > 0; $i--) { \App\Entitlement::create( [ 'wallet_id' => $wallet_id, 'sku_id' => $sku->id, 'cost' => $sku->pivot->cost(), 'entitleable_id' => $this->id, 'entitleable_type' => Domain::class ] ); } } return $this; } + /** + * Return the entitlement to which this domain belongs, if any. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphOne<\App\Entitlement> + */ public function entitlement() { return $this->morphOne('App\Entitlement', 'entitleable'); } /** * Return list of public+active domain names */ public static function getPublicDomains(): array { $where = sprintf('(type & %s)', Domain::TYPE_PUBLIC); return self::whereRaw($where)->get(['namespace'])->pluck('namespace')->toArray(); } /** * Returns whether this domain is active. * * @return bool */ public function isActive(): bool { return ($this->status & self::STATUS_ACTIVE) > 0; } /** * Returns whether this domain is confirmed the ownership of. * * @return bool */ public function isConfirmed(): bool { return ($this->status & self::STATUS_CONFIRMED) > 0; } /** * Returns whether this domain is deleted. * * @return bool */ public function isDeleted(): bool { return ($this->status & self::STATUS_DELETED) > 0; } /** * Returns whether this domain is registered with us. * * @return bool */ public function isExternal(): bool { return ($this->type & self::TYPE_EXTERNAL) > 0; } /** * Returns whether this domain is hosted with us. * * @return bool */ public function isHosted(): bool { return ($this->type & self::TYPE_HOSTED) > 0; } /** * Returns whether this domain is new. * * @return bool */ public function isNew(): bool { return ($this->status & self::STATUS_NEW) > 0; } /** * Returns whether this domain is public. * * @return bool */ public function isPublic(): bool { return ($this->type & self::TYPE_PUBLIC) > 0; } /** * Returns whether this domain is registered in LDAP. * * @return bool */ public function isLdapReady(): bool { return ($this->status & self::STATUS_LDAP_READY) > 0; } /** * Returns whether this domain is suspended. * * @return bool */ public function isSuspended(): bool { return ($this->status & self::STATUS_SUSPENDED) > 0; } /** * Returns whether this (external) domain has been verified * to exist in DNS. * * @return bool */ public function isVerified(): bool { return ($this->status & self::STATUS_VERIFIED) > 0; } /** * Ensure the namespace is appropriately cased. */ public function setNamespaceAttribute($namespace) { $this->attributes['namespace'] = strtolower($namespace); } /** * Domain status mutator * * @throws \Exception */ public function setStatusAttribute($status) { $new_status = 0; $allowed_values = [ self::STATUS_NEW, self::STATUS_ACTIVE, self::STATUS_SUSPENDED, self::STATUS_DELETED, self::STATUS_CONFIRMED, self::STATUS_VERIFIED, self::STATUS_LDAP_READY, ]; foreach ($allowed_values as $value) { if ($status & $value) { $new_status |= $value; $status ^= $value; } } if ($status > 0) { throw new \Exception("Invalid domain status: {$status}"); } if ($this->isPublic()) { $this->attributes['status'] = $new_status; return; } if ($new_status & self::STATUS_CONFIRMED) { // if we have confirmed ownership of or management access to the domain, then we have // also confirmed the domain exists in DNS. $new_status |= self::STATUS_VERIFIED; $new_status |= self::STATUS_ACTIVE; } if ($new_status & self::STATUS_DELETED && $new_status & self::STATUS_ACTIVE) { $new_status ^= self::STATUS_ACTIVE; } if ($new_status & self::STATUS_SUSPENDED && $new_status & self::STATUS_ACTIVE) { $new_status ^= self::STATUS_ACTIVE; } // if the domain is now active, it is not new anymore. if ($new_status & self::STATUS_ACTIVE && $new_status & self::STATUS_NEW) { $new_status ^= self::STATUS_NEW; } $this->attributes['status'] = $new_status; } /** * Ownership verification by checking for a TXT (or CNAME) record * in the domain's DNS (that matches the verification hash). * * @return bool True if verification was successful, false otherwise * @throws \Exception Throws exception on DNS or DB errors */ public function confirm(): bool { if ($this->isConfirmed()) { return true; } $hash = $this->hash(self::HASH_TEXT); $confirmed = false; // Get DNS records and find a matching TXT entry $records = \dns_get_record($this->namespace, DNS_TXT); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } foreach ($records as $record) { if ($record['txt'] === $hash) { $confirmed = true; break; } } // Get DNS records and find a matching CNAME entry // Note: some servers resolve every non-existing name // so we need to define left and right side of the CNAME record // i.e.: kolab-verify IN CNAME .domain.tld. if (!$confirmed) { $cname = $this->hash(self::HASH_CODE) . '.' . $this->namespace; $records = \dns_get_record('kolab-verify.' . $this->namespace, DNS_CNAME); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } foreach ($records as $records) { if ($records['target'] === $cname) { $confirmed = true; break; } } } if ($confirmed) { $this->status |= Domain::STATUS_CONFIRMED; $this->save(); } return $confirmed; } /** * Generate a verification hash for this domain * * @param int $mod One of: HASH_CNAME, HASH_CODE (Default), HASH_TEXT * * @return string Verification hash */ public function hash($mod = null): string { $cname = 'kolab-verify'; if ($mod === self::HASH_CNAME) { return $cname; } $hash = \md5('hkccp-verify-' . $this->namespace); return $mod === self::HASH_TEXT ? "$cname=$hash" : $hash; } /** * Suspend this domain. * * @return void */ public function suspend(): void { if ($this->isSuspended()) { return; } $this->status |= Domain::STATUS_SUSPENDED; $this->save(); } /** * Unsuspend this domain. * * The domain is unsuspended through either of the following courses of actions; * * * The account balance has been topped up, or * * a suspected spammer has resolved their issues, or * * the command-line is triggered. * * Therefore, we can also confidently set the domain status to 'active' should the ownership of or management * access to have been confirmed before. * * @return void */ public function unsuspend(): void { if (!$this->isSuspended()) { return; } $this->status ^= Domain::STATUS_SUSPENDED; if ($this->isConfirmed() && $this->isVerified()) { $this->status |= Domain::STATUS_ACTIVE; } $this->save(); } /** * Verify if a domain exists in DNS * * @return bool True if registered, False otherwise * @throws \Exception Throws exception on DNS or DB errors */ public function verify(): bool { if ($this->isVerified()) { return true; } $records = \dns_get_record($this->namespace, DNS_ANY); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } // It may happen that result contains other domains depending on the host DNS setup // that's why in_array() and not just !empty() if (in_array($this->namespace, array_column($records, 'host'))) { $this->status |= Domain::STATUS_VERIFIED; $this->save(); return true; } return false; } /** * Returns the wallet by which the domain is controlled * * @return \App\Wallet A wallet object */ public function wallet(): ?Wallet { // Note: Not all domains have a entitlement/wallet $entitlement = $this->entitlement()->withTrashed()->first(); return $entitlement ? $entitlement->wallet : null; } } diff --git a/src/app/User.php b/src/app/User.php index da9c21f3..17f131ca 100644 --- a/src/app/User.php +++ b/src/app/User.php @@ -1,734 +1,734 @@ belongsToMany( 'App\Wallet', // The foreign object definition 'user_accounts', // The table name 'user_id', // The local foreign key 'wallet_id' // The remote foreign key ); } /** * Email aliases of this user. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function aliases() { return $this->hasMany('App\UserAlias', 'user_id'); } /** * Assign a package to a user. The user should not have any existing entitlements. * * @param \App\Package $package The package to assign. * @param \App\User|null $user Assign the package to another user. * * @return \App\User */ public function assignPackage($package, $user = null) { if (!$user) { $user = $this; } $wallet_id = $this->wallets()->first()->id; foreach ($package->skus as $sku) { for ($i = $sku->pivot->qty; $i > 0; $i--) { \App\Entitlement::create( [ 'wallet_id' => $wallet_id, 'sku_id' => $sku->id, 'cost' => $sku->pivot->cost(), 'entitleable_id' => $user->id, 'entitleable_type' => User::class ] ); } } return $user; } /** * Assign a package plan to a user. * * @param \App\Plan $plan The plan to assign * @param \App\Domain $domain Optional domain object * * @return \App\User Self */ public function assignPlan($plan, $domain = null): User { $this->setSetting('plan_id', $plan->id); foreach ($plan->packages as $package) { if ($package->isDomain()) { $domain->assignPackage($package, $this); } else { $this->assignPackage($package); } } return $this; } /** * Assign a Sku to a user. * * @param \App\Sku $sku The sku to assign. * @param int $count Count of entitlements to add * * @return \App\User Self * @throws \Exception */ public function assignSku(Sku $sku, int $count = 1): User { // TODO: I guess wallet could be parametrized in future $wallet = $this->wallet(); $exists = $this->entitlements()->where('sku_id', $sku->id)->count(); // TODO: Sanity check, this probably should be in preReq() on handlers // or in EntitlementObserver if ($sku->handler_class::entitleableClass() != User::class) { throw new \Exception("Cannot assign non-user SKU ({$sku->title}) to a user"); } while ($count > 0) { \App\Entitlement::create([ 'wallet_id' => $wallet->id, 'sku_id' => $sku->id, 'cost' => $exists >= $sku->units_free ? $sku->cost : 0, 'entitleable_id' => $this->id, 'entitleable_type' => User::class ]); $exists++; $count--; } return $this; } /** * Check if current user can delete another object. * * @param \App\User|\App\Domain $object A user|domain object * * @return bool True if he can, False otherwise */ public function canDelete($object): bool { if (!method_exists($object, 'wallet')) { return false; } $wallet = $object->wallet(); // TODO: For now controller can delete/update the account owner, // this may change in future, controllers are not 0-regression feature return $this->wallets->contains($wallet) || $this->accounts->contains($wallet); } /** * Check if current user can read data of another object. * * @param \App\User|\App\Domain|\App\Wallet $object A user|domain|wallet object * * @return bool True if he can, False otherwise */ public function canRead($object): bool { if ($this->role == "admin") { return true; } if ($object instanceof User && $this->id == $object->id) { return true; } if ($object instanceof Wallet) { return $object->user_id == $this->id || $object->controllers->contains($this); } if (!method_exists($object, 'wallet')) { return false; } $wallet = $object->wallet(); return $this->wallets->contains($wallet) || $this->accounts->contains($wallet); } /** * Check if current user can update data of another object. * * @param \App\User|\App\Domain $object A user|domain object * * @return bool True if he can, False otherwise */ public function canUpdate($object): bool { if (!method_exists($object, 'wallet')) { return false; } if ($object instanceof User && $this->id == $object->id) { return true; } return $this->canDelete($object); } /** * Return the \App\Domain for this user. * * @return \App\Domain|null */ public function domain() { list($local, $domainName) = explode('@', $this->email); $domain = \App\Domain::withTrashed()->where('namespace', $domainName)->first(); return $domain; } /** * List the domains to which this user is entitled. * * @return Domain[] */ public function domains() { $dbdomains = Domain::whereRaw( sprintf( '(type & %s) AND (status & %s)', Domain::TYPE_PUBLIC, Domain::STATUS_ACTIVE ) )->get(); $domains = []; foreach ($dbdomains as $dbdomain) { $domains[] = $dbdomain; } foreach ($this->wallets as $wallet) { $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get(); foreach ($entitlements as $entitlement) { $domain = $entitlement->entitleable; \Log::info("Found domain for {$this->email}: {$domain->namespace} (owned)"); $domains[] = $domain; } } foreach ($this->accounts as $wallet) { $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get(); foreach ($entitlements as $entitlement) { $domain = $entitlement->entitleable; \Log::info("Found domain {$this->email}: {$domain->namespace} (charged)"); $domains[] = $domain; } } return $domains; } public function entitlement() { return $this->morphOne('App\Entitlement', 'entitleable'); } /** * Entitlements for this user. * * Note that these are entitlements that apply to the user account, and not entitlements that * this user owns. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function entitlements() { return $this->hasMany('App\Entitlement', 'entitleable_id', 'id'); } public function addEntitlement($entitlement) { if (!$this->entitlements->contains($entitlement)) { return $this->entitlements()->save($entitlement); } } /** * Find whether an email address exists (user or alias). * Note: This will also find deleted users. * * @param string $email Email address * @param bool $return_user Return User instance instead of boolean * @param bool $is_alias Set to True if the existing email is an alias * @param bool $existing Ignore deleted users * * @return \App\User|bool True or User model object if found, False otherwise */ public static function emailExists(string $email, bool $return_user = false, &$is_alias = false, $existing = false) { if (strpos($email, '@') === false) { return false; } $email = \strtolower($email); if ($existing) { $user = self::where('email', $email)->first(); } else { $user = self::withTrashed()->where('email', $email)->first(); } if ($user) { return $return_user ? $user : true; } $aliases = UserAlias::where('alias', $email); if ($existing) { $aliases = $aliases->join('users', 'user_id', '=', 'users.id') ->whereNull('users.deleted_at'); } $alias = $aliases->first(); if ($alias) { $is_alias = true; return $return_user ? self::withTrashed()->find($alias->user_id) : true; } return false; } /** * Helper to find user by email address, whether it is * main email address, alias or an external email. * * If there's more than one alias NULL will be returned. * * @param string $email Email address * @param bool $external Search also for an external email * * @return \App\User User model object if found */ public static function findByEmail(string $email, bool $external = false): ?User { if (strpos($email, '@') === false) { return null; } $email = \strtolower($email); $user = self::where('email', $email)->first(); if ($user) { return $user; } $aliases = UserAlias::where('alias', $email)->get(); if (count($aliases) == 1) { return $aliases->first()->user; } // TODO: External email return null; } public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; } /** * Check if user has an entitlement for the specified SKU. * * @param string $title The SKU title * * @return bool True if specified SKU entitlement exists */ public function hasSku($title): bool { $sku = Sku::where('title', $title)->first(); if (!$sku) { return false; } return $this->entitlements()->where('sku_id', $sku->id)->count() > 0; } /** * Returns whether this domain is active. * * @return bool */ public function isActive(): bool { return ($this->status & self::STATUS_ACTIVE) > 0; } /** * Returns whether this domain is deleted. * * @return bool */ public function isDeleted(): bool { return ($this->status & self::STATUS_DELETED) > 0; } /** * Returns whether this (external) domain has been verified * to exist in DNS. * * @return bool */ public function isImapReady(): bool { return ($this->status & self::STATUS_IMAP_READY) > 0; } /** * Returns whether this user is registered in LDAP. * * @return bool */ public function isLdapReady(): bool { return ($this->status & self::STATUS_LDAP_READY) > 0; } /** * Returns whether this user is new. * * @return bool */ public function isNew(): bool { return ($this->status & self::STATUS_NEW) > 0; } /** * Returns whether this domain is suspended. * * @return bool */ public function isSuspended(): bool { return ($this->status & self::STATUS_SUSPENDED) > 0; } /** * A shortcut to get the user name. * * @param bool $fallback Return " User" if there's no name * * @return string Full user name */ public function name(bool $fallback = false): string { $firstname = $this->getSetting('first_name'); $lastname = $this->getSetting('last_name'); $name = trim($firstname . ' ' . $lastname); if (empty($name) && $fallback) { return \config('app.name') . ' User'; } return $name; } /** * Remove a number of entitlements for the SKU. * * @param \App\Sku $sku The SKU * @param int $count The number of entitlements to remove * * @return User Self */ public function removeSku(Sku $sku, int $count = 1): User { $entitlements = $this->entitlements() ->where('sku_id', $sku->id) ->orderBy('cost', 'desc') ->orderBy('created_at') ->get(); $entitlements_count = count($entitlements); foreach ($entitlements as $entitlement) { if ($entitlements_count <= $sku->units_free) { continue; } if ($count > 0) { $entitlement->delete(); $entitlements_count--; $count--; } } return $this; } + /** + * User password mutator + * + * @param string $password The password in plain text. + * + * @return void + */ + public function setPasswordAttribute($password) + { + if (!empty($password)) { + $this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]); + $this->attributes['password_ldap'] = '{SSHA512}' . base64_encode( + pack('H*', hash('sha512', $password)) + ); + } + } + + /** + * User LDAP password mutator + * + * @param string $password The password in plain text. + * + * @return void + */ + public function setPasswordLdapAttribute($password) + { + $this->setPasswordAttribute($password); + } + + /** + * User status mutator + * + * @throws \Exception + */ + public function setStatusAttribute($status) + { + $new_status = 0; + + $allowed_values = [ + self::STATUS_NEW, + self::STATUS_ACTIVE, + self::STATUS_SUSPENDED, + self::STATUS_DELETED, + self::STATUS_LDAP_READY, + self::STATUS_IMAP_READY, + ]; + + foreach ($allowed_values as $value) { + if ($status & $value) { + $new_status |= $value; + $status ^= $value; + } + } + + if ($status > 0) { + throw new \Exception("Invalid user status: {$status}"); + } + + $this->attributes['status'] = $new_status; + } + /** * Any (additional) properties of this user. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function settings() { return $this->hasMany('App\UserSetting', 'user_id'); } /** * Suspend this domain. * * @return void */ public function suspend(): void { if ($this->isSuspended()) { return; } $this->status |= User::STATUS_SUSPENDED; $this->save(); } /** * Unsuspend this domain. * * @return void */ public function unsuspend(): void { if (!$this->isSuspended()) { return; } $this->status ^= User::STATUS_SUSPENDED; $this->save(); } /** * Return users controlled by the current user. * * @param bool $with_accounts Include users assigned to wallets * the current user controls but not owns. * * @return \Illuminate\Database\Eloquent\Builder Query builder */ public function users($with_accounts = true) { $wallets = $this->wallets()->pluck('id')->all(); if ($with_accounts) { $wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all()); } return $this->select(['users.*', 'entitlements.wallet_id']) ->distinct() ->leftJoin('entitlements', 'entitlements.entitleable_id', '=', 'users.id') ->whereIn('entitlements.wallet_id', $wallets) ->where('entitlements.entitleable_type', 'App\User'); } /** * Verification codes for this user. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function verificationcodes() { return $this->hasMany('App\VerificationCode', 'user_id', 'id'); } /** * Returns the wallet by which the user is controlled * * @return \App\Wallet A wallet object */ public function wallet(): Wallet { $entitlement = $this->entitlement()->first(); // TODO: No entitlement should not happen, but in tests we have // such cases, so we fallback to the user's wallet in this case return $entitlement ? $entitlement->wallet : $this->wallets()->first(); } /** * Wallets this user owns. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function wallets() { return $this->hasMany('App\Wallet'); } - - /** - * User password mutator - * - * @param string $password The password in plain text. - * - * @return void - */ - public function setPasswordAttribute($password) - { - if (!empty($password)) { - $this->attributes['password'] = bcrypt($password, [ "rounds" => 12 ]); - $this->attributes['password_ldap'] = '{SSHA512}' . base64_encode( - pack('H*', hash('sha512', $password)) - ); - } - } - - /** - * User LDAP password mutator - * - * @param string $password The password in plain text. - * - * @return void - */ - public function setPasswordLdapAttribute($password) - { - $this->setPasswordAttribute($password); - } - - /** - * User status mutator - * - * @throws \Exception - */ - public function setStatusAttribute($status) - { - $new_status = 0; - - $allowed_values = [ - self::STATUS_NEW, - self::STATUS_ACTIVE, - self::STATUS_SUSPENDED, - self::STATUS_DELETED, - self::STATUS_LDAP_READY, - self::STATUS_IMAP_READY, - ]; - - foreach ($allowed_values as $value) { - if ($status & $value) { - $new_status |= $value; - $status ^= $value; - } - } - - if ($status > 0) { - throw new \Exception("Invalid user status: {$status}"); - } - - $this->attributes['status'] = $new_status; - } } diff --git a/src/app/Utils.php b/src/app/Utils.php index b595ed80..5643d5d3 100644 --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -1,181 +1,198 @@ diffInDays($end) + 1; } /** * Generate a passphrase. Not intended for use in production, so limited to environments that are not production. * * @return string * * @throws \Exception */ public static function generatePassphrase() { if (\config('app.env') == "production") { throw new \Exception("Thou shall not pass"); } $alphaLow = 'abcdefghijklmnopqrstuvwxyz'; $alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $num = '0123456789'; $stdSpecial = '~`!@#$%^&*()-_+=[{]}\\|\'";:/?.>,<'; $source = $alphaLow . $alphaUp . $num . $stdSpecial; $result = ''; for ($x = 0; $x < 16; $x++) { $result .= substr($source, rand(0, (strlen($source) - 1)), 1); } return $result; } + /** + * Validate an email address against RFC conventions + * + * @param string $email The email address + * + * @return bool + */ + public static function isValidEmailAddress($email): bool + { + // the email address can not start with a dot. + if (substr($email, 0, 1) == '.') { + return false; + } + + return true; + } + /** * Provide all unique combinations of elements in $input, with order and duplicates irrelevant. * * @param array $input The input array of elements. * * @return array[] */ public static function powerSet(array $input): array { $output = []; for ($x = 0; $x < count($input); $x++) { self::combine($input, $x + 1, 0, [], 0, $output); } return $output; } /** * Returns the current user's email address or null. * * @return string */ public static function userEmailOrNull(): ?string { $user = Auth::user(); if (!$user) { return null; } return $user->email; } /** * Returns a UUID in the form of an integer. * * @return integer */ public static function uuidInt(): int { $hex = Uuid::uuid4(); $bin = pack('h*', str_replace('-', '', $hex)); $ids = unpack('L', $bin); $id = array_shift($ids); return $id; } /** * Returns a UUID in the form of a string. * * @return string */ public static function uuidStr(): string { return Uuid::uuid4()->toString(); } private static function combine($input, $r, $index, $data, $i, &$output): void { $n = count($input); // Current cobination is ready if ($index == $r) { $output[] = array_slice($data, 0, $r); return; } // When no more elements are there to put in data[] if ($i >= $n) { return; } // current is included, put next at next location $data[$index] = $input[$i]; self::combine($input, $r, $index + 1, $data, $i + 1, $output); // current is excluded, replace it with next (Note that i+1 // is passed, but index is not changed) self::combine($input, $r, $index, $data, $i + 1, $output); } /** * Create self URL * * @param string $route Route/Path * * @return string Full URL */ public static function serviceUrl(string $route): string { $url = \config('app.public_url'); if (!$url) { $url = \config('app.url'); } return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/'); } /** * Create a configuration/environment data to be passed to * the UI * * @todo For a lack of better place this is put here for now * * @return array Configuration data */ public static function uiEnv(): array { $opts = ['app.name', 'app.url', 'app.domain']; $env = \app('config')->getMany($opts); $countries = include resource_path('countries.php'); $env['countries'] = $countries ?: []; $isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0; $env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js'; $env['paymentProvider'] = \config('services.payment_provider'); $env['stripePK'] = \config('services.stripe.public_key'); return $env; } } diff --git a/src/composer b/src/composer new file mode 120000 index 00000000..12b9da26 --- /dev/null +++ b/src/composer @@ -0,0 +1 @@ +../bin/composer \ No newline at end of file diff --git a/src/doctum.config.php b/src/doctum.config.php index 56365a6d..cf75418f 100644 --- a/src/doctum.config.php +++ b/src/doctum.config.php @@ -1,26 +1,35 @@ files() ->name('*.php') ->exclude('bootstrap') ->exclude('cache') ->exclude('database') ->exclude('include') ->exclude('node_modules') ->exclude('tests') ->exclude('vendor') ->in(__DIR__); -return new Doctum( +$doctum = new Doctum( $iterator, [ 'build_dir' => __DIR__ . '/../docs/build/%version%/', 'cache_dir' => __DIR__ . '/cache/', 'default_opened_level' => 1, - //'include_parent_data' => false, + 'include_parent_data' => false, ] ); + +/* +$doctum['filter'] = function () { + return new TrueFilter(); +}; +*/ + +return $doctum; diff --git a/src/phpstan.neon b/src/phpstan.neon index bed643e9..05c03fb4 100644 --- a/src/phpstan.neon +++ b/src/phpstan.neon @@ -1,14 +1,20 @@ includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: ignoreErrors: - '#Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable#' - '#Access to an undefined property App\\Package::\$pivot#' - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id#' - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$created_at#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::toString\(\)#' - '#Call to an undefined method Tests\\Browser::#' level: 4 paths: - app/ - - tests/ + - tests/Browser/ + - tests/Unit/ + - tests/Functional/ + - tests/Feature/ + - tests/TestCase.php + - tests/TestCaseDusk.php + - tests/TestCaseTrait.php diff --git a/src/phpunit.xml b/src/phpunit.xml index e3d49429..84fad8bc 100644 --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -1,44 +1,40 @@ tests/Unit tests/Functional tests/Feature - - - tests/Browser - ./app diff --git a/src/tests/Browser.php b/src/tests/Browser.php index e319856f..d53abb66 100644 --- a/src/tests/Browser.php +++ b/src/tests/Browser.php @@ -1,245 +1,279 @@ elements($selector); $count = count($elements); if ($visible) { foreach ($elements as $element) { if (!$element->isDisplayed()) { $count--; } } } Assert::assertEquals($expected_count, $count, "Count of [$selector] elements is not $expected_count"); return $this; } /** * Assert Tip element content */ public function assertTip($selector, $content) { return $this->click($selector) ->withinBody(function ($browser) use ($content) { $browser->waitFor('div.tooltip .tooltip-inner') ->assertSeeIn('div.tooltip .tooltip-inner', $content); }) ->click($selector); } /** * Assert Toast element content (and close it) */ public function assertToast(string $type, string $message, $title = null) { return $this->withinBody(function ($browser) use ($type, $title, $message) { $browser->with(new Toast($type), function (Browser $browser) use ($title, $message) { $browser->assertToastTitle($title) ->assertToastMessage($message) ->closeToast(); }); }); } /** * Assert specified error page is displayed. */ public function assertErrorPage(int $error_code) { $this->with(new Error($error_code), function ($browser) { // empty, assertions will be made by the Error component itself }); return $this; } /** * Assert that the given element has specified class assigned. */ public function assertHasClass($selector, $class_name) { $element = $this->resolver->findOrFail($selector); $classes = explode(' ', (string) $element->getAttribute('class')); Assert::assertContains($class_name, $classes, "[$selector] has no class '{$class_name}'"); return $this; } /** * Assert that the given element is readonly */ public function assertReadonly($selector) { $element = $this->resolver->findOrFail($selector); $value = $element->getAttribute('readonly'); Assert::assertTrue($value == 'true', "Element [$selector] is not readonly"); return $this; } /** * Assert that the given element is not readonly */ public function assertNotReadonly($selector) { $element = $this->resolver->findOrFail($selector); $value = $element->getAttribute('readonly'); Assert::assertTrue($value != 'true', "Element [$selector] is not readonly"); return $this; } /** * Assert that the given element contains specified text, * no matter it's displayed or not. */ public function assertText($selector, $text) { $element = $this->resolver->findOrFail($selector); if ($text === '') { Assert::assertTrue((string) $element->getText() === $text, "Element's text is not empty [$selector]"); } else { Assert::assertTrue(strpos($element->getText(), $text) !== false, "No expected text in [$selector]"); } return $this; } /** * Assert that the given element contains specified text, * no matter it's displayed or not - using a regular expression. */ public function assertTextRegExp($selector, $regexp) { $element = $this->resolver->findOrFail($selector); Assert::assertRegExp($regexp, $element->getText(), "No expected text in [$selector]"); return $this; } /** * Remove all toast messages */ public function clearToasts() { $this->script("jQuery('.toast-container > *').remove()"); return $this; } /** * Check if in Phone mode */ public static function isPhone() { return getenv('TESTS_MODE') == 'phone'; } /** * Check if in Tablet mode */ public static function isTablet() { return getenv('TESTS_MODE') == 'tablet'; } /** * Check if in Desktop mode */ public static function isDesktop() { return !self::isPhone() && !self::isTablet(); } /** * Returns content of a downloaded file */ public function readDownloadedFile($filename, $sleep = 5) { $filename = __DIR__ . "/Browser/downloads/$filename"; // Give the browser a chance to finish download if (!file_exists($filename) && $sleep) { sleep($sleep); } Assert::assertFileExists($filename); return file_get_contents($filename); } /** * Removes downloaded file */ public function removeDownloadedFile($filename) { @unlink(__DIR__ . "/Browser/downloads/$filename"); return $this; } + /** + * Take a screenshot + */ + public function screenshot($lineno) + { + $backtrace = debug_backtrace(); + $class = null; + $function = null; + $line = null; + + foreach ($backtrace as $step) { + if (!array_key_exists('class', $step)) { + continue; + } + + if (substr($step['class'], 0, 5) == "Tests" && substr($step['class'], -4, 4) == "Test") { + if (substr($step['function'], 0, 4) == "test") { + $class = $step['class']; + $function = $step['function']; + $line = $step['line']; + break; + } + } + } + + $screenshotName = str_replace( + ['\\', '/'], + ['.'], + "{$class}.{$function}.{$lineno}" + ); + + return parent::screenshot($screenshotName); + } + /** * Clears the input field and related vue v-model data. */ public function vueClear($selector) { if ($this->resolver->prefix != 'body') { $selector = $this->resolver->prefix . ' ' . $selector; } // The existing clear(), and type() with empty string do not work. // We have to clear the field and dispatch 'input' event programatically. $this->script( "var element = document.querySelector('$selector');" . "element.value = '';" . "element.dispatchEvent(new Event('input'))" ); return $this; } /** * Execute code within body context. * Useful to execute code that selects elements outside of a component context */ public function withinBody($callback) { if ($this->resolver->prefix != 'body') { $orig_prefix = $this->resolver->prefix; $this->resolver->prefix = 'body'; } call_user_func($callback, $this); if (isset($orig_prefix)) { $this->resolver->prefix = $orig_prefix; } return $this; } } diff --git a/src/tests/Browser/Admin/DashboardTest.php b/src/tests/Browser/Admin/DashboardTest.php deleted file mode 100644 index a6077e53..00000000 --- a/src/tests/Browser/Admin/DashboardTest.php +++ /dev/null @@ -1,140 +0,0 @@ -getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', null); - - $this->deleteTestUser('test@testsearch.com'); - $this->deleteTestDomain('testsearch.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $jack = $this->getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', null); - - $this->deleteTestUser('test@testsearch.com'); - $this->deleteTestDomain('testsearch.com'); - - parent::tearDown(); - } - - /** - * Test user search - */ - public function testSearch(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) - ->on(new Dashboard()) - ->assertFocused('@search input') - ->assertMissing('@search table'); - - // Test search with no results - $browser->type('@search input', 'unknown') - ->click('@search form button') - ->assertToast(Toast::TYPE_INFO, '0 user accounts have been found.') - ->assertMissing('@search table'); - - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', 'john.doe.external@gmail.com'); - - // Test search with multiple results - $browser->type('@search input', 'john.doe.external@gmail.com') - ->click('@search form button') - ->assertToast(Toast::TYPE_INFO, '2 user accounts have been found.') - ->whenAvailable('@search table', function (Browser $browser) use ($john, $jack) { - $browser->assertElementsCount('tbody tr', 2) - ->with('tbody tr:first-child', function (Browser $browser) use ($jack) { - $browser->assertSeeIn('td:nth-child(1) a', $jack->email) - ->assertSeeIn('td:nth-child(2) a', $jack->id) - ->assertVisible('td:nth-child(3)') - ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') - ->assertVisible('td:nth-child(4)') - ->assertText('td:nth-child(4)', ''); - }) - ->with('tbody tr:last-child', function (Browser $browser) use ($john) { - $browser->assertSeeIn('td:nth-child(1) a', $john->email) - ->assertSeeIn('td:nth-child(2) a', $john->id) - ->assertVisible('td:nth-child(3)') - ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') - ->assertVisible('td:nth-child(4)') - ->assertText('td:nth-child(4)', ''); - }); - }); - - // Test search with single record result -> redirect to user page - $browser->type('@search input', 'kolab.org') - ->click('@search form button') - ->assertMissing('@search table') - ->waitForLocation('/user/' . $john->id) - ->waitUntilMissing('.app-loader') - ->whenAvailable('#user-info', function (Browser $browser) use ($john) { - $browser->assertSeeIn('.card-title', $john->email); - }); - }); - } - - /** - * Test user search deleted user/domain - */ - public function testSearchDeleted(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) - ->on(new Dashboard()) - ->assertFocused('@search input') - ->assertMissing('@search table'); - - // Deleted users/domains - $domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]); - $user = $this->getTestUser('test@testsearch.com'); - $plan = \App\Plan::where('title', 'group')->first(); - $user->assignPlan($plan, $domain); - $user->setAliases(['alias@testsearch.com']); - Queue::fake(); - $user->delete(); - - // Test search with multiple results - $browser->type('@search input', 'testsearch.com') - ->click('@search form button') - ->assertToast(Toast::TYPE_INFO, '1 user accounts have been found.') - ->whenAvailable('@search table', function (Browser $browser) use ($user) { - $browser->assertElementsCount('tbody tr', 1) - ->assertVisible('tbody tr:first-child.text-secondary') - ->with('tbody tr:first-child', function (Browser $browser) use ($user) { - $browser->assertSeeIn('td:nth-child(1) span', $user->email) - ->assertSeeIn('td:nth-child(2) span', $user->id) - ->assertVisible('td:nth-child(3)') - ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') - ->assertVisible('td:nth-child(4)') - ->assertTextRegExp('td:nth-child(4)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/'); - }); - }); - }); - } -} diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php deleted file mode 100644 index 109c55bd..00000000 --- a/src/tests/Browser/Admin/DomainTest.php +++ /dev/null @@ -1,119 +0,0 @@ -browse(function (Browser $browser) { - $domain = $this->getTestDomain('kolab.org'); - $browser->visit('/domain/' . $domain->id)->on(new Home()); - }); - } - - /** - * Test domain info page - */ - public function testDomainInfo(): void - { - $this->browse(function (Browser $browser) { - $domain = $this->getTestDomain('kolab.org'); - $domain_page = new DomainPage($domain->id); - $john = $this->getTestUser('john@kolab.org'); - $user_page = new UserPage($john->id); - - // Goto the domain page - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) - ->on(new Dashboard()) - ->visit($user_page) - ->on($user_page) - ->click('@nav #tab-domains') - ->pause(1000) - ->click('@user-domains table tbody tr:first-child td a'); - - $browser->on($domain_page) - ->assertSeeIn('@domain-info .card-title', 'kolab.org') - ->with('@domain-info form', function (Browser $browser) use ($domain) { - $browser->assertElementsCount('.row', 2) - ->assertSeeIn('.row:nth-child(1) label', 'ID (Created at)') - ->assertSeeIn('.row:nth-child(1) #domainid', "{$domain->id} ({$domain->created_at})") - ->assertSeeIn('.row:nth-child(2) label', 'Status') - ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active'); - }); - - // Some tabs are loaded in background, wait a second - $browser->pause(500) - ->assertElementsCount('@nav a', 1); - - // Assert Configuration tab - $browser->assertSeeIn('@nav #tab-config', 'Configuration') - ->with('@domain-config', function (Browser $browser) { - $browser->assertSeeIn('pre#dns-verify', 'kolab-verify.kolab.org.') - ->assertSeeIn('pre#dns-config', 'kolab.org.'); - }); - }); - } - - /** - * Test suspending/unsuspending a domain - * - * @depends testDomainInfo - */ - public function testSuspendAndUnsuspend(): void - { - $this->browse(function (Browser $browser) { - $domain = $this->getTestDomain('domainscontroller.com', [ - 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE - | Domain::STATUS_LDAP_READY | Domain::STATUS_CONFIRMED - | Domain::STATUS_VERIFIED, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - $browser->visit(new DomainPage($domain->id)) - ->assertVisible('@domain-info #button-suspend') - ->assertMissing('@domain-info #button-unsuspend') - ->click('@domain-info #button-suspend') - ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.') - ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended') - ->assertMissing('@domain-info #button-suspend') - ->click('@domain-info #button-unsuspend') - ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.') - ->assertSeeIn('@domain-info #status span.text-success', 'Active') - ->assertVisible('@domain-info #button-suspend') - ->assertMissing('@domain-info #button-unsuspend'); - }); - } -} diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php deleted file mode 100644 index b258c23b..00000000 --- a/src/tests/Browser/Admin/LogonTest.php +++ /dev/null @@ -1,145 +0,0 @@ -browse(function (Browser $browser) { - $browser->visit(new Home()) - ->with(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }) - ->assertMissing('@second-factor-input') - ->assertMissing('@forgot-password'); - }); - } - - /** - * Test redirect to /login if user is unauthenticated - */ - public function testLogonRedirect(): void - { - $this->browse(function (Browser $browser) { - $browser->visit('/dashboard'); - - // Checks if we're really on the login page - $browser->waitForLocation('/login') - ->on(new Home()); - }); - } - - /** - * Logon with wrong password/user test - */ - public function testLogonWrongCredentials(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'wrong') - // Error message - ->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.') - // Checks if we're still on the logon page - ->on(new Home()); - }); - } - - /** - * Successful logon test - */ - public function testLogonSuccessful(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true); - - // Checks if we're really on Dashboard page - $browser->on(new Dashboard()) - ->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']); - }) - ->assertUser('jeroen@jeroen.jeroen'); - - // Test that visiting '/' with logged in user does not open logon form - // but "redirects" to the dashboard - $browser->visit('/')->on(new Dashboard()); - }); - } - - /** - * Logout test - * - * @depends testLogonSuccessful - */ - public function testLogout(): void - { - $this->browse(function (Browser $browser) { - $browser->on(new Dashboard()); - - // Click the Logout button - $browser->within(new Menu(), function ($browser) { - $browser->clickMenuItem('logout'); - }); - - // We expect the logon page - $browser->waitForLocation('/login') - ->on(new Home()); - - // with default menu - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }); - - // Success toast message - $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); - }); - } - - /** - * Logout by URL test - */ - public function testLogoutByURL(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true); - - // Checks if we're really on Dashboard page - $browser->on(new Dashboard()); - - // Use /logout url, and expect the logon page - $browser->visit('/logout') - ->waitForLocation('/login') - ->on(new Home()); - - // with default menu - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }); - - // Success toast message - $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); - }); - } -} diff --git a/src/tests/Browser/Admin/UserFinancesTest.php b/src/tests/Browser/Admin/UserFinancesTest.php deleted file mode 100644 index 0ff4bd73..00000000 --- a/src/tests/Browser/Admin/UserFinancesTest.php +++ /dev/null @@ -1,325 +0,0 @@ -getTestUser('john@kolab.org'); - $wallet = $john->wallets()->first(); - $wallet->discount()->dissociate(); - $wallet->balance = 0; - $wallet->save(); - $wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]); - } - - /** - * Test Finances tab (and transactions) - */ - public function testFinances(): void - { - // Assert Jack's Finances tab - $this->browse(function (Browser $browser) { - $jack = $this->getTestUser('jack@kolab.org'); - $wallet = $jack->wallets()->first(); - $wallet->transactions()->delete(); - $wallet->setSetting('stripe_id', 'abc'); - $page = new UserPage($jack->id); - - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) - ->on(new Dashboard()) - ->visit($page) - ->on($page) - ->assertSeeIn('@nav #tab-finances', 'Finances') - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title:first-child', 'Account balance') - ->assertSeeIn('.card-title:first-child .text-success', '0,00 CHF') - ->with('form', function (Browser $browser) { - $browser->assertElementsCount('.row', 2) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', 'none') - ->assertSeeIn('.row:nth-child(2) label', 'Stripe ID') - ->assertSeeIn('.row:nth-child(2) a', 'abc'); - }) - ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') - ->with('table', function (Browser $browser) { - $browser->assertMissing('tbody') - ->assertSeeIn('tfoot td', "There are no transactions for this account."); - }) - ->assertMissing('table + button'); - }); - }); - - // Assert John's Finances tab (with discount, and debit) - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - $page = new UserPage($john->id); - $discount = Discount::where('code', 'TEST')->first(); - $wallet = $john->wallet(); - $wallet->transactions()->delete(); - $wallet->discount()->associate($discount); - $wallet->debit(2010); - $wallet->save(); - - // Create test transactions - $transaction = Transaction::create([ - 'user_email' => 'jeroen@jeroen.jeroen', - 'object_id' => $wallet->id, - 'object_type' => Wallet::class, - 'type' => Transaction::WALLET_CREDIT, - 'amount' => 100, - 'description' => 'Payment', - ]); - $transaction->created_at = Carbon::now()->subMonth(); - $transaction->save(); - - // Click the managed-by link on Jack's page - $browser->click('@user-info #manager a') - ->on($page) - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title:first-child', 'Account balance') - ->assertSeeIn('.card-title:first-child .text-danger', '-20,10 CHF') - ->with('form', function (Browser $browser) { - $browser->assertElementsCount('.row', 1) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', '10% - Test voucher'); - }) - ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') - ->with('table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 2) - ->assertMissing('tfoot'); - - if (!$browser->isPhone()) { - $browser->assertSeeIn('tbody tr:last-child td.email', 'jeroen@jeroen.jeroen'); - } - }); - }); - }); - - // Now we go to Ned's info page, he's a controller on John's wallet - $this->browse(function (Browser $browser) { - $ned = $this->getTestUser('ned@kolab.org'); - $wallet = $ned->wallets()->first(); - $wallet->balance = 0; - $wallet->save(); - $page = new UserPage($ned->id); - - $browser->click('@nav #tab-users') - ->click('@user-users tbody tr:nth-child(4) td:first-child a') - ->on($page) - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title:first-child', 'Account balance') - ->assertSeeIn('.card-title:first-child .text-success', '0,00 CHF') - ->with('form', function (Browser $browser) { - $browser->assertElementsCount('.row', 1) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', 'none'); - }) - ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') - ->with('table', function (Browser $browser) { - $browser->assertMissing('tbody') - ->assertSeeIn('tfoot td', "There are no transactions for this account."); - }) - ->assertMissing('table + button'); - }); - }); - } - - /** - * Test editing wallet discount - * - * @depends testFinances - */ - public function testWalletDiscount(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->pause(100) - ->waitUntilMissing('@user-finances .app-loader') - ->click('@user-finances #discount button') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Account discount') - ->assertFocused('@body select') - ->assertSelected('@body select', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#discount-dialog') - ->click('@user-finances #discount button') - // Change the discount - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->click('@body select') - ->click('@body select option:nth-child(2)') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') - ->assertSeeIn('#discount span', '10% - Test voucher') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') - ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); - }) - // Change back to 'none' - ->click('@nav #tab-finances') - ->click('@user-finances #discount button') - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->click('@body select') - ->click('@body select option:nth-child(1)') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') - ->assertSeeIn('#discount span', 'none') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF/month') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF/month') - ->assertMissing('table + .hint'); - }); - }); - } - - /** - * Test awarding/penalizing a wallet - * - * @depends testFinances - */ - public function testBonusPenalty(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->waitFor('@user-finances #button-award') - ->click('@user-finances #button-award') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Add a bonus to the wallet') - ->assertFocused('@body input#oneoff_amount') - ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') - ->assertvalue('@body input#oneoff_amount', '') - ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') - ->assertvalue('@body input#oneoff_description', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#oneoff-dialog'); - - // Test bonus - $browser->click('@user-finances #button-award') - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - // Test input validation for a bonus - $browser->type('@body #oneoff_amount', 'aaa') - ->type('@body #oneoff_description', '') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #oneoff_amount.is-invalid') - ->assertVisible('@body #oneoff_description.is-invalid') - ->assertSeeIn( - '@body #oneoff_amount + span + .invalid-feedback', - 'The amount must be a number.' - ) - ->assertSeeIn( - '@body #oneoff_description + .invalid-feedback', - 'The description field is required.' - ); - - // Test adding a bonus - $browser->type('@body #oneoff_amount', '12.34') - ->type('@body #oneoff_description', 'Test bonus') - ->click('@button-action') - ->assertToast(Toast::TYPE_SUCCESS, 'The bonus has been added to the wallet successfully.'); - }) - ->assertMissing('#oneoff-dialog') - ->assertSeeIn('@user-finances .card-title span.text-success', '12,34 CHF') - ->waitUntilMissing('.app-loader') - ->with('table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 3) - ->assertMissing('tfoot') - ->assertSeeIn('tbody tr:first-child td.description', 'Bonus: Test bonus') - ->assertSeeIn('tbody tr:first-child td.price', '12,34 CHF'); - - if (!$browser->isPhone()) { - $browser->assertSeeIn('tbody tr:first-child td.email', 'jeroen@jeroen.jeroen'); - } - }); - - $this->assertSame(1234, $john->wallets()->first()->balance); - - // Test penalty - $browser->click('@user-finances #button-penalty') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Add a penalty to the wallet') - ->assertFocused('@body input#oneoff_amount') - ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') - ->assertvalue('@body input#oneoff_amount', '') - ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') - ->assertvalue('@body input#oneoff_description', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#oneoff-dialog') - ->click('@user-finances #button-penalty') - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - // Test input validation for a penalty - $browser->type('@body #oneoff_amount', '') - ->type('@body #oneoff_description', '') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #oneoff_amount.is-invalid') - ->assertVisible('@body #oneoff_description.is-invalid') - ->assertSeeIn( - '@body #oneoff_amount + span + .invalid-feedback', - 'The amount field is required.' - ) - ->assertSeeIn( - '@body #oneoff_description + .invalid-feedback', - 'The description field is required.' - ); - - // Test adding a penalty - $browser->type('@body #oneoff_amount', '12.35') - ->type('@body #oneoff_description', 'Test penalty') - ->click('@button-action') - ->assertToast(Toast::TYPE_SUCCESS, 'The penalty has been added to the wallet successfully.'); - }) - ->assertMissing('#oneoff-dialog') - ->assertSeeIn('@user-finances .card-title span.text-danger', '-0,01 CHF'); - - $this->assertSame(-1, $john->wallets()->first()->balance); - }); - } -} diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php deleted file mode 100644 index 0755a234..00000000 --- a/src/tests/Browser/Admin/UserTest.php +++ /dev/null @@ -1,439 +0,0 @@ -getTestUser('john@kolab.org'); - $john->setSettings([ - 'phone' => '+48123123123', - 'external_email' => 'john.doe.external@gmail.com', - ]); - if ($john->isSuspended()) { - User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); - } - $wallet = $john->wallets()->first(); - $wallet->discount()->dissociate(); - $wallet->save(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $john = $this->getTestUser('john@kolab.org'); - $john->setSettings([ - 'phone' => null, - 'external_email' => 'john.doe.external@gmail.com', - ]); - if ($john->isSuspended()) { - User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]); - } - $wallet = $john->wallets()->first(); - $wallet->discount()->dissociate(); - $wallet->save(); - - parent::tearDown(); - } - - /** - * Test user info page (unauthenticated) - */ - public function testUserUnauth(): void - { - // Test that the page requires authentication - $this->browse(function (Browser $browser) { - $jack = $this->getTestUser('jack@kolab.org'); - $browser->visit('/user/' . $jack->id)->on(new Home()); - }); - } - - /** - * Test user info page - */ - public function testUserInfo(): void - { - $this->browse(function (Browser $browser) { - $jack = $this->getTestUser('jack@kolab.org'); - $page = new UserPage($jack->id); - - $browser->visit(new Home()) - ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) - ->on(new Dashboard()) - ->visit($page) - ->on($page); - - // Assert main info box content - $browser->assertSeeIn('@user-info .card-title', $jack->email) - ->with('@user-info form', function (Browser $browser) use ($jack) { - $browser->assertElementsCount('.row', 7) - ->assertSeeIn('.row:nth-child(1) label', 'Managed by') - ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org') - ->assertSeeIn('.row:nth-child(2) label', 'ID (Created at)') - ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})") - ->assertSeeIn('.row:nth-child(3) label', 'Status') - ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active') - ->assertSeeIn('.row:nth-child(4) label', 'First name') - ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack') - ->assertSeeIn('.row:nth-child(5) label', 'Last name') - ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels') - ->assertSeeIn('.row:nth-child(6) label', 'External email') - ->assertMissing('.row:nth-child(6) #external_email a') - ->assertSeeIn('.row:nth-child(7) label', 'Country') - ->assertSeeIn('.row:nth-child(7) #country', 'United States'); - }); - - // Some tabs are loaded in background, wait a second - $browser->pause(500) - ->assertElementsCount('@nav a', 5); - - // Note: Finances tab is tested in UserFinancesTest.php - $browser->assertSeeIn('@nav #tab-finances', 'Finances'); - - // Assert Aliases tab - $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') - ->click('@nav #tab-aliases') - ->whenAvailable('@user-aliases', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 1) - ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org') - ->assertMissing('table tfoot'); - }); - - // Assert Subscriptions tab - $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 3) - ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') - ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF') - ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF') - ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF') - ->assertMissing('table tfoot') - ->assertMissing('#reset2fa'); - }); - - // Assert Domains tab - $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') - ->click('@nav #tab-domains') - ->with('@user-domains', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 0) - ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); - }); - - // Assert Users tab - $browser->assertSeeIn('@nav #tab-users', 'Users (0)') - ->click('@nav #tab-users') - ->with('@user-users', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 0) - ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); - }); - }); - } - - /** - * Test user info page (continue) - * - * @depends testUserInfo - */ - public function testUserInfo2(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - $page = new UserPage($john->id); - $discount = Discount::where('code', 'TEST')->first(); - $wallet = $john->wallet(); - $wallet->discount()->associate($discount); - $wallet->debit(2010); - $wallet->save(); - - // Click the managed-by link on Jack's page - $browser->click('@user-info #manager a') - ->on($page); - - // Assert main info box content - $browser->assertSeeIn('@user-info .card-title', $john->email) - ->with('@user-info form', function (Browser $browser) use ($john) { - $ext_email = $john->getSetting('external_email'); - - $browser->assertElementsCount('.row', 9) - ->assertSeeIn('.row:nth-child(1) label', 'ID (Created at)') - ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})") - ->assertSeeIn('.row:nth-child(2) label', 'Status') - ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active') - ->assertSeeIn('.row:nth-child(3) label', 'First name') - ->assertSeeIn('.row:nth-child(3) #first_name', 'John') - ->assertSeeIn('.row:nth-child(4) label', 'Last name') - ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe') - ->assertSeeIn('.row:nth-child(5) label', 'Organization') - ->assertSeeIn('.row:nth-child(5) #organization', 'Kolab Developers') - ->assertSeeIn('.row:nth-child(6) label', 'Phone') - ->assertSeeIn('.row:nth-child(6) #phone', $john->getSetting('phone')) - ->assertSeeIn('.row:nth-child(7) label', 'External email') - ->assertSeeIn('.row:nth-child(7) #external_email a', $ext_email) - ->assertAttribute('.row:nth-child(7) #external_email a', 'href', "mailto:$ext_email") - ->assertSeeIn('.row:nth-child(8) label', 'Address') - ->assertSeeIn('.row:nth-child(8) #billing_address', $john->getSetting('billing_address')) - ->assertSeeIn('.row:nth-child(9) label', 'Country') - ->assertSeeIn('.row:nth-child(9) #country', 'United States'); - }); - - // Some tabs are loaded in background, wait a second - $browser->pause(500) - ->assertElementsCount('@nav a', 5); - - // Note: Finances tab is tested in UserFinancesTest.php - $browser->assertSeeIn('@nav #tab-finances', 'Finances'); - - // Assert Aliases tab - $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') - ->click('@nav #tab-aliases') - ->whenAvailable('@user-aliases', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 1) - ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org') - ->assertMissing('table tfoot'); - }); - - // Assert Subscriptions tab - $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 3) - ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') - ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') - ->assertMissing('table tfoot') - ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); - }); - - // Assert Domains tab - $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)') - ->click('@nav #tab-domains') - ->with('@user-domains table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 1) - ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org') - ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') - ->assertMissing('tfoot'); - }); - - // Assert Users tab - $browser->assertSeeIn('@nav #tab-users', 'Users (4)') - ->click('@nav #tab-users') - ->with('@user-users table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 4) - ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org') - ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success') - ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org') - ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success') - ->assertSeeIn('tbody tr:nth-child(3) td:first-child span', 'john@kolab.org') - ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success') - ->assertSeeIn('tbody tr:nth-child(4) td:first-child a', 'ned@kolab.org') - ->assertVisible('tbody tr:nth-child(4) td:first-child svg.text-success') - ->assertMissing('tfoot'); - }); - }); - - // Now we go to Ned's info page, he's a controller on John's wallet - $this->browse(function (Browser $browser) { - $ned = $this->getTestUser('ned@kolab.org'); - $page = new UserPage($ned->id); - - $browser->click('@user-users tbody tr:nth-child(4) td:first-child a') - ->on($page); - - // Assert main info box content - $browser->assertSeeIn('@user-info .card-title', $ned->email) - ->with('@user-info form', function (Browser $browser) use ($ned) { - $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created at)') - ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})"); - }); - - // Some tabs are loaded in background, wait a second - $browser->pause(500) - ->assertElementsCount('@nav a', 5); - - // Note: Finances tab is tested in UserFinancesTest.php - $browser->assertSeeIn('@nav #tab-finances', 'Finances'); - - // Assert Aliases tab - $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)') - ->click('@nav #tab-aliases') - ->whenAvailable('@user-aliases', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 0) - ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.'); - }); - - // Assert Subscriptions tab, we expect John's discount here - $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (5)') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 5) - ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox') - ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync') - ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,90 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication') - ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹') - ->assertMissing('table tfoot') - ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher') - ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth'); - }); - - // We don't expect John's domains here - $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)') - ->click('@nav #tab-domains') - ->with('@user-domains', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 0) - ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.'); - }); - - // We don't expect John's users here - $browser->assertSeeIn('@nav #tab-users', 'Users (0)') - ->click('@nav #tab-users') - ->with('@user-users', function (Browser $browser) { - $browser->assertElementsCount('table tbody tr', 0) - ->assertSeeIn('table tfoot tr td', 'There are no users in this account.'); - }); - }); - } - - /** - * Test editing an external email - * - * @depends testUserInfo2 - */ - public function testExternalEmail(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->waitFor('@user-info #external_email button') - ->click('@user-info #external_email button') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#email-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'External email') - ->assertFocused('@body input') - ->assertValue('@body input', 'john.doe.external@gmail.com') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#email-dialog') - ->click('@user-info #external_email button') - // Test email validation error handling, and email update - ->with(new Dialog('#email-dialog'), function (Browser $browser) { - $browser->type('@body input', 'test') - ->click('@button-action') - ->waitFor('@body input.is-invalid') - ->assertSeeIn( - '@body input + .invalid-feedback', - 'The external email must be a valid email address.' - ) - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->type('@body input', 'test@test.com') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') - ->assertSeeIn('@user-info #external_email a', 'test@test.com') - ->click('@user-info #external_email button') - ->with(new Dialog('#email-dialog'), function (Browser $browser) { - $browser->assertValue('@body input', 'test@test.com') - ->assertMissing('@body input.is-invalid') - ->assertMissing('@body input + .invalid-feedback') - ->click('@button-cancel'); - }) - ->assertSeeIn('@user-info #external_email a', 'test@test.com'); - - // $john->getSetting() may not work here as it uses internal cache - // read the value form database - $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value; - $this->assertSame('test@test.com', $current_ext_email); - }); - } - - /** - * Test suspending/unsuspending the user - */ - public function testSuspendAndUnsuspend(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->assertVisible('@user-info #button-suspend') - ->assertMissing('@user-info #button-unsuspend') - ->click('@user-info #button-suspend') - ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.') - ->assertSeeIn('@user-info #status span.text-warning', 'Suspended') - ->assertMissing('@user-info #button-suspend') - ->click('@user-info #button-unsuspend') - ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.') - ->assertSeeIn('@user-info #status span.text-success', 'Active') - ->assertVisible('@user-info #button-suspend') - ->assertMissing('@user-info #button-unsuspend'); - }); - } - - /** - * Test resetting 2FA for the user - */ - public function testReset2FA(): void - { - $this->browse(function (Browser $browser) { - $this->deleteTestUser('userstest1@kolabnow.com'); - $user = $this->getTestUser('userstest1@kolabnow.com'); - $sku2fa = Sku::firstOrCreate(['title' => '2fa']); - $user->assignSku($sku2fa); - SecondFactor::seed('userstest1@kolabnow.com'); - - $browser->visit(new UserPage($user->id)) - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) { - $browser->waitFor('#reset2fa') - ->assertVisible('#sku' . $sku2fa->id); - }) - ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)') - ->click('#reset2fa') - ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', '2-Factor Authentication Reset') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Reset') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.') - ->assertMissing('#sku' . $sku2fa->id) - ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)'); - }); - } -} diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php deleted file mode 100644 index 2462458a..00000000 --- a/src/tests/Browser/DomainTest.php +++ /dev/null @@ -1,163 +0,0 @@ -browse(function ($browser) { - $browser->visit('/domain/123')->on(new Home()); - }); - } - - /** - * Test domain info page (non-existing domain id) - */ - public function testDomainInfo404(): void - { - $this->browse(function ($browser) { - // FIXME: I couldn't make loginAs() method working - - // Note: Here we're also testing that unauthenticated request - // is passed to logon form and then "redirected" to the requested page - $browser->visit('/domain/123') - ->on(new Home()) - ->submitLogon('john@kolab.org', 'simple123') - ->assertErrorPage(404); - }); - } - - /** - * Test domain info page (existing domain) - * - * @depends testDomainInfo404 - */ - public function testDomainInfo(): void - { - $this->browse(function ($browser) { - // Unconfirmed domain - $domain = Domain::where('namespace', 'kolab.org')->first(); - if ($domain->isConfirmed()) { - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); - } - - $browser->visit('/domain/' . $domain->id) - ->on(new DomainInfo()) - ->whenAvailable('@verify', function ($browser) use ($domain) { - $browser->assertSeeIn('pre', $domain->namespace) - ->assertSeeIn('pre', $domain->hash()) - ->click('button') - ->assertToast(Toast::TYPE_ERROR, 'Domain ownership verification failed.'); - - // Make sure the domain is confirmed now - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - $browser->click('button') - ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.'); - }) - ->whenAvailable('@config', function ($browser) use ($domain) { - $browser->assertSeeIn('pre', $domain->namespace); - }) - ->assertMissing('@verify'); - - // Check that confirmed domain page contains only the config box - $browser->visit('/domain/' . $domain->id) - ->on(new DomainInfo()) - ->assertMissing('@verify') - ->assertPresent('@config'); - }); - } - - /** - * Test domains list page (unauthenticated) - */ - public function testDomainListUnauth(): void - { - // Test that the page requires authentication - $this->browse(function ($browser) { - $browser->visit('/logout') - ->visit('/domains') - ->on(new Home()); - }); - } - - /** - * Test domains list page - * - * @depends testDomainListUnauth - */ - public function testDomainList(): void - { - $this->browse(function ($browser) { - // Login the user - $browser->visit('/login') - ->on(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - // On dashboard click the "Domains" link - ->on(new Dashboard()) - ->assertSeeIn('@links a.link-domains', 'Domains') - ->click('@links a.link-domains') - // On Domains List page click the domain entry - ->on(new DomainList()) - ->waitFor('@table tbody tr') - ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-success') - ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active') - ->assertSeeIn('@table tbody tr:first-child td:first-child', 'kolab.org') - ->assertMissing('@table tfoot') - ->click('@table tbody tr:first-child td:first-child a') - // On Domain Info page verify that's the clicked domain - ->on(new DomainInfo()) - ->whenAvailable('@config', function ($browser) { - $browser->assertSeeIn('pre', 'kolab.org'); - }); - }); - - // TODO: Test domains list acting as Ned (John's "delegatee") - } - - /** - * Test domains list page (user with no domains) - */ - public function testDomainListEmpty(): void - { - $this->browse(function ($browser) { - // Login the user - $browser->visit('/login') - ->on(new Home()) - ->submitLogon('jack@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertVisible('@links a.link-profile') - ->assertMissing('@links a.link-domains') - ->assertMissing('@links a.link-users') - ->assertMissing('@links a.link-wallet'); -/* - // On dashboard click the "Domains" link - ->assertSeeIn('@links a.link-domains', 'Domains') - ->click('@links a.link-domains') - // On Domains List page click the domain entry - ->on(new DomainList()) - ->assertMissing('@table tbody') - ->assertSeeIn('tfoot td', 'There are no domains in this account.'); -*/ - }); - } -} diff --git a/src/tests/Browser/ErrorTest.php b/src/tests/Browser/ErrorTest.php deleted file mode 100644 index aaeec604..00000000 --- a/src/tests/Browser/ErrorTest.php +++ /dev/null @@ -1,38 +0,0 @@ -browse(function (Browser $browser) { - $browser->visit('/unknown') - ->waitFor('#app > #error-page') - ->assertVisible('#app > #header-menu') - ->assertVisible('#app > #footer-menu'); - - $this->assertSame('404', $browser->text('#error-page .code')); - $this->assertSame('Not Found', $browser->text('#error-page .message')); - }); - - $this->browse(function (Browser $browser) { - $browser->visit('/login/unknown') - ->waitFor('#app > #error-page') - ->assertVisible('#app > #header-menu') - ->assertVisible('#app > #footer-menu'); - - $this->assertSame('404', $browser->text('#error-page .code')); - $this->assertSame('Not Found', $browser->text('#error-page .message')); - }); - } -} diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php deleted file mode 100644 index 6ca98b9c..00000000 --- a/src/tests/Browser/LogonTest.php +++ /dev/null @@ -1,211 +0,0 @@ -browse(function (Browser $browser) { - $browser->visit(new Home()) - ->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }); - - if ($browser->isDesktop()) { - $browser->within(new Menu('footer'), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'webmail']); - }); - } else { - $browser->assertMissing('#footer-menu .navbar-nav'); - } - }); - } - - /** - * Test redirect to /login if user is unauthenticated - */ - public function testLogonRedirect(): void - { - $this->browse(function (Browser $browser) { - $browser->visit('/dashboard'); - - // Checks if we're really on the login page - $browser->waitForLocation('/login') - ->on(new Home()); - }); - } - - /** - * Logon with wrong password/user test - */ - public function testLogonWrongCredentials(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'wrong'); - - // Error message - $browser->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.'); - - // Checks if we're still on the logon page - $browser->on(new Home()); - }); - } - - /** - * Successful logon test - */ - public function testLogonSuccessful(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - // Checks if we're really on Dashboard page - ->on(new Dashboard()) - ->assertVisible('@links a.link-profile') - ->assertVisible('@links a.link-domains') - ->assertVisible('@links a.link-users') - ->assertVisible('@links a.link-wallet') - ->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']); - }); - - if ($browser->isDesktop()) { - $browser->within(new Menu('footer'), function ($browser) { - $browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']); - }); - } else { - $browser->assertMissing('#footer-menu .navbar-nav'); - } - - $browser->assertUser('john@kolab.org'); - - // Assert no "Account status" for this account - $browser->assertMissing('@status'); - - // Goto /domains and assert that the link on logo element - // leads to the dashboard - $browser->visit('/domains') - ->waitForText('Domains') - ->click('a.navbar-brand') - ->on(new Dashboard()); - - // Test that visiting '/' with logged in user does not open logon form - // but "redirects" to the dashboard - $browser->visit('/')->on(new Dashboard()); - }); - } - - /** - * Logout test - * - * @depends testLogonSuccessful - */ - public function testLogout(): void - { - $this->browse(function (Browser $browser) { - $browser->on(new Dashboard()); - - // Click the Logout button - $browser->within(new Menu(), function ($browser) { - $browser->clickMenuItem('logout'); - }); - - // We expect the logon page - $browser->waitForLocation('/login') - ->on(new Home()); - - // with default menu - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }); - - // Success toast message - $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); - }); - } - - /** - * Logout by URL test - */ - public function testLogoutByURL(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true); - - // Checks if we're really on Dashboard page - $browser->on(new Dashboard()); - - // Use /logout url, and expect the logon page - $browser->visit('/logout') - ->waitForLocation('/login') - ->on(new Home()); - - // with default menu - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']); - }); - - // Success toast message - $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); - }); - } - - /** - * Test 2-Factor Authentication - * - * @depends testLogoutByURL - */ - public function test2FA(): void - { - $this->browse(function (Browser $browser) { - // Test missing 2fa code - $browser->on(new Home()) - ->type('@email-input', 'ned@kolab.org') - ->type('@password-input', 'simple123') - ->press('form button') - ->waitFor('@second-factor-input.is-invalid + .invalid-feedback') - ->assertSeeIn( - '@second-factor-input.is-invalid + .invalid-feedback', - 'Second factor code is required.' - ) - ->assertFocused('@second-factor-input') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - - // Test invalid code - $browser->type('@second-factor-input', '123456') - ->press('form button') - ->waitUntilMissing('@second-factor-input.is-invalid') - ->waitFor('@second-factor-input.is-invalid + .invalid-feedback') - ->assertSeeIn( - '@second-factor-input.is-invalid + .invalid-feedback', - 'Second factor code is invalid.' - ) - ->assertFocused('@second-factor-input') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - - $code = \App\Auth\SecondFactor::code('ned@kolab.org'); - - // Test valid (TOTP) code - $browser->type('@second-factor-input', $code) - ->press('form button') - ->waitUntilMissing('@second-factor-input.is-invalid') - ->waitForLocation('/dashboard') - ->on(new Dashboard()); - }); - } -} diff --git a/src/tests/Browser/Pages/Home.php b/src/tests/Browser/Pages/Home.php index f99b60ef..c05e54e6 100644 --- a/src/tests/Browser/Pages/Home.php +++ b/src/tests/Browser/Pages/Home.php @@ -1,86 +1,88 @@ waitForLocation($this->url()) ->waitUntilMissing('.app-loader') ->assertVisible('form.form-signin'); } /** * Get the element shortcuts for the page. * * @return array */ public function elements() { return [ '@app' => '#app', '@email-input' => '#inputEmail', '@password-input' => '#inputPassword', '@second-factor-input' => '#secondfactor', ]; } /** * Submit logon form. * * @param \Laravel\Dusk\Browser $browser The browser object * @param string $username User name * @param string $password User password * @param bool $wait_for_dashboard * @param array $config Client-site config * * @return void */ public function submitLogon( $browser, $username, $password, $wait_for_dashboard = false, $config = [] ) { $browser->type('@email-input', $username) ->type('@password-input', $password); - if ($username == 'ned@kolab.org') { - $code = \App\Auth\SecondFactor::code('ned@kolab.org'); + $user = \App\User::where('email', $username)->first(); + + if ($user->hasSku('2fa')) { + $code = \App\Auth\SecondFactor::code($username); $browser->type('@second-factor-input', $code); } if (!empty($config)) { $browser->script( sprintf('Object.assign(window.config, %s)', \json_encode($config)) ); } $browser->press('form button'); if ($wait_for_dashboard) { $browser->waitForLocation('/dashboard'); } } } diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php deleted file mode 100644 index 64762b8d..00000000 --- a/src/tests/Browser/PasswordResetTest.php +++ /dev/null @@ -1,276 +0,0 @@ -deleteTestUser('passwordresettestdusk@' . \config('app.domain')); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('passwordresettestdusk@' . \config('app.domain')); - - parent::tearDown(); - } - - /** - * Test the link from logon to password-reset page - */ - public function testPasswordResetLinkOnLogon(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()); - - $browser->assertSeeLink('Forgot password?'); - $browser->clickLink('Forgot password?'); - - $browser->on(new PasswordReset()); - $browser->assertVisible('@step1'); - }); - } - - /** - * Test 1st step of password-reset - */ - public function testPasswordResetStep1(): void - { - $user = $this->getTestUser('passwordresettestdusk@' . \config('app.domain')); - $user->setSetting('external_email', 'external@domain.tld'); - - $this->browse(function (Browser $browser) { - $browser->visit(new PasswordReset()); - - $browser->assertVisible('@step1'); - - // Here we expect email input and submit button - $browser->with('@step1', function ($step) { - $step->assertVisible('#reset_email'); - $step->assertFocused('#reset_email'); - $step->assertVisible('[type=submit]'); - }); - - // Submit empty form - $browser->with('@step1', function ($step) { - $step->click('[type=submit]'); - $step->assertFocused('#reset_email'); - }); - - // Submit invalid email - // We expect email input to have is-invalid class added, with .invalid-feedback element - $browser->with('@step1', function ($step) use ($browser) { - $step->type('#reset_email', '@test'); - $step->click('[type=submit]'); - - $step->waitFor('#reset_email.is-invalid'); - $step->waitFor('#reset_email + .invalid-feedback'); - $browser->waitFor('.toast-error'); - $browser->click('.toast-error'); // remove the toast - }); - - // Submit valid data - $browser->with('@step1', function ($step) { - $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain')); - $step->click('[type=submit]'); - - $step->assertMissing('#reset_email.is-invalid'); - $step->assertMissing('#reset_email + .invalid-feedback'); - }); - - $browser->waitUntilMissing('@step2 #reset_code[value=""]'); - $browser->waitFor('@step2'); - $browser->assertMissing('@step1'); - }); - } - - /** - * Test 2nd Step of the password reset process - * - * @depends testPasswordResetStep1 - */ - public function testPasswordResetStep2(): void - { - $user = $this->getTestUser('passwordresettestdusk@' . \config('app.domain')); - $user->setSetting('external_email', 'external@domain.tld'); - - $this->browse(function (Browser $browser) { - $browser->assertVisible('@step2'); - - // Here we expect one text input, Back and Continue buttons - $browser->with('@step2', function ($step) { - $step->assertVisible('#reset_short_code'); - $step->assertFocused('#reset_short_code'); - $step->assertVisible('[type=button]'); - $step->assertVisible('[type=submit]'); - }); - - // Test Back button functionality - $browser->click('@step2 [type=button]'); - $browser->waitFor('@step1'); - $browser->assertFocused('@step1 #reset_email'); - $browser->assertMissing('@step2'); - - // Submit valid Step 1 data (again) - $browser->with('@step1', function ($step) { - $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain')); - $step->click('[type=submit]'); - }); - - $browser->waitFor('@step2'); - $browser->assertMissing('@step1'); - - // Submit invalid code - // We expect code input to have is-invalid class added, with .invalid-feedback element - $browser->with('@step2', function ($step) use ($browser) { - $step->type('#reset_short_code', 'XXXXX'); - $step->click('[type=submit]'); - - $browser->waitFor('.toast-error'); - - $step->waitFor('#reset_short_code.is-invalid') - ->assertVisible('#reset_short_code.is-invalid') - ->assertVisible('#reset_short_code + .invalid-feedback') - ->assertFocused('#reset_short_code'); - - $browser->click('.toast-error'); // remove the toast - }); - - // Submit valid code - // We expect error state on code input to be removed, and Step 3 form visible - $browser->with('@step2', function ($step) { - // Get the code and short_code from database - // FIXME: Find a nice way to read javascript data without using hidden inputs - $code = $step->value('#reset_code'); - - $this->assertNotEmpty($code); - - $code = VerificationCode::find($code); - - $step->type('#reset_short_code', $code->short_code); - $step->click('[type=submit]'); - - $step->assertMissing('#reset_short_code.is-invalid'); - $step->assertMissing('#reset_short_code + .invalid-feedback'); - }); - - $browser->waitFor('@step3'); - $browser->assertMissing('@step2'); - }); - } - - /** - * Test 3rd Step of the password reset process - * - * @depends testPasswordResetStep2 - */ - public function testPasswordResetStep3(): void - { - $user = $this->getTestUser('passwordresettestdusk@' . \config('app.domain')); - $user->setSetting('external_email', 'external@domain.tld'); - - $this->browse(function (Browser $browser) { - $browser->assertVisible('@step3'); - - // Here we expect 2 text inputs, Back and Continue buttons - $browser->with('@step3', function ($step) { - $step->assertVisible('#reset_password'); - $step->assertVisible('#reset_confirm'); - $step->assertVisible('[type=button]'); - $step->assertVisible('[type=submit]'); - $step->assertFocused('#reset_password'); - }); - - // Test Back button - $browser->click('@step3 [type=button]'); - $browser->waitFor('@step2'); - $browser->assertFocused('@step2 #reset_short_code'); - $browser->assertMissing('@step3'); - $browser->assertMissing('@step1'); - - // TODO: Test form reset when going back - - // Because the verification code is removed in tearDown() - // we'll start from the beginning (Step 1) - $browser->click('@step2 [type=button]'); - $browser->waitFor('@step1'); - $browser->assertFocused('@step1 #reset_email'); - $browser->assertMissing('@step3'); - $browser->assertMissing('@step2'); - - // Submit valid data - $browser->with('@step1', function ($step) { - $step->type('#reset_email', 'passwordresettestdusk@' . \config('app.domain')); - $step->click('[type=submit]'); - }); - - $browser->waitFor('@step2'); - $browser->waitUntilMissing('@step2 #reset_code[value=""]'); - - // Submit valid code again - $browser->with('@step2', function ($step) { - $code = $step->value('#reset_code'); - - $this->assertNotEmpty($code); - - $code = VerificationCode::find($code); - - $step->type('#reset_short_code', $code->short_code); - $step->click('[type=submit]'); - }); - - $browser->waitFor('@step3'); - - // Submit invalid data - $browser->with('@step3', function ($step) use ($browser) { - $step->assertFocused('#reset_password'); - - $step->type('#reset_password', '12345678'); - $step->type('#reset_confirm', '123456789'); - - $step->click('[type=submit]'); - - $browser->waitFor('.toast-error'); - - $step->waitFor('#reset_password.is-invalid') - ->assertVisible('#reset_password.is-invalid') - ->assertVisible('#reset_password + .invalid-feedback') - ->assertFocused('#reset_password'); - - $browser->click('.toast-error'); // remove the toast - }); - - // Submit valid data - $browser->with('@step3', function ($step) { - $step->type('#reset_confirm', '12345678'); - - $step->click('[type=submit]'); - }); - - $browser->waitUntilMissing('@step3'); - - // At this point we should be auto-logged-in to dashboard - $browser->on(new Dashboard()); - - // FIXME: Is it enough to be sure user is logged in? - }); - } -} diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php deleted file mode 100644 index 5b66eefe..00000000 --- a/src/tests/Browser/PaymentMollieTest.php +++ /dev/null @@ -1,233 +0,0 @@ -deleteTestUser('payment-test@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('payment-test@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test the payment process - * - * @group mollie - */ - public function testPayment(): void - { - $user = $this->getTestUser('payment-test@kolabnow.com', [ - 'password' => 'simple123', - ]); - - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) - ->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->assertSeeIn('@main button', 'Add credit') - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Top up your wallet') - ->assertFocused('#amount') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@body #payment-form button', 'Continue') - // Test error handling - ->type('@body #amount', 'aaa') - ->click('@body #payment-form button') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.') - // Submit valid data - ->type('@body #amount', '12.34') - ->click('@body #payment-form button'); - }) - ->on(new PaymentMollie()) - ->assertSeeIn('@title', \config('app.name') . ' Payment') - ->assertSeeIn('@amount', 'CHF 12.34'); - - // Looks like the Mollie testing mode is limited. - // We'll select credit card method and mark the payment as paid - // We can't do much more, we have to trust Mollie their page works ;) - - // For some reason I don't get the method selection form, it - // immediately jumps to the next step. Let's detect that - if ($browser->element('@methods')) { - $browser->click('@methods button.grid-button-creditcard') - ->waitFor('button.form__button'); - } - - $browser->click('@status-table input[value="paid"]') - ->click('button.form__button'); - - // Now it should redirect back to wallet page and in background - // use the webhook to update payment status (and balance). - - // Looks like in test-mode the webhook is executed before redirect - // so we can expect balance updated on the wallet page - - $browser->waitForLocation('/wallet') - ->on(new WalletPage()) - ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF'); - }); - } - - /** - * Test the auto-payment setup process - * - * @group mollie - */ - public function testAutoPaymentSetup(): void - { - $user = $this->getTestUser('payment-test@kolabnow.com', [ - 'password' => 'simple123', - ]); - - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) - ->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Top up your wallet') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@body #mandate-form button', 'Set up auto-payment') - ->click('@body #mandate-form button') - ->assertSeeIn('@title', 'Add auto-payment') - ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') - ->assertValue('@body #mandate_amount', PaymentProvider::MIN_AMOUNT / 100) - ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore - ->assertValue('@body #mandate_balance', '0') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Continue') - // Test error handling - ->type('@body #mandate_amount', 'aaa') - ->type('@body #mandate_balance', '-1') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertVisible('@body #mandate_balance.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - ->assertSeeIn('#mandate_balance + span + .invalid-feedback', 'The balance must be at least 0.') - ->type('@body #mandate_amount', 'aaa') - ->type('@body #mandate_balance', '0') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertMissing('@body #mandate_balance.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - ->assertMissing('#mandate_balance + span + .invalid-feedback') - // Submit valid data - ->type('@body #mandate_amount', '100') - ->type('@body #mandate_balance', '0') - ->click('@button-action'); - }) - ->on(new PaymentMollie()) - ->assertSeeIn('@title', \config('app.name') . ' Auto-Payment Setup') - ->assertMissing('@amount') - ->submitValidCreditCard() - ->waitForLocation('/wallet') - ->visit('/wallet?paymentProvider=mollie') - ->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $expected = 'Auto-payment is set to fill up your account by 100 CHF every' - . ' time your account balance gets under 0 CHF. You will be charged' - . ' via Mastercard (**** **** **** 6787).'; - - $browser->assertSeeIn('@title', 'Top up your wallet') - ->waitFor('#mandate-info') - ->assertSeeIn('#mandate-info p:first-child', $expected) - ->click('@button-cancel'); - }); - }); - - // Test updating auto-payment - $this->browse(function (Browser $browser) use ($user) { - $wallet = $user->wallets()->first(); - $wallet->setSetting('mandate_disabled', 1); - - $browser->refresh() - ->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->waitFor('@body #mandate-info') - ->assertSeeIn( - '@body #mandate-info p.disabled-mandate', - 'The configured auto-payment has been disabled' - ) - ->assertSeeIn('@body #mandate-info button.btn-primary', 'Change auto-payment') - ->click('@body #mandate-info button.btn-primary') - ->assertSeeIn('@title', 'Update auto-payment') - ->assertSeeIn( - '@body form p.disabled-mandate', - 'The auto-payment is disabled.' - ) - ->assertValue('@body #mandate_amount', '100') - ->assertValue('@body #mandate_balance', '0') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - // Test error handling - ->type('@body #mandate_amount', 'aaa') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - // Submit valid data - ->type('@body #mandate_amount', '50') - ->click('@button-action'); - }) - ->waitUntilMissing('#payment-dialog') - ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been updated.') - // Open the dialog again and make sure the "disabled" text isn't there - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertMissing('@body #mandate-info p.disabled-mandate') - ->click('@body #mandate-info button.btn-primary') - ->assertMissing('@body form p.disabled-mandate') - ->click('@button-cancel'); - }); - }); - - // Test deleting auto-payment - $this->browse(function (Browser $browser) { - $browser->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@body #mandate-info button.btn-danger', 'Cancel auto-payment') - ->click('@body #mandate-info button.btn-danger') - ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.') - ->assertVisible('@body #mandate-form') - ->assertMissing('@body #mandate-info'); - }); - }); - } -} diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php deleted file mode 100644 index c37e47aa..00000000 --- a/src/tests/Browser/PaymentStripeTest.php +++ /dev/null @@ -1,202 +0,0 @@ -deleteTestUser('payment-test@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('payment-test@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test the payment process - * - * @group stripe - */ - public function testPayment(): void - { - $user = $this->getTestUser('payment-test@kolabnow.com', [ - 'password' => 'simple123', - ]); - - $this->browse(function (Browser $browser) use ($user) { - $browser->visit(new Home()) - ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'stripe']) - ->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->assertSeeIn('@main button', 'Add credit') - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Top up your wallet') - ->assertFocused('#amount') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@body #payment-form button', 'Continue') - // Test error handling - ->type('@body #amount', 'aaa') - ->click('@body #payment-form button') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.') - // Submit valid data - ->type('@body #amount', '12.34') - ->click('@body #payment-form button'); - }) - ->on(new PaymentStripe()) - ->assertSeeIn('@title', \config('app.name') . ' Payment') - ->assertSeeIn('@amount', 'CHF 12.34') - ->assertValue('@email-input', $user->email) - ->submitValidCreditCard(); - - // Now it should redirect back to wallet page and in background - // use the webhook to update payment status (and balance). - - // Looks like in test-mode the webhook is executed before redirect - // so we can expect balance updated on the wallet page - - $browser->waitForLocation('/wallet', 30) // need more time than default 5 sec. - ->on(new WalletPage()) - ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF'); - }); - } - - /** - * Test the auto-payment setup process - * - * @group stripe - */ - public function testAutoPaymentSetup(): void - { - $user = $this->getTestUser('payment-test@kolabnow.com', [ - 'password' => 'simple123', - ]); - - // Test creating auto-payment - $this->browse(function (Browser $browser) use ($user) { - $browser->visit(new Home()) - ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'stripe']) - ->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Top up your wallet') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@body #mandate-form button', 'Set up auto-payment') - ->click('@body #mandate-form button') - ->assertSeeIn('@title', 'Add auto-payment') - ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') - ->assertValue('@body #mandate_amount', PaymentProvider::MIN_AMOUNT / 100) - ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore - ->assertValue('@body #mandate_balance', '0') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Continue') - // Test error handling - ->type('@body #mandate_amount', 'aaa') - ->type('@body #mandate_balance', '-1') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertVisible('@body #mandate_balance.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - ->assertSeeIn('#mandate_balance + span + .invalid-feedback', 'The balance must be at least 0.') - ->type('@body #mandate_amount', 'aaa') - ->type('@body #mandate_balance', '0') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertMissing('@body #mandate_balance.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - ->assertMissing('#mandate_balance + span + .invalid-feedback') - // Submit valid data - ->type('@body #mandate_amount', '100') - ->type('@body #mandate_balance', '0') - ->click('@button-action'); - }) - ->on(new PaymentStripe()) - ->assertMissing('@title') - ->assertMissing('@amount') - ->assertValue('@email-input', $user->email) - ->submitValidCreditCard() - ->waitForLocation('/wallet', 30) // need more time than default 5 sec. - ->visit('/wallet?paymentProvider=stripe') - ->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $expected = 'Auto-payment is set to fill up your account by 100 CHF every' - . ' time your account balance gets under 0 CHF. You will be charged' - . ' via Visa (**** **** **** 4242).'; - - $browser->assertSeeIn('@title', 'Top up your wallet') - ->waitFor('#mandate-info') - ->assertSeeIn('#mandate-info p:first-child', $expected) - ->click('@button-cancel'); - }); - }); - - // Test updating auto-payment - $this->browse(function (Browser $browser) { - $browser->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@body #mandate-info button.btn-primary', 'Change auto-payment') - ->click('@body #mandate-info button.btn-primary') - ->assertSeeIn('@title', 'Update auto-payment') - ->assertValue('@body #mandate_amount', '100') - ->assertValue('@body #mandate_balance', '0') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - // Test error handling - ->type('@body #mandate_amount', 'aaa') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #mandate_amount.is-invalid') - ->assertSeeIn('#mandate_amount + span + .invalid-feedback', 'The amount must be a number.') - // Submit valid data - ->type('@body #mandate_amount', '50') - ->click('@button-action'); - }) - ->waitUntilMissing('#payment-dialog') - ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been updated.'); - }); - - // Test deleting auto-payment - $this->browse(function (Browser $browser) { - $browser->on(new WalletPage()) - ->click('@main button') - ->with(new Dialog('@payment-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@body #mandate-info button.btn-danger', 'Cancel auto-payment') - ->click('@body #mandate-info button.btn-danger') - ->assertToast(Toast::TYPE_SUCCESS, 'The auto-payment has been removed.') - ->assertVisible('@body #mandate-form') - ->assertMissing('@body #mandate-info'); - }); - }); - } -} diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php deleted file mode 100644 index d8ba4bb2..00000000 --- a/src/tests/Browser/SignupTest.php +++ /dev/null @@ -1,545 +0,0 @@ -deleteTestUser('signuptestdusk@' . \config('app.domain')); - $this->deleteTestUser('admin@user-domain-signup.com'); - $this->deleteTestDomain('user-domain-signup.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('signuptestdusk@' . \config('app.domain')); - $this->deleteTestUser('admin@user-domain-signup.com'); - $this->deleteTestDomain('user-domain-signup.com'); - - parent::tearDown(); - } - - /** - * Test signup code verification with a link - */ - public function testSignupCodeByLink(): void - { - // Test invalid code (invalid format) - $this->browse(function (Browser $browser) { - // Register Signup page element selectors we'll be using - $browser->onWithoutAssert(new Signup()); - - // TODO: Test what happens if user is logged in - - $browser->visit('/signup/invalid-code'); - - // TODO: According to https://github.com/vuejs/vue-router/issues/977 - // it is not yet easily possible to display error page component (route) - // without changing the URL - // TODO: Instead of css selector we should probably define page/component - // and use it instead - $browser->waitFor('#error-page'); - }); - - // Test invalid code (valid format) - $this->browse(function (Browser $browser) { - $browser->visit('/signup/XXXXX-code'); - - // FIXME: User will not be able to continue anyway, so we should - // either display 1st step or 404 error page - $browser->waitFor('@step1') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Test valid code - $this->browse(function (Browser $browser) { - $code = SignupCode::create([ - 'data' => [ - 'email' => 'User@example.org', - 'first_name' => 'User', - 'last_name' => 'Name', - 'plan' => 'individual', - 'voucher' => '', - ] - ]); - - $browser->visit('/signup/' . $code->short_code . '-' . $code->code) - ->waitFor('@step3') - ->assertMissing('@step1') - ->assertMissing('@step2'); - - // FIXME: Find a nice way to read javascript data without using hidden inputs - $this->assertSame($code->code, $browser->value('@step2 #signup_code')); - - // TODO: Test if the signup process can be completed - }); - } - - /** - * Test signup "welcome" page - */ - public function testSignupStep0(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Signup()); - - $browser->assertVisible('@step0') - ->assertMissing('@step1') - ->assertMissing('@step2') - ->assertMissing('@step3'); - - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup'); - }); - - $browser->waitFor('@step0 .plan-selector > .plan-box'); - - // Assert first plan box and press the button - $browser->with('@step0 .plan-selector > .plan-individual', function ($step) { - $step->assertVisible('button') - ->assertSeeIn('button', 'Individual Account') - ->assertVisible('.plan-description') - ->click('button'); - }); - - $browser->waitForLocation('/signup/individual') - ->assertVisible('@step1') - ->assertMissing('@step0') - ->assertMissing('@step2') - ->assertMissing('@step3') - ->assertFocused('@step1 #signup_first_name'); - - // Click Back button - $browser->click('@step1 [type=button]') - ->waitForLocation('/signup') - ->assertVisible('@step0') - ->assertMissing('@step1') - ->assertMissing('@step2') - ->assertMissing('@step3'); - - // Choose the group account plan - $browser->click('@step0 .plan-selector > .plan-group button') - ->waitForLocation('/signup/group') - ->assertVisible('@step1') - ->assertMissing('@step0') - ->assertMissing('@step2') - ->assertMissing('@step3') - ->assertFocused('@step1 #signup_first_name'); - - // TODO: Test if 'plan' variable is set properly in vue component - }); - } - - /** - * Test 1st step of the signup process - */ - public function testSignupStep1(): void - { - $this->browse(function (Browser $browser) { - $browser->visit('/signup/individual') - ->onWithoutAssert(new Signup()); - - // Here we expect two text inputs and Back and Continue buttons - $browser->with('@step1', function ($step) { - $step->assertVisible('#signup_last_name') - ->assertVisible('#signup_first_name') - ->assertFocused('#signup_first_name') - ->assertVisible('#signup_email') - ->assertVisible('[type=button]') - ->assertVisible('[type=submit]'); - }); - - // Submit empty form - // Email is required, so after pressing Submit - // we expect focus to be moved to the email input - $browser->with('@step1', function ($step) { - $step->click('[type=submit]'); - $step->assertFocused('#signup_email'); - }); - - $browser->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup'); - }); - - // Submit invalid email, and first_name - // We expect both inputs to have is-invalid class added, with .invalid-feedback element - $browser->with('@step1', function ($step) { - $step->type('#signup_first_name', str_repeat('a', 250)) - ->type('#signup_email', '@test') - ->click('[type=submit]') - ->waitFor('#signup_email.is-invalid') - ->assertVisible('#signup_first_name.is-invalid') - ->assertVisible('#signup_email + .invalid-feedback') - ->assertVisible('#signup_last_name + .invalid-feedback') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit valid data - // We expect error state on email input to be removed, and Step 2 form visible - $browser->with('@step1', function ($step) { - $step->type('#signup_first_name', 'Test') - ->type('#signup_last_name', 'User') - ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org') - ->click('[type=submit]') - ->assertMissing('#signup_email.is-invalid') - ->assertMissing('#signup_email + .invalid-feedback'); - }); - - $browser->waitUntilMissing('@step2 #signup_code[value=""]'); - $browser->waitFor('@step2'); - $browser->assertMissing('@step1'); - }); - } - - /** - * Test 2nd Step of the signup process - * - * @depends testSignupStep1 - */ - public function testSignupStep2(): void - { - $this->browse(function (Browser $browser) { - $browser->assertVisible('@step2') - ->assertMissing('@step0') - ->assertMissing('@step1') - ->assertMissing('@step3'); - - // Here we expect one text input, Back and Continue buttons - $browser->with('@step2', function ($step) { - $step->assertVisible('#signup_short_code') - ->assertFocused('#signup_short_code') - ->assertVisible('[type=button]') - ->assertVisible('[type=submit]'); - }); - - // Test Back button functionality - $browser->click('@step2 [type=button]') - ->waitFor('@step1') - ->assertFocused('@step1 #signup_first_name') - ->assertMissing('@step2'); - - // Submit valid Step 1 data (again) - $browser->with('@step1', function ($step) { - $step->type('#signup_first_name', 'User') - ->type('#signup_last_name', 'User') - ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org') - ->click('[type=submit]'); - }); - - $browser->waitFor('@step2'); - $browser->assertMissing('@step1'); - - // Submit invalid code - // We expect code input to have is-invalid class added, with .invalid-feedback element - $browser->with('@step2', function ($step) { - $step->type('#signup_short_code', 'XXXXX'); - $step->click('[type=submit]'); - - $step->waitFor('#signup_short_code.is-invalid') - ->assertVisible('#signup_short_code + .invalid-feedback') - ->assertFocused('#signup_short_code') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit valid code - // We expect error state on code input to be removed, and Step 3 form visible - $browser->with('@step2', function ($step) { - // Get the code and short_code from database - // FIXME: Find a nice way to read javascript data without using hidden inputs - $code = $step->value('#signup_code'); - - $this->assertNotEmpty($code); - - $code = SignupCode::find($code); - - $step->type('#signup_short_code', $code->short_code); - $step->click('[type=submit]'); - - $step->assertMissing('#signup_short_code.is-invalid'); - $step->assertMissing('#signup_short_code + .invalid-feedback'); - }); - - $browser->waitFor('@step3'); - $browser->assertMissing('@step2'); - }); - } - - /** - * Test 3rd Step of the signup process - * - * @depends testSignupStep2 - */ - public function testSignupStep3(): void - { - $this->browse(function (Browser $browser) { - $browser->assertVisible('@step3'); - - // Here we expect 3 text inputs, Back and Continue buttons - $browser->with('@step3', function ($step) { - $step->assertVisible('#signup_login'); - $step->assertVisible('#signup_password'); - $step->assertVisible('#signup_confirm'); - $step->assertVisible('select#signup_domain'); - $step->assertVisible('[type=button]'); - $step->assertVisible('[type=submit]'); - $step->assertFocused('#signup_login'); - $step->assertValue('select#signup_domain', \config('app.domain')); - $step->assertValue('#signup_login', ''); - $step->assertValue('#signup_password', ''); - $step->assertValue('#signup_confirm', ''); - - // TODO: Test domain selector - }); - - // Test Back button - $browser->click('@step3 [type=button]'); - $browser->waitFor('@step2'); - $browser->assertFocused('@step2 #signup_short_code'); - $browser->assertMissing('@step3'); - - // TODO: Test form reset when going back - - // Submit valid code again - $browser->with('@step2', function ($step) { - $code = $step->value('#signup_code'); - - $this->assertNotEmpty($code); - - $code = SignupCode::find($code); - - $step->type('#signup_short_code', $code->short_code); - $step->click('[type=submit]'); - }); - - $browser->waitFor('@step3'); - - // Submit invalid data - $browser->with('@step3', function ($step) { - $step->assertFocused('#signup_login') - ->type('#signup_login', '*') - ->type('#signup_password', '12345678') - ->type('#signup_confirm', '123456789') - ->click('[type=submit]') - ->waitFor('#signup_login.is-invalid') - ->assertVisible('#signup_domain + .invalid-feedback') - ->assertVisible('#signup_password.is-invalid') - ->assertVisible('#signup_password + .invalid-feedback') - ->assertFocused('#signup_login') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit invalid data (valid login, invalid password) - $browser->with('@step3', function ($step) { - $step->type('#signup_login', 'SignupTestDusk') - ->click('[type=submit]') - ->waitFor('#signup_password.is-invalid') - ->assertVisible('#signup_password + .invalid-feedback') - ->assertMissing('#signup_login.is-invalid') - ->assertMissing('#signup_domain + .invalid-feedback') - ->assertFocused('#signup_password') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit valid data - $browser->with('@step3', function ($step) { - $step->type('#signup_confirm', '12345678'); - - $step->click('[type=submit]'); - }); - - // At this point we should be auto-logged-in to dashboard - $browser->waitUntilMissing('@step3') - ->waitUntilMissing('.app-loader') - ->on(new Dashboard()) - ->assertUser('signuptestdusk@' . \config('app.domain')) - ->assertVisible('@links a.link-profile') - ->assertMissing('@links a.link-domains') - ->assertVisible('@links a.link-users') - ->assertVisible('@links a.link-wallet'); - - // Logout the user - $browser->within(new Menu(), function ($browser) { - $browser->clickMenuItem('logout'); - }); - }); - } - - /** - * Test signup for a group account - */ - public function testSignupGroup(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Signup()); - - // Choose the group account plan - $browser->waitFor('@step0 .plan-group button') - ->click('@step0 .plan-group button'); - - // Submit valid data - // We expect error state on email input to be removed, and Step 2 form visible - $browser->whenAvailable('@step1', function ($step) { - $step->type('#signup_first_name', 'Test') - ->type('#signup_last_name', 'User') - ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org') - ->click('[type=submit]'); - }); - - // Submit valid code - $browser->whenAvailable('@step2', function ($step) { - // Get the code and short_code from database - // FIXME: Find a nice way to read javascript data without using hidden inputs - $code = $step->value('#signup_code'); - $code = SignupCode::find($code); - - $step->type('#signup_short_code', $code->short_code) - ->click('[type=submit]'); - }); - - // Here we expect 4 text inputs, Back and Continue buttons - $browser->whenAvailable('@step3', function ($step) { - $step->assertVisible('#signup_login') - ->assertVisible('#signup_password') - ->assertVisible('#signup_confirm') - ->assertVisible('input#signup_domain') - ->assertVisible('[type=button]') - ->assertVisible('[type=submit]') - ->assertFocused('#signup_login') - ->assertValue('input#signup_domain', '') - ->assertValue('#signup_login', '') - ->assertValue('#signup_password', '') - ->assertValue('#signup_confirm', ''); - }); - - // Submit invalid login and password data - $browser->with('@step3', function ($step) { - $step->assertFocused('#signup_login') - ->type('#signup_login', '*') - ->type('#signup_domain', 'test.com') - ->type('#signup_password', '12345678') - ->type('#signup_confirm', '123456789') - ->click('[type=submit]') - ->waitFor('#signup_login.is-invalid') - ->assertVisible('#signup_domain + .invalid-feedback') - ->assertVisible('#signup_password.is-invalid') - ->assertVisible('#signup_password + .invalid-feedback') - ->assertFocused('#signup_login') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit invalid domain - $browser->with('@step3', function ($step) { - $step->type('#signup_login', 'admin') - ->type('#signup_domain', 'aaa') - ->type('#signup_password', '12345678') - ->type('#signup_confirm', '12345678') - ->click('[type=submit]') - ->waitUntilMissing('#signup_login.is-invalid') - ->waitFor('#signup_domain.is-invalid + .invalid-feedback') - ->assertMissing('#signup_password.is-invalid') - ->assertMissing('#signup_password + .invalid-feedback') - ->assertFocused('#signup_domain') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - }); - - // Submit invalid domain - $browser->with('@step3', function ($step) { - $step->type('#signup_domain', 'user-domain-signup.com') - ->click('[type=submit]'); - }); - - // At this point we should be auto-logged-in to dashboard - $browser->waitUntilMissing('@step3') - ->waitUntilMissing('.app-loader') - ->on(new Dashboard()) - ->assertUser('admin@user-domain-signup.com') - ->assertVisible('@links a.link-profile') - ->assertVisible('@links a.link-domains') - ->assertVisible('@links a.link-users') - ->assertVisible('@links a.link-wallet'); - - $browser->within(new Menu(), function ($browser) { - $browser->clickMenuItem('logout'); - }); - }); - } - - /** - * Test signup with voucher - */ - public function testSignupVoucherLink(): void - { - $this->browse(function (Browser $browser) { - $browser->visit('/signup/voucher/TEST') - ->onWithoutAssert(new Signup()) - ->waitUntilMissing('.app-loader') - ->waitFor('@step0') - ->click('.plan-individual button') - ->whenAvailable('@step1', function (Browser $browser) { - $browser->type('#signup_first_name', 'Test') - ->type('#signup_last_name', 'User') - ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org') - ->click('[type=submit]'); - }) - ->whenAvailable('@step2', function (Browser $browser) { - // Get the code and short_code from database - // FIXME: Find a nice way to read javascript data without using hidden inputs - $code = $browser->value('#signup_code'); - - $this->assertNotEmpty($code); - - $code = SignupCode::find($code); - - $browser->type('#signup_short_code', $code->short_code) - ->click('[type=submit]'); - }) - ->whenAvailable('@step3', function (Browser $browser) { - // Assert that the code is filled in the input - // Change it and test error handling - $browser->assertValue('#signup_voucher', 'TEST') - ->type('#signup_voucher', 'TESTXX') - ->type('#signup_login', 'signuptestdusk') - ->type('#signup_password', '123456789') - ->type('#signup_confirm', '123456789') - ->click('[type=submit]') - ->waitFor('#signup_voucher.is-invalid') - ->assertVisible('#signup_voucher + .invalid-feedback') - ->assertFocused('#signup_voucher') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - // Submit the correct code - ->type('#signup_voucher', 'TEST') - ->click('[type=submit]'); - }) - ->waitUntilMissing('@step3') - ->waitUntilMissing('.app-loader') - ->on(new Dashboard()) - ->assertUser('signuptestdusk@' . \config('app.domain')) - // Logout the user - ->within(new Menu(), function ($browser) { - $browser->clickMenuItem('logout'); - }); - }); - - $user = $this->getTestUser('signuptestdusk@' . \config('app.domain')); - $discount = Discount::where('code', 'TEST')->first(); - $this->assertSame($discount->id, $user->wallets()->first()->discount_id); - } -} diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php deleted file mode 100644 index 781e2e76..00000000 --- a/src/tests/Browser/StatusTest.php +++ /dev/null @@ -1,289 +0,0 @@ -first(); - - if ($domain->isConfirmed()) { - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); - } - - $john = $this->getTestUser('john@kolab.org'); - - $john->created_at = Carbon::now(); - - if ($john->isImapReady()) { - $john->status ^= User::STATUS_IMAP_READY; - } - - $john->save(); - - $this->browse(function ($browser) use ($john, $domain) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->with(new Status(), function ($browser) use ($john) { - $browser->assertSeeIn('@body', 'We are preparing your account') - ->assertProgress(71, 'Creating a mailbox...', 'pending') - ->assertMissing('#status-verify') - ->assertMissing('#status-link') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text'); - - $john->status |= User::STATUS_IMAP_READY; - $john->save(); - - // Wait for auto-refresh, expect domain-confirmed step - $browser->pause(6000) - ->assertSeeIn('@body', 'Your account is almost ready') - ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text') - ->assertMissing('#status-verify') - ->assertVisible('#status-link'); - }) - // check if the link to domain info page works - ->click('#status-link') - ->on(new DomainInfo()) - ->back() - ->on(new Dashboard()) - ->with(new Status(), function ($browser) { - $browser->assertMissing('@refresh-button') - ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed'); - }); - - // Confirm the domain and wait until the whole status box disappears - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - // This should take less than 10 seconds - $browser->waitUntilMissing('@status', 10); - }); - - // Test the Refresh button - if ($domain->isConfirmed()) { - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); - } - - $john->created_at = Carbon::now()->subSeconds(3600); - - if ($john->isImapReady()) { - $john->status ^= User::STATUS_IMAP_READY; - } - - $john->save(); - - $this->browse(function ($browser) use ($john, $domain) { - $browser->visit(new Dashboard()) - ->with(new Status(), function ($browser) use ($john, $domain) { - $browser->assertSeeIn('@body', 'We are preparing your account') - ->assertProgress(71, 'Creating a mailbox...', 'failed') - ->assertVisible('@refresh-button') - ->assertVisible('@refresh-text'); - - if ($john->refresh()->isImapReady()) { - $john->status ^= User::STATUS_IMAP_READY; - $john->save(); - } - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - $browser->click('@refresh-button') - ->assertToast(Toast::TYPE_SUCCESS, 'Setup process finished successfully.'); - }) - ->assertMissing('@status'); - }); - } - - /** - * Test domain status on domains list and domain info page - * - * @depends testDashboard - */ - public function testDomainStatus(): void - { - $domain = Domain::where('namespace', 'kolab.org')->first(); - $domain->created_at = Carbon::now(); - $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; - $domain->save(); - - // side-step - $this->assertFalse($domain->isNew()); - $this->assertTrue($domain->isActive()); - $this->assertTrue($domain->isLdapReady()); - $this->assertTrue($domain->isExternal()); - - $this->assertFalse($domain->isHosted()); - $this->assertFalse($domain->isConfirmed()); - $this->assertFalse($domain->isVerified()); - $this->assertFalse($domain->isSuspended()); - $this->assertFalse($domain->isDeleted()); - - $this->browse(function ($browser) use ($domain) { - // Test auto-refresh - $browser->on(new Dashboard()) - ->click('@links a.link-domains') - ->on(new DomainList()) - ->waitFor('@table tbody tr') - // Assert domain status icon - ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-danger') - ->assertText('@table tbody tr:first-child td:first-child svg title', 'Not Ready') - ->click('@table tbody tr:first-child td:first-child a') - ->on(new DomainInfo()) - ->with(new Status(), function ($browser) { - $browser->assertSeeIn('@body', 'We are preparing the domain') - ->assertProgress(50, 'Verifying a custom domain...', 'pending') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text') - ->assertMissing('#status-link') - ->assertMissing('#status-verify'); - }); - - $domain->status |= Domain::STATUS_VERIFIED; - $domain->save(); - - // This should take less than 10 seconds - $browser->waitFor('@status.process-failed') - ->with(new Status(), function ($browser) { - $browser->assertSeeIn('@body', 'The domain is almost ready') - ->assertProgress(75, 'Verifying an ownership of a custom domain...', 'failed') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text') - ->assertMissing('#status-link') - ->assertVisible('#status-verify'); - }); - - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - // Test Verify button - $browser->click('@status #status-verify') - ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.') - ->waitUntilMissing('@status') - ->assertMissing('@verify') - ->assertVisible('@config'); - }); - } - - /** - * Test user status on users list and user info page - * - * @depends testDashboard - */ - public function testUserStatus(): void - { - $john = $this->getTestUser('john@kolab.org'); - $john->created_at = Carbon::now(); - if ($john->isImapReady()) { - $john->status ^= User::STATUS_IMAP_READY; - } - $john->save(); - - $domain = Domain::where('namespace', 'kolab.org')->first(); - if ($domain->isConfirmed()) { - $domain->status ^= Domain::STATUS_CONFIRMED; - $domain->save(); - } - - $this->browse(function ($browser) use ($john, $domain) { - $browser->visit(new Dashboard()) - ->click('@links a.link-users') - ->on(new UserList()) - ->waitFor('@table tbody tr') - // Assert user status icons - ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-user.text-success') - ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active') - ->assertVisible('@table tbody tr:nth-child(3) td:first-child svg.fa-user.text-danger') - ->assertText('@table tbody tr:nth-child(3) td:first-child svg title', 'Not Ready') - ->click('@table tbody tr:nth-child(3) td:first-child a') - ->on(new UserInfo()) - ->with('@form', function (Browser $browser) { - // Assert state in the user edit form - $browser->assertSeeIn('div.row:nth-child(1) label', 'Status') - ->assertSeeIn('div.row:nth-child(1) #status', 'Not Ready'); - }) - ->with(new Status(), function ($browser) use ($john) { - $browser->assertSeeIn('@body', 'We are preparing the user account') - ->assertProgress(71, 'Creating a mailbox...', 'pending') - ->assertMissing('#status-verify') - ->assertMissing('#status-link') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text'); - - - $john->status |= User::STATUS_IMAP_READY; - $john->save(); - - // Wait for auto-refresh, expect domain-confirmed step - $browser->pause(6000) - ->assertSeeIn('@body', 'The user account is almost ready') - ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed') - ->assertMissing('@refresh-button') - ->assertMissing('@refresh-text') - ->assertMissing('#status-verify') - ->assertVisible('#status-link'); - }) - ->assertSeeIn('#status', 'Active'); - - // Confirm the domain and wait until the whole status box disappears - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - // This should take less than 10 seconds - $browser->waitUntilMissing('@status', 10); - }); - } -} diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php deleted file mode 100644 index bbd5d2a2..00000000 --- a/src/tests/Browser/UserProfileTest.php +++ /dev/null @@ -1,194 +0,0 @@ - 'John', - 'last_name' => 'Doe', - 'currency' => 'USD', - 'country' => 'US', - 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", - 'external_email' => 'john.doe.external@gmail.com', - 'phone' => '+1 509-248-1111', - 'organization' => 'Kolab Developers', - ]; - - /** - * {@inheritDoc} - */ - public function setUp(): void - { - parent::setUp(); - - User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); - $this->deleteTestUser('profile-delete@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); - $this->deleteTestUser('profile-delete@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test profile page (unauthenticated) - */ - public function testProfileUnauth(): void - { - // Test that the page requires authentication - $this->browse(function (Browser $browser) { - $browser->visit('/profile')->on(new Home()); - }); - } - - /** - * Test profile page - */ - public function testProfile(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->assertSeeIn('#user-profile .button-delete', 'Delete account') - ->whenAvailable('@form', function (Browser $browser) { - $user = User::where('email', 'john@kolab.org')->first(); - // Assert form content - $browser->assertFocused('div.row:nth-child(2) input') - ->assertSeeIn('div.row:nth-child(1) label', 'Customer No.') - ->assertSeeIn('div.row:nth-child(1) .form-control-plaintext', $user->id) - ->assertSeeIn('div.row:nth-child(2) label', 'First name') - ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name']) - ->assertSeeIn('div.row:nth-child(3) label', 'Last name') - ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name']) - ->assertSeeIn('div.row:nth-child(4) label', 'Organization') - ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization']) - ->assertSeeIn('div.row:nth-child(5) label', 'Phone') - ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['phone']) - ->assertSeeIn('div.row:nth-child(6) label', 'External email') - ->assertValue('div.row:nth-child(6) input[type=text]', $this->profile['external_email']) - ->assertSeeIn('div.row:nth-child(7) label', 'Address') - ->assertValue('div.row:nth-child(7) textarea', $this->profile['billing_address']) - ->assertSeeIn('div.row:nth-child(8) label', 'Country') - ->assertValue('div.row:nth-child(8) select', $this->profile['country']) - ->assertSeeIn('div.row:nth-child(9) label', 'Password') - ->assertValue('div.row:nth-child(9) input[type=password]', '') - ->assertSeeIn('div.row:nth-child(10) label', 'Confirm password') - ->assertValue('div.row:nth-child(10) input[type=password]', '') - ->assertSeeIn('button[type=submit]', 'Submit'); - - // Test form error handling - $browser->type('#phone', 'aaaaaa') - ->type('#external_email', 'bbbbb') - ->click('button[type=submit]') - ->waitFor('#phone + .invalid-feedback') - ->assertSeeIn('#phone + .invalid-feedback', 'The phone format is invalid.') - ->assertSeeIn( - '#external_email + .invalid-feedback', - 'The external email must be a valid email address.' - ) - ->assertFocused('#phone') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->clearToasts(); - - // Clear all fields and submit - // FIXME: Should any of these fields be required? - $browser->vueClear('#first_name') - ->vueClear('#last_name') - ->vueClear('#organization') - ->vueClear('#phone') - ->vueClear('#external_email') - ->vueClear('#billing_address') - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - }) - // On success we're redirected to Dashboard - ->on(new Dashboard()); - }); - } - - /** - * Test profile of non-controller user - */ - public function testProfileNonController(): void - { - // Test acting as non-controller - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->visit(new Home()) - ->submitLogon('jack@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->assertMissing('#user-profile .button-delete') - ->whenAvailable('@form', function (Browser $browser) { - // TODO: decide on what fields the non-controller user should be able - // to see/change - }); - - // Test that /profile/delete page is not accessible - $browser->visit('/profile/delete') - ->assertErrorPage(403); - }); - } - - /** - * Test profile delete page - */ - public function testProfileDelete(): void - { - $user = $this->getTestUser('profile-delete@kolabnow.com', ['password' => 'simple123']); - - $this->browse(function (Browser $browser) use ($user) { - $browser->visit('/logout') - ->on(new Home()) - ->submitLogon('profile-delete@kolabnow.com', 'simple123', true) - ->on(new Dashboard()) - ->clearToasts() - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->click('#user-profile .button-delete') - ->waitForLocation('/profile/delete') - ->assertSeeIn('#user-delete .card-title', 'Delete this account?') - ->assertSeeIn('#user-delete .button-cancel', 'Cancel') - ->assertSeeIn('#user-delete .card-text', 'This operation is irreversible') - ->assertFocused('#user-delete .button-cancel') - ->click('#user-delete .button-cancel') - ->waitForLocation('/profile') - ->on(new UserProfile()); - - // Test deleting the user - $browser->click('#user-profile .button-delete') - ->waitForLocation('/profile/delete') - ->click('#user-delete .button-delete') - ->waitForLocation('/login') - ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.'); - - $this->assertTrue($user->fresh()->trashed()); - }); - } - - // TODO: Test that Ned (John's "delegatee") can delete himself - // TODO: Test that Ned (John's "delegatee") can/can't delete John ? -} diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php deleted file mode 100644 index 68ae4107..00000000 --- a/src/tests/Browser/UsersTest.php +++ /dev/null @@ -1,681 +0,0 @@ - 'John', - 'last_name' => 'Doe', - 'organization' => 'Test Domain Owner', - ]; - - private $users = []; - - /** - * {@inheritDoc} - */ - public function setUp(): void - { - parent::setUp(); - - $this->password = \App\Utils::generatePassphrase(); - - $this->domain = $this->getTestDomain( - 'test.domain', - [ - 'type' => \App\Domain::TYPE_EXTERNAL, - 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED - ] - ); - - $packageKolab = \App\Package::where('title', 'kolab')->first(); - - $this->owner = $this->getTestUser('john@test.domain', ['password' => $this->password]); - $this->owner->assignPackage($packageKolab); - $this->owner->setSettings($this->profile); - - $this->users[] = $this->getTestUser('jack@test.domain', ['password' => $this->password]); - $this->users[] = $this->getTestUser('jane@test.domain', ['password' => $this->password]); - $this->users[] = $this->getTestUser('jill@test.domain', ['password' => $this->password]); - $this->users[] = $this->getTestUser('joe@test.domain', ['password' => $this->password]); - - foreach ($this->users as $user) { - $this->owner->assignPackage($packageKolab, $user); - } - - $this->users[] = $this->owner; - - usort( - $this->users, - function ($a, $b) { - return $a->email > $b->email; - } - ); - - $this->domain->assignPackage(\App\Package::where('title', 'domain-hosting')->first(), $this->owner); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - foreach ($this->users as $user) { - if ($user == $this->owner) { - continue; - } - - $this->deleteTestUser($user->email); - } - - $this->deleteTestUser('john@test.domain'); - $this->deleteTestDomain('test.domain'); - - parent::tearDown(); - } - - /** - * Verify that a user page requires authentication. - */ - public function testUserPageRequiresAuth(): void - { - // Test that the page requires authentication - $this->browse( - function (Browser $browser) { - $browser->visit('/user/' . $this->owner->id)->on(new Home()); - } - ); - } - - /** - * VErify that the page with a list of users requires authentication - */ - public function testUserListPageRequiresAuthentication(): void - { - // Test that the page requires authentication - $this->browse( - function (Browser $browser) { - $browser->visit('/users')->on(new Home()); - } - ); - } - - /** - * Test users list page - */ - public function testUsersListPageAsOwner(): void - { - // Test that the page requires authentication - $this->browse( - function (Browser $browser) { - $browser->visit(new Home()); - $browser->submitLogon($this->owner->email, $this->password, true); - $browser->on(new Dashboard()); - $browser->assertSeeIn('@links .link-users', 'User accounts'); - $browser->click('@links .link-users'); - $browser->on(new UserList()); - $browser->whenAvailable( - '@table', - function (Browser $browser) { - $browser->waitFor('tbody tr'); - $browser->assertElementsCount('tbody tr', sizeof($this->users)); - - foreach ($this->users as $user) { - $arrayPosition = array_search($user, $this->users); - $listPosition = $arrayPosition + 1; - - $browser->assertSeeIn("tbody tr:nth-child({$listPosition}) a", $user->email); - $browser->assertVisible("tbody tr:nth-child({$listPosition}) button.button-delete"); - } - - $browser->assertMissing('tfoot'); - } - ); - } - ); - } - - /** - * Test user account editing page (not profile page) - * - * @depends testUsersListPageAsOwner - */ - public function testUserInfoPageAsOwner(): void - { - $this->browse( - function (Browser $browser) { - $browser->on(new UserList()); - $browser->click('@table tr:nth-child(' . (array_search($this->owner, $this->users) + 1) . ') a'); - $browser->on(new UserInfo()); - $browser->assertSeeIn('#user-info .card-title', 'User account'); - $browser->with( - '@form', - function (Browser $browser) { - // Assert form content - $browser->assertSeeIn('div.row:nth-child(1) label', 'Status'); - $browser->assertSeeIn('div.row:nth-child(1) #status', 'Active'); - $browser->assertFocused('div.row:nth-child(2) input'); - $browser->assertSeeIn('div.row:nth-child(2) label', 'First name'); - $browser->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name']); - $browser->assertSeeIn('div.row:nth-child(3) label', 'Last name'); - $browser->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name']); - $browser->assertSeeIn('div.row:nth-child(4) label', 'Organization'); - $browser->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization']); - $browser->assertSeeIn('div.row:nth-child(5) label', 'Email'); - $browser->assertValue('div.row:nth-child(5) input[type=text]', 'john@kolab.org'); - $browser->assertDisabled('div.row:nth-child(5) input[type=text]'); - $browser->assertSeeIn('div.row:nth-child(6) label', 'Email aliases'); - $browser->assertVisible('div.row:nth-child(6) .list-input'); - - $browser->with( - new ListInput('#aliases'), - function (Browser $browser) { - $browser->assertListInputValue(['john.doe@' . $this->domain->namespace]) - ->assertValue('@input', ''); - } - ); - - $browser->assertSeeIn('div.row:nth-child(7) label', 'Password'); - $browser->assertValue('div.row:nth-child(7) input[type=password]', ''); - $browser->assertSeeIn('div.row:nth-child(8) label', 'Confirm password'); - $browser->assertValue('div.row:nth-child(8) input[type=password]', ''); - $browser->assertSeeIn('button[type=submit]', 'Submit'); - - // Clear some fields and submit - $browser->vueClear('#first_name'); - $browser->vueClear('#last_name'); - $browser->click('button[type=submit]'); - } - ); - $browser->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - $browser->on(new UserList()); - $browser->click('@table tr:nth-child(3) a'); - $browser->on(new UserInfo()); - $browser->assertSeeIn('#user-info .card-title', 'User account'); - $browser->with( - '@form', - function (Browser $browser) { - // Test error handling (password) - $browser->type('#password', 'aaaaaa'); - $browser->vueClear('#password_confirmation'); - $browser->click('button[type=submit]'); - $browser->waitFor('#password + .invalid-feedback'); - $browser->assertSeeIn( - '#password + .invalid-feedback', - 'The password confirmation does not match.' - ); - - $browser->assertFocused('#password'); - $browser->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - - // TODO: Test password change - - // Test form error handling (aliases) - $browser->vueClear('#password'); - $browser->vueClear('#password_confirmation'); - - $browser->with( - new ListInput('#aliases'), - function (Browser $browser) { - $browser->addListEntry('invalid address'); - } - ); - - $browser->click('button[type=submit]'); - $browser->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - - $browser->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertFormError(2, 'The specified alias is invalid.', false); - }); - - // Test adding aliases - $browser->with( - new ListInput('#aliases'), - function (Browser $browser) { - $browser->removeListEntry(2); - $browser->addListEntry('john.test@' . $this->domain->namespace); - } - ); - - $browser->click('button[type=submit]'); - $browser->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - } - ); - - $browser->on(new UserList()); - $browser->click('@table tr:nth-child(' . (array_search($this->owner, $this->users) + 1) . ') a'); - $browser->on(new UserInfo()); - - $alias = $this->owner->aliases(); - $this->assertTrue(!empty($alias)); - - // Test subscriptions - $browser->with( - '@form', - function (Browser $browser) { - $browser->assertSeeIn('div.row:nth-child(9) label', 'Subscriptions'); - $browser->assertVisible('@skus.row:nth-child(9)'); - $browser->with( - '@skus', - function ($browser) { - $browser->assertElementsCount('tbody tr', 5); - // Mailbox SKU - $browser->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox'); - $browser->assertSeeIn('tbody tr:nth-child(1) td.price', '4,44 CHF/month'); - $browser->assertChecked('tbody tr:nth-child(1) td.selection input'); - $browser->assertDisabled('tbody tr:nth-child(1) td.selection input'); - $browser->assertTip( - 'tbody tr:nth-child(1) td.buttons button', - 'Just a mailbox' - ); - - // Storage SKU - $browser->assertSeeIn('tbody tr:nth-child(2) td.name', 'Storage Quota'); - $browser->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month'); - $browser->assertChecked('tbody tr:nth-child(2) td.selection input'); - $browser->assertDisabled('tbody tr:nth-child(2) td.selection input'); - $browser->assertTip( - 'tbody tr:nth-child(2) td.buttons button', - 'Some wiggle room' - ); - - $browser->with( - new QuotaInput('tbody tr:nth-child(2) .range-input'), - function ($browser) { - $browser->assertQuotaValue(2)->setQuotaValue(3); - } - ); - - $browser->assertSeeIn('tr:nth-child(2) td.price', '0,25 CHF/month'); - - // groupware SKU - $browser->assertSeeIn('tbody tr:nth-child(3) td.name', 'Groupware Features'); - $browser->assertSeeIn('tbody tr:nth-child(3) td.price', '5,55 CHF/month'); - $browser->assertChecked('tbody tr:nth-child(3) td.selection input'); - $browser->assertEnabled('tbody tr:nth-child(3) td.selection input'); - $browser->assertTip( - 'tbody tr:nth-child(3) td.buttons button', - 'Groupware functions like Calendar, Tasks, Notes, etc.' - ); - - // ActiveSync SKU - $browser->assertSeeIn('tbody tr:nth-child(4) td.name', 'Activesync'); - $browser->assertSeeIn('tbody tr:nth-child(4) td.price', '1,00 CHF/month'); - $browser->assertNotChecked('tbody tr:nth-child(4) td.selection input'); - $browser->assertEnabled('tbody tr:nth-child(4) td.selection input'); - $browser->assertTip( - 'tbody tr:nth-child(4) td.buttons button', - 'Mobile synchronization' - ); - - // 2FA SKU - $browser->assertSeeIn('tbody tr:nth-child(5) td.name', '2-Factor Authentication'); - $browser->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month'); - $browser->assertNotChecked('tbody tr:nth-child(5) td.selection input'); - $browser->assertEnabled('tbody tr:nth-child(5) td.selection input'); - $browser->assertTip( - 'tbody tr:nth-child(5) td.buttons button', - 'Two factor authentication for webmail and administration panel' - ); - - $browser->click('tbody tr:nth-child(4) td.selection input'); - } - ); - - $browser->assertMissing('@skus table + .hint'); - $browser->click('button[type=submit]'); - $browser->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - } - ); - - $browser->on(new UserList()); - $browser->click('@table tr:nth-child(' (array_search($this->owner, $this->users) + 1) . ') a'); - $browser->on(new UserInfo()); - - $expected = ['activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage']; - $this->assertUserEntitlements($john, $expected); - - // Test subscriptions interaction - $browser->with( - '@form', - function (Browser $browser) { - $browser->with( - '@skus', - function ($browser) { - // Uncheck 'groupware', expect activesync unchecked - $browser->click('#sku-input-groupware'); - $browser->assertNotChecked('#sku-input-groupware'); - $browser->assertNotChecked('#sku-input-activesync'); - $browser->assertEnabled('#sku-input-activesync'); - $browser->assertNotReadonly('#sku-input-activesync'); - - // Check 'activesync', expect an alert - $browser->click('#sku-input-activesync'); - $browser->assertDialogOpened('Activesync requires Groupware Features.'); - $browser->acceptDialog(); - $browser->assertNotChecked('#sku-input-activesync'); - - // Check '2FA', expect 'activesync' unchecked and readonly - $browser->click('#sku-input-2fa'); - $browser->assertChecked('#sku-input-2fa'); - $browser->assertNotChecked('#sku-input-activesync'); - $browser->assertReadonly('#sku-input-activesync'); - - // Uncheck '2FA' - $browser->click('#sku-input-2fa'); - $browser->assertNotChecked('#sku-input-2fa'); - $browser->assertNotReadonly('#sku-input-activesync'); - } - ); - } - ); - } - ); - } - - /** - * Test user adding page - * - * @depends testUsersListPageAsOwner - */ - public function testNewUser(): void - { - $this->browse(function (Browser $browser) { - $browser->visit(new UserList()) - ->assertSeeIn('button.create-user', 'Create user') - ->click('button.create-user') - ->on(new UserInfo()) - ->assertSeeIn('#user-info .card-title', 'New user account') - ->with('@form', function (Browser $browser) { - // Assert form content - $browser->assertFocused('div.row:nth-child(1) input') - ->assertSeeIn('div.row:nth-child(1) label', 'First name') - ->assertValue('div.row:nth-child(1) input[type=text]', '') - ->assertSeeIn('div.row:nth-child(2) label', 'Last name') - ->assertValue('div.row:nth-child(2) input[type=text]', '') - ->assertSeeIn('div.row:nth-child(3) label', 'Organization') - ->assertValue('div.row:nth-child(3) input[type=text]', '') - ->assertSeeIn('div.row:nth-child(4) label', 'Email') - ->assertValue('div.row:nth-child(4) input[type=text]', '') - ->assertEnabled('div.row:nth-child(4) input[type=text]') - ->assertSeeIn('div.row:nth-child(5) label', 'Email aliases') - ->assertVisible('div.row:nth-child(5) .list-input') - ->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertListInputValue([]) - ->assertValue('@input', ''); - }) - ->assertSeeIn('div.row:nth-child(6) label', 'Password') - ->assertValue('div.row:nth-child(6) input[type=password]', '') - ->assertSeeIn('div.row:nth-child(7) label', 'Confirm password') - ->assertValue('div.row:nth-child(7) input[type=password]', '') - ->assertSeeIn('div.row:nth-child(8) label', 'Package') - // assert packages list widget, select "Lite Account" - ->with('@packages', function ($browser) { - $browser->assertElementsCount('tbody tr', 2) - ->assertSeeIn('tbody tr:nth-child(1)', 'Groupware Account') - ->assertSeeIn('tbody tr:nth-child(2)', 'Lite Account') - ->assertSeeIn('tbody tr:nth-child(1) .price', '9,99 CHF/month') - ->assertSeeIn('tbody tr:nth-child(2) .price', '4,44 CHF/month') - ->assertChecked('tbody tr:nth-child(1) input') - ->click('tbody tr:nth-child(2) input') - ->assertNotChecked('tbody tr:nth-child(1) input') - ->assertChecked('tbody tr:nth-child(2) input'); - }) - ->assertMissing('@packages table + .hint') - ->assertSeeIn('button[type=submit]', 'Submit'); - - // Test browser-side required fields and error handling - $browser->click('button[type=submit]') - ->assertFocused('#email') - ->type('#email', 'invalid email') - ->click('button[type=submit]') - ->assertFocused('#password') - ->type('#password', 'simple123') - ->click('button[type=submit]') - ->assertFocused('#password_confirmation') - ->type('#password_confirmation', 'simple') - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertSeeIn('#email + .invalid-feedback', 'The specified email is invalid.') - ->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.'); - }); - - // Test form error handling (aliases) - $browser->with('@form', function (Browser $browser) { - $browser->type('#email', 'julia.roberts@kolab.org') - ->type('#password_confirmation', 'simple123') - ->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->addListEntry('invalid address'); - }) - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertFormError(1, 'The specified alias is invalid.', false); - }); - }); - - // Successful account creation - $browser->with( - '@form', - function (Browser $browser) { - $browser->type('#first_name', 'Julia'); - $browser->type('#last_name', 'Roberts'); - $browser->type('#organization', 'Test Org'); - $browser->with( - new ListInput('#aliases'), - function (Browser $browser) { - $browser->removeListEntry(1)->addListEntry('julia.roberts2@kolab.org'); - } - ); - - $browser->click('button[type=submit]'); - } - ); - - $browser->assertToast(Toast::TYPE_SUCCESS, 'User created successfully.'); - - // check redirection to users list - $browser->on(new UserList()) - ->whenAvailable('@table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 5) - ->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org'); - }); - - $julia = User::where('email', 'julia.roberts@kolab.org')->first(); - $alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first(); - $this->assertTrue(!empty($alias)); - $this->assertUserEntitlements($julia, ['mailbox', 'storage', 'storage']); - $this->assertSame('Julia', $julia->getSetting('first_name')); - $this->assertSame('Roberts', $julia->getSetting('last_name')); - $this->assertSame('Test Org', $julia->getSetting('organization')); - - // Some additional tests for the list input widget - $browser->click('tbody tr:nth-child(4) a') - ->on(new UserInfo()) - ->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertListInputValue(['julia.roberts2@kolab.org']) - ->addListEntry('invalid address') - ->type('.input-group:nth-child(2) input', '@kolab.org'); - }) - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertVisible('.input-group:nth-child(2) input.is-invalid') - ->assertVisible('.input-group:nth-child(3) input.is-invalid') - ->type('.input-group:nth-child(2) input', 'julia.roberts3@kolab.org') - ->type('.input-group:nth-child(3) input', 'julia.roberts4@kolab.org'); - }) - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - - $julia = User::where('email', 'julia.roberts@kolab.org')->first(); - $aliases = $julia->aliases()->orderBy('alias')->get()->pluck('alias')->all(); - $this->assertSame(['julia.roberts3@kolab.org', 'julia.roberts4@kolab.org'], $aliases); - }); - } - - /** - * Test user delete - * - * @depends testNewUser - */ - public function testDeleteUser(): void - { - // First create a new user - $john = $this->getTestUser('john@kolab.org'); - $julia = $this->getTestUser('julia.roberts@kolab.org'); - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $john->assignPackage($package_kolab, $julia); - - // Test deleting non-controller user - $this->browse(function (Browser $browser) { - $browser->visit(new UserList()) - ->whenAvailable('@table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 5) - ->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org') - ->click('tbody tr:nth-child(4) button.button-delete'); - }) - ->with(new Dialog('#delete-warning'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Delete julia.roberts@kolab.org') - ->assertFocused('@button-cancel') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Delete') - ->click('@button-cancel'); - }) - ->whenAvailable('@table', function (Browser $browser) { - $browser->click('tbody tr:nth-child(4) button.button-delete'); - }) - ->with(new Dialog('#delete-warning'), function (Browser $browser) { - $browser->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.') - ->with('@table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 4) - ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org') - ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org') - ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org') - ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org'); - }); - - $julia = User::where('email', 'julia.roberts@kolab.org')->first(); - $this->assertTrue(empty($julia)); - - // Test clicking Delete on the controller record redirects to /profile/delete - $browser - ->with('@table', function (Browser $browser) { - $browser->click('tbody tr:nth-child(3) button.button-delete'); - }) - ->waitForLocation('/profile/delete'); - }); - - // Test that non-controller user cannot see/delete himself on the users list - // Note: Access to /profile/delete page is tested in UserProfileTest.php - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->on(new Home()) - ->submitLogon('jack@kolab.org', 'simple123', true) - ->visit(new UserList()) - ->whenAvailable('@table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 0) - ->assertSeeIn('tfoot td', 'There are no users in this account.'); - }); - }); - - // Test that controller user (Ned) can see/delete all the users ??? - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->on(new Home()) - ->submitLogon('ned@kolab.org', 'simple123', true) - ->visit(new UserList()) - ->whenAvailable('@table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 4) - ->assertElementsCount('tbody button.button-delete', 4); - }); - - // TODO: Test the delete action in details - }); - - // TODO: Test what happens with the logged in user session after he's been deleted by another user - } - - /** - * Test discounted sku/package prices in the UI - */ - public function testDiscountedPrices(): void - { - // Add 10% discount - $discount = Discount::where('code', 'TEST')->first(); - $john = User::where('email', 'john@kolab.org')->first(); - $wallet = $john->wallet(); - $wallet->discount()->associate($discount); - $wallet->save(); - - // SKUs on user edit page - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->on(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - ->visit(new UserList()) - ->waitFor('@table tr:nth-child(2)') - ->click('@table tr:nth-child(2) a') - ->on(new UserInfo()) - ->with('@form', function (Browser $browser) { - $browser->whenAvailable('@skus', function (Browser $browser) { - $quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input'); - $browser->waitFor('tbody tr') - ->assertElementsCount('tbody tr', 5) - // Mailbox SKU - ->assertSeeIn('tbody tr:nth-child(1) td.price', '3,99 CHF/month¹') - // Storage SKU - ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹') - ->with($quota_input, function (Browser $browser) { - $browser->setQuotaValue(100); - }) - ->assertSeeIn('tr:nth-child(2) td.price', '21,56 CHF/month¹') - // groupware SKU - ->assertSeeIn('tbody tr:nth-child(3) td.price', '4,99 CHF/month¹') - // ActiveSync SKU - ->assertSeeIn('tbody tr:nth-child(4) td.price', '0,90 CHF/month¹') - // 2FA SKU - ->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month¹'); - }) - ->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher'); - }); - }); - - // Packages on new user page - $this->browse(function (Browser $browser) { - $browser->visit(new UserList()) - ->click('button.create-user') - ->on(new UserInfo()) - ->with('@form', function (Browser $browser) { - $browser->whenAvailable('@packages', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 2) - ->assertSeeIn('tbody tr:nth-child(1) .price', '8,99 CHF/month¹') // Groupware - ->assertSeeIn('tbody tr:nth-child(2) .price', '3,99 CHF/month¹'); // Lite - }) - ->assertSeeIn('@packages table + .hint', '¹ applied discount: 10% - Test voucher'); - }); - }); - } -} diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php deleted file mode 100644 index febfbe0a..00000000 --- a/src/tests/Browser/WalletTest.php +++ /dev/null @@ -1,256 +0,0 @@ -deleteTestUser('wallets-controller@kolabnow.com'); - - $john = $this->getTestUser('john@kolab.org'); - Wallet::where('user_id', $john->id)->update(['balance' => -1234]); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('wallets-controller@kolabnow.com'); - - $john = $this->getTestUser('john@kolab.org'); - Wallet::where('user_id', $john->id)->update(['balance' => 0]); - - - parent::tearDown(); - } - - /** - * Test wallet page (unauthenticated) - */ - public function testWalletUnauth(): void - { - // Test that the page requires authentication - $this->browse(function (Browser $browser) { - $browser->visit('/wallet')->on(new Home()); - }); - } - - /** - * Test wallet "box" on Dashboard - */ - public function testDashboard(): void - { - // Test that the page requires authentication - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-wallet .name', 'Wallet') - ->assertSeeIn('@links .link-wallet .badge', '-12,34 CHF'); - }); - } - - /** - * Test wallet page - * - * @depends testDashboard - */ - public function testWallet(): void - { - $this->browse(function (Browser $browser) { - $browser->click('@links .link-wallet') - ->on(new WalletPage()) - ->assertSeeIn('#wallet .card-title', 'Account balance -12,34 CHF') - ->assertSeeIn('#wallet .card-title .text-danger', '-12,34 CHF') - ->assertSeeIn('#wallet .card-text', 'You are out of credit'); - }); - } - - /** - * Test Receipts tab - */ - public function testReceipts(): void - { - $user = $this->getTestUser('wallets-controller@kolabnow.com', ['password' => 'simple123']); - $wallet = $user->wallets()->first(); - $wallet->payments()->delete(); - - // Log out John and log in the test user - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->waitForLocation('/login') - ->on(new Home()) - ->submitLogon('wallets-controller@kolabnow.com', 'simple123', true); - }); - - // Assert Receipts tab content when there's no receipts available - $this->browse(function (Browser $browser) { - $browser->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->assertSeeIn('#wallet .card-title', 'Account balance 0,00 CHF') - ->assertSeeIn('#wallet .card-title .text-success', '0,00 CHF') - ->assertSeeIn('#wallet .card-text', 'You are in your free trial period.') - ->assertSeeIn('@nav #tab-receipts', 'Receipts') - ->with('@receipts-tab', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('p', 'There are no receipts for payments') - ->assertDontSeeIn('p', 'Here you can download') - ->assertMissing('select') - ->assertMissing('button'); - }); - }); - - // Create some sample payments - $receipts = []; - $date = Carbon::create(intval(date('Y')) - 1, 3, 30); - $payment = Payment::create([ - 'id' => 'AAA1', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Paid in March', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1111, - ]); - $payment->updated_at = $date; - $payment->save(); - $receipts[] = $date->format('Y-m'); - - $date = Carbon::create(intval(date('Y')) - 1, 4, 30); - $payment = Payment::create([ - 'id' => 'AAA2', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Paid in April', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1111, - ]); - $payment->updated_at = $date; - $payment->save(); - $receipts[] = $date->format('Y-m'); - - // Assert Receipts tab with receipts available - $this->browse(function (Browser $browser) use ($receipts) { - $browser->refresh() - ->on(new WalletPage()) - ->assertSeeIn('@nav #tab-receipts', 'Receipts') - ->with('@receipts-tab', function (Browser $browser) use ($receipts) { - $browser->waitUntilMissing('.app-loader') - ->assertDontSeeIn('p', 'There are no receipts for payments') - ->assertSeeIn('p', 'Here you can download') - ->assertSeeIn('button', 'Download') - ->assertElementsCount('select > option', 2) - ->assertSeeIn('select > option:nth-child(1)', $receipts[1]) - ->assertSeeIn('select > option:nth-child(2)', $receipts[0]); - - // Download a receipt file - $browser->select('select', $receipts[0]) - ->click('button') - ->pause(2000); - - $files = glob(__DIR__ . '/downloads/*.pdf'); - - $filename = pathinfo($files[0], PATHINFO_BASENAME); - $this->assertTrue(strpos($filename, $receipts[0]) !== false); - - $content = $browser->readDownloadedFile($filename, 0); - $this->assertStringStartsWith("%PDF-1.", $content); - - $browser->removeDownloadedFile($filename); - }); - }); - } - - /** - * Test History tab - */ - public function testHistory(): void - { - $user = $this->getTestUser('wallets-controller@kolabnow.com', ['password' => 'simple123']); - - // Log out John and log in the test user - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->waitForLocation('/login') - ->on(new Home()) - ->submitLogon('wallets-controller@kolabnow.com', 'simple123', true); - }); - - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $user->assignPackage($package_kolab); - $wallet = $user->wallets()->first(); - - // Create some sample transactions - $transactions = $this->createTestTransactions($wallet); - $transactions = array_reverse($transactions); - $pages = array_chunk($transactions, 10 /* page size*/); - - $this->browse(function (Browser $browser) use ($pages) { - $browser->on(new Dashboard()) - ->click('@links .link-wallet') - ->on(new WalletPage()) - ->assertSeeIn('@nav #tab-history', 'History') - ->click('@nav #tab-history') - ->with('@history-tab', function (Browser $browser) use ($pages) { - $browser->waitUntilMissing('.app-loader') - ->assertElementsCount('table tbody tr', 10) - ->assertMissing('table td.email') - ->assertSeeIn('#transactions-loader button', 'Load more'); - - foreach ($pages[0] as $idx => $transaction) { - $selector = 'table tbody tr:nth-child(' . ($idx + 1) . ')'; - $priceStyle = $transaction->type == Transaction::WALLET_AWARD ? 'text-success' : 'text-danger'; - $browser->assertSeeIn("$selector td.description", $transaction->shortDescription()) - ->assertMissing("$selector td.selection button") - ->assertVisible("$selector td.price.{$priceStyle}"); - // TODO: Test more transaction details - } - - // Load the next page - $browser->click('#transactions-loader button') - ->waitUntilMissing('.app-loader') - ->assertElementsCount('table tbody tr', 12) - ->assertMissing('#transactions-loader button'); - - $debitEntry = null; - foreach ($pages[1] as $idx => $transaction) { - $selector = 'table tbody tr:nth-child(' . ($idx + 1 + 10) . ')'; - $priceStyle = $transaction->type == Transaction::WALLET_CREDIT ? 'text-success' : 'text-danger'; - $browser->assertSeeIn("$selector td.description", $transaction->shortDescription()); - - if ($transaction->type == Transaction::WALLET_DEBIT) { - $debitEntry = $selector; - } else { - $browser->assertMissing("$selector td.selection button"); - } - } - - // Load sub-transactions - $browser->click("$debitEntry td.selection button") - ->waitUntilMissing('.app-loader') - ->assertElementsCount("$debitEntry td.description ul li", 2) - ->assertMissing("$debitEntry td.selection button"); - }); - }); - } -} diff --git a/src/tests/Feature/Auth/SecondFactorTest.php b/src/tests/Feature/Auth/SecondFactorTest.php deleted file mode 100644 index 68bc4067..00000000 --- a/src/tests/Feature/Auth/SecondFactorTest.php +++ /dev/null @@ -1,63 +0,0 @@ -deleteTestUser('entitlement-test@kolabnow.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('entitlement-test@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test that 2FA config is removed from Roundcube database - * on entitlement delete - */ - public function testEntitlementDelete(): void - { - // Create the user, and assign 2FA to him, and add Roundcube setup - $sku_2fa = Sku::where('title', '2fa')->first(); - $user = $this->getTestUser('entitlement-test@kolabnow.com'); - $user->assignSku($sku_2fa); - SecondFactor::seed('entitlement-test@kolabnow.com'); - - $entitlement = Entitlement::where('sku_id', $sku_2fa->id) - ->where('entitleable_id', $user->id) - ->first(); - - $this->assertTrue(!empty($entitlement)); - - $sf = new SecondFactor($user); - $factors = $sf->factors(); - - $this->assertCount(1, $factors); - $this->assertSame('totp:8132a46b1f741f88de25f47e', $factors[0]); - // $this->assertSame('dummy:dummy', $factors[1]); - - // Delete the entitlement, expect all configured 2FA methods in Roundcube removed - $entitlement->delete(); - - $this->assertTrue($entitlement->trashed()); - - $sf = new SecondFactor($user); - $factors = $sf->factors(); - - $this->assertCount(0, $factors); - } -} diff --git a/src/tests/Feature/Backends/IMAPTest.php b/src/tests/Feature/Backends/IMAPTest.php deleted file mode 100644 index e6267479..00000000 --- a/src/tests/Feature/Backends/IMAPTest.php +++ /dev/null @@ -1,38 +0,0 @@ -markTestIncomplete(); - } - - /** - * Test verifying IMAP account existence (non-existing account) - * - * @group imap - */ - public function testVerifyAccountNonExisting(): void - { - $this->expectException(\Exception::class); - - IMAP::verifyAccount('non-existing@domain.tld'); - } -} diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php deleted file mode 100644 index dc2dba99..00000000 --- a/src/tests/Feature/Backends/LDAPTest.php +++ /dev/null @@ -1,278 +0,0 @@ -ldap_config = [ - 'ldap.hosts' => \config('ldap.hosts'), - ]; - - $this->deleteTestUser('user-ldap-test@' . \config('app.domain')); - $this->deleteTestDomain('testldap.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - \config($this->ldap_config); - - $this->deleteTestUser('user-ldap-test@' . \config('app.domain')); - $this->deleteTestDomain('testldap.com'); - - parent::tearDown(); - } - - /** - * Test handling connection errors - * - * @group ldap - */ - public function testConnectException(): void - { - \config(['ldap.hosts' => 'non-existing.host']); - - $this->expectException(\Exception::class); - - LDAP::connect(); - } - - /** - * Test creating/updating/deleting a domain record - * - * @group ldap - */ - public function testDomain(): void - { - Queue::fake(); - - $domain = $this->getTestDomain('testldap.com', [ - 'type' => Domain::TYPE_EXTERNAL, - 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE, - ]); - - // Create the domain - LDAP::createDomain($domain); - - $ldap_domain = LDAP::getDomain($domain->namespace); - - $expected = [ - 'associateddomain' => $domain->namespace, - 'inetdomainstatus' => $domain->status, - 'objectclass' => [ - 'top', - 'domainrelatedobject', - 'inetdomain' - ], - ]; - - foreach ($expected as $attr => $value) { - $this->assertEquals($value, isset($ldap_domain[$attr]) ? $ldap_domain[$attr] : null); - } - - // TODO: Test other attributes, aci, roles/ous - - // Update the domain - $domain->status |= User::STATUS_LDAP_READY; - - LDAP::updateDomain($domain); - - $expected['inetdomainstatus'] = $domain->status; - - $ldap_domain = LDAP::getDomain($domain->namespace); - - foreach ($expected as $attr => $value) { - $this->assertEquals($value, isset($ldap_domain[$attr]) ? $ldap_domain[$attr] : null); - } - - // Delete the domain - LDAP::deleteDomain($domain); - - $this->assertSame(null, LDAP::getDomain($domain->namespace)); - } - - /** - * Test creating/editing/deleting a user record - * - * @group ldap - */ - public function testUser(): void - { - Queue::fake(); - - $user = $this->getTestUser('user-ldap-test@' . \config('app.domain')); - - LDAP::createUser($user); - - $ldap_user = LDAP::getUser($user->email); - - $expected = [ - 'objectclass' => [ - 'top', - 'inetorgperson', - 'inetuser', - 'kolabinetorgperson', - 'mailrecipient', - 'person', - 'organizationalPerson', - ], - 'mail' => $user->email, - 'uid' => $user->email, - 'nsroledn' => [ - 'cn=imap-user,' . \config('ldap.hosted.root_dn') - ], - 'cn' => 'unknown', - 'displayname' => '', - 'givenname' => '', - 'sn' => 'unknown', - 'inetuserstatus' => $user->status, - 'mailquota' => null, - 'o' => '', - 'alias' => null, - ]; - - foreach ($expected as $attr => $value) { - $this->assertEquals($value, isset($ldap_user[$attr]) ? $ldap_user[$attr] : null); - } - - // Add aliases, and change some user settings, and entitlements - $user->setSettings([ - 'first_name' => 'Firstname', - 'last_name' => 'Lastname', - 'organization' => 'Org', - 'country' => 'PL', - ]); - $user->status |= User::STATUS_IMAP_READY; - $user->save(); - $aliases = ['t1-' . $user->email, 't2-' . $user->email]; - $user->setAliases($aliases); - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $user->assignPackage($package_kolab); - - LDAP::updateUser($user->fresh()); - - $expected['alias'] = $aliases; - $expected['o'] = 'Org'; - $expected['displayname'] = 'Lastname, Firstname'; - $expected['givenname'] = 'Firstname'; - $expected['cn'] = 'Firstname Lastname'; - $expected['sn'] = 'Lastname'; - $expected['inetuserstatus'] = $user->status; - $expected['mailquota'] = 2097152; - $expected['nsroledn'] = null; - - $ldap_user = LDAP::getUser($user->email); - - foreach ($expected as $attr => $value) { - $this->assertEquals($value, isset($ldap_user[$attr]) ? $ldap_user[$attr] : null); - } - - // Update entitlements - $sku_activesync = \App\Sku::where('title', 'activesync')->first(); - $sku_groupware = \App\Sku::where('title', 'groupware')->first(); - $user->assignSku($sku_activesync, 1); - Entitlement::where(['sku_id' => $sku_groupware->id, 'entitleable_id' => $user->id])->delete(); - - LDAP::updateUser($user->fresh()); - - $expected_roles = [ - 'activesync-user', - 'imap-user' - ]; - - $ldap_user = LDAP::getUser($user->email); - - $this->assertCount(2, $ldap_user['nsroledn']); - - $ldap_roles = array_map( - function ($role) { - if (preg_match('/^cn=([a-z0-9-]+)/', $role, $m)) { - return $m[1]; - } else { - return $role; - } - }, - $ldap_user['nsroledn'] - ); - - $this->assertSame($expected_roles, $ldap_roles); - - // Delete the user - LDAP::deleteUser($user); - - $this->assertSame(null, LDAP::getUser($user->email)); - } - - /** - * Test handling errors on user creation - * - * @group ldap - */ - public function testCreateUserException(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches('/Failed to create user/'); - - $user = new User([ - 'email' => 'test-non-existing-ldap@non-existing.org', - 'status' => User::STATUS_ACTIVE, - ]); - - LDAP::createUser($user); - } - - /** - * Test handling update of a non-existing domain - * - * @group ldap - */ - public function testUpdateDomainException(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches('/domain not found/'); - - $domain = new Domain([ - 'namespace' => 'testldap.com', - 'type' => Domain::TYPE_EXTERNAL, - 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE, - ]); - - LDAP::updateDomain($domain); - } - - /** - * Test handling update of a non-existing user - * - * @group ldap - */ - public function testUpdateUserException(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches('/user not found/'); - - $user = new User([ - 'email' => 'test-non-existing-ldap@kolab.org', - 'status' => User::STATUS_ACTIVE, - ]); - - LDAP::updateUser($user); - } -} diff --git a/src/tests/Feature/BillingTest.php b/src/tests/Feature/BillingTest.php deleted file mode 100644 index ff4249d0..00000000 --- a/src/tests/Feature/BillingTest.php +++ /dev/null @@ -1,262 +0,0 @@ -deleteTestUser('jane@kolabnow.com'); - $this->deleteTestUser('jack@kolabnow.com'); - - \App\Package::where('title', 'kolab-kube')->delete(); - - $this->user = $this->getTestUser('jane@kolabnow.com'); - $this->package = \App\Package::where('title', 'kolab')->first(); - $this->user->assignPackage($this->package); - - $this->wallet = $this->user->wallets->first(); - - $this->wallet_id = $this->wallet->id; - } - - public function tearDown(): void - { - $this->deleteTestUser('jane@kolabnow.com'); - $this->deleteTestUser('jack@kolabnow.com'); - - \App\Package::where('title', 'kolab-kube')->delete(); - - parent::tearDown(); - } - - /** - * Test the expected results for a user that registers and is almost immediately gone. - */ - public function testTouchAndGo(): void - { - $this->assertCount(4, $this->wallet->entitlements); - - $this->assertEquals(0, $this->wallet->expectedCharges()); - - $this->user->delete(); - - $this->assertCount(0, $this->wallet->fresh()->entitlements->where('deleted_at', null)); - - $this->assertCount(4, $this->wallet->entitlements); - } - - /** - * Verify the last day before the end of a full month's trial. - */ - public function testNearFullTrial(): void - { - $this->backdateEntitlements( - $this->wallet->entitlements, - Carbon::now()->subMonthsWithoutOverflow(1)->addDays(1) - ); - - $this->assertEquals(0, $this->wallet->expectedCharges()); - } - - /** - * Verify the exact end of the month's trial. - */ - public function testFullTrial(): void - { - $this->backdateEntitlements( - $this->wallet->entitlements, - Carbon::now()->subMonthsWithoutOverflow(1) - ); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - } - - /** - * Verify that over-running the trial by a single day causes charges to be incurred. - */ - public function testOutRunTrial(): void - { - $this->backdateEntitlements( - $this->wallet->entitlements, - Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) - ); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - } - - /** - * Verify additional storage configuration entitlement created 'early' does incur additional - * charges to the wallet. - */ - public function testAddtStorageEarly(): void - { - $this->backdateEntitlements( - $this->wallet->entitlements, - Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) - ); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - - $sku = \App\Sku::where(['title' => 'storage'])->first(); - - $entitlement = \App\Entitlement::create( - [ - 'wallet_id' => $this->wallet_id, - 'sku_id' => $sku->id, - 'cost' => $sku->cost, - 'entitleable_id' => $this->user->id, - 'entitleable_type' => \App\User::class - ] - ); - - $this->backdateEntitlements( - [$entitlement], - Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) - ); - - $this->assertEquals(1024, $this->wallet->expectedCharges()); - } - - /** - * Verify additional storage configuration entitlement created 'late' does not incur additional - * charges to the wallet. - */ - public function testAddtStorageLate(): void - { - $this->backdateEntitlements($this->wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - - $sku = \App\Sku::where(['title' => 'storage'])->first(); - - $entitlement = \App\Entitlement::create( - [ - 'wallet_id' => $this->wallet_id, - 'sku_id' => $sku->id, - 'cost' => $sku->cost, - 'entitleable_id' => $this->user->id, - 'entitleable_type' => \App\User::class - ] - ); - - $this->backdateEntitlements([$entitlement], Carbon::now()->subDays(14)); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - } - - public function testFifthWeek(): void - { - $targetDateA = Carbon::now()->subWeeks(5); - $targetDateB = $targetDateA->copy()->addMonthsWithoutOverflow(1); - - $this->backdateEntitlements($this->wallet->entitlements, $targetDateA); - - $this->assertEquals(999, $this->wallet->expectedCharges()); - - $this->wallet->chargeEntitlements(); - - $this->assertEquals(-999, $this->wallet->balance); - - foreach ($this->wallet->entitlements()->get() as $entitlement) { - $this->assertTrue($entitlement->created_at->isSameSecond($targetDateA)); - $this->assertTrue($entitlement->updated_at->isSameSecond($targetDateB)); - } - } - - public function testSecondMonth(): void - { - $this->backdateEntitlements($this->wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(2)); - - $this->assertCount(4, $this->wallet->entitlements); - - $this->assertEquals(1998, $this->wallet->expectedCharges()); - - $sku = \App\Sku::where(['title' => 'storage'])->first(); - - $entitlement = \App\Entitlement::create( - [ - 'entitleable_id' => $this->user->id, - 'entitleable_type' => \App\User::class, - 'cost' => $sku->cost, - 'sku_id' => $sku->id, - 'wallet_id' => $this->wallet_id - ] - ); - - $this->backdateEntitlements([$entitlement], Carbon::now()->subMonthsWithoutOverflow(1)); - - $this->assertEquals(2023, $this->wallet->expectedCharges()); - } - - public function testWithDiscountRate(): void - { - $package = \App\Package::create( - [ - 'title' => 'kolab-kube', - 'name' => 'Kolab for Kuba Fans', - 'description' => 'Kolab for Kube fans', - 'discount_rate' => 50 - ] - ); - - $skus = [ - \App\Sku::firstOrCreate(['title' => 'mailbox']), - \App\Sku::firstOrCreate(['title' => 'storage']), - \App\Sku::firstOrCreate(['title' => 'groupware']) - ]; - - $package->skus()->saveMany($skus); - - $package->skus()->updateExistingPivot( - \App\Sku::firstOrCreate(['title' => 'storage']), - ['qty' => 2], - false - ); - - $user = $this->getTestUser('jack@kolabnow.com'); - - $user->assignPackage($package); - - $wallet = $user->wallets->first(); - - $wallet_id = $wallet->id; - - $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); - - $this->assertEquals(500, $wallet->expectedCharges()); - } - - /** - * Test cost calculation with a wallet discount - */ - public function testWithWalletDiscount(): void - { - $discount = \App\Discount::where('code', 'TEST')->first(); - - $wallet = $this->user->wallets()->first(); - $wallet->discount()->associate($discount); - - $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); - - $this->assertEquals(898, $wallet->expectedCharges()); - } -} diff --git a/src/tests/Feature/Browser/UserListPageTest.php b/src/tests/Feature/Browser/UserListPageTest.php new file mode 100644 index 00000000..23b3e499 --- /dev/null +++ b/src/tests/Feature/Browser/UserListPageTest.php @@ -0,0 +1,124 @@ +browse( + function (Browser $browser) { + $browser->visit('/users')->on(new Home()); + $browser->screenshot(__LINE__); + } + ); + } + + /** + * Test users list page as an owner of a domain. + */ + public function testUsersListPageAsOwner(): void + { + // Test that the page requires authentication + $this->browse( + function (Browser $browser) { + $browser->visit(new Home()); + $browser->screenshot(__LINE__); + $browser->submitLogon($this->domainOwner->email, $this->userPassword, true); + $browser->screenshot(__LINE__); + $browser->on(new Dashboard()); + $browser->screenshot(__LINE__); + $browser->assertSeeIn('@links .link-users', 'User accounts'); + $browser->click('@links .link-users'); + $browser->screenshot(__LINE__); + $browser->on(new UserList()); + $browser->whenAvailable( + '@table', + function (Browser $browser) { + $browser->waitFor('tbody tr'); + $browser->assertElementsCount('tbody tr', sizeof($this->domainUsers)); + $browser->screenshot(__LINE__); + + foreach ($this->domainUsers as $user) { + $arrayPosition = array_search($user, $this->domainUsers); + $listPosition = $arrayPosition + 1; + + $browser->assertSeeIn("tbody tr:nth-child({$listPosition}) a", $user->email); + $browser->assertVisible("tbody tr:nth-child({$listPosition}) button.button-delete"); + } + + $browser->assertMissing('tfoot'); + } + ); + } + ); + } + + /** + * Test users list page as a user of a domain. + */ + public function testUsersListPageAsUser(): void + { + // Test that the page requires authentication + $this->browse( + function (Browser $browser) { + $browser->visit(new Home()); + $browser->screenshot(__LINE__); + $browser->submitLogon($this->jack->email, $this->userPassword, true); + $browser->on(new Dashboard()); + $browser->screenshot(__LINE__); + $browser->assertDontSee('@links .link-users'); + } + ); + } + + /** + * Test users list page as an additional controller on the wallet for a domain. + */ + public function testUsersListPageAsController(): void + { + $this->browse( + function (Browser $browser) { + $browser->visit(new Home()); + $browser->submitLogon($this->jane->email, $this->userPassword, true); + $browser->on(new Dashboard()); + $browser->assertSeeIn('@links .link-users', 'User accounts'); + $browser->click('@links .link-users'); + $browser->on(new UserList()); + $browser->whenAvailable( + '@table', + function (Browser $browser) { + $browser->waitFor('tbody tr'); + $browser->assertElementsCount('tbody tr', sizeof($this->domainUsers)); + + foreach ($this->domainUsers as $user) { + $arrayPosition = array_search($user, $this->domainUsers); + $listPosition = $arrayPosition + 1; + + $browser->assertSeeIn("tbody tr:nth-child({$listPosition}) a", $user->email); + $browser->assertVisible("tbody tr:nth-child({$listPosition}) button.button-delete"); + } + + $browser->assertMissing('tfoot'); + } + ); + } + ); + } +} diff --git a/src/tests/Feature/Console/DataCountriesTest.php b/src/tests/Feature/Console/DataCountriesTest.php deleted file mode 100644 index 79f65fa9..00000000 --- a/src/tests/Feature/Console/DataCountriesTest.php +++ /dev/null @@ -1,13 +0,0 @@ -markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/DiscountListTest.php b/src/tests/Feature/Console/DiscountListTest.php deleted file mode 100644 index 57390b21..00000000 --- a/src/tests/Feature/Console/DiscountListTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('discount:list') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/PackageSkusTest.php b/src/tests/Feature/Console/PackageSkusTest.php deleted file mode 100644 index 0f7bcb5a..00000000 --- a/src/tests/Feature/Console/PackageSkusTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('package:skus') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/PlanPackagesTest.php b/src/tests/Feature/Console/PlanPackagesTest.php deleted file mode 100644 index df22a453..00000000 --- a/src/tests/Feature/Console/PlanPackagesTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('plan:packages') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/UserDiscountTest.php b/src/tests/Feature/Console/UserDiscountTest.php deleted file mode 100644 index 3e99362b..00000000 --- a/src/tests/Feature/Console/UserDiscountTest.php +++ /dev/null @@ -1,13 +0,0 @@ -markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/UserDomainsTest.php b/src/tests/Feature/Console/UserDomainsTest.php deleted file mode 100644 index 6e9bdafe..00000000 --- a/src/tests/Feature/Console/UserDomainsTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('user:domains john@kolab.org') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/UserEntitlementsTest.php b/src/tests/Feature/Console/UserEntitlementsTest.php deleted file mode 100644 index 1a421886..00000000 --- a/src/tests/Feature/Console/UserEntitlementsTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('user:entitlements john@kolab.org') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/UserForceDeleteTest.php b/src/tests/Feature/Console/UserForceDeleteTest.php deleted file mode 100644 index 5e50718b..00000000 --- a/src/tests/Feature/Console/UserForceDeleteTest.php +++ /dev/null @@ -1,98 +0,0 @@ -deleteTestUser('user@force-delete.com'); - $this->deleteTestDomain('force-delete.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('user@force-delete.com'); - $this->deleteTestDomain('force-delete.com'); - - parent::tearDown(); - } - - /** - * Test the command - */ - public function testHandle(): void - { - // Non-existing user - $this->artisan('user:force-delete unknown@unknown.org') - ->assertExitCode(1); - - Queue::fake(); - $user = $this->getTestUser('user@force-delete.com'); - $domain = $this->getTestDomain('force-delete.com', [ - 'status' => \App\Domain::STATUS_NEW, - 'type' => \App\Domain::TYPE_HOSTED, - ]); - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - $user->assignPackage($package_kolab); - $domain->assignPackage($package_domain, $user); - $wallet = $user->wallets()->first(); - $entitlements = $wallet->entitlements->pluck('id')->all(); - - $this->assertCount(5, $entitlements); - - // Non-deleted user - $this->artisan('user:force-delete user@force-delete.com') - ->assertExitCode(1); - - $user->delete(); - - $this->assertTrue($user->trashed()); - $this->assertTrue($domain->fresh()->trashed()); - - // Deleted user - $this->artisan('user:force-delete user@force-delete.com') - ->assertExitCode(0); - - $this->assertCount( - 0, - \App\User::withTrashed()->where('email', 'user@force-delete.com')->get() - ); - $this->assertCount( - 0, - \App\Domain::withTrashed()->where('namespace', 'force-delete.com')->get() - ); - $this->assertCount( - 0, - \App\Wallet::where('id', $wallet->id)->get() - ); - $this->assertCount( - 0, - \App\Entitlement::withTrashed()->where('wallet_id', $wallet->id)->get() - ); - $this->assertCount( - 0, - \App\Entitlement::withTrashed()->where('entitleable_id', $user->id)->get() - ); - $this->assertCount( - 0, - \App\Transaction::whereIn('object_id', $entitlements) - ->where('object_type', \App\Entitlement::class) - ->get() - ); - - // TODO: Test that it also deletes users in a group account - } -} diff --git a/src/tests/Feature/Console/UserWalletsTest.php b/src/tests/Feature/Console/UserWalletsTest.php deleted file mode 100644 index 7e3e723a..00000000 --- a/src/tests/Feature/Console/UserWalletsTest.php +++ /dev/null @@ -1,16 +0,0 @@ -artisan('user:wallets john@kolab.org') - ->assertExitCode(0); - - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Console/WalletChargeTest.php b/src/tests/Feature/Console/WalletChargeTest.php deleted file mode 100644 index c56a868a..00000000 --- a/src/tests/Feature/Console/WalletChargeTest.php +++ /dev/null @@ -1,137 +0,0 @@ -deleteTestUser('wallet-charge@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('wallet-charge@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test command run for a specified wallet - */ - public function testHandleSingle(): void - { - $user = $this->getTestUser('wallet-charge@kolabnow.com'); - $wallet = $user->wallets()->first(); - $wallet->balance = 0; - $wallet->save(); - - Queue::fake(); - - // Non-existing wallet ID - $this->artisan('wallet:charge 123') - ->assertExitCode(1); - - Queue::assertNothingPushed(); - - // The wallet has no entitlements, expect no charge and no check - $this->artisan('wallet:charge ' . $wallet->id) - ->assertExitCode(0); - - Queue::assertNothingPushed(); - - // The wallet has no entitlements, but has negative balance - $wallet->balance = -100; - $wallet->save(); - - $this->artisan('wallet:charge ' . $wallet->id) - ->assertExitCode(0); - - Queue::assertPushed(\App\Jobs\WalletCharge::class, 0); - Queue::assertPushed(\App\Jobs\WalletCheck::class, 1); - Queue::assertPushed(\App\Jobs\WalletCheck::class, function ($job) use ($wallet) { - $job_wallet = TestCase::getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - Queue::fake(); - - // The wallet has entitlements to charge, and negative balance - $sku = \App\Sku::where('title', 'mailbox')->first(); - $entitlement = \App\Entitlement::create([ - 'wallet_id' => $wallet->id, - 'sku_id' => $sku->id, - 'cost' => 100, - 'entitleable_id' => $user->id, - 'entitleable_type' => \App\User::class, - ]); - \App\Entitlement::where('id', $entitlement->id)->update([ - 'created_at' => \Carbon\Carbon::now()->subMonths(1), - 'updated_at' => \Carbon\Carbon::now()->subMonths(1), - ]); - \App\User::where('id', $user->id)->update([ - 'created_at' => \Carbon\Carbon::now()->subMonths(1), - 'updated_at' => \Carbon\Carbon::now()->subMonths(1), - ]); - - $this->assertSame(100, $wallet->fresh()->chargeEntitlements(false)); - - $this->artisan('wallet:charge ' . $wallet->id) - ->assertExitCode(0); - - Queue::assertPushed(\App\Jobs\WalletCharge::class, 1); - Queue::assertPushed(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { - $job_wallet = TestCase::getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - Queue::assertPushed(\App\Jobs\WalletCheck::class, 1); - Queue::assertPushed(\App\Jobs\WalletCheck::class, function ($job) use ($wallet) { - $job_wallet = TestCase::getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - } - - /** - * Test command run for all wallets - */ - public function testHandleAll(): void - { - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - $wallet->balance = 0; - $wallet->save(); - - // backdate john's entitlements and set balance=0 for all wallets - $this->backdateEntitlements($user->entitlements, \Carbon\Carbon::now()->subWeeks(5)); - \App\Wallet::where('balance', '<', '0')->update(['balance' => 0]); - - Queue::fake(); - - // Non-existing wallet ID - $this->artisan('wallet:charge')->assertExitCode(0); - - Queue::assertPushed(\App\Jobs\WalletCheck::class, 1); - Queue::assertPushed(\App\Jobs\WalletCheck::class, function ($job) use ($wallet) { - $job_wallet = TestCase::getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - Queue::assertPushed(\App\Jobs\WalletCharge::class, 1); - Queue::assertPushed(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { - $job_wallet = TestCase::getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - } -} diff --git a/src/tests/Feature/Console/WalletDiscountTest.php b/src/tests/Feature/Console/WalletDiscountTest.php deleted file mode 100644 index bb87edfc..00000000 --- a/src/tests/Feature/Console/WalletDiscountTest.php +++ /dev/null @@ -1,13 +0,0 @@ -markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Controller/Admin/DiscountsTest.php b/src/tests/Feature/Controller/Admin/DiscountsTest.php deleted file mode 100644 index 3eac8d5e..00000000 --- a/src/tests/Feature/Controller/Admin/DiscountsTest.php +++ /dev/null @@ -1,61 +0,0 @@ -getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Non-admin user - $response = $this->actingAs($user)->get("api/v4/discounts"); - $response->assertStatus(403); - - // Admin user - $response = $this->actingAs($admin)->get("api/v4/discounts"); - $response->assertStatus(200); - - $json = $response->json(); - - $discount_test = Discount::where('code', 'TEST')->first(); - $discount_free = Discount::where('discount', 100)->first(); - - $this->assertSame(3, $json['count']); - $this->assertSame($discount_test->id, $json['list'][0]['id']); - $this->assertSame($discount_test->discount, $json['list'][0]['discount']); - $this->assertSame($discount_test->code, $json['list'][0]['code']); - $this->assertSame($discount_test->description, $json['list'][0]['description']); - $this->assertSame('10% - Test voucher [TEST]', $json['list'][0]['label']); - - $this->assertSame($discount_free->id, $json['list'][2]['id']); - $this->assertSame($discount_free->discount, $json['list'][2]['discount']); - $this->assertSame($discount_free->code, $json['list'][2]['code']); - $this->assertSame($discount_free->description, $json['list'][2]['description']); - $this->assertSame('100% - Free Account', $json['list'][2]['label']); - } -} diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php deleted file mode 100644 index 5dcaa7a8..00000000 --- a/src/tests/Feature/Controller/Admin/DomainsTest.php +++ /dev/null @@ -1,159 +0,0 @@ -deleteTestDomain('domainscontroller.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestDomain('domainscontroller.com'); - - parent::tearDown(); - } - - /** - * Test domains searching (/api/v4/domains) - */ - public function testIndex(): void - { - $john = $this->getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Non-admin user - $response = $this->actingAs($john)->get("api/v4/domains"); - $response->assertStatus(403); - - // Search with no search criteria - $response = $this->actingAs($admin)->get("api/v4/domains"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertSame([], $json['list']); - - // Search with no matches expected - $response = $this->actingAs($admin)->get("api/v4/domains?search=abcd12.org"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertSame([], $json['list']); - - // Search by a domain name - $response = $this->actingAs($admin)->get("api/v4/domains?search=kolab.org"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame('kolab.org', $json['list'][0]['namespace']); - - // Search by owner - $response = $this->actingAs($admin)->get("api/v4/domains?owner={$john->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame('kolab.org', $json['list'][0]['namespace']); - - // Search by owner (Ned is a controller on John's wallets, - // here we expect only domains assigned to Ned's wallet(s)) - $ned = $this->getTestUser('ned@kolab.org'); - $response = $this->actingAs($admin)->get("api/v4/domains?owner={$ned->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertCount(0, $json['list']); - } - - /** - * Test domain suspending (POST /api/v4/domains//suspend) - */ - public function testSuspend(): void - { - Queue::fake(); // disable jobs - - $domain = $this->getTestDomain('domainscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - $user = $this->getTestUser('test@domainscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/suspend", []); - $response->assertStatus(403); - - $this->assertFalse($domain->fresh()->isSuspended()); - - // Test suspending the user - $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/suspend", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("Domain suspended successfully.", $json['message']); - $this->assertCount(2, $json); - - $this->assertTrue($domain->fresh()->isSuspended()); - } - - /** - * Test user un-suspending (POST /api/v4/users//unsuspend) - */ - public function testUnsuspend(): void - { - Queue::fake(); // disable jobs - - $domain = $this->getTestDomain('domainscontroller.com', [ - 'status' => Domain::STATUS_NEW | Domain::STATUS_SUSPENDED, - 'type' => Domain::TYPE_EXTERNAL, - ]); - $user = $this->getTestUser('test@domainscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/unsuspend", []); - $response->assertStatus(403); - - $this->assertTrue($domain->fresh()->isSuspended()); - - // Test suspending the user - $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/unsuspend", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("Domain unsuspended successfully.", $json['message']); - $this->assertCount(2, $json); - - $this->assertFalse($domain->fresh()->isSuspended()); - } -} diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php deleted file mode 100644 index 9070c0a0..00000000 --- a/src/tests/Feature/Controller/Admin/UsersTest.php +++ /dev/null @@ -1,341 +0,0 @@ -deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestUser('test@testsearch.com'); - $this->deleteTestDomain('testsearch.com'); - - $jack = $this->getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', null); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestUser('test@testsearch.com'); - $this->deleteTestDomain('testsearch.com'); - - $jack = $this->getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', null); - - parent::tearDown(); - } - - /** - * Test users searching (/api/v4/users) - */ - public function testIndex(): void - { - $user = $this->getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Non-admin user - $response = $this->actingAs($user)->get("api/v4/users"); - $response->assertStatus(403); - - // Search with no search criteria - $response = $this->actingAs($admin)->get("api/v4/users"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertSame([], $json['list']); - - // Search with no matches expected - $response = $this->actingAs($admin)->get("api/v4/users?search=abcd1234efgh5678"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertSame([], $json['list']); - - // Search by domain - $response = $this->actingAs($admin)->get("api/v4/users?search=kolab.org"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - - // Search by user ID - $response = $this->actingAs($admin)->get("api/v4/users?search={$user->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - - // Search by email (primary) - $response = $this->actingAs($admin)->get("api/v4/users?search=john@kolab.org"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - - // Search by email (alias) - $response = $this->actingAs($admin)->get("api/v4/users?search=john.doe@kolab.org"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - - // Search by email (external), expect two users in a result - $jack = $this->getTestUser('jack@kolab.org'); - $jack->setSetting('external_email', 'john.doe.external@gmail.com'); - - $response = $this->actingAs($admin)->get("api/v4/users?search=john.doe.external@gmail.com"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(2, $json['count']); - $this->assertCount(2, $json['list']); - - $emails = array_column($json['list'], 'email'); - - $this->assertContains($user->email, $emails); - $this->assertContains($jack->email, $emails); - - // Search by owner - $response = $this->actingAs($admin)->get("api/v4/users?owner={$user->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(4, $json['count']); - $this->assertCount(4, $json['list']); - - // Search by owner (Ned is a controller on John's wallets, - // here we expect only users assigned to Ned's wallet(s)) - $ned = $this->getTestUser('ned@kolab.org'); - $response = $this->actingAs($admin)->get("api/v4/users?owner={$ned->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(0, $json['count']); - $this->assertCount(0, $json['list']); - - // Deleted users/domains - $domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]); - $user = $this->getTestUser('test@testsearch.com'); - $plan = \App\Plan::where('title', 'group')->first(); - $user->assignPlan($plan, $domain); - $user->setAliases(['alias@testsearch.com']); - Queue::fake(); - $user->delete(); - - $response = $this->actingAs($admin)->get("api/v4/users?search=test@testsearch.com"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - $this->assertTrue($json['list'][0]['isDeleted']); - - $response = $this->actingAs($admin)->get("api/v4/users?search=alias@testsearch.com"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - $this->assertTrue($json['list'][0]['isDeleted']); - - $response = $this->actingAs($admin)->get("api/v4/users?search=testsearch.com"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame(1, $json['count']); - $this->assertCount(1, $json['list']); - $this->assertSame($user->id, $json['list'][0]['id']); - $this->assertSame($user->email, $json['list'][0]['email']); - $this->assertTrue($json['list'][0]['isDeleted']); - } - - /** - * Test reseting 2FA (POST /api/v4/users//reset2FA) - */ - public function testReset2FA(): void - { - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - $sku2fa = Sku::firstOrCreate(['title' => '2fa']); - $user->assignSku($sku2fa); - SecondFactor::seed('userscontrollertest1@userscontroller.com'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/reset2FA", []); - $response->assertStatus(403); - - $entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get(); - $this->assertCount(1, $entitlements); - - $sf = new SecondFactor($user); - $this->assertCount(1, $sf->factors()); - - // Test reseting 2FA - $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/reset2FA", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("2-Factor authentication reset successfully.", $json['message']); - $this->assertCount(2, $json); - - $entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get(); - $this->assertCount(0, $entitlements); - - $sf = new SecondFactor($user); - $this->assertCount(0, $sf->factors()); - } - - /** - * Test user suspending (POST /api/v4/users//suspend) - */ - public function testSuspend(): void - { - Queue::fake(); // disable jobs - - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/suspend", []); - $response->assertStatus(403); - - $this->assertFalse($user->isSuspended()); - - // Test suspending the user - $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/suspend", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("User suspended successfully.", $json['message']); - $this->assertCount(2, $json); - - $this->assertTrue($user->fresh()->isSuspended()); - } - - /** - * Test user un-suspending (POST /api/v4/users//unsuspend) - */ - public function testUnsuspend(): void - { - Queue::fake(); // disable jobs - - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/unsuspend", []); - $response->assertStatus(403); - - $this->assertFalse($user->isSuspended()); - $user->suspend(); - $this->assertTrue($user->isSuspended()); - - // Test suspending the user - $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/unsuspend", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("User unsuspended successfully.", $json['message']); - $this->assertCount(2, $json); - - $this->assertFalse($user->fresh()->isSuspended()); - } - - /** - * Test user update (PUT /api/v4/users/) - */ - public function testUpdate(): void - { - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Test unauthorized access to admin API - $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", []); - $response->assertStatus(403); - - // Test updatig the user data (empty data) - $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("User data updated successfully.", $json['message']); - $this->assertCount(2, $json); - - // Test error handling - $post = ['external_email' => 'aaa']; - $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertSame("The external email must be a valid email address.", $json['errors']['external_email'][0]); - $this->assertCount(2, $json); - - // Test real update - $post = ['external_email' => 'modified@test.com']; - $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("User data updated successfully.", $json['message']); - $this->assertCount(2, $json); - $this->assertSame('modified@test.com', $user->getSetting('external_email')); - } -} diff --git a/src/tests/Feature/Controller/Admin/WalletsTest.php b/src/tests/Feature/Controller/Admin/WalletsTest.php deleted file mode 100644 index c949dbc9..00000000 --- a/src/tests/Feature/Controller/Admin/WalletsTest.php +++ /dev/null @@ -1,228 +0,0 @@ - 'stripe']); - - $user = $this->getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - $wallet = $user->wallets()->first(); - $wallet->discount_id = null; - $wallet->save(); - - // Make sure there's no stripe/mollie identifiers - $wallet->setSetting('stripe_id', null); - $wallet->setSetting('stripe_mandate_id', null); - $wallet->setSetting('mollie_id', null); - $wallet->setSetting('mollie_mandate_id', null); - - // Non-admin user - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}"); - $response->assertStatus(403); - - // Admin user - $response = $this->actingAs($admin)->get("api/v4/wallets/{$wallet->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame($wallet->id, $json['id']); - $this->assertSame('CHF', $json['currency']); - $this->assertSame($wallet->balance, $json['balance']); - $this->assertSame(0, $json['discount']); - $this->assertTrue(empty($json['description'])); - $this->assertTrue(empty($json['discount_description'])); - $this->assertTrue(!empty($json['provider'])); - $this->assertTrue(empty($json['providerLink'])); - $this->assertTrue(!empty($json['mandate'])); - } - - /** - * Test awarding/penalizing a wallet (POST /api/v4/wallets/:id/one-off) - */ - public function testOneOff(): void - { - $user = $this->getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - $wallet = $user->wallets()->first(); - $balance = $wallet->balance; - - Transaction::where('object_id', $wallet->id) - ->whereIn('type', [Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY]) - ->delete(); - - // Non-admin user - $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/one-off", []); - $response->assertStatus(403); - - // Admin user - invalid input - $post = ['amount' => 'aaaa']; - $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertSame('The amount must be a number.', $json['errors']['amount'][0]); - $this->assertSame('The description field is required.', $json['errors']['description'][0]); - $this->assertCount(2, $json); - $this->assertCount(2, $json['errors']); - - // Admin user - a valid bonus - $post = ['amount' => '50', 'description' => 'A bonus']; - $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The bonus has been added to the wallet successfully.', $json['message']); - $this->assertSame($balance += 5000, $json['balance']); - $this->assertSame($balance, $wallet->fresh()->balance); - - $transaction = Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_AWARD)->first(); - - $this->assertSame($post['description'], $transaction->description); - $this->assertSame(5000, $transaction->amount); - $this->assertSame($admin->email, $transaction->user_email); - - // Admin user - a valid penalty - $post = ['amount' => '-40', 'description' => 'A penalty']; - $response = $this->actingAs($admin)->post("api/v4/wallets/{$wallet->id}/one-off", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The penalty has been added to the wallet successfully.', $json['message']); - $this->assertSame($balance -= 4000, $json['balance']); - $this->assertSame($balance, $wallet->fresh()->balance); - - $transaction = Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_PENALTY)->first(); - - $this->assertSame($post['description'], $transaction->description); - $this->assertSame(4000, $transaction->amount); - $this->assertSame($admin->email, $transaction->user_email); - } - - /** - * Test fetching wallet transactions (GET /api/v4/wallets/:id/transactions) - */ - public function testTransactions(): void - { - // Note: Here we're testing only that the end-point works, - // and admin can get the transaction log, response details - // are tested in Feature/Controller/WalletsTest.php - $this->deleteTestUser('wallets-controller@kolabnow.com'); - $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $wallet = $user->wallets()->first(); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - - // Non-admin - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); - $response->assertStatus(403); - - // Create some sample transactions - $transactions = $this->createTestTransactions($wallet); - $transactions = array_reverse($transactions); - $pages = array_chunk($transactions, 10 /* page size*/); - - // Get the 2nd page - $response = $this->actingAs($admin)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame(2, $json['page']); - $this->assertSame(2, $json['count']); - $this->assertSame(false, $json['hasMore']); - $this->assertCount(2, $json['list']); - foreach ($pages[1] as $idx => $transaction) { - $this->assertSame($transaction->id, $json['list'][$idx]['id']); - $this->assertSame($transaction->type, $json['list'][$idx]['type']); - $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); - $this->assertFalse($json['list'][$idx]['hasDetails']); - } - - // The 'user' key is set only on the admin end-point - $this->assertSame('jeroen@jeroen.jeroen', $json['list'][1]['user']); - } - - /** - * Test updating a wallet (PUT /api/v4/wallets/:id) - */ - public function testUpdate(): void - { - $user = $this->getTestUser('john@kolab.org'); - $admin = $this->getTestUser('jeroen@jeroen.jeroen'); - $wallet = $user->wallets()->first(); - $discount = Discount::where('code', 'TEST')->first(); - - // Non-admin user - $response = $this->actingAs($user)->put("api/v4/wallets/{$wallet->id}", []); - $response->assertStatus(403); - - // Admin user - setting a discount - $post = ['discount' => $discount->id]; - $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('User wallet updated successfully.', $json['message']); - $this->assertSame($wallet->id, $json['id']); - $this->assertSame($discount->discount, $json['discount']); - $this->assertSame($discount->id, $json['discount_id']); - $this->assertSame($discount->description, $json['discount_description']); - $this->assertSame($discount->id, $wallet->fresh()->discount->id); - - // Admin user - removing a discount - $post = ['discount' => null]; - $response = $this->actingAs($admin)->put("api/v4/wallets/{$wallet->id}", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('User wallet updated successfully.', $json['message']); - $this->assertSame($wallet->id, $json['id']); - $this->assertSame(null, $json['discount_id']); - $this->assertTrue(empty($json['discount_description'])); - $this->assertSame(null, $wallet->fresh()->discount); - } -} diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php deleted file mode 100644 index 87e5fd23..00000000 --- a/src/tests/Feature/Controller/AuthTest.php +++ /dev/null @@ -1,201 +0,0 @@ -deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestDomain('userscontroller.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestDomain('userscontroller.com'); - - parent::tearDown(); - } - - /** - * Test fetching current user info (/api/auth/info) - */ - public function testInfo(): void - { - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $domain = $this->getTestDomain('userscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_PUBLIC, - ]); - - $response = $this->actingAs($user)->get("api/auth/info"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals($user->id, $json['id']); - $this->assertEquals($user->email, $json['email']); - $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']); - $this->assertTrue(is_array($json['statusInfo'])); - $this->assertTrue(is_array($json['settings'])); - $this->assertTrue(is_array($json['aliases'])); - $this->assertTrue(!isset($json['access_token'])); - - // Note: Details of the content are tested in testUserResponse() - - // Test token refresh via the info request - // First we log in as we need the token (actingAs() will not work) - $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; - $response = $this->post("api/auth/login", $post); - $json = $response->json(); - $response = $this->withHeaders(['Authorization' => 'Bearer ' . $json['access_token']]) - ->get("api/auth/info?refresh_token=1"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals('john@kolab.org', $json['email']); - $this->assertTrue(is_array($json['statusInfo'])); - $this->assertTrue(is_array($json['settings'])); - $this->assertTrue(is_array($json['aliases'])); - $this->assertTrue(!empty($json['access_token'])); - $this->assertTrue(!empty($json['expires_in'])); - } - - /** - * Test /api/auth/login - */ - public function testLogin(): string - { - // Request with no data - $response = $this->post("api/auth/login", []); - $response->assertStatus(422); - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - - // Request with invalid password - $post = ['email' => 'john@kolab.org', 'password' => 'wrong']; - $response = $this->post("api/auth/login", $post); - $response->assertStatus(401); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertSame('Invalid username or password.', $json['message']); - - // Valid user+password - $user = $this->getTestUser('john@kolab.org'); - $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; - $response = $this->post("api/auth/login", $post); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertTrue(!empty($json['access_token'])); - $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); - $this->assertEquals('bearer', $json['token_type']); - $this->assertEquals($user->id, $json['id']); - $this->assertEquals($user->email, $json['email']); - $this->assertTrue(is_array($json['statusInfo'])); - $this->assertTrue(is_array($json['settings'])); - $this->assertTrue(is_array($json['aliases'])); - - // Valid user+password (upper-case) - $post = ['email' => 'John@Kolab.org', 'password' => 'simple123']; - $response = $this->post("api/auth/login", $post); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertTrue(!empty($json['access_token'])); - $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); - $this->assertEquals('bearer', $json['token_type']); - - // TODO: We have browser tests for 2FA but we should probably also test it here - - return $json['access_token']; - } - - /** - * Test /api/auth/logout - * - * @depends testLogin - */ - public function testLogout($token): void - { - // Request with no token, testing that it requires auth - $response = $this->post("api/auth/logout"); - $response->assertStatus(401); - - // Test the same using JSON mode - $response = $this->json('POST', "api/auth/logout", []); - $response->assertStatus(401); - - // Request with valid token - $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/logout"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals('success', $json['status']); - $this->assertEquals('Successfully logged out.', $json['message']); - - // Check if it really destroyed the token? - $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info"); - $response->assertStatus(401); - } - - /** - * Test /api/auth/refresh - */ - public function testRefresh(): void - { - // Request with no token, testing that it requires auth - $response = $this->post("api/auth/refresh"); - $response->assertStatus(401); - - // Test the same using JSON mode - $response = $this->json('POST', "api/auth/refresh", []); - $response->assertStatus(401); - - // Login the user to get a valid token - $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; - $response = $this->post("api/auth/login", $post); - $response->assertStatus(200); - $json = $response->json(); - $token = $json['access_token']; - - // Request with a valid token - $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/refresh"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertTrue(!empty($json['access_token'])); - $this->assertTrue($json['access_token'] != $token); - $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); - $this->assertEquals('bearer', $json['token_type']); - $new_token = $json['access_token']; - - // TODO: Shall we invalidate the old token? - - // And if the new token is working - $response = $this->withHeaders(['Authorization' => 'Bearer ' . $new_token])->get("api/auth/info"); - $response->assertStatus(200); - } -} diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php deleted file mode 100644 index 69fd44ba..00000000 --- a/src/tests/Feature/Controller/DomainsTest.php +++ /dev/null @@ -1,243 +0,0 @@ -deleteTestUser('test1@domainscontroller.com'); - $this->deleteTestDomain('domainscontroller.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('test1@domainscontroller.com'); - $this->deleteTestDomain('domainscontroller.com'); - - parent::tearDown(); - } - - /** - * Test domain confirm request - */ - public function testConfirm(): void - { - $sku_domain = Sku::where('title', 'domain-hosting')->first(); - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - $user = $this->getTestUser('test1@domainscontroller.com'); - $domain = $this->getTestDomain('domainscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - Entitlement::create([ - 'wallet_id' => $user->wallets()->first()->id, - 'sku_id' => $sku_domain->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class - ]); - - $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(2, $json); - $this->assertEquals('error', $json['status']); - $this->assertEquals('Domain ownership verification failed.', $json['message']); - - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals('success', $json['status']); - $this->assertEquals('Domain verified successfully.', $json['message']); - $this->assertTrue(is_array($json['statusInfo'])); - - // Not authorized access - $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm"); - $response->assertStatus(403); - - // Authorized access by additional account controller - $domain = $this->getTestDomain('kolab.org'); - $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm"); - $response->assertStatus(200); - } - - /** - * Test fetching domains list - */ - public function testIndex(): void - { - // User with no domains - $user = $this->getTestUser('test1@domainscontroller.com'); - $response = $this->actingAs($user)->get("api/v4/domains"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame([], $json); - - // User with custom domain(s) - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - - $response = $this->actingAs($john)->get("api/v4/domains"); - $response->assertStatus(200); - - $json = $response->json(); - $this->assertCount(1, $json); - $this->assertSame('kolab.org', $json[0]['namespace']); - // Values below are tested by Unit tests - $this->assertArrayHasKey('isConfirmed', $json[0]); - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isVerified', $json[0]); - $this->assertArrayHasKey('isSuspended', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); - - $response = $this->actingAs($ned)->get("api/v4/domains"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(1, $json); - $this->assertSame('kolab.org', $json[0]['namespace']); - } - - /** - * Test fetching domain info - */ - public function testShow(): void - { - $sku_domain = Sku::where('title', 'domain-hosting')->first(); - $user = $this->getTestUser('test1@domainscontroller.com'); - $domain = $this->getTestDomain('domainscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - Entitlement::create([ - 'wallet_id' => $user->wallets()->first()->id, - 'sku_id' => $sku_domain->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class - ]); - - $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals($domain->id, $json['id']); - $this->assertEquals($domain->namespace, $json['namespace']); - $this->assertEquals($domain->status, $json['status']); - $this->assertEquals($domain->type, $json['type']); - $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); - $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); - $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); - $this->assertCount(4, $json['config']); - $this->assertTrue(strpos(implode("\n", $json['config']), $domain->namespace) !== false); - $this->assertCount(8, $json['dns']); - $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); - $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); - $this->assertTrue(is_array($json['statusInfo'])); - // Values below are tested by Unit tests - $this->assertArrayHasKey('isConfirmed', $json); - $this->assertArrayHasKey('isDeleted', $json); - $this->assertArrayHasKey('isVerified', $json); - $this->assertArrayHasKey('isSuspended', $json); - $this->assertArrayHasKey('isActive', $json); - $this->assertArrayHasKey('isLdapReady', $json); - - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - - // Not authorized - Other account domain - $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}"); - $response->assertStatus(403); - - $domain = $this->getTestDomain('kolab.org'); - - // Ned is an additional controller on kolab.org's wallet - $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}"); - $response->assertStatus(200); - - // Jack has no entitlement/control over kolab.org - $response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}"); - $response->assertStatus(403); - } - - /** - * Test fetching domain status (GET /api/v4/domains//status) - * and forcing setup process update (?refresh=1) - * - * @group dns - */ - public function testStatus(): void - { - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $domain = $this->getTestDomain('kolab.org'); - - // Test unauthorized access - $response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status"); - $response->assertStatus(403); - - $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; - $domain->save(); - - // Get domain status - $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertFalse($json['isVerified']); - $this->assertFalse($json['isReady']); - $this->assertCount(4, $json['process']); - $this->assertSame('domain-verified', $json['process'][2]['label']); - $this->assertSame(false, $json['process'][2]['state']); - $this->assertTrue(empty($json['status'])); - $this->assertTrue(empty($json['message'])); - - // Now "reboot" the process and verify the domain - $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status?refresh=1"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertTrue($json['isVerified']); - $this->assertTrue($json['isReady']); - $this->assertCount(4, $json['process']); - $this->assertSame('domain-verified', $json['process'][2]['label']); - $this->assertSame(true, $json['process'][2]['state']); - $this->assertSame('domain-confirmed', $json['process'][3]['label']); - $this->assertSame(true, $json['process'][3]['state']); - $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); - - // TODO: Test completing all process steps - } -} diff --git a/src/tests/Feature/Controller/PackagesTest.php b/src/tests/Feature/Controller/PackagesTest.php deleted file mode 100644 index 1b12b6d4..00000000 --- a/src/tests/Feature/Controller/PackagesTest.php +++ /dev/null @@ -1,53 +0,0 @@ -get("api/v4/packages"); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - $package_domain = Package::where('title', 'domain-hosting')->first(); - $package_kolab = Package::where('title', 'kolab')->first(); - $package_lite = Package::where('title', 'lite')->first(); - - $response = $this->actingAs($user)->get("api/v4/packages"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(3, $json); - - $this->assertSame($package_domain->id, $json[0]['id']); - $this->assertSame($package_domain->title, $json[0]['title']); - $this->assertSame($package_domain->name, $json[0]['name']); - $this->assertSame($package_domain->description, $json[0]['description']); - $this->assertSame($package_domain->isDomain(), $json[0]['isDomain']); - $this->assertSame($package_domain->cost(), $json[0]['cost']); - - $this->assertSame($package_kolab->id, $json[1]['id']); - $this->assertSame($package_kolab->title, $json[1]['title']); - $this->assertSame($package_kolab->name, $json[1]['name']); - $this->assertSame($package_kolab->description, $json[1]['description']); - $this->assertSame($package_kolab->isDomain(), $json[1]['isDomain']); - $this->assertSame($package_kolab->cost(), $json[1]['cost']); - - $this->assertSame($package_lite->id, $json[2]['id']); - $this->assertSame($package_lite->title, $json[2]['title']); - $this->assertSame($package_lite->name, $json[2]['name']); - $this->assertSame($package_lite->description, $json[2]['description']); - $this->assertSame($package_lite->isDomain(), $json[2]['isDomain']); - $this->assertSame($package_lite->cost(), $json[2]['cost']); - } -} diff --git a/src/tests/Feature/Controller/PasswordResetTest.php b/src/tests/Feature/Controller/PasswordResetTest.php deleted file mode 100644 index 215725f6..00000000 --- a/src/tests/Feature/Controller/PasswordResetTest.php +++ /dev/null @@ -1,333 +0,0 @@ -deleteTestUser('passwordresettest@' . \config('app.domain')); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('passwordresettest@' . \config('app.domain')); - - parent::tearDown(); - } - - /** - * Test password-reset/init with invalid input - */ - public function testPasswordResetInitInvalidInput(): void - { - // Empty input data - $data = []; - - $response = $this->post('/api/auth/password-reset/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - - // Data with invalid email - $data = [ - 'email' => '@example.org', - ]; - - $response = $this->post('/api/auth/password-reset/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - - // Data with valid but non-existing email - $data = [ - 'email' => 'non-existing-password-reset@example.org', - ]; - - $response = $this->post('/api/auth/password-reset/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - - // Data with valid email af an existing user with no external email - $data = [ - 'email' => 'passwordresettest@' . \config('app.domain'), - ]; - - $response = $this->post('/api/auth/password-reset/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - } - - /** - * Test password-reset/init with valid input - * - * @return array - */ - public function testPasswordResetInitValidInput() - { - Queue::fake(); - - // Assert that no jobs were pushed... - Queue::assertNothingPushed(); - - // Add required external email address to user settings - $user = $this->getTestUser('passwordresettest@' . \config('app.domain')); - $user->setSetting('external_email', 'ext@email.com'); - - $data = [ - 'email' => 'passwordresettest@' . \config('app.domain'), - ]; - - $response = $this->post('/api/auth/password-reset/init', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(2, $json); - $this->assertSame('success', $json['status']); - $this->assertNotEmpty($json['code']); - - // Assert the email sending job was pushed once - Queue::assertPushed(\App\Jobs\PasswordResetEmail::class, 1); - - // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\PasswordResetEmail::class, function ($job) use ($user, &$code, $json) { - $code = TestCase::getObjectProperty($job, 'code'); - - return $code->user->id == $user->id && $code->code == $json['code']; - }); - - return [ - 'code' => $code - ]; - } - - /** - * Test password-reset/verify with invalid input - * - * @return void - */ - public function testPasswordResetVerifyInvalidInput() - { - // Empty data - $data = []; - - $response = $this->post('/api/auth/password-reset/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // Add verification code and required external email address to user settings - $user = $this->getTestUser('passwordresettest@' . \config('app.domain')); - $code = new VerificationCode(['mode' => 'password-reset']); - $user->verificationcodes()->save($code); - - // Data with existing code but missing short_code - $data = [ - 'code' => $code->code, - ]; - - $response = $this->post('/api/auth/password-reset/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // Data with invalid code - $data = [ - 'short_code' => '123456789', - 'code' => $code->code, - ]; - - $response = $this->post('/api/auth/password-reset/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // TODO: Test expired code - } - - /** - * Test password-reset/verify with valid input - * - * @return void - */ - public function testPasswordResetVerifyValidInput() - { - // Add verification code and required external email address to user settings - $user = $this->getTestUser('passwordresettest@' . \config('app.domain')); - $code = new VerificationCode(['mode' => 'password-reset']); - $user->verificationcodes()->save($code); - - // Data with invalid code - $data = [ - 'short_code' => $code->short_code, - 'code' => $code->code, - ]; - - $response = $this->post('/api/auth/password-reset/verify', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(1, $json); - $this->assertSame('success', $json['status']); - } - - /** - * Test password-reset with invalid input - * - * @return void - */ - public function testPasswordResetInvalidInput() - { - // Empty data - $data = []; - - $response = $this->post('/api/auth/password-reset', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - - $user = $this->getTestUser('passwordresettest@' . \config('app.domain')); - $code = new VerificationCode(['mode' => 'password-reset']); - $user->verificationcodes()->save($code); - - // Data with existing code but missing password - $data = [ - 'code' => $code->code, - ]; - - $response = $this->post('/api/auth/password-reset', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - - // Data with existing code but wrong password confirmation - $data = [ - 'code' => $code->code, - 'short_code' => $code->short_code, - 'password' => 'password', - 'password_confirmation' => 'passwrong', - ]; - - $response = $this->post('/api/auth/password-reset', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - - // Data with invalid short code - $data = [ - 'code' => $code->code, - 'short_code' => '123456789', - 'password' => 'password', - 'password_confirmation' => 'password', - ]; - - $response = $this->post('/api/auth/password-reset', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - } - - /** - * Test password reset with valid input - * - * @return void - */ - public function testPasswordResetValidInput() - { - $user = $this->getTestUser('passwordresettest@' . \config('app.domain')); - $code = new VerificationCode(['mode' => 'password-reset']); - $user->verificationcodes()->save($code); - - Queue::fake(); - Queue::assertNothingPushed(); - - $data = [ - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $code->code, - 'short_code' => $code->short_code, - ]; - - $response = $this->post('/api/auth/password-reset', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertSame('success', $json['status']); - $this->assertSame('bearer', $json['token_type']); - $this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0); - $this->assertNotEmpty($json['access_token']); - $this->assertSame($user->email, $json['email']); - $this->assertSame($user->id, $json['id']); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\User\UpdateJob::class, - function ($job) use ($user) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - $userId = TestCase::getObjectProperty($job, 'userId'); - - return $userEmail == $user->email && $userId == $user->id; - } - ); - - // Check if the code has been removed - $this->assertNull(VerificationCode::find($code->code)); - - // TODO: Check password before and after (?) - - // TODO: Check if the access token works - } -} diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php deleted file mode 100644 index f532fc03..00000000 --- a/src/tests/Feature/Controller/PaymentsMollieTest.php +++ /dev/null @@ -1,595 +0,0 @@ - 'mollie']); - - $john = $this->getTestUser('john@kolab.org'); - $wallet = $john->wallets()->first(); - Payment::where('wallet_id', $wallet->id)->delete(); - Wallet::where('id', $wallet->id)->update(['balance' => 0]); - WalletSetting::where('wallet_id', $wallet->id)->delete(); - Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_CREDIT)->delete(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $john = $this->getTestUser('john@kolab.org'); - $wallet = $john->wallets()->first(); - Payment::where('wallet_id', $wallet->id)->delete(); - Wallet::where('id', $wallet->id)->update(['balance' => 0]); - WalletSetting::where('wallet_id', $wallet->id)->delete(); - Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_CREDIT)->delete(); - - parent::tearDown(); - } - - /** - * Test creating/updating/deleting an outo-payment mandate - * - * @group mollie - */ - public function testMandates(): void - { - // Unauth access not allowed - $response = $this->get("api/v4/payments/mandate"); - $response->assertStatus(401); - $response = $this->post("api/v4/payments/mandate", []); - $response->assertStatus(401); - $response = $this->put("api/v4/payments/mandate", []); - $response->assertStatus(401); - $response = $this->delete("api/v4/payments/mandate"); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - - // Test creating a mandate (invalid input) - $post = []; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); - $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); - - // Test creating a mandate (invalid input) - $post = ['amount' => 100, 'balance' => 'a']; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); - - // Test creating a mandate (invalid input) - $post = ['amount' => -100, 'balance' => 0]; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - // Test creating a mandate (valid input) - $post = ['amount' => 20.10, 'balance' => 0]; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); - - // Test fetching the mandate information - $response = $this->actingAs($user)->get("api/v4/payments/mandate"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals(20.10, $json['amount']); - $this->assertEquals(0, $json['balance']); - $this->assertEquals('Credit Card', $json['method']); - $this->assertSame(true, $json['isPending']); - $this->assertSame(false, $json['isValid']); - $this->assertSame(false, $json['isDisabled']); - - $mandate_id = $json['id']; - - // We would have to invoke a browser to accept the "first payment" to make - // the mandate validated/completed. Instead, we'll mock the mandate object. - $mollie_response = [ - 'resource' => 'mandate', - 'id' => $mandate_id, - 'status' => 'valid', - 'method' => 'creditcard', - 'details' => [ - 'cardNumber' => '4242', - 'cardLabel' => 'Visa', - ], - 'customerId' => 'cst_GMfxGPt7Gj', - 'createdAt' => '2020-04-28T11:09:47+00:00', - ]; - - $responseStack = $this->mockMollie(); - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $wallet = $user->wallets()->first(); - $wallet->setSetting('mandate_disabled', 1); - - $response = $this->actingAs($user)->get("api/v4/payments/mandate"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals(20.10, $json['amount']); - $this->assertEquals(0, $json['balance']); - $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); - $this->assertSame(false, $json['isPending']); - $this->assertSame(true, $json['isValid']); - $this->assertSame(true, $json['isDisabled']); - - Bus::fake(); - $wallet->setSetting('mandate_disabled', null); - - // Test updating mandate details (invalid input) - $post = []; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); - $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); - - $post = ['amount' => -100, 'balance' => 0]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - // Test updating a mandate (valid input) - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $post = ['amount' => 30.10, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The auto-payment has been updated.', $json['message']); - $this->assertSame($mandate_id, $json['id']); - $this->assertFalse($json['isDisabled']); - - $wallet = $user->wallets()->first(); - - $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); - $this->assertEquals(1, $wallet->getSetting('mandate_balance')); - - // Test updating a disabled mandate (invalid input) - $wallet->setSetting('mandate_disabled', 1); - $wallet->balance = -2000; - $wallet->save(); - $user->refresh(); // required so the controller sees the wallet update from above - - $post = ['amount' => 15.10, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); - - // Test updating a disabled mandate (valid input) - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $post = ['amount' => 30, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The auto-payment has been updated.', $json['message']); - $this->assertSame($mandate_id, $json['id']); - $this->assertFalse($json['isDisabled']); - - Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); - Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { - $job_wallet = $this->getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - $this->unmockMollie(); - - // Delete mandate - $response = $this->actingAs($user)->delete("api/v4/payments/mandate"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The auto-payment has been removed.', $json['message']); - - // Confirm with Mollie the mandate does not exist - $customer_id = $wallet->getSetting('mollie_id'); - $this->expectException(\Mollie\Api\Exceptions\ApiException::class); - $this->expectExceptionMessageMatches('/410: Gone/'); - $mandate = mollie()->mandates()->getForId($customer_id, $mandate_id); - - $this->assertNull($wallet->fresh()->getSetting('mollie_mandate_id')); - } - - /** - * Test creating a payment and receiving a status via webhook - * - * @group mollie - */ - public function testStoreAndWebhook(): void - { - Bus::fake(); - - // Unauth access not allowed - $response = $this->post("api/v4/payments", []); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - - $post = ['amount' => -1]; - $response = $this->actingAs($user)->post("api/v4/payments", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - $post = ['amount' => '12.34']; - $response = $this->actingAs($user)->post("api/v4/payments", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); - - $wallet = $user->wallets()->first(); - $payments = Payment::where('wallet_id', $wallet->id)->get(); - - $this->assertCount(1, $payments); - $payment = $payments[0]; - $this->assertSame(1234, $payment->amount); - $this->assertSame(\config('app.name') . ' Payment', $payment->description); - $this->assertSame('open', $payment->status); - $this->assertEquals(0, $wallet->balance); - - // Test the webhook - // Note: Webhook end-point does not require authentication - - $mollie_response = [ - "resource" => "payment", - "id" => $payment->id, - "status" => "paid", - // Status is not enough, paidAt is used to distinguish the state - "paidAt" => date('c'), - "mode" => "test", - ]; - - // We'll trigger the webhook with payment id and use mocking for - // a request to the Mollie payments API. We cannot force Mollie - // to make the payment status change. - $responseStack = $this->mockMollie(); - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $post = ['id' => $payment->id]; - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - $transaction = $wallet->transactions() - ->where('type', Transaction::WALLET_CREDIT)->get()->last(); - - $this->assertSame(1234, $transaction->amount); - $this->assertSame( - "Payment transaction {$payment->id} using Mollie", - $transaction->description - ); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - - // Verify "paid -> open -> paid" scenario, assert that balance didn't change - $mollie_response['status'] = 'open'; - unset($mollie_response['paidAt']); - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - $mollie_response['status'] = 'paid'; - $mollie_response['paidAt'] = date('c'); - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - // Test for payment failure - Bus::fake(); - - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $mollie_response = [ - "resource" => "payment", - "id" => $payment->id, - "status" => "failed", - "mode" => "test", - ]; - - // We'll trigger the webhook with payment id and use mocking for - // a request to the Mollie payments API. We cannot force Mollie - // to make the payment status change. - $responseStack = $this->mockMollie(); - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $this->assertSame('failed', $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - } - - /** - * Test automatic payment charges - * - * @group mollie - */ - public function testTopUp(): void - { - Bus::fake(); - - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - - // Create a valid mandate first - $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 10]); - - // Expect a recurring payment as we have a valid mandate at this point - $result = PaymentsController::topUpWallet($wallet); - $this->assertTrue($result); - - // Check that the payments table contains a new record with proper amount - // There should be two records, one for the first payment and another for - // the recurring payment - $this->assertCount(1, $wallet->payments()->get()); - $payment = $wallet->payments()->first(); - $this->assertSame(2010, $payment->amount); - - // In mollie we don't have to wait for a webhook, the response to - // PaymentIntent already sets the status to 'paid', so we can test - // immediately the balance update - // Assert that email notification job has been dispatched - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); - $this->assertEquals(2010, $wallet->fresh()->balance); - $transaction = $wallet->transactions() - ->where('type', Transaction::WALLET_CREDIT)->get()->last(); - - $this->assertSame(2010, $transaction->amount); - $this->assertSame( - "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 6787)", - $transaction->description - ); - - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { - $job_payment = $this->getObjectProperty($job, 'payment'); - return $job_payment->id === $payment->id; - }); - - // Expect no payment if the mandate is disabled - $wallet->setSetting('mandate_disabled', 1); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - // Expect no payment if balance is ok - $wallet->setSetting('mandate_disabled', null); - $wallet->balance = 1000; - $wallet->save(); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - // Expect no payment if the top-up amount is not enough - $wallet->setSetting('mandate_disabled', null); - $wallet->balance = -2050; - $wallet->save(); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { - $job_wallet = $this->getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - // Expect no payment if there's no mandate - $wallet->setSetting('mollie_mandate_id', null); - $wallet->balance = 0; - $wallet->save(); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); - - // Test webhook for recurring payments - - $wallet->transactions()->delete(); - - $responseStack = $this->mockMollie(); - Bus::fake(); - - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $mollie_response = [ - "resource" => "payment", - "id" => $payment->id, - "status" => "paid", - // Status is not enough, paidAt is used to distinguish the state - "paidAt" => date('c'), - "mode" => "test", - ]; - - // We'll trigger the webhook with payment id and use mocking for - // a request to the Mollie payments API. We cannot force Mollie - // to make the payment status change. - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $post = ['id' => $payment->id]; - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(2010, $wallet->fresh()->balance); - - $transaction = $wallet->transactions() - ->where('type', Transaction::WALLET_CREDIT)->get()->last(); - - $this->assertSame(2010, $transaction->amount); - $this->assertSame( - "Auto-payment transaction {$payment->id} using Mollie", - $transaction->description - ); - - // Assert that email notification job has been dispatched - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { - $job_payment = $this->getObjectProperty($job, 'payment'); - return $job_payment->id === $payment->id; - }); - - Bus::fake(); - - // Test for payment failure - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $wallet->setSetting('mollie_mandate_id', 'xxx'); - $wallet->setSetting('mandate_disabled', null); - - $mollie_response = [ - "resource" => "payment", - "id" => $payment->id, - "status" => "failed", - "mode" => "test", - ]; - - $responseStack->append(new Response(200, [], json_encode($mollie_response))); - - $response = $this->post("api/webhooks/payment/mollie", $post); - $response->assertStatus(200); - - $wallet->refresh(); - - $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); - $this->assertEquals(2010, $wallet->balance); - $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); - - // Assert that email notification job has been dispatched - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { - $job_payment = $this->getObjectProperty($job, 'payment'); - return $job_payment->id === $payment->id; - }); - - $responseStack = $this->unmockMollie(); - } - - /** - * Create Mollie's auto-payment mandate using our API and Chrome browser - */ - protected function createMandate(Wallet $wallet, array $params) - { - // Use the API to create a first payment with a mandate - $response = $this->actingAs($wallet->owner)->post("api/v4/payments/mandate", $params); - $response->assertStatus(200); - $json = $response->json(); - - // There's no easy way to confirm a created mandate. - // The only way seems to be to fire up Chrome on checkout page - // and do actions with use of Dusk browser. - $this->startBrowser() - ->visit($json['redirectUrl']) - ->click('input[value="paid"]') - ->click('button.form__button'); - - $this->stopBrowser(); - } -} diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php deleted file mode 100644 index ea772d6b..00000000 --- a/src/tests/Feature/Controller/PaymentsStripeTest.php +++ /dev/null @@ -1,677 +0,0 @@ - 'stripe']); - - $john = $this->getTestUser('john@kolab.org'); - $wallet = $john->wallets()->first(); - Payment::where('wallet_id', $wallet->id)->delete(); - Wallet::where('id', $wallet->id)->update(['balance' => 0]); - WalletSetting::where('wallet_id', $wallet->id)->delete(); - Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_CREDIT)->delete(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $john = $this->getTestUser('john@kolab.org'); - $wallet = $john->wallets()->first(); - Payment::where('wallet_id', $wallet->id)->delete(); - Wallet::where('id', $wallet->id)->update(['balance' => 0]); - WalletSetting::where('wallet_id', $wallet->id)->delete(); - Transaction::where('object_id', $wallet->id) - ->where('type', Transaction::WALLET_CREDIT)->delete(); - - parent::tearDown(); - } - - /** - * Test creating/updating/deleting an outo-payment mandate - * - * @group stripe - */ - public function testMandates(): void - { - Bus::fake(); - - // Unauth access not allowed - $response = $this->get("api/v4/payments/mandate"); - $response->assertStatus(401); - $response = $this->post("api/v4/payments/mandate", []); - $response->assertStatus(401); - $response = $this->put("api/v4/payments/mandate", []); - $response->assertStatus(401); - $response = $this->delete("api/v4/payments/mandate"); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - - // Test creating a mandate (invalid input) - $post = []; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); - $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); - - // Test creating a mandate (invalid input) - $post = ['amount' => 100, 'balance' => 'a']; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); - - // Test creating a mandate (invalid input) - $post = ['amount' => -100, 'balance' => 0]; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - // Test creating a mandate (valid input) - $post = ['amount' => 20.10, 'balance' => 0]; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertRegExp('|^cs_test_|', $json['id']); - - // Test fetching the mandate information - $response = $this->actingAs($user)->get("api/v4/payments/mandate"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals(20.10, $json['amount']); - $this->assertEquals(0, $json['balance']); - $this->assertSame(false, $json['isDisabled']); - - // We would have to invoke a browser to accept the "first payment" to make - // the mandate validated/completed. Instead, we'll mock the mandate object. - $setupIntent = '{ - "id": "AAA", - "object": "setup_intent", - "created": 123456789, - "payment_method": "pm_YYY", - "status": "succeeded", - "usage": "off_session", - "customer": null - }'; - - $paymentMethod = '{ - "id": "pm_YYY", - "object": "payment_method", - "card": { - "brand": "visa", - "country": "US", - "last4": "4242" - }, - "created": 123456789, - "type": "card" - }'; - - $client = $this->mockStripe(); - $client->addResponse($setupIntent); - $client->addResponse($paymentMethod); - - // As we do not use checkout page, we do not receive a webworker request - // I.e. we have to fake the mandate id - $wallet = $user->wallets()->first(); - $wallet->setSetting('stripe_mandate_id', 'AAA'); - $wallet->setSetting('mandate_disabled', 1); - - $response = $this->actingAs($user)->get("api/v4/payments/mandate"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals(20.10, $json['amount']); - $this->assertEquals(0, $json['balance']); - $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); - $this->assertSame(false, $json['isPending']); - $this->assertSame(true, $json['isValid']); - $this->assertSame(true, $json['isDisabled']); - - // Test updating mandate details (invalid input) - $wallet->setSetting('mandate_disabled', null); - $user->refresh(); - $post = []; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); - $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); - - $post = ['amount' => -100, 'balance' => 0]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - // Test updating a mandate (valid input) - $client->addResponse($setupIntent); - $client->addResponse($paymentMethod); - - $post = ['amount' => 30.10, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The auto-payment has been updated.', $json['message']); - $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); - $this->assertEquals(1, $wallet->getSetting('mandate_balance')); - $this->assertSame('AAA', $json['id']); - $this->assertFalse($json['isDisabled']); - - // Test updating a disabled mandate (invalid input) - $wallet->setSetting('mandate_disabled', 1); - $wallet->balance = -2000; - $wallet->save(); - $user->refresh(); // required so the controller sees the wallet update from above - - $post = ['amount' => 15.10, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); - - // Test updating a disabled mandate (valid input) - $client->addResponse($setupIntent); - $client->addResponse($paymentMethod); - - $post = ['amount' => 30, 'balance' => 1]; - $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame('The auto-payment has been updated.', $json['message']); - $this->assertSame('AAA', $json['id']); - $this->assertFalse($json['isDisabled']); - - Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); - Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { - $job_wallet = $this->getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - - $this->unmockStripe(); - - // TODO: Delete mandate - } - - /** - * Test creating a payment and receiving a status via webhook - * - * @group stripe - */ - public function testStoreAndWebhook(): void - { - Bus::fake(); - - // Unauth access not allowed - $response = $this->post("api/v4/payments", []); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - - $post = ['amount' => -1]; - $response = $this->actingAs($user)->post("api/v4/payments", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; - $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); - - $post = ['amount' => '12.34']; - $response = $this->actingAs($user)->post("api/v4/payments", $post); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertRegExp('|^cs_test_|', $json['id']); - - $wallet = $user->wallets()->first(); - $payments = Payment::where('wallet_id', $wallet->id)->get(); - - $this->assertCount(1, $payments); - $payment = $payments[0]; - $this->assertSame(1234, $payment->amount); - $this->assertSame(\config('app.name') . ' Payment', $payment->description); - $this->assertSame('open', $payment->status); - $this->assertEquals(0, $wallet->balance); - - // Test the webhook - - $post = [ - 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", - 'object' => "event", - 'api_version' => "2020-03-02", - 'created' => 1590147209, - 'data' => [ - 'object' => [ - 'id' => $payment->id, - 'object' => "payment_intent", - 'amount' => 1234, - 'amount_capturable' => 0, - 'amount_received' => 1234, - 'capture_method' => "automatic", - 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", - 'confirmation_method' => "automatic", - 'created' => 1590147204, - 'currency' => "chf", - 'customer' => "cus_HKDZ53OsKdlM83", - 'last_payment_error' => null, - 'livemode' => false, - 'metadata' => [], - 'receipt_email' => "payment-test@kolabnow.com", - 'status' => "succeeded" - ] - ], - 'type' => "payment_intent.succeeded" - ]; - - // Test payment succeeded event - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - $transaction = $wallet->transactions() - ->where('type', Transaction::WALLET_CREDIT)->get()->last(); - - $this->assertSame(1234, $transaction->amount); - $this->assertSame( - "Payment transaction {$payment->id} using Stripe", - $transaction->description - ); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - - // Test that balance didn't change if the same event is posted - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - // Test for payment failure ('failed' status) - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $post['type'] = "payment_intent.payment_failed"; - $post['data']['object']['status'] = 'failed'; - - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - - // Test for payment failure ('canceled' status) - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $post['type'] = "payment_intent.canceled"; - $post['data']['object']['status'] = 'canceled'; - - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); - $this->assertEquals(1234, $wallet->fresh()->balance); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - } - - /** - * Test receiving webhook request for setup intent - * - * @group stripe - */ - public function testCreateMandateAndWebhook(): void - { - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - - // Test creating a mandate (valid input) - $post = ['amount' => 20.10, 'balance' => 0]; - $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); - $response->assertStatus(200); - - $payment = $wallet->payments()->first(); - - $this->assertSame(PaymentProvider::STATUS_OPEN, $payment->status); - $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); - $this->assertSame(0, $payment->amount); - - $post = [ - 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", - 'object' => "event", - 'api_version' => "2020-03-02", - 'created' => 1590147209, - 'data' => [ - 'object' => [ - 'id' => $payment->id, - 'object' => "setup_intent", - 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", - 'created' => 1590147204, - 'customer' => "cus_HKDZ53OsKdlM83", - 'last_setup_error' => null, - 'metadata' => [], - 'status' => "succeeded" - ] - ], - 'type' => "setup_intent.succeeded" - ]; - - // Test payment succeeded event - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $payment->refresh(); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); - $this->assertSame($payment->id, $wallet->fresh()->getSetting('stripe_mandate_id')); - - // TODO: test other setup_intent.* events - } - - /** - * Test automatic payment charges - * - * @group stripe - */ - public function testTopUpAndWebhook(): void - { - $this->markTestIncomplete(); - - Bus::fake(); - - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - - // Stripe API does not allow us to create a mandate easily - // That's why we we'll mock API responses - // Create a fake mandate - $wallet->setSettings([ - 'mandate_amount' => 20.10, - 'mandate_balance' => 10, - 'stripe_mandate_id' => 'AAA', - ]); - - $setupIntent = json_encode([ - "id" => "AAA", - "object" => "setup_intent", - "created" => 123456789, - "payment_method" => "pm_YYY", - "status" => "succeeded", - "usage" => "off_session", - "customer" => null - ]); - - $paymentMethod = json_encode([ - "id" => "pm_YYY", - "object" => "payment_method", - "card" => [ - "brand" => "visa", - "country" => "US", - "last4" => "4242" - ], - "created" => 123456789, - "type" => "card" - ]); - - $paymentIntent = json_encode([ - "id" => "pi_XX", - "object" => "payment_intent", - "created" => 123456789, - "amount" => 2010, - "currency" => "chf", - "description" => "Kolab Recurring Payment" - ]); - - $client = $this->mockStripe(); - $client->addResponse($setupIntent); - $client->addResponse($paymentMethod); - $client->addResponse($setupIntent); - $client->addResponse($paymentIntent); - - // Expect a recurring payment as we have a valid mandate at this point - $result = PaymentsController::topUpWallet($wallet); - $this->assertTrue($result); - - // Check that the payments table contains a new record with proper amount - // There should be two records, one for the first payment and another for - // the recurring payment - $this->assertCount(1, $wallet->payments()->get()); - $payment = $wallet->payments()->first(); - $this->assertSame(2010, $payment->amount); - $this->assertSame(\config('app.name') . " Recurring Payment", $payment->description); - $this->assertSame("pi_XX", $payment->id); - - // Expect no payment if the mandate is disabled - $wallet->setSetting('mandate_disabled', 1); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - // Expect no payment if balance is ok - $wallet->setSetting('mandate_disabled', null); - $wallet->balance = 1000; - $wallet->save(); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - // Expect no payment if the top-up amount is not enough - $wallet->setSetting('mandate_disabled', null); - $wallet->balance = -2050; - $wallet->save(); - - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { - $job_wallet = $this->getObjectProperty($job, 'wallet'); - return $job_wallet->id === $wallet->id; - }); - - // Expect no payment if there's no mandate - $wallet->setSetting('mollie_mandate_id', null); - $wallet->balance = 0; - $wallet->save(); - $result = PaymentsController::topUpWallet($wallet); - $this->assertFalse($result); - $this->assertCount(1, $wallet->payments()->get()); - - Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); - - $this->unmockStripe(); - - // Test webhook - - $post = [ - 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", - 'object' => "event", - 'api_version' => "2020-03-02", - 'created' => 1590147209, - 'data' => [ - 'object' => [ - 'id' => $payment->id, - 'object' => "payment_intent", - 'amount' => 2010, - 'capture_method' => "automatic", - 'created' => 1590147204, - 'currency' => "chf", - 'customer' => "cus_HKDZ53OsKdlM83", - 'last_payment_error' => null, - 'metadata' => [], - 'receipt_email' => "payment-test@kolabnow.com", - 'status' => "succeeded" - ] - ], - 'type' => "payment_intent.succeeded" - ]; - - // Test payment succeeded event - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); - $this->assertEquals(2010, $wallet->fresh()->balance); - $transaction = $wallet->transactions() - ->where('type', Transaction::WALLET_CREDIT)->get()->last(); - - $this->assertSame(2010, $transaction->amount); - $this->assertSame( - "Auto-payment transaction {$payment->id} using Stripe", - $transaction->description - ); - - // Assert that email notification job has been dispatched - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { - $job_payment = $this->getObjectProperty($job, 'payment'); - return $job_payment->id === $payment->id; - }); - - Bus::fake(); - - // Test for payment failure ('failed' status) - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $wallet->setSetting('mandate_disabled', null); - - $post['type'] = "payment_intent.payment_failed"; - $post['data']['object']['status'] = 'failed'; - - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $wallet->refresh(); - - $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); - $this->assertEquals(2010, $wallet->balance); - $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); - - // Assert that email notification job has been dispatched - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); - Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { - $job_payment = $this->getObjectProperty($job, 'payment'); - return $job_payment->id === $payment->id; - }); - - Bus::fake(); - - // Test for payment failure ('canceled' status) - $payment->refresh(); - $payment->status = PaymentProvider::STATUS_OPEN; - $payment->save(); - - $post['type'] = "payment_intent.canceled"; - $post['data']['object']['status'] = 'canceled'; - - $response = $this->webhookRequest($post); - $response->assertStatus(200); - - $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); - $this->assertEquals(2010, $wallet->fresh()->balance); - - // Assert that email notification job wasn't dispatched, - // it is expected only for recurring payments - Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); - } - - /** - * Generate Stripe-Signature header for a webhook payload - */ - protected function webhookRequest($post) - { - $secret = \config('services.stripe.webhook_secret'); - $ts = time(); - - $payload = "$ts." . json_encode($post); - $sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret)); - - return $this->withHeaders(['Stripe-Signature' => $sig]) - ->json('POST', "api/webhooks/payment/stripe", $post); - } -} diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php deleted file mode 100644 index 60225e05..00000000 --- a/src/tests/Feature/Controller/SignupTest.php +++ /dev/null @@ -1,689 +0,0 @@ -domain = $this->getPublicDomain(); - - $this->deleteTestUser("SignupControllerTest1@$this->domain"); - $this->deleteTestUser("signuplogin@$this->domain"); - $this->deleteTestUser("admin@external.com"); - - $this->deleteTestDomain('external.com'); - $this->deleteTestDomain('signup-domain.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser("SignupControllerTest1@$this->domain"); - $this->deleteTestUser("signuplogin@$this->domain"); - $this->deleteTestUser("admin@external.com"); - - $this->deleteTestDomain('external.com'); - $this->deleteTestDomain('signup-domain.com'); - - parent::tearDown(); - } - - /** - * Return a public domain for signup tests - */ - private function getPublicDomain(): string - { - if (!$this->domain) { - $this->refreshApplication(); - $public_domains = Domain::getPublicDomains(); - $this->domain = reset($public_domains); - - if (empty($this->domain)) { - $this->domain = 'signup-domain.com'; - Domain::create([ - 'namespace' => $this->domain, - 'status' => Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC, - ]); - } - } - - return $this->domain; - } - - /** - * Test fetching plans for signup - * - * @return void - */ - public function testSignupPlans() - { - $response = $this->get('/api/auth/signup/plans'); - $json = $response->json(); - - $response->assertStatus(200); - - $this->assertSame('success', $json['status']); - $this->assertCount(2, $json['plans']); - $this->assertArrayHasKey('title', $json['plans'][0]); - $this->assertArrayHasKey('name', $json['plans'][0]); - $this->assertArrayHasKey('description', $json['plans'][0]); - $this->assertArrayHasKey('button', $json['plans'][0]); - } - - /** - * Test signup initialization with invalid input - * - * @return void - */ - public function testSignupInitInvalidInput() - { - // Empty input data - $data = []; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - - // Data with missing name - $data = [ - 'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com', - 'first_name' => str_repeat('a', 250), - 'last_name' => str_repeat('a', 250), - ]; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('first_name', $json['errors']); - $this->assertArrayHasKey('last_name', $json['errors']); - - // Data with invalid email (but not phone number) - $data = [ - 'email' => '@example.org', - 'first_name' => 'Signup', - 'last_name' => 'User', - ]; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('email', $json['errors']); - - // Sanity check on voucher code, last/first name is optional - $data = [ - 'voucher' => '123456789012345678901234567890123', - 'email' => 'valid@email.com', - ]; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('voucher', $json['errors']); - - // TODO: Test phone validation - } - - /** - * Test signup initialization with valid input - * - * @return array - */ - public function testSignupInitValidInput() - { - Queue::fake(); - - // Assert that no jobs were pushed... - Queue::assertNothingPushed(); - - $data = [ - 'email' => 'testuser@external.com', - 'first_name' => 'Signup', - 'last_name' => 'User', - 'plan' => 'individual', - ]; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(2, $json); - $this->assertSame('success', $json['status']); - $this->assertNotEmpty($json['code']); - - // Assert the email sending job was pushed once - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); - - // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { - $code = TestCase::getObjectProperty($job, 'code'); - - return $code->code === $json['code'] - && $code->data['plan'] === $data['plan'] - && $code->data['email'] === $data['email'] - && $code->data['first_name'] === $data['first_name'] - && $code->data['last_name'] === $data['last_name']; - }); - - // Try the same with voucher - $data['voucher'] = 'TEST'; - - $response = $this->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(2, $json); - $this->assertSame('success', $json['status']); - $this->assertNotEmpty($json['code']); - - // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { - $code = TestCase::getObjectProperty($job, 'code'); - - return $code->code === $json['code'] - && $code->data['plan'] === $data['plan'] - && $code->data['email'] === $data['email'] - && $code->data['voucher'] === $data['voucher'] - && $code->data['first_name'] === $data['first_name'] - && $code->data['last_name'] === $data['last_name']; - }); - - return [ - 'code' => $json['code'], - 'email' => $data['email'], - 'first_name' => $data['first_name'], - 'last_name' => $data['last_name'], - 'plan' => $data['plan'], - 'voucher' => $data['voucher'] - ]; - } - - /** - * Test signup code verification with invalid input - * - * @depends testSignupInitValidInput - * @return void - */ - public function testSignupVerifyInvalidInput(array $result) - { - // Empty data - $data = []; - - $response = $this->post('/api/auth/signup/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('code', $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // Data with existing code but missing short_code - $data = [ - 'code' => $result['code'], - ]; - - $response = $this->post('/api/auth/signup/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // Data with invalid short_code - $data = [ - 'code' => $result['code'], - 'short_code' => 'XXXX', - ]; - - $response = $this->post('/api/auth/signup/verify', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // TODO: Test expired code - } - - /** - * Test signup code verification with valid input - * - * @depends testSignupInitValidInput - * - * @return array - */ - public function testSignupVerifyValidInput(array $result) - { - $code = SignupCode::find($result['code']); - $data = [ - 'code' => $code->code, - 'short_code' => $code->short_code, - ]; - - $response = $this->post('/api/auth/signup/verify', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(7, $json); - $this->assertSame('success', $json['status']); - $this->assertSame($result['email'], $json['email']); - $this->assertSame($result['first_name'], $json['first_name']); - $this->assertSame($result['last_name'], $json['last_name']); - $this->assertSame($result['voucher'], $json['voucher']); - $this->assertSame(false, $json['is_domain']); - $this->assertTrue(is_array($json['domains']) && !empty($json['domains'])); - - return $result; - } - - /** - * Test last signup step with invalid input - * - * @depends testSignupVerifyValidInput - * @return void - */ - public function testSignupInvalidInput(array $result) - { - // Empty data - $data = []; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(3, $json['errors']); - $this->assertArrayHasKey('login', $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - $this->assertArrayHasKey('domain', $json['errors']); - - $domain = $this->getPublicDomain(); - - // Passwords do not match and missing domain - $data = [ - 'login' => 'test', - 'password' => 'test', - 'password_confirmation' => 'test2', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('password', $json['errors']); - $this->assertArrayHasKey('domain', $json['errors']); - - $domain = $this->getPublicDomain(); - - // Login too short - $data = [ - 'login' => '1', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('login', $json['errors']); - - // Missing codes - $data = [ - 'login' => 'login-valid', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertArrayHasKey('code', $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - // Data with invalid short_code - $data = [ - 'login' => 'TestLogin', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $result['code'], - 'short_code' => 'XXXX', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('short_code', $json['errors']); - - $code = SignupCode::find($result['code']); - - // Data with invalid voucher - $data = [ - 'login' => 'TestLogin', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $result['code'], - 'short_code' => $code->short_code, - 'voucher' => 'XXX', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('voucher', $json['errors']); - - // Valid code, invalid login - $data = [ - 'login' => 'żżżżżż', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $result['code'], - 'short_code' => $code->short_code, - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(422); - $this->assertSame('error', $json['status']); - $this->assertCount(1, $json['errors']); - $this->assertArrayHasKey('login', $json['errors']); - } - - /** - * Test last signup step with valid input (user creation) - * - * @depends testSignupVerifyValidInput - * @return void - */ - public function testSignupValidInput(array $result) - { - $queue = Queue::fake(); - - $domain = $this->getPublicDomain(); - $identity = \strtolower('SignupLogin@') . $domain; - $code = SignupCode::find($result['code']); - $data = [ - 'login' => 'SignupLogin', - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $code->code, - 'short_code' => $code->short_code, - 'voucher' => 'TEST', - ]; - - $response = $this->post('/api/auth/signup', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertSame('success', $json['status']); - $this->assertSame('bearer', $json['token_type']); - $this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0); - $this->assertNotEmpty($json['access_token']); - $this->assertSame($identity, $json['email']); - - Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\User\CreateJob::class, - function ($job) use ($data) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - return $userEmail === \strtolower($data['login'] . '@' . $data['domain']); - } - ); - - // Check if the code has been removed - $this->assertNull(SignupCode::where('code', $result['code'])->first()); - - // Check if the user has been created - $user = User::where('email', $identity)->first(); - - $this->assertNotEmpty($user); - $this->assertSame($identity, $user->email); - - // Check user settings - $this->assertSame($result['first_name'], $user->getSetting('first_name')); - $this->assertSame($result['last_name'], $user->getSetting('last_name')); - $this->assertSame($result['email'], $user->getSetting('external_email')); - - // Discount - $discount = Discount::where('code', 'TEST')->first(); - $this->assertSame($discount->id, $user->wallets()->first()->discount_id); - - // TODO: Check SKUs/Plan - - // TODO: Check if the access token works - } - - /** - * Test signup for a group (custom domain) account - * - * @return void - */ - public function testSignupGroupAccount() - { - Queue::fake(); - - // Initial signup request - $user_data = $data = [ - 'email' => 'testuser@external.com', - 'first_name' => 'Signup', - 'last_name' => 'User', - 'plan' => 'group', - ]; - - $response = $this->withoutMiddleware()->post('/api/auth/signup/init', $data); - $json = $response->json(); - - $response->assertStatus(200); - $this->assertCount(2, $json); - $this->assertSame('success', $json['status']); - $this->assertNotEmpty($json['code']); - - // Assert the email sending job was pushed once - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); - - // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { - $code = TestCase::getObjectProperty($job, 'code'); - - return $code->code === $json['code'] - && $code->data['plan'] === $data['plan'] - && $code->data['email'] === $data['email'] - && $code->data['first_name'] === $data['first_name'] - && $code->data['last_name'] === $data['last_name']; - }); - - // Verify the code - $code = SignupCode::find($json['code']); - $data = [ - 'code' => $code->code, - 'short_code' => $code->short_code, - ]; - - $response = $this->post('/api/auth/signup/verify', $data); - $result = $response->json(); - - $response->assertStatus(200); - $this->assertCount(7, $result); - $this->assertSame('success', $result['status']); - $this->assertSame($user_data['email'], $result['email']); - $this->assertSame($user_data['first_name'], $result['first_name']); - $this->assertSame($user_data['last_name'], $result['last_name']); - $this->assertSame(null, $result['voucher']); - $this->assertSame(true, $result['is_domain']); - $this->assertSame([], $result['domains']); - - // Final signup request - $login = 'admin'; - $domain = 'external.com'; - $data = [ - 'login' => $login, - 'domain' => $domain, - 'password' => 'test', - 'password_confirmation' => 'test', - 'code' => $code->code, - 'short_code' => $code->short_code, - ]; - - $response = $this->post('/api/auth/signup', $data); - $result = $response->json(); - - $response->assertStatus(200); - $this->assertSame('success', $result['status']); - $this->assertSame('bearer', $result['token_type']); - $this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0); - $this->assertNotEmpty($result['access_token']); - $this->assertSame("$login@$domain", $result['email']); - - Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\Domain\CreateJob::class, - function ($job) use ($domain) { - $domainNamespace = TestCase::getObjectProperty($job, 'domainNamespace'); - - return $domainNamespace === $domain; - } - ); - - Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\User\CreateJob::class, - function ($job) use ($data) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - - return $userEmail === $data['login'] . '@' . $data['domain']; - } - ); - - // Check if the code has been removed - $this->assertNull(SignupCode::find($code->id)); - - // Check if the user has been created - $user = User::where('email', $login . '@' . $domain)->first(); - - $this->assertNotEmpty($user); - - // Check user settings - $this->assertSame($user_data['email'], $user->getSetting('external_email')); - $this->assertSame($user_data['first_name'], $user->getSetting('first_name')); - $this->assertSame($user_data['last_name'], $user->getSetting('last_name')); - - // TODO: Check domain record - - // TODO: Check SKUs/Plan - - // TODO: Check if the access token works - } - - /** - * List of login/domain validation cases for testValidateLogin() - * - * @return array Arguments for testValidateLogin() - */ - public function dataValidateLogin(): array - { - $domain = $this->getPublicDomain(); - - return [ - // Individual account - ['', $domain, false, ['login' => 'The login field is required.']], - ['test123456', 'localhost', false, ['domain' => 'The specified domain is invalid.']], - ['test123456', 'unknown-domain.org', false, ['domain' => 'The specified domain is invalid.']], - ['test.test', $domain, false, null], - ['test_test', $domain, false, null], - ['test-test', $domain, false, null], - ['admin', $domain, false, ['login' => 'The specified login is not available.']], - ['administrator', $domain, false, ['login' => 'The specified login is not available.']], - ['sales', $domain, false, ['login' => 'The specified login is not available.']], - ['root', $domain, false, ['login' => 'The specified login is not available.']], - - // TODO existing (public domain) user - // ['signuplogin', $domain, false, ['login' => 'The specified login is not available.']], - - // Domain account - ['admin', 'kolabsys.com', true, null], - ['testnonsystemdomain', 'invalid', true, ['domain' => 'The specified domain is invalid.']], - ['testnonsystemdomain', '.com', true, ['domain' => 'The specified domain is invalid.']], - - // existing custom domain - ['jack', 'kolab.org', true, ['domain' => 'The specified domain is not available.']], - ]; - } - - /** - * Signup login/domain validation. - * - * Note: Technically these include unit tests, but let's keep it here for now. - * FIXME: Shall we do a http request for each case? - * - * @dataProvider dataValidateLogin - */ - public function testValidateLogin($login, $domain, $external, $expected_result): void - { - $result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]); - - $this->assertSame($expected_result, $result); - } -} diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php deleted file mode 100644 index b1245308..00000000 --- a/src/tests/Feature/Controller/SkusTest.php +++ /dev/null @@ -1,70 +0,0 @@ -get("api/v4/skus"); - $response->assertStatus(401); - - $user = $this->getTestUser('john@kolab.org'); - $sku = Sku::where('title', 'mailbox')->first(); - - $response = $this->actingAs($user)->get("api/v4/skus"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(9, $json); - - $this->assertSame(100, $json[0]['prio']); - $this->assertSame($sku->id, $json[0]['id']); - $this->assertSame($sku->title, $json[0]['title']); - $this->assertSame($sku->name, $json[0]['name']); - $this->assertSame($sku->description, $json[0]['description']); - $this->assertSame($sku->cost, $json[0]['cost']); - $this->assertSame($sku->units_free, $json[0]['units_free']); - $this->assertSame($sku->period, $json[0]['period']); - $this->assertSame($sku->active, $json[0]['active']); - $this->assertSame('user', $json[0]['type']); - $this->assertSame('mailbox', $json[0]['handler']); - } - - /** - * Test for SkusController::skuElement() - */ - public function testSkuElement(): void - { - $sku = Sku::where('title', 'storage')->first(); - $result = $this->invokeMethod(new SkusController(), 'skuElement', [$sku]); - - $this->assertSame($sku->id, $result['id']); - $this->assertSame($sku->title, $result['title']); - $this->assertSame($sku->name, $result['name']); - $this->assertSame($sku->description, $result['description']); - $this->assertSame($sku->cost, $result['cost']); - $this->assertSame($sku->units_free, $result['units_free']); - $this->assertSame($sku->period, $result['period']); - $this->assertSame($sku->active, $result['active']); - $this->assertSame('user', $result['type']); - $this->assertSame('storage', $result['handler']); - $this->assertSame($sku->units_free, $result['range']['min']); - $this->assertSame($sku->handler_class::MAX_ITEMS, $result['range']['max']); - $this->assertSame($sku->handler_class::ITEM_UNIT, $result['range']['unit']); - $this->assertTrue($result['readonly']); - $this->assertTrue($result['enabled']); - - // Test all SKU types - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php deleted file mode 100644 index 4033397d..00000000 --- a/src/tests/Feature/Controller/UsersTest.php +++ /dev/null @@ -1,1068 +0,0 @@ -deleteTestUser('jane@kolabnow.com'); - $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); - $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); - $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); - $this->deleteTestUser('john2.doe2@kolab.org'); - $this->deleteTestUser('deleted@kolab.org'); - $this->deleteTestUser('deleted@kolabnow.com'); - $this->deleteTestDomain('userscontroller.com'); - - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - $wallet->discount()->dissociate(); - $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); - $wallet->save(); - $user->status |= User::STATUS_IMAP_READY; - $user->save(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('jane@kolabnow.com'); - $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); - $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); - $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); - $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); - $this->deleteTestUser('john2.doe2@kolab.org'); - $this->deleteTestUser('deleted@kolab.org'); - $this->deleteTestUser('deleted@kolabnow.com'); - $this->deleteTestDomain('userscontroller.com'); - - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - $wallet->discount()->dissociate(); - $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); - $wallet->save(); - $user->status |= User::STATUS_IMAP_READY; - $user->save(); - - parent::tearDown(); - } - - /** - * Test user deleting (DELETE /api/v4/users/) - */ - public function testDestroy(): void - { - // First create some users/accounts to delete - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - - $john = $this->getTestUser('john@kolab.org'); - $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); - $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); - $domain = $this->getTestDomain('userscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_PUBLIC, - ]); - $user1->assignPackage($package_kolab); - $domain->assignPackage($package_domain, $user1); - $user1->assignPackage($package_kolab, $user2); - $user1->assignPackage($package_kolab, $user3); - - // Test unauth access - $response = $this->delete("api/v4/users/{$user2->id}"); - $response->assertStatus(401); - - // Test access to other user/account - $response = $this->actingAs($john)->delete("api/v4/users/{$user2->id}"); - $response->assertStatus(403); - $response = $this->actingAs($john)->delete("api/v4/users/{$user1->id}"); - $response->assertStatus(403); - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertSame("Access denied", $json['message']); - $this->assertCount(2, $json); - - // Test that non-controller cannot remove himself - $response = $this->actingAs($user3)->delete("api/v4/users/{$user3->id}"); - $response->assertStatus(403); - - // Test removing a non-controller user - $response = $this->actingAs($user1)->delete("api/v4/users/{$user3->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals('success', $json['status']); - $this->assertEquals('User deleted successfully.', $json['message']); - - // Test removing self (an account with users) - $response = $this->actingAs($user1)->delete("api/v4/users/{$user1->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertEquals('success', $json['status']); - $this->assertEquals('User deleted successfully.', $json['message']); - } - - /** - * Test user deleting (DELETE /api/v4/users/) - */ - public function testDestroyByController(): void - { - // Create an account with additional controller - $user2 - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); - $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); - $domain = $this->getTestDomain('userscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_PUBLIC, - ]); - $user1->assignPackage($package_kolab); - $domain->assignPackage($package_domain, $user1); - $user1->assignPackage($package_kolab, $user2); - $user1->assignPackage($package_kolab, $user3); - $user1->wallets()->first()->addController($user2); - - // TODO/FIXME: - // For now controller can delete himself, as well as - // the whole account he has control to, including the owner - // Probably he should not be able to do none of those - // However, this is not 0-regression scenario as we - // do not fully support additional controllers. - - //$response = $this->actingAs($user2)->delete("api/v4/users/{$user2->id}"); - //$response->assertStatus(403); - - $response = $this->actingAs($user2)->delete("api/v4/users/{$user3->id}"); - $response->assertStatus(200); - - $response = $this->actingAs($user2)->delete("api/v4/users/{$user1->id}"); - $response->assertStatus(200); - - // Note: More detailed assertions in testDestroy() above - - $this->assertTrue($user1->fresh()->trashed()); - $this->assertTrue($user2->fresh()->trashed()); - $this->assertTrue($user3->fresh()->trashed()); - } - - /** - * Test user listing (GET /api/v4/users) - */ - public function testIndex(): void - { - // Test unauth access - $response = $this->get("api/v4/users"); - $response->assertStatus(401); - - $jack = $this->getTestUser('jack@kolab.org'); - $joe = $this->getTestUser('joe@kolab.org'); - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - - $response = $this->actingAs($jack)->get("/api/v4/users"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(0, $json); - - $response = $this->actingAs($john)->get("/api/v4/users"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(4, $json); - $this->assertSame($jack->email, $json[0]['email']); - $this->assertSame($joe->email, $json[1]['email']); - $this->assertSame($john->email, $json[2]['email']); - $this->assertSame($ned->email, $json[3]['email']); - // Values below are tested by Unit tests - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isSuspended', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); - $this->assertArrayHasKey('isImapReady', $json[0]); - - $response = $this->actingAs($ned)->get("/api/v4/users"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(4, $json); - $this->assertSame($jack->email, $json[0]['email']); - $this->assertSame($joe->email, $json[1]['email']); - $this->assertSame($john->email, $json[2]['email']); - $this->assertSame($ned->email, $json[3]['email']); - } - - /** - * Test fetching user data/profile (GET /api/v4/users/) - */ - public function testShow(): void - { - $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); - - // Test getting profile of self - $response = $this->actingAs($userA)->get("/api/v4/users/{$userA->id}"); - - $json = $response->json(); - - $response->assertStatus(200); - $this->assertEquals($userA->id, $json['id']); - $this->assertEquals($userA->email, $json['email']); - $this->assertTrue(is_array($json['statusInfo'])); - $this->assertTrue(is_array($json['settings'])); - $this->assertTrue(is_array($json['aliases'])); - $this->assertSame([], $json['skus']); - // Values below are tested by Unit tests - $this->assertArrayHasKey('isDeleted', $json); - $this->assertArrayHasKey('isSuspended', $json); - $this->assertArrayHasKey('isActive', $json); - $this->assertArrayHasKey('isLdapReady', $json); - $this->assertArrayHasKey('isImapReady', $json); - - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - - // Test unauthorized access to a profile of other user - $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}"); - $response->assertStatus(403); - - // Test authorized access to a profile of other user - // Ned: Additional account controller - $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}"); - $response->assertStatus(200); - $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}"); - $response->assertStatus(200); - - // John: Account owner - $response = $this->actingAs($john)->get("/api/v4/users/{$jack->id}"); - $response->assertStatus(200); - $response = $this->actingAs($john)->get("/api/v4/users/{$ned->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $storage_sku = Sku::where('title', 'storage')->first(); - $groupware_sku = Sku::where('title', 'groupware')->first(); - $mailbox_sku = Sku::where('title', 'mailbox')->first(); - $secondfactor_sku = Sku::where('title', '2fa')->first(); - - $this->assertCount(5, $json['skus']); - - $this->assertSame(2, $json['skus'][$storage_sku->id]['count']); - $this->assertSame(1, $json['skus'][$groupware_sku->id]['count']); - $this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']); - $this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']); - } - - /** - * Test fetching user status (GET /api/v4/users//status) - * and forcing setup process update (?refresh=1) - * - * @group imap - * @group dns - */ - public function testStatus(): void - { - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - - // Test unauthorized access - $response = $this->actingAs($jack)->get("/api/v4/users/{$john->id}/status"); - $response->assertStatus(403); - - if ($john->isImapReady()) { - $john->status ^= User::STATUS_IMAP_READY; - $john->save(); - } - - // Get user status - $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertFalse($json['isImapReady']); - $this->assertFalse($json['isReady']); - $this->assertCount(7, $json['process']); - $this->assertSame('user-imap-ready', $json['process'][2]['label']); - $this->assertSame(false, $json['process'][2]['state']); - $this->assertTrue(empty($json['status'])); - $this->assertTrue(empty($json['message'])); - - // Make sure the domain is confirmed (other test might unset that status) - $domain = $this->getTestDomain('kolab.org'); - $domain->status |= Domain::STATUS_CONFIRMED; - $domain->save(); - - // Now "reboot" the process and verify the user in imap synchronously - $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertTrue($json['isImapReady']); - $this->assertTrue($json['isReady']); - $this->assertCount(7, $json['process']); - $this->assertSame('user-imap-ready', $json['process'][2]['label']); - $this->assertSame(true, $json['process'][2]['state']); - $this->assertSame('success', $json['status']); - $this->assertSame('Setup process finished successfully.', $json['message']); - } - - /** - * Test UsersController::statusInfo() - */ - public function testStatusInfo(): void - { - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $domain = $this->getTestDomain('userscontroller.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_PUBLIC, - ]); - - $user->created_at = Carbon::now(); - $user->status = User::STATUS_NEW; - $user->save(); - - $result = UsersController::statusInfo($user); - - $this->assertFalse($result['isReady']); - $this->assertCount(3, $result['process']); - $this->assertSame('user-new', $result['process'][0]['label']); - $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); - $this->assertSame(false, $result['process'][1]['state']); - $this->assertSame('user-imap-ready', $result['process'][2]['label']); - $this->assertSame(false, $result['process'][2]['state']); - $this->assertSame('running', $result['processState']); - - $user->created_at = Carbon::now()->subSeconds(181); - $user->save(); - - $result = UsersController::statusInfo($user); - - $this->assertSame('failed', $result['processState']); - - $user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY; - $user->save(); - - $result = UsersController::statusInfo($user); - - $this->assertTrue($result['isReady']); - $this->assertCount(3, $result['process']); - $this->assertSame('user-new', $result['process'][0]['label']); - $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - $this->assertSame('user-imap-ready', $result['process'][2]['label']); - $this->assertSame(true, $result['process'][2]['state']); - $this->assertSame('done', $result['processState']); - - $domain->status |= Domain::STATUS_VERIFIED; - $domain->type = Domain::TYPE_EXTERNAL; - $domain->save(); - - $result = UsersController::statusInfo($user); - - $this->assertFalse($result['isReady']); - $this->assertCount(7, $result['process']); - $this->assertSame('user-new', $result['process'][0]['label']); - $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - $this->assertSame('user-imap-ready', $result['process'][2]['label']); - $this->assertSame(true, $result['process'][2]['state']); - $this->assertSame('domain-new', $result['process'][3]['label']); - $this->assertSame(true, $result['process'][3]['state']); - $this->assertSame('domain-ldap-ready', $result['process'][4]['label']); - $this->assertSame(false, $result['process'][4]['state']); - $this->assertSame('domain-verified', $result['process'][5]['label']); - $this->assertSame(true, $result['process'][5]['state']); - $this->assertSame('domain-confirmed', $result['process'][6]['label']); - $this->assertSame(false, $result['process'][6]['state']); - } - /** - * Test user creation (POST /api/v4/users) - */ - public function testStore(): void - { - $jack = $this->getTestUser('jack@kolab.org'); - $john = $this->getTestUser('john@kolab.org'); - $deleted_priv = $this->getTestUser('deleted@kolab.org'); - $deleted_priv->delete(); - - // Test empty request - $response = $this->actingAs($john)->post("/api/v4/users", []); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertSame("The email field is required.", $json['errors']['email']); - $this->assertSame("The password field is required.", $json['errors']['password'][0]); - $this->assertCount(2, $json); - - // Test access by user not being a wallet controller - $post = ['first_name' => 'Test']; - $response = $this->actingAs($jack)->post("/api/v4/users", $post); - $json = $response->json(); - - $response->assertStatus(403); - - $this->assertSame('error', $json['status']); - $this->assertSame("Access denied", $json['message']); - $this->assertCount(2, $json); - - // Test some invalid data - $post = ['password' => '12345678', 'email' => 'invalid']; - $response = $this->actingAs($john)->post("/api/v4/users", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json); - $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); - $this->assertSame('The specified email is invalid.', $json['errors']['email']); - - // Test existing user email - $post = [ - 'password' => 'simple', - 'password_confirmation' => 'simple', - 'first_name' => 'John2', - 'last_name' => 'Doe2', - 'email' => 'jack.daniels@kolab.org', - ]; - - $response = $this->actingAs($john)->post("/api/v4/users", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json); - $this->assertSame('The specified email is not available.', $json['errors']['email']); - - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - - $post = [ - 'password' => 'simple', - 'password_confirmation' => 'simple', - 'first_name' => 'John2', - 'last_name' => 'Doe2', - 'email' => 'john2.doe2@kolab.org', - 'organization' => 'TestOrg', - 'aliases' => ['useralias1@kolab.org', 'deleted@kolab.org'], - ]; - - // Missing package - $response = $this->actingAs($john)->post("/api/v4/users", $post); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertSame("Package is required.", $json['errors']['package']); - $this->assertCount(2, $json); - - // Invalid package - $post['package'] = $package_domain->id; - $response = $this->actingAs($john)->post("/api/v4/users", $post); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertSame("Invalid package selected.", $json['errors']['package']); - $this->assertCount(2, $json); - - // Test full and valid data - $post['package'] = $package_kolab->id; - $response = $this->actingAs($john)->post("/api/v4/users", $post); - $json = $response->json(); - - $response->assertStatus(200); - - $this->assertSame('success', $json['status']); - $this->assertSame("User created successfully.", $json['message']); - $this->assertCount(2, $json); - - $user = User::where('email', 'john2.doe2@kolab.org')->first(); - $this->assertInstanceOf(User::class, $user); - $this->assertSame('John2', $user->getSetting('first_name')); - $this->assertSame('Doe2', $user->getSetting('last_name')); - $this->assertSame('TestOrg', $user->getSetting('organization')); - $aliases = $user->aliases()->orderBy('alias')->get(); - $this->assertCount(2, $aliases); - $this->assertSame('deleted@kolab.org', $aliases[0]->alias); - $this->assertSame('useralias1@kolab.org', $aliases[1]->alias); - // Assert the new user entitlements - $this->assertUserEntitlements($user, ['groupware', 'mailbox', 'storage', 'storage']); - // Assert the wallet to which the new user should be assigned to - $wallet = $user->wallet(); - $this->assertSame($john->wallets()->first()->id, $wallet->id); - - // Test acting as account controller (not owner) - /* - // FIXME: How do we know to which wallet the new user should be assigned to? - - $this->deleteTestUser('john2.doe2@kolab.org'); - $response = $this->actingAs($ned)->post("/api/v4/users", $post); - $json = $response->json(); - - $response->assertStatus(200); - - $this->assertSame('success', $json['status']); - */ - - $this->markTestIncomplete(); - } - - /** - * Test user update (PUT /api/v4/users/) - */ - public function testUpdate(): void - { - $userA = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $jack = $this->getTestUser('jack@kolab.org'); - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - $domain = $this->getTestDomain( - 'userscontroller.com', - ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL] - ); - - // Test unauthorized update of other user profile - $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}", []); - $response->assertStatus(403); - - // Test authorized update of account owner by account controller - $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}", []); - $response->assertStatus(200); - - // Test updating of self (empty request) - $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", []); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame('success', $json['status']); - $this->assertSame("User data updated successfully.", $json['message']); - $this->assertCount(2, $json); - - // Test some invalid data - $post = ['password' => '12345678', 'currency' => 'invalid']; - $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); - $response->assertStatus(422); - - $json = $response->json(); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json); - $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); - $this->assertSame('The currency must be 3 characters.', $json['errors']['currency'][0]); - - // Test full profile update including password - $post = [ - 'password' => 'simple', - 'password_confirmation' => 'simple', - 'first_name' => 'John2', - 'last_name' => 'Doe2', - 'organization' => 'TestOrg', - 'phone' => '+123 123 123', - 'external_email' => 'external@gmail.com', - 'billing_address' => 'billing', - 'country' => 'CH', - 'currency' => 'CHF', - 'aliases' => ['useralias1@' . \config('app.domain'), 'useralias2@' . \config('app.domain')] - ]; - - $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); - $json = $response->json(); - - $response->assertStatus(200); - - $this->assertSame('success', $json['status']); - $this->assertSame("User data updated successfully.", $json['message']); - $this->assertCount(2, $json); - $this->assertTrue($userA->password != $userA->fresh()->password); - unset($post['password'], $post['password_confirmation'], $post['aliases']); - foreach ($post as $key => $value) { - $this->assertSame($value, $userA->getSetting($key)); - } - $aliases = $userA->aliases()->orderBy('alias')->get(); - $this->assertCount(2, $aliases); - $this->assertSame('useralias1@' . \config('app.domain'), $aliases[0]->alias); - $this->assertSame('useralias2@' . \config('app.domain'), $aliases[1]->alias); - - // Test unsetting values - $post = [ - 'first_name' => '', - 'last_name' => '', - 'organization' => '', - 'phone' => '', - 'external_email' => '', - 'billing_address' => '', - 'country' => '', - 'currency' => '', - 'aliases' => ['useralias2@' . \config('app.domain')] - ]; - - $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); - $json = $response->json(); - - $response->assertStatus(200); - - $this->assertSame('success', $json['status']); - $this->assertSame("User data updated successfully.", $json['message']); - $this->assertCount(2, $json); - unset($post['aliases']); - foreach ($post as $key => $value) { - $this->assertNull($userA->getSetting($key)); - } - $aliases = $userA->aliases()->get(); - $this->assertCount(1, $aliases); - $this->assertSame('useralias2@' . \config('app.domain'), $aliases[0]->alias); - - // Test error on some invalid aliases missing password confirmation - $post = [ - 'password' => 'simple123', - 'aliases' => [ - 'useralias2@' . \config('app.domain'), - 'useralias1@kolab.org', - '@kolab.org', - ] - ]; - - $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); - $json = $response->json(); - - $response->assertStatus(422); - - $this->assertSame('error', $json['status']); - $this->assertCount(2, $json['errors']); - $this->assertCount(2, $json['errors']['aliases']); - $this->assertSame("The specified domain is not available.", $json['errors']['aliases'][1]); - $this->assertSame("The specified alias is invalid.", $json['errors']['aliases'][2]); - $this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]); - - // Test authorized update of other user - $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}", []); - $response->assertStatus(200); - - // TODO: Test error on aliases with invalid/non-existing/other-user's domain - - // Create entitlements and additional user for following tests - $owner = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $user = $this->getTestUser('UsersControllerTest2@userscontroller.com'); - $package_domain = Package::where('title', 'domain-hosting')->first(); - $package_kolab = Package::where('title', 'kolab')->first(); - $package_lite = Package::where('title', 'lite')->first(); - $sku_mailbox = Sku::where('title', 'mailbox')->first(); - $sku_storage = Sku::where('title', 'storage')->first(); - $sku_groupware = Sku::where('title', 'groupware')->first(); - - $domain = $this->getTestDomain( - 'userscontroller.com', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - $domain->assignPackage($package_domain, $owner); - $owner->assignPackage($package_kolab); - $owner->assignPackage($package_lite, $user); - - // Non-controller cannot update his own entitlements - $post = ['skus' => []]; - $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post); - $response->assertStatus(422); - - // Test updating entitlements - $post = [ - 'skus' => [ - $sku_mailbox->id => 1, - $sku_storage->id => 3, - $sku_groupware->id => 1, - ], - ]; - - $response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post); - $response->assertStatus(200); - - $storage_cost = $user->entitlements() - ->where('sku_id', $sku_storage->id) - ->orderBy('cost') - ->pluck('cost')->all(); - - $this->assertUserEntitlements( - $user, - ['groupware', 'mailbox', 'storage', 'storage', 'storage'] - ); - - $this->assertSame([0, 0, 25], $storage_cost); - } - - /** - * Test UsersController::updateEntitlements() - */ - public function testUpdateEntitlements(): void - { - $jane = $this->getTestUser('jane@kolabnow.com'); - - $kolab = \App\Package::where('title', 'kolab')->first(); - $storage = \App\Sku::where('title', 'storage')->first(); - $activesync = \App\Sku::where('title', 'activesync')->first(); - $groupware = \App\Sku::where('title', 'groupware')->first(); - $mailbox = \App\Sku::where('title', 'mailbox')->first(); - - // standard package, 1 mailbox, 1 groupware, 2 storage - $jane->assignPackage($kolab); - - // add 2 storage, 1 activesync - $post = [ - 'skus' => [ - $mailbox->id => 1, - $groupware->id => 1, - $storage->id => 4, - $activesync->id => 1 - ] - ]; - - $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); - $response->assertStatus(200); - - $this->assertUserEntitlements( - $jane, - [ - 'activesync', - 'groupware', - 'mailbox', - 'storage', - 'storage', - 'storage', - 'storage' - ] - ); - - // add 2 storage, remove 1 activesync - $post = [ - 'skus' => [ - $mailbox->id => 1, - $groupware->id => 1, - $storage->id => 6, - $activesync->id => 0 - ] - ]; - - $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); - $response->assertStatus(200); - - $this->assertUserEntitlements( - $jane, - [ - 'groupware', - 'mailbox', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage' - ] - ); - - // add mailbox - $post = [ - 'skus' => [ - $mailbox->id => 2, - $groupware->id => 1, - $storage->id => 6, - $activesync->id => 0 - ] - ]; - - $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); - $response->assertStatus(500); - - $this->assertUserEntitlements( - $jane, - [ - 'groupware', - 'mailbox', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage' - ] - ); - - // remove mailbox - $post = [ - 'skus' => [ - $mailbox->id => 0, - $groupware->id => 1, - $storage->id => 6, - $activesync->id => 0 - ] - ]; - - $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); - $response->assertStatus(500); - - $this->assertUserEntitlements( - $jane, - [ - 'groupware', - 'mailbox', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage', - 'storage' - ] - ); - - // less than free storage - $post = [ - 'skus' => [ - $mailbox->id => 1, - $groupware->id => 1, - $storage->id => 1, - $activesync->id => 0 - ] - ]; - - $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); - $response->assertStatus(200); - - $this->assertUserEntitlements( - $jane, - [ - 'groupware', - 'mailbox', - 'storage', - 'storage' - ] - ); - } - - /** - * Test user data response used in show and info actions - */ - public function testUserResponse(): void - { - $provider = \config('services.payment_provider') ?: 'mollie'; - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - $wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]); - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); - - $this->assertEquals($user->id, $result['id']); - $this->assertEquals($user->email, $result['email']); - $this->assertEquals($user->status, $result['status']); - $this->assertTrue(is_array($result['statusInfo'])); - - $this->assertTrue(is_array($result['aliases'])); - $this->assertCount(1, $result['aliases']); - $this->assertSame('john.doe@kolab.org', $result['aliases'][0]); - - $this->assertTrue(is_array($result['settings'])); - $this->assertSame('US', $result['settings']['country']); - $this->assertSame('USD', $result['settings']['currency']); - - $this->assertTrue(is_array($result['accounts'])); - $this->assertTrue(is_array($result['wallets'])); - $this->assertCount(0, $result['accounts']); - $this->assertCount(1, $result['wallets']); - $this->assertSame($wallet->id, $result['wallet']['id']); - $this->assertArrayNotHasKey('discount', $result['wallet']); - - $this->assertTrue($result['statusInfo']['enableDomains']); - $this->assertTrue($result['statusInfo']['enableWallets']); - $this->assertTrue($result['statusInfo']['enableUsers']); - - // Ned is John's wallet controller - $ned = $this->getTestUser('ned@kolab.org'); - $ned_wallet = $ned->wallets()->first(); - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]); - - $this->assertEquals($ned->id, $result['id']); - $this->assertEquals($ned->email, $result['email']); - $this->assertTrue(is_array($result['accounts'])); - $this->assertTrue(is_array($result['wallets'])); - $this->assertCount(1, $result['accounts']); - $this->assertCount(1, $result['wallets']); - $this->assertSame($wallet->id, $result['wallet']['id']); - $this->assertSame($wallet->id, $result['accounts'][0]['id']); - $this->assertSame($ned_wallet->id, $result['wallets'][0]['id']); - $this->assertSame($provider, $result['wallet']['provider']); - $this->assertSame($provider, $result['wallets'][0]['provider']); - - $this->assertTrue($result['statusInfo']['enableDomains']); - $this->assertTrue($result['statusInfo']['enableWallets']); - $this->assertTrue($result['statusInfo']['enableUsers']); - - // Test discount in a response - $discount = Discount::where('code', 'TEST')->first(); - $wallet->discount()->associate($discount); - $wallet->save(); - $mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie'; - $wallet->setSetting($mod_provider . '_id', 123); - $user->refresh(); - - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); - - $this->assertEquals($user->id, $result['id']); - $this->assertSame($discount->id, $result['wallet']['discount_id']); - $this->assertSame($discount->discount, $result['wallet']['discount']); - $this->assertSame($discount->description, $result['wallet']['discount_description']); - $this->assertSame($mod_provider, $result['wallet']['provider']); - $this->assertSame($discount->id, $result['wallets'][0]['discount_id']); - $this->assertSame($discount->discount, $result['wallets'][0]['discount']); - $this->assertSame($discount->description, $result['wallets'][0]['discount_description']); - $this->assertSame($mod_provider, $result['wallets'][0]['provider']); - - // Jack is not a John's wallet controller - $jack = $this->getTestUser('jack@kolab.org'); - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$jack]); - - $this->assertFalse($result['statusInfo']['enableDomains']); - $this->assertFalse($result['statusInfo']['enableWallets']); - $this->assertFalse($result['statusInfo']['enableUsers']); - } - - /** - * List of alias validation cases for testValidateEmail() - * - * @return array Arguments for testValidateEmail() - */ - public function dataValidateEmail(): array - { - $this->refreshApplication(); - $public_domains = Domain::getPublicDomains(); - $domain = reset($public_domains); - - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - - return [ - // Invalid format - ["$domain", $john, true, 'The specified alias is invalid.'], - [".@$domain", $john, true, 'The specified alias is invalid.'], - ["test123456@localhost", $john, true, 'The specified domain is invalid.'], - ["test123456@unknown-domain.org", $john, true, 'The specified domain is invalid.'], - - ["$domain", $john, false, 'The specified email is invalid.'], - [".@$domain", $john, false, 'The specified email is invalid.'], - - // forbidden local part on public domains - ["admin@$domain", $john, true, 'The specified alias is not available.'], - ["administrator@$domain", $john, true, 'The specified alias is not available.'], - - // forbidden (other user's domain) - ["testtest@kolab.org", $user, true, 'The specified domain is not available.'], - - // existing alias of other user, to be a user email - ["jack.daniels@kolab.org", $john, false, 'The specified email is not available.'], - - // existing alias of other user, to be an alias, user in the same group account - ["jack.daniels@kolab.org", $john, true, null], - - // existing user - ["jack@kolab.org", $john, true, 'The specified alias is not available.'], - - // valid (user domain) - ["admin@kolab.org", $john, true, null], - - // valid (public domain) - ["test.test@$domain", $john, true, null], - ]; - } - - /** - * User email/alias validation. - * - * Note: Technically these include unit tests, but let's keep it here for now. - * FIXME: Shall we do a http request for each case? - * - * @dataProvider dataValidateEmail - */ - public function testValidateEmail($alias, $user, $is_alias, $expected_result): void - { - $args = [$alias, $user, $is_alias]; - $result = $this->invokeMethod(new UsersController(), 'validateEmail', $args); - - $this->assertSame($expected_result, $result); - } - - /** - * User email/alias validation - more cases. - * - * Note: Technically these include unit tests, but let's keep it here for now. - * FIXME: Shall we do a http request for each case? - */ - public function testValidateEmail2(): void - { - Queue::fake(); - - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); - $deleted_priv = $this->getTestUser('deleted@kolab.org'); - $deleted_priv->setAliases(['deleted-alias@kolab.org']); - $deleted_priv->delete(); - $deleted_pub = $this->getTestUser('deleted@kolabnow.com'); - $deleted_pub->setAliases(['deleted-alias@kolabnow.com']); - $deleted_pub->delete(); - - // An alias that was a user email before is allowed, but only for custom domains - $result = UsersController::validateEmail('deleted@kolab.org', $john, true); - $this->assertSame(null, $result); - - $result = UsersController::validateEmail('deleted-alias@kolab.org', $john, true); - $this->assertSame(null, $result); - - $result = UsersController::validateEmail('deleted@kolabnow.com', $john, true); - $this->assertSame('The specified alias is not available.', $result); - - $result = UsersController::validateEmail('deleted-alias@kolabnow.com', $john, true); - $this->assertSame('The specified alias is not available.', $result); - } -} diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php deleted file mode 100644 index 25291f9f..00000000 --- a/src/tests/Feature/Controller/WalletsTest.php +++ /dev/null @@ -1,336 +0,0 @@ -deleteTestUser('wallets-controller@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('wallets-controller@kolabnow.com'); - - parent::tearDown(); - } - - - /** - * Test for getWalletNotice() method - */ - public function testGetWalletNotice(): void - { - $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $package = \App\Package::where('title', 'kolab')->first(); - $user->assignPackage($package); - $wallet = $user->wallets()->first(); - - $controller = new WalletsController(); - $method = new \ReflectionMethod($controller, 'getWalletNotice'); - $method->setAccessible(true); - - // User/entitlements created today, balance=0 - $notice = $method->invoke($controller, $wallet); - - $this->assertSame('You are in your free trial period.', $notice); - - $wallet->owner->created_at = Carbon::now()->subDays(15); - $wallet->owner->save(); - - $notice = $method->invoke($controller, $wallet); - - $this->assertSame('Your free trial is about to end, top up to continue.', $notice); - - // User/entitlements created today, balance=-10 CHF - $wallet->balance = -1000; - $notice = $method->invoke($controller, $wallet); - - $this->assertSame('You are out of credit, top up your balance now.', $notice); - - // User/entitlements created slightly more than a month ago, balance=9,99 CHF (monthly) - $wallet->owner->created_at = Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1); - $wallet->owner->save(); - - $wallet->balance = 999; - $notice = $method->invoke($controller, $wallet); - - $this->assertRegExp('/\((1 month|4 weeks)\)/', $notice); - - // Old entitlements, 100% discount - $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); - $discount = \App\Discount::where('discount', 100)->first(); - $wallet->discount()->associate($discount); - - $notice = $method->invoke($controller, $wallet->refresh()); - - $this->assertSame(null, $notice); - } - - /** - * Test fetching pdf receipt - */ - public function testReceiptDownload(): void - { - $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $john = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - - // Unauth access not allowed - $response = $this->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); - $response->assertStatus(401); - $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); - $response->assertStatus(403); - - // Invalid receipt id (current month) - $receiptId = date('Y-m'); - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); - $response->assertStatus(404); - - // Invalid receipt id - $receiptId = '1000-03'; - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); - $response->assertStatus(404); - - // Valid receipt id - $year = intval(date('Y')) - 1; - $receiptId = "$year-12"; - $filename = \config('app.name') . " Receipt for $year-12"; - - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); - - $response->assertStatus(200); - $response->assertHeader('content-type', 'application/pdf'); - $response->assertHeader('content-disposition', 'attachment; filename="' . $filename . '"'); - $response->assertHeader('content-length'); - - $length = $response->headers->get('content-length'); - $content = $response->content(); - $this->assertStringStartsWith("%PDF-1.", $content); - $this->assertEquals(strlen($content), $length); - } - - /** - * Test fetching list of receipts - */ - public function testReceipts(): void - { - $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $john = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - $wallet->payments()->delete(); - - // Unauth access not allowed - $response = $this->get("api/v4/wallets/{$wallet->id}/receipts"); - $response->assertStatus(401); - $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts"); - $response->assertStatus(403); - - // Empty list expected - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame([], $json['list']); - $this->assertSame(1, $json['page']); - $this->assertSame(0, $json['count']); - $this->assertSame(false, $json['hasMore']); - - // Insert a payment to the database - $date = Carbon::create(intval(date('Y')) - 1, 4, 30); - $payment = Payment::create([ - 'id' => 'AAA1', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Paid in April', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1111, - ]); - $payment->updated_at = $date; - $payment->save(); - - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame([$date->format('Y-m')], $json['list']); - $this->assertSame(1, $json['page']); - $this->assertSame(1, $json['count']); - $this->assertSame(false, $json['hasMore']); - } - - /** - * Test fetching a wallet (GET /api/v4/wallets/:id) - */ - public function testShow(): void - { - $john = $this->getTestUser('john@kolab.org'); - $jack = $this->getTestUser('jack@kolab.org'); - $wallet = $john->wallets()->first(); - - // Accessing a wallet of someone else - $response = $this->actingAs($jack)->get("api/v4/wallets/{$wallet->id}"); - $response->assertStatus(403); - - // Accessing non-existing wallet - $response = $this->actingAs($jack)->get("api/v4/wallets/aaa"); - $response->assertStatus(404); - - // Wallet owner - $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertSame($wallet->id, $json['id']); - $this->assertSame('CHF', $json['currency']); - $this->assertSame($wallet->balance, $json['balance']); - $this->assertTrue(empty($json['description'])); - $this->assertTrue(!empty($json['notice'])); - } - - /** - * Test fetching wallet transactions - */ - public function testTransactions(): void - { - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $user->assignPackage($package_kolab); - $john = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); - - // Unauth access not allowed - $response = $this->get("api/v4/wallets/{$wallet->id}/transactions"); - $response->assertStatus(401); - $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions"); - $response->assertStatus(403); - - // Expect empty list - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame([], $json['list']); - $this->assertSame(1, $json['page']); - $this->assertSame(0, $json['count']); - $this->assertSame(false, $json['hasMore']); - - // Create some sample transactions - $transactions = $this->createTestTransactions($wallet); - $transactions = array_reverse($transactions); - $pages = array_chunk($transactions, 10 /* page size*/); - - // Get the first page - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame(1, $json['page']); - $this->assertSame(10, $json['count']); - $this->assertSame(true, $json['hasMore']); - $this->assertCount(10, $json['list']); - foreach ($pages[0] as $idx => $transaction) { - $this->assertSame($transaction->id, $json['list'][$idx]['id']); - $this->assertSame($transaction->type, $json['list'][$idx]['type']); - $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); - $this->assertFalse($json['list'][$idx]['hasDetails']); - $this->assertFalse(array_key_exists('user', $json['list'][$idx])); - } - - $search = null; - - // Get the second page - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame(2, $json['page']); - $this->assertSame(2, $json['count']); - $this->assertSame(false, $json['hasMore']); - $this->assertCount(2, $json['list']); - foreach ($pages[1] as $idx => $transaction) { - $this->assertSame($transaction->id, $json['list'][$idx]['id']); - $this->assertSame($transaction->type, $json['list'][$idx]['type']); - $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); - $this->assertSame( - $transaction->type == Transaction::WALLET_DEBIT, - $json['list'][$idx]['hasDetails'] - ); - $this->assertFalse(array_key_exists('user', $json['list'][$idx])); - - if ($transaction->type == Transaction::WALLET_DEBIT) { - $search = $transaction->id; - } - } - - // Get a non-existing page - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=3"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame(3, $json['page']); - $this->assertSame(0, $json['count']); - $this->assertSame(false, $json['hasMore']); - $this->assertCount(0, $json['list']); - - // Sub-transaction searching - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction=123"); - $response->assertStatus(404); - - $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); - $response->assertStatus(200); - - $json = $response->json(); - - $this->assertCount(5, $json); - $this->assertSame('success', $json['status']); - $this->assertSame(1, $json['page']); - $this->assertSame(2, $json['count']); - $this->assertSame(false, $json['hasMore']); - $this->assertCount(2, $json['list']); - $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][0]['type']); - $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][1]['type']); - - // Test that John gets 404 if he tries to access - // someone else's transaction ID on his wallet's endpoint - $wallet = $john->wallets()->first(); - $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); - $response->assertStatus(404); - } -} diff --git a/src/tests/Feature/DiscountTest.php b/src/tests/Feature/DiscountTest.php deleted file mode 100644 index 62e31014..00000000 --- a/src/tests/Feature/DiscountTest.php +++ /dev/null @@ -1,31 +0,0 @@ -discount = -1; - - $this->assertTrue($discount->discount == 0); - } - - /** - * Test setting discount value - */ - public function testDiscountValueMoreThanHundred(): void - { - $discount = new Discount(); - $discount->discount = 101; - - $this->assertTrue($discount->discount == 100); - } -} diff --git a/src/tests/Feature/Documents/ReceiptTest.php b/src/tests/Feature/Documents/ReceiptTest.php deleted file mode 100644 index 3a815b09..00000000 --- a/src/tests/Feature/Documents/ReceiptTest.php +++ /dev/null @@ -1,323 +0,0 @@ -paymentIDs)->delete(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('receipt-test@kolabnow.com'); - - Payment::whereIn('id', $this->paymentIDs)->delete(); - - parent::tearDown(); - } - - /** - * Test receipt HTML output (without VAT) - */ - public function testHtmlOutput(): void - { - $appName = \config('app.name'); - $wallet = $this->getTestData(); - $receipt = new Receipt($wallet, 2020, 5); - $html = $receipt->htmlOutput(); - - $this->assertStringStartsWith('', $html); - - $dom = new \DOMDocument('1.0', 'UTF-8'); - $dom->loadHTML($html); - - // Title - $title = $dom->getElementById('title'); - $this->assertSame("Receipt for May 2020", $title->textContent); - - // Company name/address - $header = $dom->getElementById('header'); - $companyOutput = $this->getNodeContent($header->getElementsByTagName('td')[0]); - $companyExpected = \config('app.company.name') . "\n" . \config('app.company.address'); - $this->assertSame($companyExpected, $companyOutput); - - // The main table content - $content = $dom->getElementById('content'); - $records = $content->getElementsByTagName('tr'); - $this->assertCount(5, $records); - - $headerCells = $records[0]->getElementsByTagName('th'); - $this->assertCount(3, $headerCells); - $this->assertSame('Date', $this->getNodeContent($headerCells[0])); - $this->assertSame('Description', $this->getNodeContent($headerCells[1])); - $this->assertSame('Amount', $this->getNodeContent($headerCells[2])); - $cells = $records[1]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('12,34 CHF', $this->getNodeContent($cells[2])); - $cells = $records[2]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); - $cells = $records[3]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('1,00 CHF', $this->getNodeContent($cells[2])); - $summaryCells = $records[4]->getElementsByTagName('td'); - $this->assertCount(2, $summaryCells); - $this->assertSame('Total', $this->getNodeContent($summaryCells[0])); - $this->assertSame('13,35 CHF', $this->getNodeContent($summaryCells[1])); - - // Customer data - $customer = $dom->getElementById('customer'); - $customerCells = $customer->getElementsByTagName('td'); - $customerOutput = $this->getNodeContent($customerCells[0]); - $customerExpected = "Firstname Lastname\nTest Unicode Straße 150\n10115 Berlin"; - $this->assertSame($customerExpected, $this->getNodeContent($customerCells[0])); - $customerIdents = $this->getNodeContent($customerCells[1]); - //$this->assertTrue(strpos($customerIdents, "Account ID {$wallet->id}") !== false); - $this->assertTrue(strpos($customerIdents, "Customer No. {$wallet->owner->id}") !== false); - - // Company details in the footer - $footer = $dom->getElementById('footer'); - $footerOutput = $footer->textContent; - $this->assertStringStartsWith(\config('app.company.details'), $footerOutput); - $this->assertTrue(strpos($footerOutput, \config('app.company.email')) !== false); - } - - /** - * Test receipt HTML output (with VAT) - */ - public function testHtmlOutputVat(): void - { - \config(['app.vat.rate' => 7.7]); - \config(['app.vat.countries' => 'ch']); - - $appName = \config('app.name'); - $wallet = $this->getTestData('CH'); - $receipt = new Receipt($wallet, 2020, 5); - $html = $receipt->htmlOutput(); - - $this->assertStringStartsWith('', $html); - - $dom = new \DOMDocument('1.0', 'UTF-8'); - $dom->loadHTML($html); - - // The main table content - $content = $dom->getElementById('content'); - $records = $content->getElementsByTagName('tr'); - $this->assertCount(7, $records); - - $cells = $records[1]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('11,39 CHF', $this->getNodeContent($cells[2])); - $cells = $records[2]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); - $cells = $records[3]->getElementsByTagName('td'); - $this->assertCount(3, $cells); - $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); - $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('0,92 CHF', $this->getNodeContent($cells[2])); - $subtotalCells = $records[4]->getElementsByTagName('td'); - $this->assertCount(2, $subtotalCells); - $this->assertSame('Subtotal', $this->getNodeContent($subtotalCells[0])); - $this->assertSame('12,32 CHF', $this->getNodeContent($subtotalCells[1])); - $vatCells = $records[5]->getElementsByTagName('td'); - $this->assertCount(2, $vatCells); - $this->assertSame('VAT (7.7%)', $this->getNodeContent($vatCells[0])); - $this->assertSame('1,03 CHF', $this->getNodeContent($vatCells[1])); - $totalCells = $records[6]->getElementsByTagName('td'); - $this->assertCount(2, $totalCells); - $this->assertSame('Total', $this->getNodeContent($totalCells[0])); - $this->assertSame('13,35 CHF', $this->getNodeContent($totalCells[1])); - } - - /** - * Test receipt PDF output - */ - public function testPdfOutput(): void - { - $wallet = $this->getTestData(); - $receipt = new Receipt($wallet, 2020, 5); - $pdf = $receipt->PdfOutput(); - - $this->assertStringStartsWith("%PDF-1.", $pdf); - $this->assertTrue(strlen($pdf) > 5000); - - // TODO: Test the content somehow - } - - /** - * Prepare data for a test - * - * @param string $country User country code - * - * @return \App\Wallet - */ - protected function getTestData(string $country = null): Wallet - { - Bus::fake(); - - $user = $this->getTestUser('receipt-test@kolabnow.com'); - $user->setSettings([ - 'first_name' => 'Firstname', - 'last_name' => 'Lastname', - 'billing_address' => "Test Unicode Straße 150\n10115 Berlin", - 'country' => $country - ]); - - $wallet = $user->wallets()->first(); - - // Create two payments out of the 2020-05 period - // and three in it, plus one in the period but unpaid, - // and one with amount 0 - - $payment = Payment::create([ - 'id' => 'AAA1', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Paid in April', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1111, - ]); - $payment->updated_at = Carbon::create(2020, 4, 30, 12, 0, 0); - $payment->save(); - - $payment = Payment::create([ - 'id' => 'AAA2', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Paid in June', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 2222, - ]); - $payment->updated_at = Carbon::create(2020, 6, 1, 0, 0, 0); - $payment->save(); - - $payment = Payment::create([ - 'id' => 'AAA3', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Auto-Payment Setup', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 0, - ]); - $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); - $payment->save(); - - $payment = Payment::create([ - 'id' => 'AAA4', - 'status' => PaymentProvider::STATUS_OPEN, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Payment not yet paid', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 999, - ]); - $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); - $payment->save(); - - // ... so we expect the last three on the receipt - $payment = Payment::create([ - 'id' => 'AAA5', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Payment OK', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1234, - ]); - $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); - $payment->save(); - - $payment = Payment::create([ - 'id' => 'AAA6', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_ONEOFF, - 'description' => 'Payment OK', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 1, - ]); - $payment->updated_at = Carbon::create(2020, 5, 10, 0, 0, 0); - $payment->save(); - - $payment = Payment::create([ - 'id' => 'AAA7', - 'status' => PaymentProvider::STATUS_PAID, - 'type' => PaymentProvider::TYPE_RECURRING, - 'description' => 'Payment OK', - 'wallet_id' => $wallet->id, - 'provider' => 'stripe', - 'amount' => 100, - ]); - $payment->updated_at = Carbon::create(2020, 5, 31, 23, 59, 0); - $payment->save(); - - // Make sure some config is set so we can test it's put into the receipt - if (empty(\config('app.company.name'))) { - \config(['app.company.name' => 'Company Co.']); - } - if (empty(\config('app.company.email'))) { - \config(['app.company.email' => 'email@domina.tld']); - } - if (empty(\config('app.company.details'))) { - \config(['app.company.details' => 'VAT No. 123456789']); - } - if (empty(\config('app.company.address'))) { - \config(['app.company.address' => "Test Street 12\n12345 Some Place"]); - } - - return $wallet; - } - - /** - * Extract text from a HTML element replacing
with \n - * - * @param \DOMElement $node The HTML element - * - * @return string The content - */ - protected function getNodeContent(\DOMElement $node) - { - $content = []; - foreach ($node->childNodes as $child) { - if ($child->nodeName == 'br') { - $content[] = "\n"; - } else { - $content[] = $child->textContent; - } - } - - return trim(implode($content)); - } -} diff --git a/src/tests/Feature/DomainOwnerTest.php b/src/tests/Feature/DomainOwnerTest.php deleted file mode 100644 index 42d5d556..00000000 --- a/src/tests/Feature/DomainOwnerTest.php +++ /dev/null @@ -1,52 +0,0 @@ -deleteTestUser('jane@kolab.org'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('jane@kolab.org'); - - parent::tearDown(); - } - - public function testJohnCreateJane(): void - { - $john = User::where('email', 'john@kolab.org')->first(); - - $jane = User::create( - [ - 'name' => 'Jane Doe', - 'email' => 'jane@kolab.org', - 'password' => 'simple123', - 'email_verified_at' => now() - ] - ); - - $package = Package::where('title', 'kolab')->first(); - - $john->assignPackage($package, $jane); - - // assert jane has a mailbox entitlement - $this->assertTrue($jane->entitlements->count() == 4); - } -} diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php deleted file mode 100644 index 63f47c59..00000000 --- a/src/tests/Feature/DomainTest.php +++ /dev/null @@ -1,221 +0,0 @@ -domains as $domain) { - $this->deleteTestDomain($domain); - } - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - foreach ($this->domains as $domain) { - $this->deleteTestDomain($domain); - } - - parent::tearDown(); - } - - /** - * Test domain create/creating observer - */ - public function testCreate(): void - { - Queue::fake(); - - $domain = Domain::create([ - 'namespace' => 'GMAIL.COM', - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - $result = Domain::where('namespace', 'gmail.com')->first(); - - $this->assertSame('gmail.com', $result->namespace); - $this->assertSame($domain->id, $result->id); - $this->assertSame($domain->type, $result->type); - $this->assertSame(Domain::STATUS_NEW, $result->status); - } - - /** - * Test domain creating jobs - */ - public function testCreateJobs(): void - { - // Fake the queue, assert that no jobs were pushed... - Queue::fake(); - Queue::assertNothingPushed(); - - $domain = Domain::create([ - 'namespace' => 'gmail.com', - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\Domain\CreateJob::class, - function ($job) use ($domain) { - $domainId = TestCase::getObjectProperty($job, 'domainId'); - $domainNamespace = TestCase::getObjectProperty($job, 'domainNamespace'); - - return $domainId === $domain->id && - $domainNamespace === $domain->namespace; - } - ); - - $job = new \App\Jobs\Domain\CreateJob($domain->id); - $job->handle(); - } - - /** - * Tests getPublicDomains() method - */ - public function testGetPublicDomains(): void - { - $public_domains = Domain::getPublicDomains(); - - $this->assertNotContains('public-active.com', $public_domains); - - $queue = Queue::fake(); - - $domain = Domain::create([ - 'namespace' => 'public-active.com', - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ]); - - // External domains should not be returned - $public_domains = Domain::getPublicDomains(); - $this->assertNotContains('public-active.com', $public_domains); - - $domain = Domain::where('namespace', 'public-active.com')->first(); - $domain->type = Domain::TYPE_PUBLIC; - $domain->save(); - - $public_domains = Domain::getPublicDomains(); - $this->assertContains('public-active.com', $public_domains); - } - - /** - * Test domain (ownership) confirmation - * - * @group dns - */ - public function testConfirm(): void - { - /* - DNS records for positive and negative tests - kolab.org: - - ci-success-cname A 212.103.80.148 - ci-success-cname MX 10 mx01.kolabnow.com. - ci-success-cname TXT "v=spf1 mx -all" - kolab-verify.ci-success-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-success-cname - - ci-failure-cname A 212.103.80.148 - ci-failure-cname MX 10 mx01.kolabnow.com. - kolab-verify.ci-failure-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-failure-cname - - ci-success-txt A 212.103.80.148 - ci-success-txt MX 10 mx01.kolabnow.com. - ci-success-txt TXT "v=spf1 mx -all" - ci-success-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" - - ci-failure-txt A 212.103.80.148 - ci-failure-txt MX 10 mx01.kolabnow.com. - kolab-verify.ci-failure-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" - - ci-failure-none A 212.103.80.148 - ci-failure-none MX 10 mx01.kolabnow.com. - */ - - $queue = Queue::fake(); - - $domain_props = ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]; - - $domain = $this->getTestDomain('ci-failure-none.kolab.org', $domain_props); - - $this->assertTrue($domain->confirm() === false); - $this->assertFalse($domain->isConfirmed()); - - $domain = $this->getTestDomain('ci-failure-txt.kolab.org', $domain_props); - - $this->assertTrue($domain->confirm() === false); - $this->assertFalse($domain->isConfirmed()); - - $domain = $this->getTestDomain('ci-failure-cname.kolab.org', $domain_props); - - $this->assertTrue($domain->confirm() === false); - $this->assertFalse($domain->isConfirmed()); - - $domain = $this->getTestDomain('ci-success-txt.kolab.org', $domain_props); - - $this->assertTrue($domain->confirm()); - $this->assertTrue($domain->isConfirmed()); - - $domain = $this->getTestDomain('ci-success-cname.kolab.org', $domain_props); - - $this->assertTrue($domain->confirm()); - $this->assertTrue($domain->isConfirmed()); - } - - /** - * Test domain deletion - */ - public function testDelete(): void - { - Queue::fake(); - - $domain = $this->getTestDomain('gmail.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_PUBLIC, - ]); - - $domain->delete(); - - $this->assertTrue($domain->fresh()->trashed()); - $this->assertFalse($domain->fresh()->isDeleted()); - - // Delete the domain for real - $job = new \App\Jobs\Domain\DeleteJob($domain->id); - $job->handle(); - - $this->assertTrue(Domain::withTrashed()->where('id', $domain->id)->first()->isDeleted()); - - $domain->forceDelete(); - - $this->assertCount(0, Domain::withTrashed()->where('id', $domain->id)->get()); - } -} diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php deleted file mode 100644 index 7f86d0a4..00000000 --- a/src/tests/Feature/EntitlementTest.php +++ /dev/null @@ -1,186 +0,0 @@ -deleteTestUser('entitlement-test@kolabnow.com'); - $this->deleteTestUser('entitled-user@custom-domain.com'); - $this->deleteTestDomain('custom-domain.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('entitlement-test@kolabnow.com'); - $this->deleteTestUser('entitled-user@custom-domain.com'); - $this->deleteTestDomain('custom-domain.com'); - - parent::tearDown(); - } - - public function testCostsPerDay(): void - { - // 444 - // 28 days: 15.86 - // 31 days: 14.32 - $user = $this->getTestUser('entitlement-test@kolabnow.com'); - $package = Package::where('title', 'kolab')->first(); - $mailbox = Sku::where('title', 'mailbox')->first(); - - $user->assignPackage($package); - - $entitlement = $user->entitlements->where('sku_id', $mailbox->id)->first(); - - $costsPerDay = $entitlement->costsPerDay(); - - $this->assertTrue($costsPerDay < 15.86); - $this->assertTrue($costsPerDay > 14.32); - } - - /** - * Tests for User::AddEntitlement() - */ - public function testUserAddEntitlement(): void - { - $packageDomain = Package::where('title', 'domain-hosting')->first(); - $packageKolab = Package::where('title', 'kolab')->first(); - - $skuDomain = Sku::where('title', 'domain-hosting')->first(); - $skuMailbox = Sku::where('title', 'mailbox')->first(); - - $owner = $this->getTestUser('entitlement-test@kolabnow.com'); - $user = $this->getTestUser('entitled-user@custom-domain.com'); - - $domain = $this->getTestDomain( - 'custom-domain.com', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - $domain->assignPackage($packageDomain, $owner); - - $owner->assignPackage($packageKolab); - $owner->assignPackage($packageKolab, $user); - - $wallet = $owner->wallets->first(); - - $this->assertCount(4, $owner->entitlements()->get()); - $this->assertCount(1, $skuDomain->entitlements()->where('wallet_id', $wallet->id)->get()); - $this->assertCount(2, $skuMailbox->entitlements()->where('wallet_id', $wallet->id)->get()); - $this->assertCount(9, $wallet->entitlements); - - $this->backdateEntitlements( - $owner->entitlements, - Carbon::now()->subMonthsWithoutOverflow(1) - ); - - $wallet->chargeEntitlements(); - - $this->assertTrue($wallet->fresh()->balance < 0); - } - - public function testAddExistingEntitlement(): void - { - $this->markTestIncomplete(); - } - - public function testEntitlementFunctions(): void - { - $user = $this->getTestUser('entitlement-test@kolabnow.com'); - - $package = \App\Package::where('title', 'kolab')->first(); - - $user->assignPackage($package); - - $wallet = $user->wallets()->first(); - $this->assertNotNull($wallet); - - $sku = \App\Sku::where('title', 'mailbox')->first(); - $this->assertNotNull($sku); - - $entitlement = Entitlement::where('wallet_id', $wallet->id) - ->where('sku_id', $sku->id)->first(); - - $this->assertNotNull($entitlement); - - $eSKU = $entitlement->sku; - $this->assertSame($sku->id, $eSKU->id); - - $eWallet = $entitlement->wallet; - $this->assertSame($wallet->id, $eWallet->id); - - $eEntitleable = $entitlement->entitleable; - $this->assertEquals($user->id, $eEntitleable->id); - $this->assertTrue($eEntitleable instanceof \App\User); - } - - public function testBillDeletedEntitlement(): void - { - $user = $this->getTestUser('entitlement-test@kolabnow.com'); - $package = \App\Package::where('title', 'kolab')->first(); - - $storage = \App\Sku::where('title', 'storage')->first(); - - $user->assignPackage($package); - // some additional SKUs so we have something to delete. - $user->assignSku($storage, 4); - - // the mailbox, the groupware, the 2 original storage and the additional 4 - $this->assertCount(8, $user->fresh()->entitlements); - - $wallet = $user->wallets()->first(); - - $backdate = Carbon::now()->subWeeks(7); - $this->backdateEntitlements($user->entitlements, $backdate); - - $charge = $wallet->chargeEntitlements(); - - $this->assertSame(-1099, $wallet->balance); - - $balance = $wallet->balance; - $discount = \App\Discount::where('discount', 30)->first(); - $wallet->discount()->associate($discount); - $wallet->save(); - - $user->removeSku($storage, 4); - - // we expect the wallet to have been charged for ~3 weeks of use of - // 4 deleted storage entitlements, it should also take discount into account - $backdate->addMonthsWithoutOverflow(1); - $diffInDays = $backdate->diffInDays(Carbon::now()); - - // entitlements-num * cost * discount * days-in-month - $max = intval(4 * 25 * 0.7 * $diffInDays / 28); - $min = intval(4 * 25 * 0.7 * $diffInDays / 31); - - $wallet->refresh(); - $this->assertTrue($wallet->balance >= $balance - $max); - $this->assertTrue($wallet->balance <= $balance - $min); - - $transactions = \App\Transaction::where('object_id', $wallet->id) - ->where('object_type', \App\Wallet::class)->get(); - - // one round of the monthly invoicing, four sku deletions getting invoiced - $this->assertCount(5, $transactions); - } -} diff --git a/src/tests/Feature/Jobs/DomainCreateTest.php b/src/tests/Feature/Jobs/DomainCreateTest.php deleted file mode 100644 index ad509ac1..00000000 --- a/src/tests/Feature/Jobs/DomainCreateTest.php +++ /dev/null @@ -1,68 +0,0 @@ -deleteTestDomain('domain-create-test.com'); - } - - public function tearDown(): void - { - $this->deleteTestDomain('domain-create-test.com'); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @group ldap - */ - public function testHandle(): void - { - $domain = $this->getTestDomain( - 'domain-create-test.com', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - $this->assertFalse($domain->isLdapReady()); - - // Fake the queue, assert that no jobs were pushed... - Queue::fake(); - Queue::assertNothingPushed(); - - $job = new \App\Jobs\Domain\CreateJob($domain->id); - $job->handle(); - - $this->assertTrue($domain->fresh()->isLdapReady()); - - Queue::assertPushed(\App\Jobs\Domain\VerifyJob::class, 1); - - Queue::assertPushed( - \App\Jobs\Domain\VerifyJob::class, - function ($job) use ($domain) { - $domainId = TestCase::getObjectProperty($job, 'domainId'); - $domainNamespace = TestCase::getObjectProperty($job, 'domainNamespace'); - - return $domainId === $domain->id && - $domainNamespace === $domain->namespace; - } - ); - } -} diff --git a/src/tests/Feature/Jobs/DomainVerifyTest.php b/src/tests/Feature/Jobs/DomainVerifyTest.php deleted file mode 100644 index 7dff2a24..00000000 --- a/src/tests/Feature/Jobs/DomainVerifyTest.php +++ /dev/null @@ -1,75 +0,0 @@ -deleteTestDomain('gmail.com'); - $this->deleteTestDomain('some-non-existing-domain.fff'); - } - - public function tearDown(): void - { - $this->deleteTestDomain('gmail.com'); - $this->deleteTestDomain('some-non-existing-domain.fff'); - - parent::tearDown(); - } - - /** - * Test job handle (existing domain) - * - * @group dns - */ - public function testHandle(): void - { - $domain = $this->getTestDomain( - 'gmail.com', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - $this->assertFalse($domain->isVerified()); - - $job = new \App\Jobs\Domain\VerifyJob($domain->id); - $job->handle(); - - $this->assertTrue($domain->fresh()->isVerified()); - } - - /** - * Test job handle (non-existing domain) - * - * @group dns - */ - public function testHandleNonExisting(): void - { - $domain = $this->getTestDomain( - 'some-non-existing-domain.fff', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - $this->assertFalse($domain->isVerified()); - - $job = new \App\Jobs\Domain\VerifyJob($domain->id); - $job->handle(); - - $this->assertFalse($domain->fresh()->isVerified()); - } -} diff --git a/src/tests/Feature/Jobs/PasswordResetEmailTest.php b/src/tests/Feature/Jobs/PasswordResetEmailTest.php deleted file mode 100644 index 355fee06..00000000 --- a/src/tests/Feature/Jobs/PasswordResetEmailTest.php +++ /dev/null @@ -1,71 +0,0 @@ -deleteTestUser('PasswordReset@UserAccount.com'); - } - - /** - * {@inheritDoc} - * - * @return void - */ - public function tearDown(): void - { - $this->deleteTestUser('PasswordReset@UserAccount.com'); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @return void - */ - public function testPasswordResetEmailHandle() - { - $code = new VerificationCode([ - 'mode' => 'password-reset', - ]); - - $user = $this->getTestUser('PasswordReset@UserAccount.com'); - $user->verificationcodes()->save($code); - $user->setSettings(['external_email' => 'etx@email.com']); - - Mail::fake(); - - // Assert that no jobs were pushed... - Mail::assertNothingSent(); - - $job = new PasswordResetEmail($code); - $job->handle(); - - // Assert the email sending job was pushed once - Mail::assertSent(PasswordReset::class, 1); - - // Assert the mail was sent to the code's email - Mail::assertSent(PasswordReset::class, function ($mail) use ($code) { - return $mail->hasTo($code->user->getSetting('external_email')); - }); - } -} diff --git a/src/tests/Feature/Jobs/PaymentEmailTest.php b/src/tests/Feature/Jobs/PaymentEmailTest.php deleted file mode 100644 index 9ef6cfca..00000000 --- a/src/tests/Feature/Jobs/PaymentEmailTest.php +++ /dev/null @@ -1,120 +0,0 @@ -deleteTestUser('PaymentEmail@UserAccount.com'); - } - - /** - * {@inheritDoc} - * - * @return void - */ - public function tearDown(): void - { - $this->deleteTestUser('PaymentEmail@UserAccount.com'); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @return void - */ - public function testHandle() - { - $user = $this->getTestUser('PaymentEmail@UserAccount.com'); - $user->setSetting('external_email', 'ext@email.tld'); - $wallet = $user->wallets()->first(); - - $payment = new Payment(); - $payment->id = 'test-payment'; - $payment->wallet_id = $wallet->id; - $payment->amount = 100; - $payment->status = PaymentProvider::STATUS_PAID; - $payment->description = 'test'; - $payment->provider = 'stripe'; - $payment->type = PaymentProvider::TYPE_ONEOFF; - $payment->save(); - - Mail::fake(); - - // Assert that no jobs were pushed... - Mail::assertNothingSent(); - - $job = new PaymentEmail($payment); - $job->handle(); - - // Assert the email sending job was pushed once - Mail::assertSent(PaymentSuccess::class, 1); - - // Assert the mail was sent to the user's email - Mail::assertSent(PaymentSuccess::class, function ($mail) { - return $mail->hasTo('ext@email.tld') && !$mail->hasCc('ext@email.tld'); - }); - - $payment->status = PaymentProvider::STATUS_FAILED; - $payment->save(); - - $job = new PaymentEmail($payment); - $job->handle(); - - // Assert the email sending job was pushed once - Mail::assertSent(PaymentFailure::class, 1); - - // Assert the mail was sent to the user's email - Mail::assertSent(PaymentFailure::class, function ($mail) { - return $mail->hasTo('ext@email.tld') && !$mail->hasCc('ext@email.tld'); - }); - - $payment->status = PaymentProvider::STATUS_EXPIRED; - $payment->save(); - - $job = new PaymentEmail($payment); - $job->handle(); - - // Assert the email sending job was pushed twice - Mail::assertSent(PaymentFailure::class, 2); - - // None of statuses below should trigger an email - Mail::fake(); - - $states = [ - PaymentProvider::STATUS_OPEN, - PaymentProvider::STATUS_CANCELED, - PaymentProvider::STATUS_PENDING, - PaymentProvider::STATUS_AUTHORIZED, - ]; - - foreach ($states as $state) { - $payment->status = $state; - $payment->save(); - - $job = new PaymentEmail($payment); - $job->handle(); - } - - // Assert that no mailables were sent... - Mail::assertNothingSent(); - } -} diff --git a/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php b/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php deleted file mode 100644 index f2c90556..00000000 --- a/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php +++ /dev/null @@ -1,63 +0,0 @@ -deleteTestUser('PaymentEmail@UserAccount.com'); - } - - /** - * {@inheritDoc} - * - * @return void - */ - public function tearDown(): void - { - $this->deleteTestUser('PaymentEmail@UserAccount.com'); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @return void - */ - public function testHandle() - { - $user = $this->getTestUser('PaymentEmail@UserAccount.com'); - $user->setSetting('external_email', 'ext@email.tld'); - $wallet = $user->wallets()->first(); - - Mail::fake(); - - // Assert that no jobs were pushed... - Mail::assertNothingSent(); - - $job = new PaymentMandateDisabledEmail($wallet); - $job->handle(); - - // Assert the email sending job was pushed once - Mail::assertSent(PaymentMandateDisabled::class, 1); - - // Assert the mail was sent to the user's email - Mail::assertSent(PaymentMandateDisabled::class, function ($mail) { - return $mail->hasTo('ext@email.tld') && !$mail->hasCc('ext@email.tld'); - }); - } -} diff --git a/src/tests/Feature/Jobs/SignupVerificationEmailTest.php b/src/tests/Feature/Jobs/SignupVerificationEmailTest.php deleted file mode 100644 index f9a97cb7..00000000 --- a/src/tests/Feature/Jobs/SignupVerificationEmailTest.php +++ /dev/null @@ -1,66 +0,0 @@ -code = SignupCode::create([ - 'data' => [ - 'email' => 'SignupVerificationEmailTest1@' . \config('app.domain'), - 'first_name' => "Test", - 'last_name' => "Job" - ] - ]); - } - - /** - * {@inheritDoc} - * - * @return void - */ - public function tearDown(): void - { - $this->code->delete(); - } - - /** - * Test job handle - * - * @return void - */ - public function testSignupVerificationEmailHandle() - { - Mail::fake(); - - // Assert that no jobs were pushed... - Mail::assertNothingSent(); - - $job = new SignupVerificationEmail($this->code); - $job->handle(); - - // Assert the email sending job was pushed once - Mail::assertSent(SignupVerification::class, 1); - - // Assert the mail was sent to the code's email - Mail::assertSent(SignupVerification::class, function ($mail) { - return $mail->hasTo($this->code->data['email']); - }); - } -} diff --git a/src/tests/Feature/Jobs/UserCreateTest.php b/src/tests/Feature/Jobs/UserCreateTest.php deleted file mode 100644 index 266e4345..00000000 --- a/src/tests/Feature/Jobs/UserCreateTest.php +++ /dev/null @@ -1,42 +0,0 @@ -deleteTestUser('new-job-user@' . \config('app.domain')); - } - - public function tearDown(): void - { - $this->deleteTestUser('new-job-user@' . \config('app.domain')); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @group ldap - */ - public function testHandle(): void - { - $user = $this->getTestUser('new-job-user@' . \config('app.domain')); - - $this->assertFalse($user->isLdapReady()); - - $job = new \App\Jobs\User\CreateJob($user->id); - $job->handle(); - - $this->assertTrue($user->fresh()->isLdapReady()); - } -} diff --git a/src/tests/Feature/Jobs/UserUpdateTest.php b/src/tests/Feature/Jobs/UserUpdateTest.php deleted file mode 100644 index 2ec60bee..00000000 --- a/src/tests/Feature/Jobs/UserUpdateTest.php +++ /dev/null @@ -1,87 +0,0 @@ -deleteTestUser('new-job-user@' . \config('app.domain')); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('new-job-user@' . \config('app.domain')); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @group ldap - */ - public function testHandle(): void - { - // Ignore any jobs created here (e.g. on setAliases() use) - Queue::fake(); - - $user = $this->getTestUser('new-job-user@' . \config('app.domain')); - - // Create the user in LDAP - $job = new \App\Jobs\User\CreateJob($user->id); - $job->handle(); - - // Test setting two aliases - $aliases = [ - 'new-job-user1@' . \config('app.domain'), - 'new-job-user2@' . \config('app.domain'), - ]; - - $user->setAliases($aliases); - - $job = new \App\Jobs\User\UpdateJob($user->id); - $job->handle(); - - $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); - - $this->assertSame($aliases, $ldap_user['alias'], var_export($ldap_user, true)); - - // Test updating aliases list - $aliases = [ - 'new-job-user1@' . \config('app.domain'), - ]; - - $user->setAliases($aliases); - - $job = new \App\Jobs\User\UpdateJob($user->id); - $job->handle(); - - $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); - - $this->assertSame($aliases, (array) $ldap_user['alias']); - - // Test unsetting aliases list - $aliases = []; - $user->setAliases($aliases); - - $job = new \App\Jobs\User\UpdateJob($user->id); - $job->handle(); - - $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); - - $this->assertTrue(empty($ldap_user['alias'])); - } -} diff --git a/src/tests/Feature/Jobs/UserVerifyTest.php b/src/tests/Feature/Jobs/UserVerifyTest.php deleted file mode 100644 index 325bcdc7..00000000 --- a/src/tests/Feature/Jobs/UserVerifyTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getTestUser('ned@kolab.org'); - $ned->status |= User::STATUS_IMAP_READY; - $ned->save(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $ned = $this->getTestUser('ned@kolab.org'); - $ned->status |= User::STATUS_IMAP_READY; - $ned->save(); - - parent::tearDown(); - } - - /** - * Test job handle - * - * @group imap - */ - public function testHandle(): void - { - Queue::fake(); - - $user = $this->getTestUser('ned@kolab.org'); - - if ($user->isImapReady()) { - $user->status ^= User::STATUS_IMAP_READY; - $user->save(); - } - - $this->assertFalse($user->isImapReady()); - - for ($i = 0; $i < 10; $i++) { - $job = new \App\Jobs\User\VerifyJob($user->id); - $job->handle(); - - if ($user->fresh()->isImapReady()) { - $this->assertTrue(true); - return; - } - - sleep(1); - } - - $this->assertTrue(false, "Unable to verify the IMAP account is set up in time"); - } -} diff --git a/src/tests/Feature/Jobs/WalletCheckTest.php b/src/tests/Feature/Jobs/WalletCheckTest.php deleted file mode 100644 index 9dbfb406..00000000 --- a/src/tests/Feature/Jobs/WalletCheckTest.php +++ /dev/null @@ -1,275 +0,0 @@ -getTestUser('ned@kolab.org'); - if ($ned->isSuspended()) { - $ned->status -= User::STATUS_SUSPENDED; - $ned->save(); - } - - $this->deleteTestUser('wallet-check@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $ned = $this->getTestUser('ned@kolab.org'); - if ($ned->isSuspended()) { - $ned->status -= User::STATUS_SUSPENDED; - $ned->save(); - } - - $this->deleteTestUser('wallet-check@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test job handle, initial negative-balance notification - */ - public function testHandleInitial(): void - { - Mail::fake(); - - $user = $this->getTestUser('ned@kolab.org'); - $user->setSetting('external_email', 'external@test.com'); - $wallet = $user->wallets()->first(); - $now = Carbon::now(); - - // Balance is not negative, double-update+save for proper resetting of the state - $wallet->balance = -100; - $wallet->save(); - $wallet->balance = 0; - $wallet->save(); - - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - - // Balance is negative now - $wallet->balance = -100; - $wallet->save(); - - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - - // Balance turned negative 2 hours ago, expect mail sent - $wallet->setSetting('balance_negative_since', $now->subHours(2)->toDateTimeString()); - $wallet->setSetting('balance_warning_initial', null); - - $job = new WalletCheck($wallet); - $job->handle(); - - // Assert the mail was sent to the user's email, but not to his external email - Mail::assertSent(\App\Mail\NegativeBalance::class, 1); - Mail::assertSent(\App\Mail\NegativeBalance::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); - }); - - // Run the job again to make sure the notification is not sent again - Mail::fake(); - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - - // Test the migration scenario where a negative wallet has no balance_negative_since set yet - Mail::fake(); - $wallet->setSetting('balance_negative_since', null); - $wallet->setSetting('balance_warning_initial', null); - - $job = new WalletCheck($wallet); - $job->handle(); - - // Assert the mail was sent to the user's email, but not to his external email - Mail::assertSent(\App\Mail\NegativeBalance::class, 1); - Mail::assertSent(\App\Mail\NegativeBalance::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); - }); - - $wallet->refresh(); - $today_regexp = '/' . Carbon::now()->toDateString() . ' [0-9]{2}:[0-9]{2}:[0-9]{2}/'; - $this->assertRegExp($today_regexp, $wallet->getSetting('balance_negative_since')); - $this->assertRegExp($today_regexp, $wallet->getSetting('balance_warning_initial')); - } - - /** - * Test job handle, reminder notification - * - * @depends testHandleInitial - */ - public function testHandleReminder(): void - { - Mail::fake(); - - $user = $this->getTestUser('ned@kolab.org'); - $user->setSetting('external_email', 'external@test.com'); - $wallet = $user->wallets()->first(); - $now = Carbon::now(); - - // Balance turned negative 7+1 days ago, expect mail sent - $wallet->setSetting('balance_negative_since', $now->subDays(7 + 1)->toDateTimeString()); - - $job = new WalletCheck($wallet); - $job->handle(); - - // Assert the mail was sent to the user's email, but not to his external email - Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, 1); - Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); - }); - - // Run the job again to make sure the notification is not sent again - Mail::fake(); - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - } - - /** - * Test job handle, account suspending - * - * @depends testHandleReminder - */ - public function testHandleSuspended(): void - { - Mail::fake(); - - $user = $this->getTestUser('ned@kolab.org'); - $user->setSetting('external_email', 'external@test.com'); - $wallet = $user->wallets()->first(); - $now = Carbon::now(); - - // Balance turned negative 7+14+1 days ago, expect mail sent - $days = 7 + 14 + 1; - $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); - - $job = new WalletCheck($wallet); - $job->handle(); - - // Assert the mail was sent to the user's email, but not to his external email - Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, 1); - Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); - }); - - // Check that it has been suspended - $this->assertTrue($user->fresh()->isSuspended()); - - // TODO: Test that group account members/domain are also being suspended - /* - foreach ($wallet->entitlements()->fresh()->get() as $entitlement) { - if ( - $entitlement->entitleable_type == \App\Domain::class - || $entitlement->entitleable_type == \App\User::class - ) { - $this->assertTrue($entitlement->entitleable->isSuspended()); - } - } - */ - - // Run the job again to make sure the notification is not sent again - Mail::fake(); - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - } - - /** - * Test job handle, final warning before delete - * - * @depends testHandleSuspended - */ - public function testHandleBeforeDelete(): void - { - Mail::fake(); - - $user = $this->getTestUser('ned@kolab.org'); - $user->setSetting('external_email', 'external@test.com'); - $wallet = $user->wallets()->first(); - $now = Carbon::now(); - - // Balance turned negative 7+14+21-3+1 days ago, expect mail sent - $days = 7 + 14 + 21 - 3 + 1; - $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); - - $job = new WalletCheck($wallet); - $job->handle(); - - // Assert the mail was sent to the user's email, and his external email - Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, 1); - Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->hasCc('external@test.com'); - }); - - // Check that it has not been deleted yet - $this->assertFalse($user->fresh()->isDeleted()); - - // Run the job again to make sure the notification is not sent again - Mail::fake(); - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - } - - /** - * Test job handle, account delete - * - * @depends testHandleBeforeDelete - */ - public function testHandleDelete(): void - { - Mail::fake(); - - $user = $this->getTestUser('wallet-check@kolabnow.com'); - $wallet = $user->wallets()->first(); - $wallet->balance = -100; - $wallet->save(); - $now = Carbon::now(); - - $package = \App\Package::where('title', 'kolab')->first(); - $user->assignPackage($package); - - $this->assertFalse($user->isDeleted()); - $this->assertCount(4, $user->entitlements()->get()); - - // Balance turned negative 7+14+21+1 days ago, expect mail sent - $days = 7 + 14 + 21 + 1; - $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); - - $job = new WalletCheck($wallet); - $job->handle(); - - Mail::assertNothingSent(); - - // Check that it has not been deleted - $this->assertTrue($user->fresh()->trashed()); - $this->assertCount(0, $user->entitlements()->get()); - - // TODO: Test it deletes all members of the group account - } -} diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php deleted file mode 100644 index b3a0dd0d..00000000 --- a/src/tests/Feature/PlanTest.php +++ /dev/null @@ -1,109 +0,0 @@ -delete(); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - Plan::where('title', 'test-plan')->delete(); - - parent::tearDown(); - } - - /** - * Tests for plan attributes localization - */ - public function testPlanLocalization(): void - { - $plan = Plan::create([ - 'title' => 'test-plan', - 'description' => [ - 'en' => 'Plan-EN', - 'de' => 'Plan-DE', - ], - 'name' => 'Test', - ]); - - $this->assertSame('Plan-EN', $plan->description); - $this->assertSame('Test', $plan->name); - - $plan->save(); - $plan = Plan::where('title', 'test-plan')->first(); - - $this->assertSame('Plan-EN', $plan->description); - $this->assertSame('Test', $plan->name); - $this->assertSame('Plan-DE', $plan->getTranslation('description', 'de')); - $this->assertSame('Test', $plan->getTranslation('name', 'de')); - - $plan->setTranslation('name', 'de', 'Prüfung')->save(); - - $this->assertSame('Prüfung', $plan->getTranslation('name', 'de')); - $this->assertSame('Test', $plan->getTranslation('name', 'en')); - - $plan = Plan::where('title', 'test-plan')->first(); - - $this->assertSame('Prüfung', $plan->getTranslation('name', 'de')); - $this->assertSame('Test', $plan->getTranslation('name', 'en')); - - // TODO: Test system locale change - } - - /** - * Tests for Plan::hasDomain() - */ - public function testHasDomain(): void - { - $plan = Plan::where('title', 'individual')->first(); - - $this->assertTrue($plan->hasDomain() === false); - - $plan = Plan::where('title', 'group')->first(); - - $this->assertTrue($plan->hasDomain() === true); - } - - /** - * Test for a plan's cost. - */ - public function testCost(): void - { - $plan = Plan::where('title', 'individual')->first(); - - $package_costs = 0; - - foreach ($plan->packages as $package) { - $package_costs += $package->cost(); - } - - $this->assertTrue( - $package_costs == 999, - "The total costs of all packages for this plan is not 9.99" - ); - - $this->assertTrue( - $plan->cost() == 999, - "The total costs for this plan is not 9.99" - ); - - $this->assertTrue($plan->cost() == $package_costs); - } -} diff --git a/src/tests/Feature/SignupCodeTest.php b/src/tests/Feature/SignupCodeTest.php deleted file mode 100644 index d6576559..00000000 --- a/src/tests/Feature/SignupCodeTest.php +++ /dev/null @@ -1,45 +0,0 @@ - [ - 'email' => 'User@email.org', - ] - ]; - - $now = Carbon::now(); - - $code = SignupCode::create($data); - - $code_length = env('VERIFICATION_CODE_LENGTH', SignupCode::SHORTCODE_LENGTH); - $exp = Carbon::now()->addHours(env('SIGNUP_CODE_EXPIRY', SignupCode::CODE_EXP_HOURS)); - - $this->assertFalse($code->isExpired()); - $this->assertTrue(strlen($code->code) === SignupCode::CODE_LENGTH); - $this->assertTrue(strlen($code->short_code) === $code_length); - $this->assertSame($data['data'], $code->data); - $this->assertInstanceOf(Carbon::class, $code->expires_at); - $this->assertSame($code->expires_at->toDateTimeString(), $exp->toDateTimeString()); - - $inst = SignupCode::find($code->code); - - $this->assertInstanceOf(SignupCode::class, $inst); - $this->assertSame($inst->code, $code->code); - } -} diff --git a/src/tests/Feature/SkuTest.php b/src/tests/Feature/SkuTest.php deleted file mode 100644 index 406ed56b..00000000 --- a/src/tests/Feature/SkuTest.php +++ /dev/null @@ -1,94 +0,0 @@ -deleteTestUser('jane@kolabnow.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('jane@kolabnow.com'); - - parent::tearDown(); - } - - public function testPackageEntitlements(): void - { - $user = $this->getTestUser('jane@kolabnow.com'); - - $wallet = $user->wallets()->first(); - - $package = Package::where('title', 'lite')->first(); - - $sku_mailbox = Sku::where('title', 'mailbox')->first(); - $sku_storage = Sku::where('title', 'storage')->first(); - - $user = $user->assignPackage($package); - - $this->backdateEntitlements($user->fresh()->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); - - $wallet->chargeEntitlements(); - - $this->assertTrue($wallet->balance < 0); - } - - public function testSkuEntitlements(): void - { - $this->assertCount(4, Sku::where('title', 'mailbox')->first()->entitlements); - } - - public function testSkuPackages(): void - { - $this->assertCount(2, Sku::where('title', 'mailbox')->first()->packages); - } - - public function testSkuHandlerDomainHosting(): void - { - $sku = Sku::where('title', 'domain-hosting')->first(); - - $entitlement = $sku->entitlements->first(); - - $this->assertSame( - Handlers\DomainHosting::entitleableClass(), - $entitlement->entitleable_type - ); - } - - public function testSkuHandlerMailbox(): void - { - $sku = Sku::where('title', 'mailbox')->first(); - - $entitlement = $sku->entitlements->first(); - - $this->assertSame( - Handlers\Mailbox::entitleableClass(), - $entitlement->entitleable_type - ); - } - - public function testSkuHandlerStorage(): void - { - $sku = Sku::where('title', 'storage')->first(); - - $entitlement = $sku->entitlements->first(); - - $this->assertSame( - Handlers\Storage::entitleableClass(), - $entitlement->entitleable_type - ); - } -} diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php deleted file mode 100644 index a17016c7..00000000 --- a/src/tests/Feature/UserTest.php +++ /dev/null @@ -1,554 +0,0 @@ -deleteTestUser('user-test@' . \config('app.domain')); - $this->deleteTestUser('UserAccountA@UserAccount.com'); - $this->deleteTestUser('UserAccountB@UserAccount.com'); - $this->deleteTestUser('UserAccountC@UserAccount.com'); - $this->deleteTestDomain('UserAccount.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('user-test@' . \config('app.domain')); - $this->deleteTestUser('UserAccountA@UserAccount.com'); - $this->deleteTestUser('UserAccountB@UserAccount.com'); - $this->deleteTestUser('UserAccountC@UserAccount.com'); - $this->deleteTestDomain('UserAccount.com'); - - parent::tearDown(); - } - - /** - * Tests for User::assignPackage() - */ - public function testAssignPackage(): void - { - $this->markTestIncomplete(); - } - - /** - * Tests for User::assignPlan() - */ - public function testAssignPlan(): void - { - $this->markTestIncomplete(); - } - - /** - * Tests for User::assignSku() - */ - public function testAssignSku(): void - { - $this->markTestIncomplete(); - } - - /** - * Verify a wallet assigned a controller is among the accounts of the assignee. - */ - public function testAccounts(): void - { - $userA = $this->getTestUser('UserAccountA@UserAccount.com'); - $userB = $this->getTestUser('UserAccountB@UserAccount.com'); - - $this->assertTrue($userA->wallets()->count() == 1); - - $userA->wallets()->each( - function ($wallet) use ($userB) { - $wallet->addController($userB); - } - ); - - $this->assertTrue($userB->accounts()->get()[0]->id === $userA->wallets()->get()[0]->id); - } - - public function testCanDelete(): void - { - $this->markTestIncomplete(); - } - - public function testCanRead(): void - { - $this->markTestIncomplete(); - } - - public function testCanUpdate(): void - { - $this->markTestIncomplete(); - } - - /** - * Test user create/creating observer - */ - public function testCreate(): void - { - Queue::fake(); - - $domain = \config('app.domain'); - - $user = User::create(['email' => 'USER-test@' . \strtoupper($domain)]); - - $result = User::where('email', 'user-test@' . $domain)->first(); - - $this->assertSame('user-test@' . $domain, $result->email); - $this->assertSame($user->id, $result->id); - $this->assertSame(User::STATUS_NEW | User::STATUS_ACTIVE, $result->status); - } - - /** - * Verify user creation process - */ - public function testCreateJobs(): void - { - // Fake the queue, assert that no jobs were pushed... - Queue::fake(); - Queue::assertNothingPushed(); - - $user = User::create([ - 'email' => 'user-test@' . \config('app.domain') - ]); - - Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); - - Queue::assertPushed( - \App\Jobs\User\CreateJob::class, - function ($job) use ($user) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - $userId = TestCase::getObjectProperty($job, 'userId'); - - return $userEmail === $user->email - && $userId === $user->id; - } - ); - - Queue::assertPushedWithChain( - \App\Jobs\User\CreateJob::class, - [ - \App\Jobs\User\VerifyJob::class, - ] - ); -/* - FIXME: Looks like we can't really do detailed assertions on chained jobs - Another thing to consider is if we maybe should run these jobs - independently (not chained) and make sure there's no race-condition - in status update - - Queue::assertPushed(\App\Jobs\User\VerifyJob::class, 1); - Queue::assertPushed(\App\Jobs\User\VerifyJob::class, function ($job) use ($user) { - $userEmail = TestCase::getObjectProperty($job, 'userEmail'); - $userId = TestCase::getObjectProperty($job, 'userId'); - - return $userEmail === $user->email - && $userId === $user->id; - }); -*/ - } - - /** - * Tests for User::domains() - */ - public function testDomains(): void - { - $user = $this->getTestUser('john@kolab.org'); - $domains = []; - - foreach ($user->domains() as $domain) { - $domains[] = $domain->namespace; - } - - $this->assertContains(\config('app.domain'), $domains); - $this->assertContains('kolab.org', $domains); - - // Jack is not the wallet controller, so for him the list should not - // include John's domains, kolab.org specifically - $user = $this->getTestUser('jack@kolab.org'); - $domains = []; - - foreach ($user->domains() as $domain) { - $domains[] = $domain->namespace; - } - - $this->assertContains(\config('app.domain'), $domains); - $this->assertNotContains('kolab.org', $domains); - } - - public function testUserQuota(): void - { - // TODO: This test does not test much, probably could be removed - // or moved to somewhere else, or extended with - // other entitlements() related cases. - - $user = $this->getTestUser('john@kolab.org'); - $storage_sku = \App\Sku::where('title', 'storage')->first(); - - $count = 0; - - foreach ($user->entitlements()->get() as $entitlement) { - if ($entitlement->sku_id == $storage_sku->id) { - $count += 1; - } - } - - $this->assertTrue($count == 2); - } - - /** - * Test user deletion - */ - public function testDelete(): void - { - Queue::fake(); - - $user = $this->getTestUser('user-test@' . \config('app.domain')); - $package = \App\Package::where('title', 'kolab')->first(); - $user->assignPackage($package); - - $id = $user->id; - - $this->assertCount(4, $user->entitlements()->get()); - - $user->delete(); - - $this->assertCount(0, $user->entitlements()->get()); - $this->assertTrue($user->fresh()->trashed()); - $this->assertFalse($user->fresh()->isDeleted()); - - // Delete the user for real - $job = new \App\Jobs\User\DeleteJob($id); - $job->handle(); - - $this->assertTrue(User::withTrashed()->where('id', $id)->first()->isDeleted()); - - $user->forceDelete(); - - $this->assertCount(0, User::withTrashed()->where('id', $id)->get()); - - // Test an account with users - $userA = $this->getTestUser('UserAccountA@UserAccount.com'); - $userB = $this->getTestUser('UserAccountB@UserAccount.com'); - $userC = $this->getTestUser('UserAccountC@UserAccount.com'); - $package_kolab = \App\Package::where('title', 'kolab')->first(); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - $domain = $this->getTestDomain('UserAccount.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_HOSTED, - ]); - $userA->assignPackage($package_kolab); - $domain->assignPackage($package_domain, $userA); - $userA->assignPackage($package_kolab, $userB); - $userA->assignPackage($package_kolab, $userC); - - $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); - $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); - $entitlementsC = \App\Entitlement::where('entitleable_id', $userC->id); - $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id); - $this->assertSame(4, $entitlementsA->count()); - $this->assertSame(4, $entitlementsB->count()); - $this->assertSame(4, $entitlementsC->count()); - $this->assertSame(1, $entitlementsDomain->count()); - - // Delete non-controller user - $userC->delete(); - - $this->assertTrue($userC->fresh()->trashed()); - $this->assertFalse($userC->fresh()->isDeleted()); - $this->assertSame(0, $entitlementsC->count()); - - // Delete the controller (and expect "sub"-users to be deleted too) - $userA->delete(); - - $this->assertSame(0, $entitlementsA->count()); - $this->assertSame(0, $entitlementsB->count()); - $this->assertSame(0, $entitlementsDomain->count()); - $this->assertTrue($userA->fresh()->trashed()); - $this->assertTrue($userB->fresh()->trashed()); - $this->assertTrue($domain->fresh()->trashed()); - $this->assertFalse($userA->isDeleted()); - $this->assertFalse($userB->isDeleted()); - $this->assertFalse($domain->isDeleted()); - } - - /** - * Tests for User::emailExists() - */ - public function testEmailExists(): void - { - $this->markTestIncomplete(); - } - - /** - * Tests for User::findByEmail() - */ - public function testFindByEmail(): void - { - $user = $this->getTestUser('john@kolab.org'); - - $result = User::findByEmail('john'); - $this->assertNull($result); - - $result = User::findByEmail('non-existing@email.com'); - $this->assertNull($result); - - $result = User::findByEmail('john@kolab.org'); - $this->assertInstanceOf(User::class, $result); - $this->assertSame($user->id, $result->id); - - // Use an alias - $result = User::findByEmail('john.doe@kolab.org'); - $this->assertInstanceOf(User::class, $result); - $this->assertSame($user->id, $result->id); - - // A case where two users have the same alias - $ned = $this->getTestUser('ned@kolab.org'); - $ned->setAliases(['joe.monster@kolab.org']); - $result = User::findByEmail('joe.monster@kolab.org'); - $this->assertNull($result); - $ned->setAliases([]); - - // TODO: searching by external email (setting) - $this->markTestIncomplete(); - } - - /** - * Test User::name() - */ - public function testName(): void - { - Queue::fake(); - - $user = $this->getTestUser('user-test@' . \config('app.domain')); - - $this->assertSame('', $user->name()); - $this->assertSame(\config('app.name') . ' User', $user->name(true)); - - $user->setSetting('first_name', 'First'); - - $this->assertSame('First', $user->name()); - $this->assertSame('First', $user->name(true)); - - $user->setSetting('last_name', 'Last'); - - $this->assertSame('First Last', $user->name()); - $this->assertSame('First Last', $user->name(true)); - } - - /** - * Tests for UserAliasesTrait::setAliases() - */ - public function testSetAliases(): void - { - Queue::fake(); - Queue::assertNothingPushed(); - - $user = $this->getTestUser('UserAccountA@UserAccount.com'); - $domain = $this->getTestDomain('UserAccount.com', [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_HOSTED, - ]); - - $this->assertCount(0, $user->aliases->all()); - - // Add an alias - $user->setAliases(['UserAlias1@UserAccount.com']); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); - - $aliases = $user->aliases()->get(); - $this->assertCount(1, $aliases); - $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); - - // Add another alias - $user->setAliases(['UserAlias1@UserAccount.com', 'UserAlias2@UserAccount.com']); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 2); - - $aliases = $user->aliases()->orderBy('alias')->get(); - $this->assertCount(2, $aliases); - $this->assertSame('useralias1@useraccount.com', $aliases[0]->alias); - $this->assertSame('useralias2@useraccount.com', $aliases[1]->alias); - - // Remove an alias - $user->setAliases(['UserAlias1@UserAccount.com']); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 3); - - $aliases = $user->aliases()->get(); - $this->assertCount(1, $aliases); - $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); - - // Remove all aliases - $user->setAliases([]); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 4); - - $this->assertCount(0, $user->aliases()->get()); - - // The test below fail since we removed validation code from the UserAliasObserver - $this->markTestIncomplete(); - - // Test sanity checks in UserAliasObserver - Queue::fake(); - - // Existing user - $user->setAliases(['john@kolab.org']); - $this->assertCount(0, $user->aliases()->get()); - - // Existing alias (in another account) - $user->setAliases(['john.doe@kolab.org']); - $this->assertCount(0, $user->aliases()->get()); - - Queue::assertNothingPushed(); - - // Existing user (in the same group account) - $ned = $this->getTestUser('ned@kolab.org'); - $ned->setAliases(['john@kolab.org']); - $this->assertCount(0, $ned->aliases()->get()); - - // Existing alias (in the same group account) - $ned = $this->getTestUser('ned@kolab.org'); - $ned->setAliases(['john.doe@kolab.org']); - $this->assertSame('john.doe@kolab.org', $ned->aliases()->first()->alias); - - // Existing alias (in another account, public domain) - $user->setAliases(['alias@kolabnow.com']); - $ned->setAliases(['alias@kolabnow.com']); - $this->assertCount(0, $ned->aliases()->get()); - - // cleanup - $ned->setAliases([]); - } - - /** - * Tests for UserSettingsTrait::setSettings() and getSetting() - */ - public function testUserSettings(): void - { - Queue::fake(); - Queue::assertNothingPushed(); - - $user = $this->getTestUser('UserAccountA@UserAccount.com'); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 0); - - // Test default settings - // Note: Technicly this tests UserObserver::created() behavior - $all_settings = $user->settings()->orderBy('key')->get(); - $this->assertCount(2, $all_settings); - $this->assertSame('country', $all_settings[0]->key); - $this->assertSame('CH', $all_settings[0]->value); - $this->assertSame('currency', $all_settings[1]->key); - $this->assertSame('CHF', $all_settings[1]->value); - - // Add a setting - $user->setSetting('first_name', 'Firstname'); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); - - // Note: We test both current user as well as fresh user object - // to make sure cache works as expected - $this->assertSame('Firstname', $user->getSetting('first_name')); - $this->assertSame('Firstname', $user->fresh()->getSetting('first_name')); - - // Update a setting - $user->setSetting('first_name', 'Firstname1'); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 2); - - // Note: We test both current user as well as fresh user object - // to make sure cache works as expected - $this->assertSame('Firstname1', $user->getSetting('first_name')); - $this->assertSame('Firstname1', $user->fresh()->getSetting('first_name')); - - // Delete a setting (null) - $user->setSetting('first_name', null); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 3); - - // Note: We test both current user as well as fresh user object - // to make sure cache works as expected - $this->assertSame(null, $user->getSetting('first_name')); - $this->assertSame(null, $user->fresh()->getSetting('first_name')); - - // Delete a setting (empty string) - $user->setSetting('first_name', 'Firstname1'); - $user->setSetting('first_name', ''); - - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 5); - - // Note: We test both current user as well as fresh user object - // to make sure cache works as expected - $this->assertSame(null, $user->getSetting('first_name')); - $this->assertSame(null, $user->fresh()->getSetting('first_name')); - - // Set multiple settings at once - $user->setSettings([ - 'first_name' => 'Firstname2', - 'last_name' => 'Lastname2', - 'country' => null, - ]); - - // TODO: This really should create a single UserUpdate job, not 3 - Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 7); - - // Note: We test both current user as well as fresh user object - // to make sure cache works as expected - $this->assertSame('Firstname2', $user->getSetting('first_name')); - $this->assertSame('Firstname2', $user->fresh()->getSetting('first_name')); - $this->assertSame('Lastname2', $user->getSetting('last_name')); - $this->assertSame('Lastname2', $user->fresh()->getSetting('last_name')); - $this->assertSame(null, $user->getSetting('country')); - $this->assertSame(null, $user->fresh()->getSetting('country')); - - $all_settings = $user->settings()->orderBy('key')->get(); - $this->assertCount(3, $all_settings); - } - - /** - * Tests for User::users() - */ - public function testUsers(): void - { - $jack = $this->getTestUser('jack@kolab.org'); - $joe = $this->getTestUser('joe@kolab.org'); - $john = $this->getTestUser('john@kolab.org'); - $ned = $this->getTestUser('ned@kolab.org'); - $wallet = $john->wallets()->first(); - - $users = $john->users()->orderBy('email')->get(); - - $this->assertCount(4, $users); - $this->assertEquals($jack->id, $users[0]->id); - $this->assertEquals($joe->id, $users[1]->id); - $this->assertEquals($john->id, $users[2]->id); - $this->assertEquals($ned->id, $users[3]->id); - $this->assertSame($wallet->id, $users[0]->wallet_id); - $this->assertSame($wallet->id, $users[1]->wallet_id); - $this->assertSame($wallet->id, $users[2]->wallet_id); - $this->assertSame($wallet->id, $users[3]->wallet_id); - - $users = $jack->users()->orderBy('email')->get(); - - $this->assertCount(0, $users); - - $users = $ned->users()->orderBy('email')->get(); - - $this->assertCount(4, $users); - } - - public function testWallets(): void - { - $this->markTestIncomplete(); - } -} diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php deleted file mode 100644 index 4742ff5e..00000000 --- a/src/tests/Feature/VerificationCodeTest.php +++ /dev/null @@ -1,60 +0,0 @@ -deleteTestUser('UserAccountA@UserAccount.com'); - } - - public function tearDown(): void - { - $this->deleteTestUser('UserAccountA@UserAccount.com'); - - parent::tearDown(); - } - - /** - * Test VerificationCode creation - */ - public function testVerificationCodeCreate(): void - { - $user = $this->getTestUser('UserAccountA@UserAccount.com'); - $data = [ - 'user_id' => $user->id, - 'mode' => 'password-reset', - ]; - - $now = new \DateTime('now'); - - $code = VerificationCode::create($data); - - $code_length = env('VERIFICATION_CODE_LENGTH', VerificationCode::SHORTCODE_LENGTH); - $code_exp_hrs = env('VERIFICATION_CODE_EXPIRY', VerificationCode::CODE_EXP_HOURS); - $exp = Carbon::now()->addHours($code_exp_hrs); - - $this->assertFalse($code->isExpired()); - $this->assertTrue(strlen($code->code) === VerificationCode::CODE_LENGTH); - $this->assertTrue(strlen($code->short_code) === $code_length); - $this->assertSame($data['mode'], $code->mode); - $this->assertEquals($user->id, $code->user->id); - $this->assertInstanceOf(\DateTime::class, $code->expires_at); - $this->assertSame($code->expires_at->toDateTimeString(), $exp->toDateTimeString()); - - $inst = VerificationCode::find($code->code); - - $this->assertInstanceOf(VerificationCode::class, $inst); - $this->assertSame($inst->code, $code->code); - } -} diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php deleted file mode 100644 index 7dc96664..00000000 --- a/src/tests/Feature/WalletTest.php +++ /dev/null @@ -1,284 +0,0 @@ -users as $user) { - $this->deleteTestUser($user); - } - } - - public function tearDown(): void - { - foreach ($this->users as $user) { - $this->deleteTestUser($user); - } - - parent::tearDown(); - } - - /** - * Test that turning wallet balance from negative to positive - * unsuspends the account - */ - public function testBalancePositiveUnsuspend(): void - { - $user = $this->getTestUser('UserWallet1@UserWallet.com'); - $user->suspend(); - - $wallet = $user->wallets()->first(); - $wallet->balance = -100; - $wallet->save(); - - $this->assertTrue($user->isSuspended()); - $this->assertNotNull($wallet->getSetting('balance_negative_since')); - - $wallet->balance = 100; - $wallet->save(); - - $this->assertFalse($user->fresh()->isSuspended()); - $this->assertNull($wallet->getSetting('balance_negative_since')); - - // TODO: Test group account and unsuspending domain/members - } - - /** - * Test for Wallet::balanceLastsUntil() - */ - public function testBalanceLastsUntil(): void - { - // Monthly cost of all entitlements: 999 - // 28 days: 35.68 per day - // 31 days: 32.22 per day - - $user = $this->getTestUser('jane@kolabnow.com'); - $package = Package::where('title', 'kolab')->first(); - $user->assignPackage($package); - $wallet = $user->wallets()->first(); - - // User/entitlements created today, balance=0 - $until = $wallet->balanceLastsUntil(); - - $this->assertSame( - Carbon::now()->addMonthsWithoutOverflow(1)->toDateString(), - $until->toDateString() - ); - - // User/entitlements created today, balance=-10 CHF - $wallet->balance = -1000; - $until = $wallet->balanceLastsUntil(); - - $this->assertSame(null, $until); - - // User/entitlements created today, balance=-9,99 CHF (monthly cost) - $wallet->balance = 999; - $until = $wallet->balanceLastsUntil(); - - $daysInLastMonth = \App\Utils::daysInLastMonth(); - - $this->assertSame( - Carbon::now()->addMonthsWithoutOverflow(1)->addDays($daysInLastMonth)->toDateString(), - $until->toDateString() - ); - - // Old entitlements, 100% discount - $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); - $discount = \App\Discount::where('discount', 100)->first(); - $wallet->discount()->associate($discount); - - $until = $wallet->refresh()->balanceLastsUntil(); - - $this->assertSame(null, $until); - - // User with no entitlements - $wallet->discount()->dissociate($discount); - $wallet->entitlements()->delete(); - - $until = $wallet->refresh()->balanceLastsUntil(); - - $this->assertSame(null, $until); - } - - /** - * Test for Wallet::costsPerDay() - */ - public function testCostsPerDay(): void - { - // 999 - // 28 days: 35.68 - // 31 days: 32.22 - $user = $this->getTestUser('jane@kolabnow.com'); - - $package = Package::where('title', 'kolab')->first(); - $mailbox = Sku::where('title', 'mailbox')->first(); - - $user->assignPackage($package); - - $wallet = $user->wallets()->first(); - - $costsPerDay = $wallet->costsPerDay(); - - $this->assertTrue($costsPerDay < 35.68); - $this->assertTrue($costsPerDay > 32.22); - } - - /** - * Verify a wallet is created, when a user is created. - */ - public function testCreateUserCreatesWallet(): void - { - $user = $this->getTestUser('UserWallet1@UserWallet.com'); - - $this->assertCount(1, $user->wallets); - } - - /** - * Verify a user can haz more wallets. - */ - public function testAddWallet(): void - { - $user = $this->getTestUser('UserWallet2@UserWallet.com'); - - $user->wallets()->save( - new Wallet(['currency' => 'USD']) - ); - - $this->assertCount(2, $user->wallets); - - $user->wallets()->each( - function ($wallet) { - $this->assertEquals(0, $wallet->balance); - } - ); - } - - /** - * Verify we can not delete a user wallet that holds balance. - */ - public function testDeleteWalletWithCredit(): void - { - $user = $this->getTestUser('UserWallet3@UserWallet.com'); - - $user->wallets()->each( - function ($wallet) { - $wallet->credit(100)->save(); - } - ); - - $user->wallets()->each( - function ($wallet) { - $this->assertFalse($wallet->delete()); - } - ); - } - - /** - * Verify we can not delete a wallet that is the last wallet. - */ - public function testDeleteLastWallet(): void - { - $user = $this->getTestUser('UserWallet4@UserWallet.com'); - - $this->assertCount(1, $user->wallets); - - $user->wallets()->each( - function ($wallet) { - $this->assertFalse($wallet->delete()); - } - ); - } - - /** - * Verify we can remove a wallet that is an additional wallet. - */ - public function testDeleteAddtWallet(): void - { - $user = $this->getTestUser('UserWallet5@UserWallet.com'); - - $user->wallets()->save( - new Wallet(['currency' => 'USD']) - ); - - $user->wallets()->each( - function ($wallet) { - if ($wallet->currency == 'USD') { - $this->assertNotFalse($wallet->delete()); - } - } - ); - } - - /** - * Verify a wallet can be assigned a controller. - */ - public function testAddWalletController(): void - { - $userA = $this->getTestUser('WalletControllerA@WalletController.com'); - $userB = $this->getTestUser('WalletControllerB@WalletController.com'); - - $userA->wallets()->each( - function ($wallet) use ($userB) { - $wallet->addController($userB); - } - ); - - $this->assertCount(1, $userB->accounts); - - $aWallet = $userA->wallets()->first(); - $bAccount = $userB->accounts()->first(); - - $this->assertTrue($bAccount->id === $aWallet->id); - } - - /** - * Verify controllers can also be removed from wallets. - */ - public function testRemoveWalletController(): void - { - $userA = $this->getTestUser('WalletController2A@WalletController.com'); - $userB = $this->getTestUser('WalletController2B@WalletController.com'); - - $userA->wallets()->each( - function ($wallet) use ($userB) { - $wallet->addController($userB); - } - ); - - $userB->refresh(); - - $userB->accounts()->each( - function ($wallet) use ($userB) { - $wallet->removeController($userB); - } - ); - - $this->assertCount(0, $userB->accounts); - } -} diff --git a/src/tests/Functional/Methods/Auth/LDAPUserProviderTest.php b/src/tests/Functional/Methods/Auth/LDAPUserProviderTest.php new file mode 100644 index 00000000..b001d8a1 --- /dev/null +++ b/src/tests/Functional/Methods/Auth/LDAPUserProviderTest.php @@ -0,0 +1,153 @@ +createApplication(); + $this->userProvider = new LDAPUserProvider($app['hash'], \App\User::class); + } + + public function testRetrieveByCredentialsNone() + { + $result = $this->userProvider->retrieveByCredentials( + [ + 'email' => 'nobody.owns@this.email.domain', + 'password' => 'any password will do' + ] + ); + + $this->assertNull($result); + } + + /** + * Test + */ + public function testRetrieveByCredentialsDomainOwnerSuccess() + { + $result = $this->userProvider->retrieveByCredentials( + [ + 'email' => $this->domainOwner->email, + 'password' => $this->userPassword + ] + ); + + $this->assertNotNull($result); + $this->assertInstanceOf(\App\User::class, $result); + } + + public function testRetrieveByCredentialsDomainOwnerWrongPasswordSuccess() + { + $result = $this->userProvider->retrieveByCredentials( + [ + 'email' => $this->domainOwner->email, + 'password' => 'definitely not the one that was randomly generated' + ] + ); + + $this->assertNotNull($result); + $this->assertInstanceOf(\App\User::class, $result); + } + + public function testValidateCredentials() + { + $result = $this->userProvider->validateCredentials( + $this->domainOwner, + [ + 'email' => $this->domainOwner->email, + 'password' => $this->userPassword + ] + ); + + $this->assertTrue($result); + } + + public function testValidateCredentialsCaseSensitivity() + { + $result = $this->userProvider->validateCredentials( + $this->domainOwner, + [ + 'email' => strtoupper($this->domainOwner->email), + 'password' => $this->userPassword + ] + ); + + $this->assertTrue($result); + } + + public function testValidateCredentialsWrongPassword() + { + $result = $this->userProvider->validateCredentials( + $this->domainOwner, + [ + 'email' => $this->domainOwner->email, + 'password' => 'definitely not the one that was randomly generated' + ] + ); + + $this->assertFalse($result); + } + + public function testValidateCredentialsEmptyPassword() + { + DB::update("UPDATE users SET password = null WHERE id = ?", [$this->domainOwner->id]); + + $result = $this->userProvider->validateCredentials( + $this->domainOwner->fresh(), + [ + 'email' => $this->domainOwner->email, + 'password' => $this->userPassword + ] + ); + + $this->assertTrue($result); + } + + public function testValidateCredentialsEmptyLDAPPassword() + { + DB::update("UPDATE users SET password_ldap = null WHERE id = ?", [$this->domainOwner->id]); + + $result = $this->userProvider->validateCredentials( + $this->domainOwner->fresh(), + [ + 'email' => $this->domainOwner->email, + 'password' => $this->userPassword + ] + ); + + $this->assertTrue($result); + } + + public function testValidateCredentialsEmptyPasswords() + { + DB::update("UPDATE users SET password = null, password_ldap = null WHERE id = ?", [$this->domainOwner->id]); + + $result = $this->userProvider->validateCredentials( + $this->domainOwner->fresh(), + [ + 'email' => $this->domainOwner->email, + 'password' => $this->userPassword + ] + ); + + $this->assertFalse($result); + } +} diff --git a/src/tests/Functional/Methods/Auth/SecondFactorTest.php b/src/tests/Functional/Methods/Auth/SecondFactorTest.php new file mode 100644 index 00000000..572a33f0 --- /dev/null +++ b/src/tests/Functional/Methods/Auth/SecondFactorTest.php @@ -0,0 +1,63 @@ +domainUsers as $user) { + if ($user->hasSku('2fa')) { + $this->testUser = $user; + break; + } + } + + // select any user without a second factor + foreach ($this->domainUsers as $user) { + if (!$user->hasSku('2fa')) { + $this->testUserNone = $user; + break; + } + } + } + + /** + * Verify factors exist for the test user. + */ + public function testFactors() + { + $mf = new \App\Auth\SecondFactor($this->testUser); + $factors = $mf->factors(); + $this->assertNotEmpty($factors); + } + + /** + * Verify no factors exist for the test user without factors. + */ + public function testFactorsNone() + { + $mf = new \App\Auth\SecondFactor($this->testUserNone); + $factors = $mf->factors(); + $this->assertEmpty($factors); + } +} diff --git a/src/tests/Functional/Methods/DomainTest.php b/src/tests/Functional/Methods/DomainTest.php index 604f6c29..bebba58f 100644 --- a/src/tests/Functional/Methods/DomainTest.php +++ b/src/tests/Functional/Methods/DomainTest.php @@ -1,114 +1,202 @@ domain = $this->getTestDomain( - 'test.domain', - [ - 'status' => \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED, - 'type' => \App\Domain::TYPE_EXTERNAL - ] - ); + $this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first(); + $this->publicDomainUser = $this->getTestUser('john@' . $this->publicDomain->namespace); } public function tearDown(): void { - $this->deleteTestDomain('test.domain'); + $this->deleteTestUser($this->publicDomainUser->email); parent::tearDown(); } + /** + * Test that a public domain can not be assigned a package. + */ + public function testAssignPackagePublicDomain() + { + $domain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first(); + $package = \App\Package::where('title', 'domain-hosting')->first(); + $sku = \App\Sku::where('title', 'domain-hosting')->first(); + + $numEntitlementsBefore = $sku->entitlements->count(); + + $domain->assignPackage($package, $this->publicDomainUser); + + // the domain is not associated with any entitlements. + $entitlement = $domain->entitlement; + $this->assertNull($entitlement); + + // the sku is not associated with more entitlements than before + $numEntitlementsAfter = $sku->fresh()->entitlements->count(); + $this->assertEqual($numEntitlementsBefore, $numEntitlementsAfter); + } + + /** + * Verify a domain that is assigned to a wallet already, can not be assigned to another wallet. + */ + public function testAssignPackageDomainWithWallet() + { + $package = \App\Package::where('title', 'domain-hosting')->first(); + $sku = \App\Sku::where('title', 'domain-hosting')->first(); + + $this->assertSame($this->domainHosted->wallet()->owner->email, $this->domainOwner->email); + + $numEntitlementsBefore = $sku->entitlements->count(); + + $this->domainHosted->assignPackage($package, $this->publicDomainUser); + + // the sku is not associated with more entitlements than before + $numEntitlementsAfter = $sku->fresh()->entitlements->count(); + $this->assertEqual($numEntitlementsBefore, $numEntitlementsAfter); + + // the wallet for this temporary user still holds no entitlements + $wallet = $this->publicDomainUser->wallets()->first(); + $this->assertCount(0, $wallet->entitlements); + } + + /** + * Verify the function getPublicDomains returns a flat, single-dimensional, disassociative array of strings. + */ + public function testGetPublicDomainsIsFlatArray() + { + $domains = \App\Domain::getPublicDomains(); + + $this->assertisArray($domains); + + foreach ($domains as $domain) { + $this->assertIsString($domain); + } + + foreach ($domains as $num => $domain) { + $this->assertIsInt($num); + $this->assertIsString($domain); + } + } + + public function testGetPublicDomainsIsSorted() + { + $domains = \App\Domain::getPublicDomains(); + + sort($domains); + + $this->assertSame($domains, \App\Domain::getPublicDomains()); + } + + /** * Verify we can suspend an active domain. */ public function testSuspendForActiveDomain() { Queue::fake(); - $this->domain->status |= \App\Domain::STATUS_ACTIVE; + $this->domainHosted->status |= \App\Domain::STATUS_ACTIVE; - $this->assertFalse($this->domain->isSuspended()); - $this->assertTrue($this->domain->isActive()); + $this->assertFalse($this->domainHosted->isSuspended()); + $this->assertTrue($this->domainHosted->isActive()); - $this->domain->suspend(); + $this->domainHosted->suspend(); - $this->assertTrue($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); + $this->assertTrue($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); } /** * Verify we can unsuspend a suspended domain */ public function testUnsuspendForSuspendedDomain() { Queue::fake(); - $this->domain->status |= \App\Domain::STATUS_SUSPENDED; + $this->domainHosted->status |= \App\Domain::STATUS_SUSPENDED; - $this->assertTrue($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); + $this->assertTrue($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); - $this->domain->unsuspend(); + $this->domainHosted->unsuspend(); - $this->assertFalse($this->domain->isSuspended()); - $this->assertTrue($this->domain->isActive()); + $this->assertFalse($this->domainHosted->isSuspended()); + $this->assertTrue($this->domainHosted->isActive()); } /** * Verify we can unsuspend a suspended domain that wasn't confirmed */ public function testUnsuspendForSuspendedUnconfirmedDomain() { Queue::fake(); - $this->domain->status = \App\Domain::STATUS_NEW | \App\Domain::STATUS_SUSPENDED; + $this->domainHosted->status = \App\Domain::STATUS_NEW | \App\Domain::STATUS_SUSPENDED; - $this->assertTrue($this->domain->isNew()); - $this->assertTrue($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); - $this->assertFalse($this->domain->isConfirmed()); - $this->assertFalse($this->domain->isVerified()); + $this->assertTrue($this->domainHosted->isNew()); + $this->assertTrue($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); + $this->assertFalse($this->domainHosted->isConfirmed()); + $this->assertFalse($this->domainHosted->isVerified()); - $this->domain->unsuspend(); + $this->domainHosted->unsuspend(); - $this->assertTrue($this->domain->isNew()); - $this->assertFalse($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); - $this->assertFalse($this->domain->isConfirmed()); - $this->assertFalse($this->domain->isVerified()); + $this->assertTrue($this->domainHosted->isNew()); + $this->assertFalse($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); + $this->assertFalse($this->domainHosted->isConfirmed()); + $this->assertFalse($this->domainHosted->isVerified()); } /** * Verify we can unsuspend a suspended domain that was verified but not confirmed */ public function testUnsuspendForSuspendedVerifiedUnconfirmedDomain() { Queue::fake(); - $this->domain->status = \App\Domain::STATUS_NEW | \App\Domain::STATUS_SUSPENDED | \App\Domain::STATUS_VERIFIED; + $this->domainHosted->status = \App\Domain::STATUS_NEW + | \App\Domain::STATUS_SUSPENDED + | \App\Domain::STATUS_VERIFIED; - $this->assertTrue($this->domain->isNew()); - $this->assertTrue($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); - $this->assertFalse($this->domain->isConfirmed()); - $this->assertTrue($this->domain->isVerified()); + $this->assertTrue($this->domainHosted->isNew()); + $this->assertTrue($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); + $this->assertFalse($this->domainHosted->isConfirmed()); + $this->assertTrue($this->domainHosted->isVerified()); - $this->domain->unsuspend(); + $this->domainHosted->unsuspend(); - $this->assertTrue($this->domain->isNew()); - $this->assertFalse($this->domain->isSuspended()); - $this->assertFalse($this->domain->isActive()); - $this->assertFalse($this->domain->isConfirmed()); - $this->assertTrue($this->domain->isVerified()); + $this->assertTrue($this->domainHosted->isNew()); + $this->assertFalse($this->domainHosted->isSuspended()); + $this->assertFalse($this->domainHosted->isActive()); + $this->assertFalse($this->domainHosted->isConfirmed()); + $this->assertTrue($this->domainHosted->isVerified()); } - } diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php index 7f3e2d29..64919232 100644 --- a/src/tests/TestCase.php +++ b/src/tests/TestCase.php @@ -1,35 +1,35 @@ created_at = $targetDate; $entitlement->updated_at = $targetDate; $entitlement->save(); - $owner = $entitlement->wallet->owner; + $owner = $entitlement->wallet->domainOwner; $owner->created_at = $targetDate; $owner->save(); } } /** * Set baseURL to the admin UI location */ protected static function useAdminUrl(): void { // This will set base URL for all tests in a file. // If we wanted to access both user and admin in one test // we can also just call post/get/whatever with full url \config(['app.url' => str_replace('//', '//admin.', \config('app.url'))]); url()->forceRootUrl(config('app.url')); } } diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php index aa9dfdf2..8d315d5f 100644 --- a/src/tests/TestCaseTrait.php +++ b/src/tests/TestCaseTrait.php @@ -1,203 +1,378 @@ 'John', + 'last_name' => 'Doe', + 'organization' => 'Test Domain Owner', + ]; + + /** + * Some users for the hosted domain, ultimately including the owner. + * + * @var \App\User[] + */ + protected $domainUsers = []; + + /** + * A specific user that is a regular user in the hosted domain. + */ + protected $jack; + + /** + * A specific user that is a controller on the wallet to which the hosted domain is charged. + */ + protected $jane; + + /** + * A specific user that has a second factor configured. + */ + protected $joe; + + /** + * Assert two numeric values are the same. + * + * @param int|double|float $a + * @param int|double|float $b + */ + protected function assertEqual($a, $b) + { + Assert::assertTrue(is_numeric($a)); + Assert::assertTrue(is_numeric($b)); + + Assert::assertSame($a, $b); + } + + /** + * Assert that the entitlements for the user match the expected list of entitlements. + * + * @param \App\User $user The user for which the entitlements need to be pulled. + * @param array $expected An array of expected \App\SKU titles. + */ protected function assertUserEntitlements($user, $expected) { // Assert the user entitlements $skus = $user->entitlements()->get() ->map(function ($ent) { return $ent->sku->title; }) ->toArray(); sort($skus); Assert::assertSame($expected, $skus); } /** * Creates the application. * * @return \Illuminate\Foundation\Application */ public function createApplication() { $app = require __DIR__ . '/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); return $app; } /** * Create a set of transaction log entries for a wallet */ protected function createTestTransactions($wallet) { $result = []; $date = Carbon::now(); $debit = 0; $entitlementTransactions = []; + foreach ($wallet->entitlements as $entitlement) { if ($entitlement->cost) { $debit += $entitlement->cost; $entitlementTransactions[] = $entitlement->createTransaction( Transaction::ENTITLEMENT_BILLED, $entitlement->cost ); } } - $transaction = Transaction::create([ + $transaction = Transaction::create( + [ 'user_email' => 'jeroen@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_DEBIT, 'amount' => $debit, 'description' => 'Payment', - ]); + ] + ); + $result[] = $transaction; Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]); - $transaction = Transaction::create([ + $transaction = Transaction::create( + [ 'user_email' => null, 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_CREDIT, 'amount' => 2000, 'description' => 'Payment', - ]); + ] + ); + $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); + $result[] = $transaction; $types = [ Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY, ]; // The page size is 10, so we generate so many to have at least two pages $loops = 10; while ($loops-- > 0) { - $transaction = Transaction::create([ - 'user_email' => 'jeroen.@jeroen.jeroen', - 'object_id' => $wallet->id, - 'object_type' => \App\Wallet::class, - 'type' => $types[count($result) % count($types)], - 'amount' => 11 * (count($result) + 1), - 'description' => 'TRANS' . $loops, - ]); + $transaction = Transaction::create( + [ + 'user_email' => 'jeroen.@jeroen.jeroen', + 'object_id' => $wallet->id, + 'object_type' => \App\Wallet::class, + 'type' => $types[count($result) % count($types)], + 'amount' => 11 * (count($result) + 1), + 'description' => 'TRANS' . $loops, + ] + ); + $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); + $result[] = $transaction; } return $result; } + /** + * Delete a test domain whatever it takes. + * + * @coversNothing + */ protected function deleteTestDomain($name) { Queue::fake(); $domain = Domain::withTrashed()->where('namespace', $name)->first(); if (!$domain) { return; } $job = new \App\Jobs\Domain\DeleteJob($domain->id); $job->handle(); $domain->forceDelete(); } + /** + * Delete a test user whatever it takes. + * + * @coversNothing + */ protected function deleteTestUser($email) { Queue::fake(); $user = User::withTrashed()->where('email', $email)->first(); if (!$user) { return; } $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $user->forceDelete(); } /** * Get Domain object by namespace, create it if needed. * Skip LDAP jobs. + * + * @coversNothing */ protected function getTestDomain($name, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); return Domain::firstOrCreate(['namespace' => $name], $attrib); } /** * Get User object by email, create it if needed. * Skip LDAP jobs. + * + * @coversNothing */ protected function getTestUser($email, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); $user = User::withTrashed()->where('email', $email)->first(); if (!$user) { return User::firstOrCreate(['email' => $email], $attrib); } if ($user->deleted_at) { $user->restore(); } return $user; } /** * Helper to access protected property of an object */ protected static function getObjectProperty($object, $property_name) { $reflection = new \ReflectionClass($object); $property = $reflection->getProperty($property_name); $property->setAccessible(true); return $property->getValue($object); } /** * Call protected/private method of a class. * * @param object $object Instantiated object that we will run method on. * @param string $methodName Method name to call * @param array $parameters Array of parameters to pass into method. * * @return mixed Method return. */ protected function invokeMethod($object, $methodName, array $parameters = array()) { $reflection = new \ReflectionClass(get_class($object)); $method = $reflection->getMethod($methodName); $method->setAccessible(true); return $method->invokeArgs($object, $parameters); } + + public function setUp(): void + { + parent::setUp(); + + $this->userPassword = \App\Utils::generatePassphrase(); + + $this->domainHosted = $this->getTestDomain( + 'test.domain', + [ + 'type' => \App\Domain::TYPE_EXTERNAL, + 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED + ] + ); + + $packageKolab = \App\Package::where('title', 'kolab')->first(); + + $this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]); + $this->domainOwner->assignPackage($packageKolab); + $this->domainOwner->setSettings($this->domainOwnerSettings); + + // separate for regular user + $this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]); + + // separate for wallet controller + $this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]); + + $this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]); + + $this->domainUsers[] = $this->jack; + $this->domainUsers[] = $this->jane; + $this->domainUsers[] = $this->joe; + $this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]); + + foreach ($this->domainUsers as $user) { + $this->domainOwner->assignPackage($packageKolab, $user); + } + + $this->domainUsers[] = $this->domainOwner; + + // assign second factor to joe + $this->joe->assignSku(\App\Sku::where('title', '2fa')->first()); + \App\Auth\SecondFactor::seed($this->joe->email); + + usort( + $this->domainUsers, + function ($a, $b) { + return $a->email > $b->email; + } + ); + + $this->domainHosted->assignPackage(\App\Package::where('title', 'domain-hosting')->first(), $this->domainOwner); + + $wallet = $this->domainOwner->wallets()->first(); + + $wallet->addController($this->jane); + } + + public function tearDown(): void + { + foreach ($this->domainUsers as $user) { + if ($user == $this->domainOwner) { + continue; + } + + $this->deleteTestUser($user->email); + } + + $this->deleteTestUser($this->domainOwner->email); + $this->deleteTestDomain($this->domainHosted->namespace); + + parent::tearDown(); + } } diff --git a/src/tests/Unit/Methods/DomainTest.php b/src/tests/Unit/Methods/DomainTest.php index 5300bc40..a0c516ec 100644 --- a/src/tests/Unit/Methods/DomainTest.php +++ b/src/tests/Unit/Methods/DomainTest.php @@ -1,159 +1,162 @@ domain = new \App\Domain(); } /** * Test lower-casing namespace attribute. */ public function testSetNamespaceAttributeLowercases() { $this->domain = new \App\Domain(); $this->domain->namespace = 'UPPERCASE'; + // @phpstan-ignore-next-line $this->assertTrue($this->domain->namespace === 'uppercase'); } /** * Test setting the status to something invalid */ public function testSetStatusAttributeInvalid() { $this->expectException(\Exception::class); $this->domain->status = 123456; } /** * Test public domain. */ public function testSetStatusAttributeOnPublicDomain() { $this->domain->{'type'} = \App\Domain::TYPE_PUBLIC; $this->domain->status = 115; $this->assertTrue($this->domain->status == 115); } /** * Test status mutations */ public function testSetStatusAttributeActiveMakesForNotNew() { $this->domain->status = \App\Domain::STATUS_NEW; $this->assertTrue($this->domain->isNew()); $this->assertFalse($this->domain->isActive()); $this->domain->status |= \App\Domain::STATUS_ACTIVE; $this->assertFalse($this->domain->isNew()); $this->assertTrue($this->domain->isActive()); } /** * Verify setting confirmed sets verified. */ public function testSetStatusAttributeConfirmedMakesForVerfied() { $this->domain->status = \App\Domain::STATUS_CONFIRMED; $this->assertTrue($this->domain->isConfirmed()); $this->assertTrue($this->domain->isVerified()); } /** * Verify setting confirmed sets active. */ public function testSetStatusAttributeConfirmedMakesForActive() { $this->domain->status = \App\Domain::STATUS_CONFIRMED; $this->assertTrue($this->domain->isConfirmed()); $this->assertTrue($this->domain->isActive()); } /** * Verify setting deleted drops active. */ public function testSetStatusAttributeDeletedVoidsActive() { $this->domain->status = \App\Domain::STATUS_ACTIVE; $this->assertTrue($this->domain->isActive()); $this->assertFalse($this->domain->isNew()); $this->assertFalse($this->domain->isDeleted()); $this->domain->status |= \App\Domain::STATUS_DELETED; $this->assertFalse($this->domain->isActive()); $this->assertFalse($this->domain->isNew()); $this->assertTrue($this->domain->isDeleted()); } /** * Verify setting suspended drops active. */ public function testSetStatusAttributeSuspendedVoidsActive() { $this->domain->status = \App\Domain::STATUS_ACTIVE; $this->assertTrue($this->domain->isActive()); $this->assertFalse($this->domain->isSuspended()); $this->domain->status |= \App\Domain::STATUS_SUSPENDED; $this->assertFalse($this->domain->isActive()); $this->assertTrue($this->domain->isSuspended()); } /** * Verify we can suspend a suspended domain without disaster. * * This doesn't change anything to trigger a save. */ public function testSuspendForSuspendedDomain() { $this->domain->status = \App\Domain::STATUS_ACTIVE; $this->domain->status |= \App\Domain::STATUS_SUSPENDED; $this->assertTrue($this->domain->isSuspended()); $this->assertFalse($this->domain->isActive()); $this->domain->suspend(); $this->assertTrue($this->domain->isSuspended()); $this->assertFalse($this->domain->isActive()); } /** * Verify we can unsuspend an active (unsuspended) domain * * This doesn't change anything to trigger a save. */ public function testUnsuspendForActiveDomain() { $this->domain->status = \App\Domain::STATUS_ACTIVE; $this->assertFalse($this->domain->isSuspended()); $this->assertTrue($this->domain->isActive()); $this->domain->unsuspend(); $this->assertFalse($this->domain->isSuspended()); $this->assertTrue($this->domain->isActive()); } } diff --git a/src/tests/Unit/Rules/UserEmailLocalTest.php b/src/tests/Unit/Rules/UserEmailLocalTest.php index 8476a8fb..ecf233c2 100644 --- a/src/tests/Unit/Rules/UserEmailLocalTest.php +++ b/src/tests/Unit/Rules/UserEmailLocalTest.php @@ -1,18 +1,21 @@ markTestIncomplete(); + //$this->markTestIncomplete(); + + // the email address can not start with a dot. + $this->assertFalse(\App\Utils::isValidEmailAddress('.something@test.domain')); } }