[DatabaseTenancyBootstrapper::class], "tenancy.database.managers.{$connection}" => $manager, ]); // Set up and migrate the central database $centralConnection = config('tenancy.database.central_connection'); DB::purge($centralConnection); config(["database.connections.{$centralConnection}" => config("database.connections.{$connection}")]); pest()->artisan('migrate:fresh', [ '--force' => true, '--path' => __DIR__ . '/../../assets/migrations', '--realpath' => true, ]); DatabaseTenancyBootstrapper::$harden = $harden; Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { return $event->tenant; })->toListener()); // Create the tenant with its own database, then repoint it at the central database/schema. $tenant = Tenant::create(['tenancy_db_connection' => $connection]); $tenant->update([ 'tenancy_db_name' => $tenant->database()->manager()->getCurrentDatabaseName(DB::connection($centralConnection)), ]); if ($harden) { // Harden blocks initialization for tenants that use the central database expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class); // Connection should be reverted back to central expect(DB::connection()->getName())->toBe($centralConnection); } else { expect(fn () => tenancy()->initialize($tenant))->not()->toThrow(Throwable::class); // Connection not reverted to central expect(DB::connection()->getName())->toBe('tenant'); } })->with([ 'hardening enabled' => true, 'hardening disabled' => false, ])->with('db_managers'); test('harden prevents tenants from using a database of another tenant', function (bool $harden, string $connection, string $manager) { config([ 'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class], "tenancy.database.managers.{$connection}" => $manager, ]); DatabaseTenancyBootstrapper::$harden = $harden; Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { return $event->tenant; })->toListener()); $tenant = Tenant::create(['tenancy_db_connection' => $connection]); $dbName = Str::random(8) . ($connection === 'sqlite' ? '.sqlite' : ''); Tenant::create(['tenancy_db_name' => $dbName, 'tenancy_db_connection' => $connection]); $tenant->update(['tenancy_db_name' => $dbName]); if ($harden) { // Harden blocks initialization for tenants that use a database of another tenant expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class); // Connection should be reverted back to central expect(DB::connection()->getName())->toBe('central'); } else { expect(fn() => tenancy()->initialize($tenant))->not()->toThrow(Throwable::class); // Connection not reverted to central expect(DB::connection()->getName())->toBe('tenant'); } })->with([ 'hardening enabled' => true, 'hardening disabled' => false, ])->with('db_managers'); test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) { config(['database.connections.central.url' => $databaseUrl]); config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { return $event->tenant; })->toListener()); if ($databaseUrl) { expect(fn() => Tenant::create())->toThrow(QueryException::class); } else { expect(function() { $tenant1 = Tenant::create(); pest()->artisan('tenants:migrate'); tenancy()->initialize($tenant1); })->not()->toThrow(Throwable::class); } })->with(['abc.us-east-1.rds.amazonaws.com', null]); // Database managers to test with hardening. // Permission controlled managers omitted as they inherit the non-perm controlled managers (= they share the same code paths), // each important code path is covered by testing the non-permission controlled manager, so adding permission controlled managers // would add unnecessary complexity to the tests. dataset('db_managers', [ 'mysql' => ['mysql', MySQLDatabaseManager::class], 'pgsql (database)' => ['pgsql', PostgreSQLDatabaseManager::class], 'pgsql (schema)' => ['pgsql', PostgreSQLSchemaManager::class], 'sqlite' => ['sqlite', SQLiteDatabaseManager::class], ]);