mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 14:14:04 +00:00
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>
This commit is contained in:
parent
943b960718
commit
eecf6f21c8
40 changed files with 1856 additions and 1177 deletions
247
tests/SessionSeparationTest.php
Normal file
247
tests/SessionSeparationTest.php
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<?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],
|
||||
]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue