1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 09:34:04 +00:00
tenancy/tests/SessionSeparationTest.php
Samuel Štancl eecf6f21c8
Cache prefixing logic rewrite, session scoping improvements, tests refactor (#43)
* Run cache tests on all supported drivers

* update ci healthcheck for memcached

* remove memcached healthcheck

* fix typos in test comments, expand internal.md [ci skip]

* add empty line [ci skip]

* switch to using $store->setPrefix()

* add dynamodb

* refactor try-finally to try-catch

* remove unnecessary clearResolvedInstances() call

* add dual Cache:: and cache() assertions

* add apc

* Flush APCu cache in test setup

* Revert "add dual Cache:: and cache() assertions"

This reverts commit a0bab162fbe2dd0d25e7056ceca4fb7ce54efc77.

* phpstan fix

* Add logic for scoping 'file' disks to FilesystemTenancyBootstrapper

* minor changes, add todos

* refactor how the session.connection is used in the DB session bootstrapper

* add session forgery prevention logic to the db session bootstrapper

* only use the fs bootstrapper for file disk in 'cache data is separated' dataset

* minor session scoping test changes

* Add session scoping logic to FilesystemTenancyBootstrapper, correctly update disk roots even with storage_path_tenancy disabled

* Fix code style (php-cs-fixer)

* update docblock

* make not-null check more explicit

* separate bootstrapper tests, fix swapped test names for two tests

* refactor cache bootstrapper tests

* resolve global cache todo

* expand tests: session separation tests, more filesystem separation assertions; change prefix_base-type config keys to templates/formats

* add apc session scoping test, various session separation bugfixes

* phpstan + minor logic fixes

* prefix_format -> prefix

* fix database session separation test

* revert composer.json changes, update laravel dependencies to expected next release

* only run session scoping logic in cache bootstrapper for redis, memcached, dynamodb, apc; update gitattributes

* tenancy.central_domains -> tenancy.identification.central_domains

* db session separation test: add datasets

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-04-09 20:40:27 +02:00

247 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;
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'));
} 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');
$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],
]);