diff --git a/.gitignore b/.gitignore index 64d9dc21..5a5960b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.DS_Store composer.lock vendor/ .vscode/ diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 7c52e295..a38aee42 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -61,6 +61,12 @@ class TenancyServiceProvider extends ServiceProvider Events\TenantMaintenanceModeEnabled::class => [], Events\TenantMaintenanceModeDisabled::class => [], + // Pending tenant events + Events\CreatingPendingTenant::class => [], + Events\PendingTenantCreated::class => [], + Events\PullingPendingTenant::class => [], + Events\PendingTenantPulled::class => [], + // Domain events Events\CreatingDomain::class => [], Events\DomainCreated::class => [], diff --git a/assets/config.php b/assets/config.php index 82a4c722..20826d7d 100644 --- a/assets/config.php +++ b/assets/config.php @@ -86,6 +86,25 @@ return [ // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed ], + + /** + * Pending tenants config. + * This is useful if you're looking for a way to always have a tenant ready to be used. + */ + 'pending' => [ + /** + * If disabled, pending tenants will be excluded from all tenant queries. + * You can still use ::withPending(), ::withoutPending() and ::onlyPending() to include or exclude the pending tenants regardless of this setting. + * Note: when disabled, this will also ignore pending tenants when running the tenant commands (migration, seed, etc.) + */ + 'include_in_queries' => true, + /** + * Defines how many pending tenants you want to have ready in the pending tenant pool. + * This depends on the volume of tenants you're creating. + */ + 'count' => env('TENANCY_PENDING_COUNT', 5), + ], + /** * Database tenancy config. Used by DatabaseTenancyBootstrapper. */ diff --git a/composer.json b/composer.json index b30ea94c..49657912 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "spatie/ignition": "^1.4", "ramsey/uuid": "^4.0", "stancl/jobpipeline": "^1.0", - "stancl/virtualcolumn": "^1.0" + "stancl/virtualcolumn": "^1.3" }, "require-dev": { "laravel/framework": "^9.0", diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php new file mode 100644 index 00000000..18d9fa42 --- /dev/null +++ b/src/Commands/ClearPendingTenants.php @@ -0,0 +1,74 @@ +info('Removing pending tenants.'); + + $expirationDate = now(); + // We compare the original expiration date to the new one to check if the new one is different later + $originalExpirationDate = $expirationDate->copy()->toImmutable(); + + // Skip the time constraints if the 'all' option is given + if (! $this->option('all')) { + $olderThanDays = $this->option('older-than-days'); + $olderThanHours = $this->option('older-than-hours'); + + if ($olderThanDays && $olderThanHours) { + $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components + $this->line('Please, choose only one of these options.'); + + return 1; // Exit code for failure + } + + if ($olderThanDays) { + $expirationDate->subDays($olderThanDays); + } + + if ($olderThanHours) { + $expirationDate->subHours($olderThanHours); + } + } + + $deletedTenantCount = tenancy() + ->query() + ->onlyPending() + ->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) { + $query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp); + }) + ->get() + ->each // Trigger the model events by deleting the tenants one by one + ->delete() + ->count(); + + $this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); + } +} diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php new file mode 100644 index 00000000..88202093 --- /dev/null +++ b/src/Commands/CreatePendingTenants.php @@ -0,0 +1,62 @@ +info('Creating pending tenants.'); + + $maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count')); + $pendingTenantCount = $this->getPendingTenantCount(); + $createdCount = 0; + + while ($pendingTenantCount < $maxPendingTenantCount) { + tenancy()->model()::createPending(); + + // Fetching the pending tenant count in each iteration prevents creating too many tenants + // If pending tenants are being created somewhere else while running this command + $pendingTenantCount = $this->getPendingTenantCount(); + + $createdCount++; + } + + $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); + $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); + + return 1; + } + + /** + * Calculate the number of currently available pending tenants. + */ + private function getPendingTenantCount(): int + { + return tenancy() + ->query() + ->onlyPending() + ->count(); + } +} diff --git a/src/Commands/Down.php b/src/Commands/Down.php index e7341d7f..3b68bcb2 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Foundation\Console\DownCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Down extends DownCommand { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:down {--redirect= : The path that users should be redirected to} diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 0a587122..a6dd6c5f 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -9,11 +9,11 @@ use Illuminate\Console\Command; use Illuminate\Support\LazyCollection; use Stancl\Tenancy\Actions\CreateStorageSymlinksAction; use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Link extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:link {--tenants=* : The tenant(s) to run the command for. Default: all} diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 82395fcc..0d2fceaa 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -7,14 +7,15 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Console\Migrations\MigrateCommand; use Illuminate\Database\Migrations\Migrator; +use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseMigrated; use Stancl\Tenancy\Events\MigratingDatabase; class Migrate extends MigrateCommand { - use HasATenantsOption, ExtendsLaravelCommand; + use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand; protected $description = 'Run migrations for tenant(s)'; diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 657c4990..45a93115 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\InputOption; class MigrateFresh extends Command { - use HasATenantsOption; + use HasTenantOptions, DealsWithMigrations; protected $description = 'Drop all tables and re-run all migrations for tenant(s)'; diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 1e84ab12..f9d9dac0 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -6,14 +6,15 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Migrations\Migrator; +use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseRolledBack; use Stancl\Tenancy\Events\RollingBackDatabase; class Rollback extends RollbackCommand { - use HasATenantsOption, ExtendsLaravelCommand; + use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand; protected $description = 'Rollback migrations for tenant(s).'; diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 5ecc7c77..afc9871a 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; class Run extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $description = 'Run a command for tenant(s)'; diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 8ed0b6d9..5cf468e9 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\Seeds\SeedCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseSeeded; use Stancl\Tenancy\Events\SeedingDatabase; class Seed extends SeedCommand { - use HasATenantsOption; + use HasTenantOptions; protected $description = 'Seed tenant database(s).'; diff --git a/src/Commands/Up.php b/src/Commands/Up.php index 08c935c3..cf005251 100644 --- a/src/Commands/Up.php +++ b/src/Commands/Up.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Up extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:up'; diff --git a/src/Concerns/HasATenantsOption.php b/src/Concerns/HasTenantOptions.php similarity index 63% rename from src/Concerns/HasATenantsOption.php rename to src/Concerns/HasTenantOptions.php index 32d508ec..f8a763a7 100644 --- a/src/Concerns/HasATenantsOption.php +++ b/src/Concerns/HasTenantOptions.php @@ -5,14 +5,19 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; use Illuminate\Support\LazyCollection; +use Stancl\Tenancy\Database\Concerns\PendingScope; use Symfony\Component\Console\Input\InputOption; -trait HasATenantsOption +/** + * Adds 'tenants' and 'with-pending' options. + */ +trait HasTenantOptions { protected function getOptions() { return array_merge([ ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null], + ['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query'], ], parent::getOptions()); } @@ -23,6 +28,9 @@ trait HasATenantsOption ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) + ->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) { + $query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending')); + }) ->cursor(); } diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php new file mode 100644 index 00000000..3fa9399d --- /dev/null +++ b/src/Database/Concerns/HasPending.php @@ -0,0 +1,103 @@ +casts['pending_since'] = 'timestamp'; + } + + /** + * Determine if the model instance is in a pending state. + * + * @return bool + */ + public function pending() + { + return ! is_null($this->pending_since); + } + + /** Create a pending tenant. */ + public static function createPending($attributes = []): Tenant + { + $tenant = static::create($attributes); + + event(new CreatingPendingTenant($tenant)); + + // Update the pending_since value only after the tenant is created so it's + // Not marked as pending until finishing running the migrations, seeders, etc. + $tenant->update([ + 'pending_since' => now()->timestamp, + ]); + + event(new PendingTenantCreated($tenant)); + + return $tenant; + } + + /** Pull a pending tenant. */ + public static function pullPending(): Tenant + { + return static::pullPendingFromPool(true); + } + + /** Try to pull a tenant from the pool of pending tenants. */ + public static function pullPendingFromPool(bool $firstOrCreate = false): ?Tenant + { + if (! static::onlyPending()->exists()) { + if (! $firstOrCreate) { + return null; + } + + static::createPending(); + } + + // A pending tenant is surely available at this point + $tenant = static::onlyPending()->first(); + + event(new PullingPendingTenant($tenant)); + + $tenant->update([ + 'pending_since' => null, + ]); + + event(new PendingTenantPulled($tenant)); + + return $tenant; + } +} diff --git a/src/Database/Concerns/PendingScope.php b/src/Database/Concerns/PendingScope.php new file mode 100644 index 00000000..8a6ad913 --- /dev/null +++ b/src/Database/Concerns/PendingScope.php @@ -0,0 +1,88 @@ +when(! config('tenancy.pending.include_in_queries'), function (Builder $builder) { + $builder->whereNull($builder->getModel()->getColumnForQuery('pending_since')); + }); + } + + /** + * Extend the query builder with the needed functions. + * + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) { + $this->{"add{$extension}"}($builder); + } + } + /** + * Add the with-pending extension to the builder. + * + * @return void + */ + protected function addWithPending(Builder $builder) + { + $builder->macro('withPending', function (Builder $builder, $withPending = true) { + if (! $withPending) { + return $builder->withoutPending(); + } + + return $builder->withoutGlobalScope($this); + }); + } + + /** + * Add the without-pending extension to the builder. + * + * @return void + */ + protected function addWithoutPending(Builder $builder) + { + $builder->macro('withoutPending', function (Builder $builder) { + $builder->withoutGlobalScope($this) + ->whereNull($builder->getModel()->getColumnForQuery('pending_since')) + ->orWhereNull($builder->getModel()->getDataColumn()); + + return $builder; + }); + } + + /** + * Add the only-pending extension to the builder. + * + * @return void + */ + protected function addOnlyPending(Builder $builder) + { + $builder->macro('onlyPending', function (Builder $builder) { + $builder->withoutGlobalScope($this)->whereNotNull($builder->getModel()->getColumnForQuery('pending_since')); + + return $builder; + }); + } +} diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 9cb5f5f3..37c2af2d 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -28,6 +28,7 @@ class Tenant extends Model implements Contracts\Tenant Concerns\GeneratesIds, Concerns\HasInternalKeys, Concerns\TenantRun, + Concerns\HasPending, Concerns\InitializationHelpers, Concerns\InvalidatesResolverCache; diff --git a/src/Events/CreatingPendingTenant.php b/src/Events/CreatingPendingTenant.php new file mode 100644 index 00000000..dfbe6c70 --- /dev/null +++ b/src/Events/CreatingPendingTenant.php @@ -0,0 +1,9 @@ +model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it $originalTenant = $this->tenant; diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 63a22a11..01770cda 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -78,7 +78,9 @@ class TenancyServiceProvider extends ServiceProvider public function boot(): void { $this->commands([ + Commands\Up::class, Commands\Run::class, + Commands\Down::class, Commands\Link::class, Commands\Seed::class, Commands\Install::class, @@ -87,8 +89,8 @@ class TenancyServiceProvider extends ServiceProvider Commands\TenantList::class, Commands\TenantDump::class, Commands\MigrateFresh::class, - Commands\Down::class, - Commands\Up::class, + Commands\ClearPendingTenants::class, + Commands\CreatePendingTenants::class, ]); $this->app->extend(FreshCommand::class, function () { diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index ea973070..95672753 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -24,7 +24,6 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; - beforeEach(function () { if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) { unlink($schemaPath); @@ -34,10 +33,6 @@ beforeEach(function () { return $event->tenant; })->toListener()); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); - config([ 'tenancy.bootstrappers' => [ DatabaseTenancyBootstrapper::class, diff --git a/tests/Etc/Console/AddUserCommand.php b/tests/Etc/Console/AddUserCommand.php index f102bae6..9b421f95 100644 --- a/tests/Etc/Console/AddUserCommand.php +++ b/tests/Etc/Console/AddUserCommand.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Tests\Etc\Console; use Illuminate\Console\Command; use Illuminate\Support\Str; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Concerns\TenantAwareCommand; use Stancl\Tenancy\Tests\Etc\User; class AddUserCommand extends Command { - use TenantAwareCommand, HasATenantsOption; + use TenantAwareCommand, HasTenantOptions; /** * The name and signature of the console command. diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index 9b59dedb..f9a11d95 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Tests\Etc; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; +use Stancl\Tenancy\Database\Concerns\HasPending; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Database\Models; @@ -15,5 +16,5 @@ use Stancl\Tenancy\Database\Models; */ class Tenant extends Models\Tenant implements TenantWithDatabase { - use HasDatabase, HasDomains, MaintenanceMode; + use HasDatabase, HasDomains, HasPending, MaintenanceMode; } diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php new file mode 100644 index 00000000..8dbda9ee --- /dev/null +++ b/tests/PendingTenantsTest.php @@ -0,0 +1,209 @@ +count())->toBe(1); + + Tenant::onlyPending()->first()->update([ + 'pending_since' => null + ]); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('pending trait adds query scopes', function () { + Tenant::createPending(); + Tenant::create(); + Tenant::create(); + + expect(Tenant::onlyPending()->count())->toBe(1) + ->and(Tenant::withPending(true)->count())->toBe(3) + ->and(Tenant::withPending(false)->count())->toBe(2) + ->and(Tenant::withoutPending()->count())->toBe(2); + +}); + +test('pending tenants can be created and deleted using commands', function () { + config(['tenancy.pending.count' => 4]); + + Artisan::call(CreatePendingTenants::class); + + expect(Tenant::onlyPending()->count())->toBe(4); + + Artisan::call(ClearPendingTenants::class); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('CreatePendingTenants command can have an older than constraint', function () { + config(['tenancy.pending.count' => 2]); + + Artisan::call(CreatePendingTenants::class); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(5)->timestamp + ]); + + Artisan::call('tenants:pending-clear --older-than-days=2'); + + expect(Tenant::onlyPending()->count())->toBe(1); +}); + +test('CreatePendingTenants command cannot run with both time constraints', function () { + pest()->artisan('tenants:pending-clear --older-than-days=2 --older-than-hours=2') + ->assertFailed(); +}); + +test('CreatePendingTenants commands all option overrides any config constraints', function () { + Tenant::createPending(); + Tenant::createPending(); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(10) + ]); + + config(['tenancy.pending.older_than_days' => 4]); + + Artisan::call(ClearPendingTenants::class, [ + '--all' => true + ]); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('tenancy can check if there are any pending tenants', function () { + expect(Tenant::onlyPending()->exists())->toBeFalse(); + + Tenant::createPending(); + + expect(Tenant::onlyPending()->exists())->toBeTrue(); +}); + +test('tenancy can pull a pending tenant', function () { + Tenant::createPending(); + + expect(Tenant::pullPendingFromPool())->toBeInstanceOf(Tenant::class); +}); + +test('pulling a tenant from the pending tenant pool removes it from the pool', function () { + Tenant::createPending(); + + expect(Tenant::onlyPending()->count())->toEqual(1); + + Tenant::pullPendingFromPool(); + + expect(Tenant::onlyPending()->count())->toEqual(0); +}); + +test('a new tenant gets created while pulling a pending tenant if the pending pool is empty', function () { + expect(Tenant::withPending()->get()->count())->toBe(0); // All tenants + + Tenant::pullPending(); + + expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants +}); + +test('pending tenants are included in all queries based on the include_in_queries config', function () { + Tenant::createPending(); + + config(['tenancy.pending.include_in_queries' => false]); + + expect(Tenant::all()->count())->toBe(0); + + config(['tenancy.pending.include_in_queries' => true]); + + expect(Tenant::all()->count())->toBe(1); +}); + +test('pending events are dispatched', function () { + Event::fake([ + CreatingPendingTenant::class, + PendingTenantCreated::class, + PullingPendingTenant::class, + PendingTenantPulled::class, + ]); + + Tenant::createPending(); + + Event::assertDispatched(CreatingPendingTenant::class); + Event::assertDispatched(PendingTenantCreated::class); + + Tenant::pullPending(); + + Event::assertDispatched(PullingPendingTenant::class); + Event::assertDispatched(PendingTenantPulled::class); +}); + +test('commands do not run for pending tenants if tenancy.pending.include_in_queries is false and the with pending option does not get passed', function() { + config(['tenancy.pending.include_in_queries' => false]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'"); + + $pendingTenants = $tenants->filter->pending(); + $readyTenants = $tenants->reject->pending(); + + $pendingTenants->each(fn ($tenant) => $artisan->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}")); + $readyTenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +}); + +test('commands run for pending tenants too if tenancy.pending.include_in_queries is true', function() { + config(['tenancy.pending.include_in_queries' => true]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'"); + + $tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +}); + +test('commands run for pending tenants too if the with pending option is passed', function() { + config(['tenancy.pending.include_in_queries' => false]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz' --with-pending"); + + $tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +});