1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-08-06 09:34:03 +00:00
tenancy/tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php
lukinovec 17706f8e2c
Correct DatabaseTenancyBootstrapperTest (#1466)
### Test for DatabaseTenancyBootstrapper throwing an exception when
`DB_URL` is set

The test meant to cover this set `central.url` before creating a tenant.
That made the `CreateDatabase` job fail with a QueryException (it tried
to use the URL host as a database name). The bootstrapper's check was
never reached, and the exception that the test intended to cover wasn't
ever thrown in the end. The test passed for the wrong reason.

Fixed by creating the tenant first, setting the url only after that, and
asserting that `tenancy()->initialize()` throws the bootstrapper's
specific exception.

### Reference env variable that Laravel uses now (`DB_URL`) instead of
`DATABASE_URL`

Changed all `DATABASE_URL` references in Tenancy to `DB_URL`. [In
Laravel
11](96508d43ec (diff-e4382b565f69d02de16eef89c8d5ca2b60a679ffca90ab8b7d85fbd78f30075bR35)),
the `DATABASE_URL` env variable got renamed to `DB_URL` in
`config/database.php`. The env var got renamed quite a while ago, so
referencing `DATABASE_URL` in Tenancy's comments and tests was a bit
confusing.

### Minor cleanup

The `harden prevents tenants from using the database of another tenant`
test now reads the central connection name from
`config('tenancy.database.central_connection')` instead of hardcoding
`'central'` for consistency with the other tests.
2026-06-30 18:59:46 -07:00

160 lines
6.5 KiB
PHP

<?php
use Illuminate\Support\Facades\Event;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager;
use function Stancl\Tenancy\Tests\pest;
afterEach($cleanup = function () {
DatabaseTenancyBootstrapper::$harden = false;
});
beforeEach(function () use ($cleanup) {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
$cleanup();
});
test('harden prevents tenants from using the central database', function (bool $harden, string $connection, string $manager) {
config([
'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
"tenancy.database.managers.{$connection}" => $manager,
]);
// Point the central connection at the tested connection's config and migrate it
// (so that the central database/schema contains the tenants table).
$centralConnection = config('tenancy.database.central_connection');
$centralConfig = config("database.connections.{$connection}");
if ($connection === 'sqlite') {
$centralConfig['database'] = database_path($sqliteCentralDb = 'central.sqlite');
}
DB::purge($centralConnection);
config(["database.connections.{$centralConnection}" => $centralConfig]);
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
// (which contains the tenants table that the hardening check looks for).
$tenant = Tenant::create(['tenancy_db_connection' => $connection]);
$central = DB::connection($centralConnection);
$centralName = match (true) {
$manager === PostgreSQLSchemaManager::class => $central->selectOne('SELECT current_schema() AS schema')->schema, // Central schema name
$connection === 'sqlite' => $sqliteCentralDb, // Central SQLite DB name
default => $central->getDatabaseName(), // Central DB name
};
$tenant->update(['tenancy_db_name' => $centralName]);
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 the 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 the database of another tenant
expect(fn () => tenancy()->initialize($tenant))->toThrow(RuntimeException::class);
// Connection should be reverted back to central
$centralConnection = config('tenancy.database.central_connection');
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('database tenancy bootstrapper throws an exception if DB_URL is set', function (string|null $databaseUrl) {
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
$tenant = Tenant::create();
config(['database.connections.central.url' => $databaseUrl]);
if ($databaseUrl) {
expect(fn() => tenancy()->initialize($tenant))
->toThrow(Exception::class, 'The template connection must NOT have URL defined.');
} else {
expect(fn() => tenancy()->initialize($tenant))->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],
]);