diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 7dd69e0f..d3435ca2 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -17,7 +17,8 @@ class Run extends Command protected $description = 'Run a command for tenant(s)'; protected $signature = 'tenants:run {commandname : The artisan command.} - {--tenants=* : The tenant(s) to run the command for. Default: all}'; + {--tenants=* : The tenant(s) to run the command for. Default: all} + {--skip-tenants=* : The tenant(s) to skip}'; public function handle(): int { diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index c1ea221f..5b41d31a 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -10,15 +10,16 @@ use Stancl\Tenancy\Database\Concerns\PendingScope; use Symfony\Component\Console\Input\InputOption; /** - * Adds 'tenants' and 'with-pending' options. + * Adds 'tenants', 'skip-tenants', and 'with-pending' options. */ trait HasTenantOptions { protected function getOptions() { return array_merge([ - new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null), - new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs + new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null), + new InputOption('skip-tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to skip when running this command', null), + new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs ], parent::getOptions()); } @@ -42,6 +43,9 @@ trait HasTenantOptions ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) + ->when($this->option('skip-tenants'), function ($query) { + $query->whereNotIn(tenancy()->model()->getTenantKeyName(), $this->option('skip-tenants')); + }) ->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) { $query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending')); }); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index a5b3b856..1cfa3c3b 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -515,3 +515,134 @@ test('migrate fresh command only deletes tenant databases if drop_tenant_databas expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh); } })->with([true, false]); + +test('migrate command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate', [ + '--skip-tenants' => [$tenant2->getTenantKey()], + ]); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant3); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +}); + +test('migrate command skips multiple tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate', [ + '--skip-tenants' => [$tenant1->getTenantKey(), $tenant2->getTenantKey()], + ]); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant3); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +}); + +test('run command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + pest()->artisan("tenants:run --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->expectsOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->expectsOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('run command skips multiple tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + pest()->artisan("tenants:run --skip-tenants=$id1 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->doesntExpectOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->expectsOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('tenants and skip-tenants options can be used together', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + // Scope to tenant1+tenant2, then skip tenant2 — only tenant1 should run + pest()->artisan("tenants:run --tenants=$id1 --tenants=$id2 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->expectsOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->doesntExpectOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('migrate-fresh command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + // Migrate all tenants first so both have the users table + Artisan::call('tenants:migrate'); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + // migrate-fresh on tenant1 only (skip tenant2) + pest()->artisan('tenants:migrate-fresh', [ + '--skip-tenants' => [$tenant2->getTenantKey()], + '--force' => true, + ])->assertExitCode(0); + + // tenant1 should still have the table (re-created by migrate-fresh) + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + // tenant2 was skipped, so its DB is untouched — table still exists + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +});