diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db2b0ffc..26de6a18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,3 +102,4 @@ jobs: author_name: "PHP CS Fixer" author_email: "phpcsfixer@example.com" message: Fix code style (php-cs-fixer) + diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 75784361..7c52e295 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -58,6 +58,8 @@ class TenancyServiceProvider extends ServiceProvider return $event->tenant; })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], + Events\TenantMaintenanceModeEnabled::class => [], + Events\TenantMaintenanceModeDisabled::class => [], // Domain events Events\CreatingDomain::class => [], diff --git a/assets/config.php b/assets/config.php index 6130bade..eb68d9b0 100644 --- a/assets/config.php +++ b/assets/config.php @@ -2,16 +2,14 @@ declare(strict_types=1); -use Stancl\Tenancy\Database\Models\Domain; -use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Resolvers; return [ - 'tenant_model' => Tenant::class, - 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class, + 'domain_model' => Stancl\Tenancy\Database\Models\Domain::class, - 'domain_model' => Domain::class, + 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, /** * The list of domains hosting your central app. @@ -116,18 +114,21 @@ return [ 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, 'sqlsrv' => Stancl\Tenancy\Database\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class, - /** - * Use this database manager for MySQL to have a DB user created for each tenant database. - * You can customize the grants given to these users by changing the $grants property. - */ + /** + * Use this database manager for MySQL to have a DB user created for each tenant database. + * You can customize the grants given to these users by changing the $grants property. + */ // 'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class, - /** - * Disable the pgsql manager above, and enable the one below if you - * want to separate tenant DBs by schemas rather than databases. - */ + /** + * Disable the pgsql manager above, and enable the one below if you + * want to separate tenant DBs by schemas rather than databases. + */ // 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], + + // todo docblock + 'drop_tenant_databases_on_migrate_fresh' => false, ], /** diff --git a/composer.json b/composer.json index 0dc9df09..b30ea94c 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", "pestphp/pest": "^1.21", - "nunomaduro/larastan": "^1.0" + "nunomaduro/larastan": "^1.0", + "spatie/invade": "^1.1" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index 3e9ba51d..0567d5ff 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/spatie/invade/phpstan-extension.neon parameters: paths: diff --git a/src/Commands/Down.php b/src/Commands/Down.php index 6b390957..e7341d7f 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -22,24 +22,23 @@ class Down extends DownCommand public function handle(): int { - // The base down command is heavily used. Instead of saving the data inside a file, - // the data is stored the tenant database, which means some Laravel features - // are not available with tenants. - $payload = $this->getDownDatabasePayload(); - // This runs for all tenants if no --tenants are specified tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->putDownForMaintenance($payload); }); - $this->comment('Tenants are now in maintenance mode.'); + $this->components->info('Tenants are now in maintenance mode.'); return 0; } - /** Get the payload to be placed in the "down" file. */ + /** + * Get the payload to be placed in the "down" file. This + * payload is the same as the original function + * but without the 'template' option. + */ protected function getDownDatabasePayload(): array { return [ diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 12a2c2c9..77c96588 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -4,50 +4,136 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; +use Closure; use Illuminate\Console\Command; class Install extends Command { protected $signature = 'tenancy:install'; - protected $description = 'Install stancl/tenancy.'; + protected $description = 'Install Tenancy for Laravel.'; - public function handle(): void + public function handle(): int { - $this->comment('Installing stancl/tenancy...'); - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'config', - ]); - $this->info('✔️ Created config/tenancy.php'); + $this->step( + name: 'Publishing config file', + tag: 'config', + file: 'config/tenancy.php', + newLineBefore: true, + ); - if (! file_exists(base_path('routes/tenant.php'))) { - $this->callSilent('vendor:publish', [ + $this->step( + name: 'Publishing routes', + tag: 'routes', + file: 'routes/tenant.php', + ); + + $this->step( + name: 'Publishing service provider', + tag: 'providers', + file: 'app/Providers/TenancyServiceProvider.php', + ); + + $this->step( + name: 'Publishing migrations', + tag: 'migrations', + files: [ + 'database/migrations/2019_09_15_000010_create_tenants_table.php', + 'database/migrations/2019_09_15_000020_create_domains_table.php', + ], + warning: 'Migrations already exist', + ); + + $this->step( + name: 'Creating [database/migrations/tenant] folder', + task: fn () => mkdir(database_path('migrations/tenant')), + unless: is_dir(database_path('migrations/tenant')), + warning: 'Folder [database/migrations/tenant] already exists.', + newLineAfter: true, + ); + + $this->components->info('✨️ Tenancy for Laravel successfully installed.'); + + $this->askForSupport(); + + return 0; + } + + /** + * Run a step of the installation process. + * + * @param string $name The name of the step. + * @param Closure|null $task The task code. + * @param bool $unless Condition specifying when the task should NOT run. + * @param string|null $warning Warning shown when the $unless condition is true. + * @param string|null $file Name of the file being added. + * @param string|null $tag The tag being published. + * @param array|null $files Names of files being added. + * @param bool $newLineBefore Should a new line be printed after the step. + * @param bool $newLineAfter Should a new line be printed after the step. + */ + protected function step( + string $name, + Closure $task = null, + bool $unless = false, + string $warning = null, + string $file = null, + string $tag = null, + array $files = null, + bool $newLineBefore = false, + bool $newLineAfter = false, + ): void { + if ($file) { + $name .= " [$file]"; // Append clickable path to the task name + $unless = file_exists(base_path($file)); // Make the condition a check for the file's existence + $warning = "File [$file] already exists."; // Make the warning a message about the file already existing + } + + if ($tag) { + $task = fn () => $this->callSilent('vendor:publish', [ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'routes', + '--tag' => $tag, ]); - $this->info('✔️ Created routes/tenant.php'); + } + + if ($files) { + // Show a warning if any of the files already exist + $unless = count(array_filter($files, fn ($file) => file_exists(base_path($file)))) !== 0; + } + + if (! $unless) { + if ($newLineBefore) { + $this->newLine(); + } + + $this->components->task($name, $task ?? fn () => null); + + if ($files) { + // Print out a clickable list of the added files + $this->components->bulletList(array_map(fn (string $file) => "[$file]", $files)); + } + + if ($newLineAfter) { + $this->newLine(); + } } else { - $this->info('Found routes/tenant.php.'); + $this->components->warn($warning); } + } - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'providers', - ]); - $this->info('✔️ Created TenancyServiceProvider.php'); - - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'migrations', - ]); - $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); - - if (! is_dir(database_path('migrations/tenant'))) { - mkdir(database_path('migrations/tenant')); - $this->info('✔️ Created database/migrations/tenant folder.'); + /** If the user accepts, opens the GitHub project in the browser. */ + public function askForSupport(): void + { + if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) { + if (PHP_OS_FAMILY === 'Darwin') { + exec('open https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Windows') { + exec('start https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Linux') { + exec('xdg-open https://github.com/archtechx/tenancy'); + } } - - $this->comment('✨️ stancl/tenancy installed successfully.'); } } diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 53f3cf6f..0a587122 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -23,7 +23,7 @@ class Link extends Command protected $description = 'Create or remove tenant symbolic links.'; - public function handle(): void + public function handle(): int { $tenants = $this->getTenants(); @@ -35,14 +35,18 @@ class Link extends Command } } catch (Exception $exception) { $this->error($exception->getMessage()); + + return 1; } + + return 0; } protected function removeLinks(LazyCollection $tenants): void { RemoveStorageSymlinksAction::handle($tenants); - $this->info('The links have been removed.'); + $this->components->info('The links have been removed.'); } protected function createLinks(LazyCollection $tenants): void @@ -53,6 +57,6 @@ class Link extends Command (bool) ($this->option('force') ?? false), ); - $this->info('The links have been created.'); + $this->components->info('The links have been created.'); } } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 56a6047f..657c4990 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -8,7 +8,7 @@ use Illuminate\Console\Command; use Stancl\Tenancy\Concerns\HasATenantsOption; use Symfony\Component\Console\Input\InputOption; -final class MigrateFresh extends Command +class MigrateFresh extends Command { use HasATenantsOption; @@ -23,23 +23,27 @@ final class MigrateFresh extends Command $this->setName('tenants:migrate-fresh'); } - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->info('Dropping tables.'); - $this->call('db:wipe', array_filter([ - '--database' => 'tenant', - '--drop-views' => $this->option('drop-views'), - '--force' => true, - ])); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); - $this->info('Migrating.'); - $this->callSilent('tenants:migrate', [ - '--tenants' => [$tenant->getTenantKey()], - '--force' => true, - ]); + $this->components->task('Dropping tables', function () { + $this->callSilently('db:wipe', array_filter([ + '--database' => 'tenant', + '--drop-views' => $this->option('drop-views'), + '--force' => true, + ])); + }); + + $this->components->task('Migrating', function () use ($tenant) { + $this->callSilent('tenants:migrate', [ + '--tenants' => [$tenant->getTenantKey()], + '--force' => true, + ]); + }); }); - $this->info('Done.'); + return 0; } } diff --git a/src/Commands/MigrateFreshOverride.php b/src/Commands/MigrateFreshOverride.php new file mode 100644 index 00000000..88e9e21e --- /dev/null +++ b/src/Commands/MigrateFreshOverride.php @@ -0,0 +1,19 @@ +model()::cursor()->each->delete(); + } + + return parent::handle(); + } +} diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index d3989cc0..1e84ab12 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -37,7 +37,7 @@ class Rollback extends RollbackCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 9bb04716..5ecc7c77 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -19,30 +19,32 @@ class Run extends Command protected $signature = 'tenants:run {commandname : The artisan command.} {--tenants=* : The tenant(s) to run the command for. Default: all}'; - public function handle(): void + public function handle(): int { $argvInput = $this->argvInput(); tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $this->getLaravel() ->make(Kernel::class) ->handle($argvInput, new ConsoleOutput); }); + + return 0; } protected function argvInput(): ArgvInput { - /** @var string $commandname */ - $commandname = $this->argument('commandname'); + /** @var string $commandName */ + $commandName = $this->argument('commandname'); // Convert string command to array - $subcommand = explode(' ', $commandname); + $subCommand = explode(' ', $commandName); // Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it - array_unshift($subcommand, 'artisan'); + array_unshift($subCommand, 'artisan'); - return new ArgvInput($subcommand); + return new ArgvInput($subCommand); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 496c04e6..8ed0b6d9 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -36,7 +36,7 @@ class Seed extends SeedCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 9c8698c6..6edae6b0 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Console\Command; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\DumpCommand; @@ -22,13 +21,6 @@ class TenantDump extends DumpCommand } public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int - { - $this->tenant()->run(fn () => parent::handle($connections, $dispatcher)); - - return Command::SUCCESS; - } - - public function tenant(): Tenant { $tenant = $this->option('tenant') ?? tenant() @@ -39,9 +31,15 @@ class TenantDump extends DumpCommand $tenant = tenancy()->find($tenant); } - throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.'); + if ($tenant === null) { + $this->components->error('Could not find tenant to use for dumping the schema.'); - return $tenant; + return 1; + } + + parent::handle($connections, $dispatcher); + + return 0; } protected function getOptions(): array diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 9fd3f8bd..c008ba59 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; @@ -14,19 +15,35 @@ class TenantList extends Command protected $description = 'List tenants.'; - public function handle(): void + public function handle(): int { - $this->info('Listing all tenants.'); - $tenants = tenancy()->query()->cursor(); + $this->components->info("Listing {$tenants->count()} tenants."); + foreach ($tenants as $tenant) { /** @var Model&Tenant $tenant */ - if ($tenant->domains) { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); - } else { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); - } + $this->components->twoColumnDetail($this->tenantCLI($tenant), $this->domainsCLI($tenant->domains)); } + + $this->newLine(); + + return 0; + } + + /** Generate the visual CLI output for the tenant name. */ + protected function tenantCLI(Model&Tenant $tenant): string + { + return "{$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"; + } + + /** Generate the visual CLI output for the domain names. */ + protected function domainsCLI(?Collection $domains): ?string + { + if (! $domains) { + return null; + } + + return "{$domains->pluck('domain')->implode(' / ')}"; } } diff --git a/src/Commands/Up.php b/src/Commands/Up.php index a3f690c2..08c935c3 100644 --- a/src/Commands/Up.php +++ b/src/Commands/Up.php @@ -15,13 +15,15 @@ class Up extends Command protected $description = 'Put tenants out of maintenance mode.'; - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->bringUpFromMaintenance(); }); - $this->comment('Tenants are now out of maintenance mode.'); + $this->components->info('Tenants are now out of maintenance mode.'); + + return 0; } } diff --git a/src/Database/Concerns/MaintenanceMode.php b/src/Database/Concerns/MaintenanceMode.php index cc4490f6..1ad173cf 100644 --- a/src/Database/Concerns/MaintenanceMode.php +++ b/src/Database/Concerns/MaintenanceMode.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; +use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled; +use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled; + /** * @mixin \Illuminate\Database\Eloquent\Model */ @@ -21,10 +24,14 @@ trait MaintenanceMode 'status' => $data['status'] ?? 503, ], ]); + + event(new TenantMaintenanceModeEnabled($this)); } public function bringUpFromMaintenance(): void { $this->update(['maintenance_mode' => null]); + + event(new TenantMaintenanceModeDisabled($this)); } } diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 88c34146..83e75332 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts; use Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Database\TenantCollection; use Stancl\Tenancy\Events; +use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; /** * @property string|int $id @@ -45,6 +46,17 @@ class Tenant extends Model implements Contracts\Tenant return $this->getAttribute($this->getTenantKeyName()); } + public static function current(): static|null + { + return tenant(); + } + + /** @throws TenancyNotInitializedException */ + public static function currentOrFail(): static + { + return static::current() ?? throw new TenancyNotInitializedException; + } + public function newCollection(array $models = []): TenantCollection { return new TenantCollection($models); diff --git a/src/Events/Contracts/TenantEvent.php b/src/Events/Contracts/TenantEvent.php index 951fabfc..e07708b7 100644 --- a/src/Events/Contracts/TenantEvent.php +++ b/src/Events/Contracts/TenantEvent.php @@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Events\Contracts; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\Tenant; -abstract class TenantEvent +abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here { use SerializesModels; diff --git a/src/Events/TenantMaintenanceModeDisabled.php b/src/Events/TenantMaintenanceModeDisabled.php new file mode 100644 index 00000000..5b2d9778 --- /dev/null +++ b/src/Events/TenantMaintenanceModeDisabled.php @@ -0,0 +1,10 @@ +hasHeader(static::$header)) { - $tenant = $request->header(static::$header); - } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { - $tenant = $request->get(static::$queryParameter); + return $request->header(static::$header); } - return $tenant; + if (static::$queryParameter && $request->has(static::$queryParameter)) { + return $request->get(static::$queryParameter); + } + + if (static::$cookie && $request->hasCookie(static::$cookie)) { + return $request->cookie(static::$cookie); + } + + return null; } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 7e12a857..63a22a11 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager; +use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; @@ -90,6 +91,10 @@ class TenancyServiceProvider extends ServiceProvider Commands\Up::class, ]); + $this->app->extend(FreshCommand::class, function () { + return new Commands\MigrateFreshOverride; + }); + $this->publishes([ __DIR__ . '/../assets/config.php' => config_path('tenancy.php'), ], 'config'); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index ebd43e11..f4383ff9 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -8,12 +8,16 @@ use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Jobs\DeleteDomains; use Illuminate\Support\Facades\Artisan; use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Jobs\DeleteDatabase; use Illuminate\Database\DatabaseManager; use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenantDeleted; use Stancl\Tenancy\Tests\Etc\TestSeeder; +use Stancl\Tenancy\Events\DeletingTenant; use Stancl\Tenancy\Tests\Etc\ExampleSeeder; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; @@ -181,7 +185,9 @@ test('install command works', function () { mkdir($dir, 0777, true); } - pest()->artisan('tenancy:install'); + pest()->artisan('tenancy:install') + ->expectsConfirmation('Would you like to show your support by starring the project on GitHub?', 'no') + ->assertExitCode(0); expect(base_path('routes/tenant.php'))->toBeFile(); expect(base_path('config/tenancy.php'))->toBeFile(); expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile(); @@ -224,7 +230,8 @@ test('run command with array of tenants works', function () { test('link command works', function() { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); - pest()->artisan('tenants:link'); + pest()->artisan('tenants:link') + ->assertExitCode(0); $this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public")); $this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1"))); @@ -234,7 +241,7 @@ test('link command works', function() { pest()->artisan('tenants:link', [ '--remove' => true, - ]); + ])->assertExitCode(0); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId1")); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId2")); @@ -266,8 +273,9 @@ test('run command works when sub command asks questions and accepts arguments', pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ") ->expectsQuestion('What is your email?', 'email@localhost') - ->expectsOutput("Tenant: $id") - ->expectsOutput("User created: Abrar(email@localhost)"); + ->expectsOutputToContain("Tenant: $id.") + ->expectsOutput("User created: Abrar(email@localhost)") + ->assertExitCode(0); // Assert we are in central context expect(tenancy()->initialized)->toBeFalse(); @@ -281,6 +289,47 @@ test('run command works when sub command asks questions and accepts arguments', expect($user->email)->toBe('email@localhost'); }); +test('migrate fresh command only deletes tenant databases if drop_tenant_databases_on_migrate_fresh is true', function (bool $dropTenantDBsOnMigrateFresh) { + Event::listen(DeletingTenant::class, + JobPipeline::make([DeleteDomains::class])->send(function (DeletingTenant $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); + + Event::listen( + TenantDeleted::class, + JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); + + config(['tenancy.database.drop_tenant_databases_on_migrate_fresh' => $dropTenantDBsOnMigrateFresh]); + $shouldHaveDBAfterMigrateFresh = ! $dropTenantDBsOnMigrateFresh; + + /** @var Tenant[] $tenants */ + $tenants = [ + Tenant::create(), + Tenant::create(), + Tenant::create(), + ]; + + $tenantHasDatabase = fn (Tenant $tenant) => $tenant->database()->manager()->databaseExists($tenant->database()->getName()); + + foreach ($tenants as $tenant) { + expect($tenantHasDatabase($tenant))->toBeTrue(); + } + + pest()->artisan('migrate:fresh', [ + '--force' => true, + '--path' => __DIR__ . '/../assets/migrations', + '--realpath' => true, + ]); + + foreach ($tenants as $tenant) { + expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh); + } +})->with([true, false]); + // todo@tests function runCommandWorks(): void { diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 6e28d1ab..86366e2f 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; @@ -32,6 +33,20 @@ test('tenants can be in maintenance mode', function () { pest()->get('http://acme.localhost/foo')->assertStatus(200); }); +test('maintenance mode events are fired', function () { + $tenant = MaintenanceTenant::create(); + + Event::fake(); + + $tenant->putDownForMaintenance(); + + Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeEnabled::class); + + $tenant->bringUpFromMaintenance(); + + Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeDisabled::class); +}); + test('tenants can be put into maintenance mode using artisan commands', function() { Route::get('/foo', function () { return 'bar'; @@ -44,12 +59,18 @@ test('tenants can be put into maintenance mode using artisan commands', function pest()->get('http://acme.localhost/foo')->assertStatus(200); + pest()->artisan('tenants:down') + ->expectsOutputToContain('Tenants are now in maintenance mode.') + ->assertExitCode(0); + Artisan::call('tenants:down'); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(503); - Artisan::call('tenants:up'); + pest()->artisan('tenants:up') + ->expectsOutputToContain('Tenants are now out of maintenance mode.') + ->assertExitCode(0); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(200); diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index e5a05f65..e10c00e1 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -20,19 +20,18 @@ beforeEach(function () { afterEach(function () { InitializeTenancyByRequestData::$header = 'X-Tenant'; + InitializeTenancyByRequestData::$cookie = 'X-Tenant'; InitializeTenancyByRequestData::$queryParameter = 'tenant'; }); test('header identification works', function () { InitializeTenancyByRequestData::$header = 'X-Tenant'; $tenant = Tenant::create(); - $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() - ->get('test', [ - 'X-Tenant' => $tenant->id, - ]) + ->withHeader('X-Tenant', $tenant->id) + ->get('test') ->assertSee($tenant->id); }); @@ -40,10 +39,20 @@ test('query parameter identification works', function () { InitializeTenancyByRequestData::$queryParameter = 'tenant'; $tenant = Tenant::create(); - $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() ->get('test?tenant=' . $tenant->id) ->assertSee($tenant->id); }); + +test('cookie identification works', function () { + InitializeTenancyByRequestData::$cookie = 'X-Tenant'; + $tenant = Tenant::create(); + + $this + ->withoutExceptionHandling() + ->withUnencryptedCookie('X-Tenant', $tenant->id) + ->get('test',) + ->assertSee($tenant->id); +}); diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index b4fd38f6..fb62260c 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -18,6 +18,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\UUIDGenerator; +use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; test('created event is dispatched', function () { Event::fake([TenantCreated::class]); @@ -141,6 +142,31 @@ test('a command can be run on a collection of tenants', function () { expect(Tenant::find('t2')->foo)->toBe('xyz'); }); +test('the current method returns the currently initialized tenant', function() { + tenancy()->initialize($tenant = Tenant::create()); + + expect(Tenant::current())->toBe($tenant); +}); + +test('the current method returns null if there is no currently initialized tenant', function() { + tenancy()->end(); + + expect(Tenant::current())->toBeNull(); +}); + +test('currentOrFail method returns the currently initialized tenant', function() { + tenancy()->initialize($tenant = Tenant::create()); + + expect(Tenant::currentOrFail())->toBe($tenant); +}); + +test('currentOrFail method throws an exception if there is no currently initialized tenant', function() { + tenancy()->end(); + + expect(fn() => Tenant::currentOrFail())->toThrow(TenancyNotInitializedException::class); +}); + + class MyTenant extends Tenant { protected $table = 'tenants';