diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php index 5681e7e1..195e3e84 100644 --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -1,306 +1,323 @@ commandPrefix == 'scalpel') { return $object; } if (!$this->tenantId) { return $object; } $modelsWithOwner = [ \App\Wallet::class, ]; // Add tenant filter if (in_array(\App\Traits\BelongsToTenantTrait::class, class_uses($object::class))) { $context = new \App\User(); $context->tenant_id = $this->tenantId; $object = $object->withObjectTenantContext($context); } elseif (in_array($object::class, $modelsWithOwner)) { $object = $object->whereExists(function ($query) { $query->select(DB::raw(1)) ->from('users') ->whereRaw('wallets.user_id = users.id') ->whereRaw("users.tenant_id = {$this->tenantId}"); }); } return $object; } /** * Shortcut to creating a progress bar of a particular format with a particular message. * * @param int $count Number of progress steps * @param string $message The description * * @return \Symfony\Component\Console\Helper\ProgressBar */ protected function createProgressBar($count, $message = null) { $bar = $this->output->createProgressBar($count); $bar->setFormat( '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% ' ); if ($message) { $bar->setMessage("{$message}..."); } $bar->start(); return $bar; } /** * Find the domain. * * @param string $domain Domain ID or namespace * @param bool $withDeleted Include deleted * * @return \App\Domain|null */ public function getDomain($domain, $withDeleted = false) { return $this->getObject(\App\Domain::class, $domain, 'namespace', $withDeleted); } /** * Find a group. * * @param string $group Group ID or email * @param bool $withDeleted Include deleted * * @return \App\Group|null */ public function getGroup($group, $withDeleted = false) { return $this->getObject(\App\Group::class, $group, 'email', $withDeleted); } /** * Find an object. * * @param string $objectClass The name of the class * @param string $objectIdOrTitle The name of a database field to match. * @param string|null $objectTitle An additional database field to match. * @param bool $withDeleted Act as if --with-deleted was used * * @return mixed */ public function getObject($objectClass, $objectIdOrTitle, $objectTitle = null, $withDeleted = false) { if (!$withDeleted) { // @phpstan-ignore-next-line $withDeleted = $this->hasOption('with-deleted') && $this->option('with-deleted'); } $object = $this->getObjectModel($objectClass, $withDeleted)->find($objectIdOrTitle); if (!$object && !empty($objectTitle)) { $object = $this->getObjectModel($objectClass, $withDeleted) ->where($objectTitle, $objectIdOrTitle)->first(); } if (!$this->tenantId && $object && !empty($object->tenant_id)) { $this->tenantId = $object->tenant_id; } return $object; } /** * Returns a preconfigured Model object for a specified class. * * @param string $objectClass The name of the class * @param bool $withDeleted Include withTrashed() query * * @return mixed */ protected function getObjectModel($objectClass, $withDeleted = false) { if ($withDeleted) { $model = $objectClass::withTrashed(); } else { $model = new $objectClass(); } return $this->applyTenant($model); } /** * Find a resource. * * @param string $resource Resource ID or email * @param bool $withDeleted Include deleted * * @return \App\Resource|null */ public function getResource($resource, $withDeleted = false) { return $this->getObject(\App\Resource::class, $resource, 'email', $withDeleted); } /** * Find a shared folder. * * @param string $folder Folder ID or email * @param bool $withDeleted Include deleted * * @return \App\SharedFolder|null */ public function getSharedFolder($folder, $withDeleted = false) { return $this->getObject(\App\SharedFolder::class, $folder, 'email', $withDeleted); } /** * Find the user. * * @param string $user User ID or email * @param bool $withDeleted Include deleted * * @return \App\User|null */ public function getUser($user, $withDeleted = false) { return $this->getObject(\App\User::class, $user, 'email', $withDeleted); } /** * Find the wallet. * * @param string $wallet Wallet ID * * @return \App\Wallet|null */ public function getWallet($wallet) { - return $this->getObject(\App\Wallet::class, $wallet, null); + return $this->getObject(\App\Wallet::class, $wallet); } /** * Execute the console command. * * @return mixed */ public function handle() { if ($this->dangerous) { $this->warn( "This command is a dangerous scalpel command with potentially significant unintended consequences" ); $confirmation = $this->confirm("Are you sure you understand what's about to happen?"); if (!$confirmation) { $this->info("Better safe than sorry."); exit(0); } $this->info("VĂ¡monos!"); } + + // @phpstan-ignore-next-line + if ($this->withTenant && $this->hasOption('tenant') && ($tenantId = $this->option('tenant'))) { + $tenant = $this->getObject(\App\Tenant::class, $tenantId, 'title'); + if (!$tenant) { + $this->error("Tenant {$tenantId} not found"); + return 1; + } + + $this->tenantId = $tenant->id; + } else { + $this->tenantId = \config('app.tenant_id'); + } } /** * Checks that a model is soft-deletable * * @param string $class Model class name * * @return bool */ protected function isSoftDeletable($class) { return class_exists($class) && method_exists($class, 'forceDelete'); } /** * Return a string for output, with any additional attributes specified as well. * * @param mixed $entry An object * * @return string */ protected function toString($entry) { /** * Haven't figured out yet, how to test if this command implements an option for additional * attributes. if (!in_array('attr', $this->options())) { return $entry->{$entry->getKeyName()}; } */ $str = [ $entry->{$entry->getKeyName()} ]; // @phpstan-ignore-next-line foreach ($this->option('attr') as $attr) { if ($attr == $entry->getKeyName()) { $this->warn("Specifying {$attr} is not useful."); continue; } if (!array_key_exists($attr, $entry->toArray())) { $this->error("Attribute {$attr} isn't available"); continue; } if (is_numeric($entry->{$attr})) { $str[] = $entry->{$attr}; } else { $str[] = !empty($entry->{$attr}) ? $entry->{$attr} : "null"; } } return implode(" ", $str); } } diff --git a/src/app/Console/Commands/Data/Import/SignupTokensCommand.php b/src/app/Console/Commands/Data/Import/SignupTokensCommand.php index 36927d70..3b2a3a5d 100644 --- a/src/app/Console/Commands/Data/Import/SignupTokensCommand.php +++ b/src/app/Console/Commands/Data/Import/SignupTokensCommand.php @@ -1,122 +1,115 @@ option('tenant')) { - $tenant = $this->getObject(\App\Tenant::class, $tenantId, 'title'); - if (!$tenant) { - $this->error("Tenant {$tenantId} not found"); - return 1; - } - - $this->tenantId = $tenant->id; - } else { - $this->tenantId = \config('app.tenant_id'); - } + parent::handle(); $plan = $this->getObject(Plan::class, $this->argument('plan'), 'title', false); if (!$plan) { $this->error("Plan not found"); return 1; } if ($plan->mode != Plan::MODE_TOKEN) { $this->error("The plan is not for tokens"); return 1; } $file = $this->argument('file'); if (!file_exists($file)) { $this->error("File '$file' does not exist"); return 1; } $list = file($file); if (empty($list)) { $this->error("File '$file' is empty"); return 1; } $bar = $this->createProgressBar(count($list), "Validating tokens"); $list = array_map('trim', $list); $list = array_map('strtoupper', $list); // Validate tokens foreach ($list as $idx => $token) { if (!strlen($token)) { // Skip empty lines unset($list[$idx]); } elseif (strlen($token) > 191) { $bar->finish(); $this->error("Token '$token' is too long"); return 1; } elseif (SignupToken::find($token)) { // Skip existing tokens unset($list[$idx]); } $bar->advance(); } $bar->finish(); $this->info("DONE"); if (empty($list)) { $this->info("Nothing to import"); return 0; } $list = array_unique($list); // remove duplicated tokens $bar = $this->createProgressBar(count($list), "Importing tokens"); // Import tokens foreach ($list as $token) { $plan->signupTokens()->create([ 'id' => $token, // This allows us to update counter when importing old tokens in migration. // It can be removed later 'counter' => \App\UserSetting::where('key', 'signup_token') ->whereRaw('UPPER(value) = ?', [$token])->count(), ]); $bar->advance(); } $bar->finish(); $this->info("DONE"); } } diff --git a/src/app/Console/Commands/Discount/MergeCommand.php b/src/app/Console/Commands/Discount/MergeCommand.php index 8ca61560..68dbd2ac 100644 --- a/src/app/Console/Commands/Discount/MergeCommand.php +++ b/src/app/Console/Commands/Discount/MergeCommand.php @@ -1,85 +1,90 @@ 158f660b-e992-4fb9-ac12-5173b5f33807 \ * > 62af659f-17d8-4527-87c1-c69eaa26653c \ * > --description="Employee discount" * ``` */ class MergeCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'discount:merge {source} {target} {--description=}'; /** * The console command description. * * @var string */ protected $description = 'Merge one discount in to another discount, ' . 'optionally set the description, and delete the source discount'; /** * Execute the console command. * * @return mixed */ public function handle() { $source = $this->getObject(\App\Discount::class, $this->argument('source')); if (!$source) { $this->error("No such source discount: {$source}"); return 1; } $target = $this->getObject(\App\Discount::class, $this->argument('target')); if (!$target) { $this->error("No such target discount: {$target}"); return 1; } if ($source->discount !== $target->discount) { $this->error("Can't merge two discounts that have different rates"); return 1; } + if ($source->tenant_id !== $target->tenant_id) { + $this->error("Can't merge two discounts that have different tenants"); + return 1; + } + foreach ($source->wallets as $wallet) { $wallet->discount_id = $target->id; $wallet->timestamps = false; $wallet->save(); } if ($description = $this->option('description')) { $target->description = $description; $target->save(); } $source->delete(); } } diff --git a/src/app/Console/Commands/Domain/CreateCommand.php b/src/app/Console/Commands/Domain/CreateCommand.php index c30a6a1a..75b7528a 100644 --- a/src/app/Console/Commands/Domain/CreateCommand.php +++ b/src/app/Console/Commands/Domain/CreateCommand.php @@ -1,99 +1,96 @@ argument('domain')); // must use withTrashed(), because unique constraint $domain = Domain::withTrashed()->where('namespace', $namespace)->first(); if ($domain && !$this->option('force')) { $this->error("Domain {$namespace} already exists."); return 1; } if ($domain) { if ($domain->trashed()) { // set the status back to new $domain->status = Domain::STATUS_NEW; $domain->save(); // remove existing entitlement $entitlement = Entitlement::withTrashed()->where( [ 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ] )->first(); if ($entitlement) { $entitlement->forceDelete(); } // restore the domain to allow for the observer to handle the create job $domain->restore(); $this->info( sprintf( "Domain %s with ID %d revived. Remember to assign it to a wallet with 'domain:set-wallet'", $domain->namespace, $domain->id ) ); } else { $this->error("Domain {$namespace} not marked as deleted... examine more closely"); return 1; } } else { - if ($tenantId = $this->option('tenant')) { - $tenant = $this->getObject(Tenant::class, $tenantId, 'title'); - if (!$tenant) { - $this->error("Tenant {$tenantId} not found"); - return 1; - } - } - $domain = new Domain(); $domain->namespace = $namespace; $domain->type = Domain::TYPE_EXTERNAL; - $domain->tenant_id = !empty($tenant) ? $tenant->id : null; + $domain->tenant_id = $this->tenantId; $domain->save(); $this->info( sprintf( "Domain %s created with ID %d. Remember to assign it to a wallet with 'domain:set-wallet'", $domain->namespace, $domain->id ) ); } } } diff --git a/src/app/Console/Commands/Package/SkusCommand.php b/src/app/Console/Commands/Package/SkusCommand.php index dc07460a..55ccdfe4 100644 --- a/src/app/Console/Commands/Package/SkusCommand.php +++ b/src/app/Console/Commands/Package/SkusCommand.php @@ -1,41 +1,46 @@ get(); + parent::handle(); + + $packages = Package::where('tenant_id', $this->tenantId)->get(); foreach ($packages as $package) { $this->info(sprintf("Package: %s", $package->title)); foreach ($package->skus as $sku) { $this->info(sprintf(" SKU: %s (%d)", $sku->title, $sku->pivot->qty)); } } } } diff --git a/src/app/Console/Commands/Plan/PackagesCommand.php b/src/app/Console/Commands/Plan/PackagesCommand.php index fbdedeaa..5d190f52 100644 --- a/src/app/Console/Commands/Plan/PackagesCommand.php +++ b/src/app/Console/Commands/Plan/PackagesCommand.php @@ -1,77 +1,82 @@ get(); + parent::handle(); + + $plans = Plan::where('tenant_id', $this->tenantId)->get(); foreach ($plans as $plan) { $this->info(sprintf("Plan: %s", $plan->title)); $plan_costs = 0; foreach ($plan->packages as $package) { $qtyMin = $package->pivot->qty_min; $qtyMax = $package->pivot->qty_max; $discountQty = $package->pivot->discount_qty; $discountRate = (100 - $package->pivot->discount_rate) / 100; $this->info( sprintf( " Package: %s (min: %d, max: %d, discount %d%% after the first %d, base cost: %d)", $package->title, $package->pivot->qty_min, $package->pivot->qty_max, $package->pivot->discount_rate, $package->pivot->discount_qty, $package->cost() ) ); foreach ($package->skus as $sku) { $this->info(sprintf(" SKU: %s (%d)", $sku->title, $sku->pivot->qty)); } if ($qtyMin < $discountQty) { $plan_costs += $qtyMin * $package->cost(); } elseif ($qtyMin == $discountQty) { $plan_costs += $package->cost(); } else { // base rate $plan_costs += $discountQty * $package->cost(); // discounted rate $plan_costs += ($qtyMin - $discountQty) * $package->cost() * $discountRate; } } $this->info(sprintf(" Plan costs per month: %d", $plan_costs)); } } } diff --git a/src/app/Console/Commands/Sku/ListUsersCommand.php b/src/app/Console/Commands/Sku/ListUsersCommand.php index f778a061..e9d5389c 100644 --- a/src/app/Console/Commands/Sku/ListUsersCommand.php +++ b/src/app/Console/Commands/Sku/ListUsersCommand.php @@ -1,60 +1,65 @@ getObject(\App\Sku::class, $this->argument('sku'), 'title'); if (!$sku) { $this->error("Unable to find the SKU."); return 1; } $fn = function ($entitlement) { $user_id = $entitlement->user_id; if ($entitlement->entitleable_type == \App\User::class) { $user_id = $entitlement->entitleable_id; } return $user_id; }; $users = \App\Entitlement::select('user_id', 'entitleable_id', 'entitleable_type') ->join('wallets', 'wallets.id', '=', 'wallet_id') ->where('sku_id', $sku->id) ->get() ->map($fn) ->unique(); // TODO: This whereIn() might not scale \App\User::whereIn('id', $users)->orderBy('email')->get() ->pluck('email') ->each(function ($email, $key) { $this->info($email); }); } } diff --git a/src/app/Console/Commands/Tenant/CreateCommand.php b/src/app/Console/Commands/Tenant/CreateCommand.php index 0e7f3669..b5fdf60c 100644 --- a/src/app/Console/Commands/Tenant/CreateCommand.php +++ b/src/app/Console/Commands/Tenant/CreateCommand.php @@ -1,192 +1,191 @@ argument('user'); if ($user = \App\User::where('email', $email)->first()) { $this->error("The user already exists."); return 1; } if ($domain = \App\Domain::where('namespace', $this->argument('domain'))->first()) { $this->error("The domain already exists."); return 1; } - DB::beginTransaction(); // Create a tenant $tenant = \App\Tenant::create(['title' => $this->option('title')]); // Clone plans, packages, skus for the tenant $sku_map = \App\Sku::withEnvTenantContext()->where('active', true)->get() ->mapWithKeys(function ($sku) use ($tenant) { $sku_new = \App\Sku::create([ 'title' => $sku->title, 'name' => $sku->getTranslations('name'), 'description' => $sku->getTranslations('description'), 'cost' => $sku->cost, 'units_free' => $sku->units_free, 'period' => $sku->period, 'handler_class' => $sku->handler_class, 'active' => true, 'fee' => $sku->fee, ]); $sku_new->tenant_id = $tenant->id; $sku_new->save(); return [$sku->id => $sku_new->id]; }) ->all(); $plan_map = \App\Plan::withEnvTenantContext()->get() ->mapWithKeys(function ($plan) use ($tenant) { $plan_new = \App\Plan::create([ 'title' => $plan->title, 'name' => $plan->getTranslations('name'), 'description' => $plan->getTranslations('description'), 'promo_from' => $plan->promo_from, 'promo_to' => $plan->promo_to, 'qty_min' => $plan->qty_min, 'qty_max' => $plan->qty_max, 'discount_qty' => $plan->discount_qty, 'discount_rate' => $plan->discount_rate, ]); $plan_new->tenant_id = $tenant->id; $plan_new->save(); return [$plan->id => $plan_new->id]; }) ->all(); $package_map = \App\Package::withEnvTenantContext()->get() ->mapWithKeys(function ($package) use ($tenant) { $package_new = \App\Package::create([ 'title' => $package->title, 'name' => $package->getTranslations('name'), 'description' => $package->getTranslations('description'), 'discount_rate' => $package->discount_rate, ]); $package_new->tenant_id = $tenant->id; $package_new->save(); return [$package->id => $package_new->id]; }) ->all(); DB::table('package_skus')->whereIn('package_id', array_keys($package_map))->get() ->each(function ($item) use ($package_map, $sku_map) { if (isset($sku_map[$item->sku_id])) { DB::table('package_skus')->insert([ 'qty' => $item->qty, 'cost' => $item->cost, 'sku_id' => $sku_map[$item->sku_id], 'package_id' => $package_map[$item->package_id], ]); } }); DB::table('plan_packages')->whereIn('plan_id', array_keys($plan_map))->get() ->each(function ($item) use ($package_map, $plan_map) { if (isset($package_map[$item->package_id])) { DB::table('plan_packages')->insert([ 'qty' => $item->qty, 'qty_min' => $item->qty_min, 'qty_max' => $item->qty_max, 'discount_qty' => $item->discount_qty, 'discount_rate' => $item->discount_rate, 'plan_id' => $plan_map[$item->plan_id], 'package_id' => $package_map[$item->package_id], ]); } }); // Disable jobs, they would fail anyway as the TENANT_ID is different // TODO: We could probably do config(['app.tenant' => $tenant->id]) here Queue::fake(); // Make sure the transaction wasn't aborted $tenant = \App\Tenant::find($tenant->id); if (!$tenant) { $this->error("Failed to create a tenant."); return 1; } $this->info("Created tenant {$tenant->id}."); // Set up the primary tenant domain $domain = \App\Domain::create( [ 'namespace' => $this->argument('domain'), 'type' => \App\Domain::TYPE_PUBLIC, ] ); $domain->tenant_id = $tenant->id; $domain->status = \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_ACTIVE; $domain->save(); $this->info("Created domain {$domain->id}."); $user = new \App\User(); $user->email = $email; $user->password = $this->option('password'); $user->role = 'reseller'; $user->tenant_id = $tenant->id; if ($error = UsersController::validateEmail($email, $user)) { $this->error("{$email}: {$error}"); return 1; } $user->save(); $this->info("Created user {$user->id}."); $tenant->setSettings([ "app.name" => $this->option("title"), "app.url" => $this->argument("domain"), "app.public_url" => "https://" . $this->argument("domain"), "app.support_url" => "https://" . $this->argument("domain") . "/support", "mail.sender.address" => "noreply@" . $this->argument("domain"), "mail.sender.name" => $this->option("title"), "mail.replyto.address" => "noreply@" . $this->argument("domain"), "mail.replyto.name" => $this->option("title"), ]); DB::commit(); $this->info("Applied default tenant settings."); } } diff --git a/src/app/Console/Commands/User/CreateCommand.php b/src/app/Console/Commands/User/CreateCommand.php index 8673552d..d95d1d4c 100644 --- a/src/app/Console/Commands/User/CreateCommand.php +++ b/src/app/Console/Commands/User/CreateCommand.php @@ -1,151 +1,156 @@ argument('email'); $packages = $this->option('package'); $password = $this->option('password') ?: \App\Utils::generatePassphrase(); $role = $this->option('role'); $existingDeletedUser = null; $packagesToAssign = []; if ($role === User::ROLE_ADMIN || $role === User::ROLE_RESELLER) { if ($error = $this->validateUserWithRole($email)) { $this->error($error); return 1; } // TODO: Assigning user to an existing account // TODO: Making him an operator of the reseller wallet } else { list($local, $domainName) = explode('@', $email, 2); $domain = $this->getDomain($domainName); if (!$domain) { $this->error("No such domain {$domainName}."); return 1; } if (!$domain->isPublic()) { $owner = $domain->wallet()->owner; } // Tenant context for the getObject() call below, and for the new user $this->tenantId = $domain->tenant_id; foreach ($packages as $package) { $userPackage = $this->getObject(\App\Package::class, $package, 'title', false); if (!$userPackage) { $this->error("Invalid package: {$package}"); return 1; } $packagesToAssign[] = $userPackage; } } try { $user = new \App\User(); $user->email = $email; $user->password = $password; $user->role = $role; $user->tenant_id = $this->tenantId; } catch (\Exception $e) { $this->error($e->getMessage()); return 1; } if (empty($owner)) { $owner = $user; } if ($role != User::ROLE_ADMIN && $role != User::ROLE_RESELLER) { // Validate email address if ($error = UsersController::validateEmail($email, $owner, $existingDeletedUser)) { $this->error("{$email}: {$error}"); return 1; } } DB::beginTransaction(); if ($existingDeletedUser) { $this->info("Force deleting existing but deleted user {$email}"); $existingDeletedUser->forceDelete(); } $user->save(); foreach ($packagesToAssign as $package) { $owner->assignPackage($package, $user); } DB::commit(); $this->info((string) $user->id); } /** * Validate email address for a new admin/reseller user * * @param string $email Email address * * @return ?string Error message */ protected function validateUserWithRole($email): ?string { // Validate the email address (basicly just the syntax) $v = Validator::make( ['email' => $email], ['email' => ['required', new ExternalEmail()]] ); if ($v->fails()) { return $v->errors()->toArray()['email'][0]; } // Check if an email is already taken if ( User::emailExists($email, true) || User::aliasExists($email) || \App\Group::emailExists($email, true) || \App\Resource::emailExists($email, true) || \App\SharedFolder::emailExists($email, true) || \App\SharedFolder::aliasExists($email) ) { return "Email address is already in use"; } return null; } } diff --git a/src/app/Console/Commands/User/GreylistCommand.php b/src/app/Console/Commands/User/GreylistCommand.php index 59ecab6f..f4f6e3f7 100644 --- a/src/app/Console/Commands/User/GreylistCommand.php +++ b/src/app/Console/Commands/User/GreylistCommand.php @@ -1,63 +1,62 @@ argument('user'); $recipientHash = hash('sha256', $recipientAddress); $lastConnect = \App\Policy\Greylist\Connect::where('recipient_hash', $recipientHash) ->orderBy('updated_at', 'desc') ->first(); if ($lastConnect) { $timestamp = $lastConnect->updated_at->copy(); $this->info("Going from timestamp (last connect) {$timestamp}"); } else { $timestamp = \Carbon\Carbon::now(); $this->info("Going from timestamp (now) {$timestamp}"); } - \App\Policy\Greylist\Connect::where('recipient_hash', $recipientHash) ->where('greylisting', true) ->whereDate('updated_at', '>=', $timestamp->copy()->subDays(7)) ->orderBy('created_at')->each( function ($connect) { $this->info( sprintf( "From %s@%s since %s", $connect->sender_local, $connect->sender_domain, $connect->created_at ) ); } ); } } diff --git a/src/database/migrations/2024_06_13_100000_signup_codes_tenant_id.php b/src/database/migrations/2024_06_13_100000_signup_codes_tenant_id.php index 1f7dc8fb..94aae02b 100644 --- a/src/database/migrations/2024_06_13_100000_signup_codes_tenant_id.php +++ b/src/database/migrations/2024_06_13_100000_signup_codes_tenant_id.php @@ -1,43 +1,44 @@ bigInteger('tenant_id')->unsigned()->nullable(); $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null'); } ); // We could set tenant_id for old records if there's only one tenant in the DB, // but I think nothing will happen if we don't do this. // Leave it to the deployment-specific migrations. } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table( 'signup_codes', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); $table->dropColumn('tenant_id'); } ); } }; diff --git a/src/tests/Feature/Console/Sku/ListUsersTest.php b/src/tests/Feature/Console/Sku/ListUsersTest.php index cfbb9b93..e2e46b9f 100644 --- a/src/tests/Feature/Console/Sku/ListUsersTest.php +++ b/src/tests/Feature/Console/Sku/ListUsersTest.php @@ -1,71 +1,60 @@ deleteTestUser('sku-list-users@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - $this->deleteTestUser('sku-list-users@kolabnow.com'); - - parent::tearDown(); - } - /** * Test command runs */ public function testHandle(): void { + $tenant = Tenant::where('title', 'kanarip.ch')->first(); + // Warning: We're not using artisan() here, as this will not // allow us to test "empty output" cases $code = \Artisan::call('sku:list-users domain-registration'); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertSame('', $output); $code = \Artisan::call('sku:list-users unknown'); $output = trim(\Artisan::output()); $this->assertSame(1, $code); $this->assertSame("Unable to find the SKU.", $output); $code = \Artisan::call('sku:list-users 2fa'); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertSame("ned@kolab.org", $output); $code = \Artisan::call('sku:list-users mailbox'); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $expected = [ "fred@" . \config('app.domain'), "jack@kolab.org", "joe@kolab.org", "john@kolab.org", "ned@kolab.org", ]; $this->assertSame(implode("\n", $expected), $output); $code = \Artisan::call('sku:list-users domain-hosting'); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertSame("john@kolab.org", $output); + + // Another tenant + $code = \Artisan::call("sku:list-users mailbox --tenant={$tenant->id}"); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertSame("user@kanarip.ch.dev-local", $output); } }