1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 12:54:05 +00:00
tenancy/tests/SessionSeparationTest.php
Samuel Štancl cab8ecebec
Create tenant storage directories in FilesystemTenancyBootstrapper (#1410)
This is because the CreateTenantStorage listener only runs when
a tenant is created, but in multi-server setups the directory may
need to be created each time a tenant is *used*, not just created.

Also changed the listeners to use TenantEvent instead of specific
events, to make it possible to use them with other events, such as
TenancyBootstrapped.

Also update permission bits in a few mkdir() calls to better scope
data to the current OS user.

Also fix a typo in CacheTenancyBootstrapper (exception message).
2025-11-04 21:16:39 +01:00

252 lines
11 KiB
PHP

<?php
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Route;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseSessionBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Jobs\MigrateDatabase;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
// todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup
beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
// Middleware priority logic
$tenancyMiddleware = array_merge([PreventAccessFromUnwantedDomains::class], config('tenancy.identification.middleware'));
foreach (array_reverse($tenancyMiddleware) as $middleware) {
app()->make(\Illuminate\Contracts\Http\Kernel::class)->prependToMiddlewarePriority($middleware);
}
});
test('file sessions are separated', function (bool $scopeSessions) {
config([
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
'tenancy.filesystem.suffix_storage_path' => false,
'tenancy.filesystem.scope_sessions' => $scopeSessions,
'session.driver' => 'file',
]);
$sessionPath = fn () => invade(app('session')->driver()->getHandler())->path;
expect($sessionPath())->toBe(storage_path('framework/sessions'));
File::cleanDirectory(storage_path("framework/sessions")); // clean up the sessions dir from past test runs
$tenant = Tenant::create();
$tenant->enter();
if ($scopeSessions) {
expect($sessionPath())->toBe(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions'));
expect(is_dir(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions')))->toBeTrue();
} else {
expect($sessionPath())->toBe(storage_path('framework/sessions'));
}
$tenant->leave();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
if ($scopeSessions) {
expect(File::files(storage_path("tenant{$tenant->id}/framework/sessions")))->toHaveCount(0);
} else {
expect(File::exists(storage_path("tenant{$tenant->id}/framework/sessions")))->toBeFalse();
}
pest()->get("/{$tenant->id}/foo");
if ($scopeSessions) {
expect(File::files(storage_path("tenant{$tenant->id}/framework/sessions")))->toHaveCount(1);
expect(File::files(storage_path("framework/sessions")))->toHaveCount(0);
} else {
expect(File::exists(storage_path("tenant{$tenant->id}/framework/sessions")))->toBeFalse();
expect(File::files(storage_path("framework/sessions")))->toHaveCount(1);
}
})->with([true, false]);
test('redis sessions are separated using the redis bootstrapper', function (bool $bootstrappedEnabled) {
config([
'tenancy.bootstrappers' => $bootstrappedEnabled ? [RedisTenancyBootstrapper::class] : [],
'session.driver' => 'redis',
]);
$redisClient = app('session')->driver()->getHandler()->getCache()->getStore()->connection()->client();
expect($redisClient->getOption($redisClient::OPT_PREFIX))->toBe('foo'); // default prefix configured in TestCase
expect(Redis::keys('*'))->toHaveCount(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled);
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
return str($key)->startsWith("tenant_{$tenant->id}_laravel_cache_");
}))->toHaveCount($bootstrappedEnabled ? 1 : 0);
})->with([true, false]);
test('redis sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
config([
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
'session.driver' => 'redis',
'tenancy.cache.stores' => [], // will be implicitly filled
'tenancy.cache.scope_sessions' => $scopeSessions,
]);
expect(Redis::keys('*'))->toHaveCount(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}");
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
test('memcached sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
config([
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
'session.driver' => 'memcached',
'tenancy.cache.stores' => [], // will be implicitly filled
'tenancy.cache.scope_sessions' => $scopeSessions,
]);
$allMemcachedKeys = fn () => cache()->store('memcached')->getStore()->getMemcached()->getAllKeys();
if (count($allMemcachedKeys()) !== 0) {
sleep(1);
}
expect($allMemcachedKeys())->toHaveCount(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
sleep(1.1); // 1s+ sleep is necessary for getAllKeys() to work. if this causes race conditions or we want to avoid the delay, we can refactor this to some type of a mock
expect(array_filter($allMemcachedKeys(), function (string $key) use ($tenant) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
}))->toHaveCount($scopeSessions ? 1 : 0);
Artisan::call('cache:clear memcached');
})->with([true, false]);
test('dynamodb sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
config([
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
'session.driver' => 'dynamodb',
'tenancy.cache.stores' => [], // will be implicitly filled
'tenancy.cache.scope_sessions' => $scopeSessions,
]);
$allDynamodbKeys = fn () => array_map(fn ($res) => $res['key']['S'], cache()->store('dynamodb')->getStore()->getClient()->scan(['TableName' => 'cache'])['Items']);
expect($allDynamodbKeys())->toHaveCount(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
test('apc sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
config([
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
'session.driver' => 'apc',
'tenancy.cache.stores' => [], // will be implicitly filled
'tenancy.cache.scope_sessions' => $scopeSessions,
]);
$allApcuKeys = fn () => array_column(apcu_cache_info()['cache_list'], 'info');
expect($allApcuKeys())->toHaveCount(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
test('database sessions are separated regardless of whether the session bootstrapper is enabled', function (bool $sessionBootstrappedEnabled, bool $connectionSet) {
config([
'tenancy.bootstrappers' => $sessionBootstrappedEnabled
? [DatabaseTenancyBootstrapper::class, DatabaseSessionBootstrapper::class]
: [DatabaseTenancyBootstrapper::class],
'session.driver' => 'database',
'session.connection' => $connectionSet ? 'central' : null,
'tenancy.migration_parameters.--schema-path' => 'tests/Etc/session_migrations',
]);
Event::listen(
TenantCreated::class,
JobPipeline::make([CreateDatabase::class, MigrateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener()
);
pest()->artisan('migrate', [
'--path' => __DIR__ . '/Etc/session_migrations',
'--realpath' => true,
])->assertExitCode(0);
expect(DB::connection('central')->table('sessions')->count())->toBe(0);
$tenant = Tenant::create();
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
pest()->get("/{$tenant->id}/foo");
expect(invade(app('session')->driver()->getHandler())->connection->getName())->toBe('tenant');
expect(DB::connection('tenant')->table('sessions')->count())->toBe(1);
expect(DB::connection('central')->table('sessions')->count())->toBe(0);
})->with([
[true, true],
[true, false],
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
[false, false],
]);