1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 11:14:04 +00:00
tenancy/tests/CacheTenancyBootstrapperTest.php
Samuel Štancl a41ad69023
Filesystem logic refactor, improved defaults for cache tenancy (#42)
* refactor FilesystemTenancyBootstrapper

* clean up tests and improve coverage

* minor maintenance mode changes

* Improve tenants:migrate --skip-failing logic

* make tenants:migrate output consistently formatted

* minor RootUrlBootstrapper + misc changes

* cache bootstrapper-related improvements

* Fix code style (php-cs-fixer)

* misc refactor

* Fix code style (php-cs-fixer)

* add %original_storage_path% to fs bootstrapper, improve default config for cache

* rename method

* inject concrete implementations where needed instead of abstracts

* Fix code style (php-cs-fixer)

* refactor DealsWithTenantSymlinks

* remove obsolete phpstan ignore

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-04-02 04:26:10 +02:00

323 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Illuminate\Cache\CacheManager;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Tests\Etc\CacheService;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\SpecificCacheStoreService;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
beforeEach(function () {
config([
'tenancy.bootstrappers' => [
CacheTenancyBootstrapper::class
],
'cache.default' => $cacheDriver = 'redis',
'cache.stores.' . $secondCacheDriver = 'redis2' => config('cache.stores.redis'),
'tenancy.cache.stores' => [$cacheDriver, $secondCacheDriver],
]);
CacheTenancyBootstrapper::$prefixGenerator = null;
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
afterEach(function () {
CacheTenancyBootstrapper::$prefixGenerator = null;
});
test('correct cache prefix is used in all contexts', function () {
$originalPrefix = config('cache.prefix');
$prefixBase = config('tenancy.cache.prefix_base');
$getDefaultPrefixForTenant = fn (Tenant $tenant) => $originalPrefix . $prefixBase . $tenant->getTenantKey();
$bootstrapper = app(CacheTenancyBootstrapper::class);
$expectCachePrefixToBe = function (string $prefix) {
expect($prefix)
->toBe(app('cache')->getPrefix())
->toBe(app('cache.store')->getPrefix())
->toBe(cache()->getPrefix())
->toBe(cache()->store('redis2')->getPrefix()); // Non-default cache stores specified in $tenantCacheStores are prefixed too
};
$expectCachePrefixToBe($originalPrefix);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
cache()->set('key', 'tenantone-value');
$tenantOnePrefix = $getDefaultPrefixForTenant($tenant1);
$expectCachePrefixToBe($tenantOnePrefix);
expect($bootstrapper->generatePrefix($tenant1))->toBe($tenantOnePrefix);
tenancy()->initialize($tenant2);
cache()->set('key', 'tenanttwo-value');
$tenantTwoPrefix = $getDefaultPrefixForTenant($tenant2);
$expectCachePrefixToBe($tenantTwoPrefix);
expect($bootstrapper->generatePrefix($tenant2))->toBe($tenantTwoPrefix);
// Prefix gets reverted to default after ending tenancy
tenancy()->end();
$expectCachePrefixToBe($originalPrefix);
// Assert tenant's data is accessible using the prefix from the central context
config(['cache.prefix' => null]); // stop prefixing cache keys in central so we can provide prefix manually
app('cache')->forgetDriver(config('cache.default'));
expect(cache($tenantOnePrefix . 'key'))->toBe('tenantone-value');
expect(cache($tenantTwoPrefix . 'key'))->toBe('tenanttwo-value');
});
test('cache is persisted when reidentification is used', function () {
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
cache(['foo' => 'bar']);
expect(cache('foo'))->toBe('bar');
tenancy()->initialize($tenant2);
expect(cache('foo'))->toBeNull();
tenancy()->end();
tenancy()->initialize($tenant1);
expect(cache('foo'))->toBe('bar');
});
test('prefixing separates the cache', function () {
$tenant1 = Tenant::create();
tenancy()->initialize($tenant1);
cache()->put('foo', 'bar');
expect(cache()->get('foo'))->toBe('bar');
$tenant2 = Tenant::create();
tenancy()->initialize($tenant2);
expect(cache()->get('foo'))->toBeNull();
cache()->put('foo', 'xyz');
expect(cache()->get('foo'))->toBe('xyz');
tenancy()->initialize($tenant1);
expect(cache()->get('foo'))->toBe('bar');
});
test('central cache is persisted', function () {
cache()->put('key', 'central');
$tenant1 = Tenant::create();
tenancy()->initialize($tenant1);
expect(cache('key'))->toBeNull();
cache()->put('key', 'tenant');
expect(cache()->get('key'))->toBe('tenant');
tenancy()->end();
cache()->put('key2', 'central-two');
expect(cache()->get('key'))->toBe('central');
expect(cache()->get('key2'))->toBe('central-two');
tenancy()->initialize($tenant1);
expect(cache()->get('key'))->toBe('tenant');
expect(cache()->get('key2'))->toBeNull();
});
test('cache base prefix is customizable', function () {
config([
'tenancy.cache.prefix_base' => $prefixBase = 'custom_'
]);
$originalPrefix = config('cache.prefix');
$tenant1 = Tenant::create();
tenancy()->initialize($tenant1);
expect($originalPrefix . $prefixBase . $tenant1->getTenantKey())
->toBe(cache()->getPrefix())
->toBe(cache()->store('redis2')->getPrefix()) // Non-default store gets prefixed correctly too
->toBe(app('cache')->getPrefix())
->toBe(app('cache.store')->getPrefix());
});
test('cache is prefixed correctly when using a repository injected in a singleton', function () {
$this->app->singleton(CacheService::class);
expect(cache('key'))->toBeNull();
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe('central-value');
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
expect(cache('key'))->toBeNull();
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($tenant1->getTenantKey());
tenancy()->initialize($tenant2);
expect(cache('key'))->toBeNull();
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($tenant2->getTenantKey());
tenancy()->end();
expect(cache('key'))->toBe('central-value');
});
test('specific central cache store can be used inside a service', function () {
// Make sure 'redis' (the default store) is the only prefixed store
config(['tenancy.cache.stores' => ['redis']]);
// Name of the non-default, central cache store that we'll use using cache()->store($cacheStore)
$cacheStore = 'redis2';
// Service uses the 'redis2' store which is central/not prefixed (not present in PrefixCacheTenancyBootstrapper::$tenantCacheStores)
// The service's handle() method sets the value of the cache key 'key' to the current tenant key
// Or to 'central-value' if tenancy isn't initialized
$this->app->singleton(SpecificCacheStoreService::class, function() use ($cacheStore) {
return new SpecificCacheStoreService($this->app->make(CacheManager::class), $cacheStore);
});
$this->app->make(SpecificCacheStoreService::class)->handle();
expect(cache()->store($cacheStore)->get('key'))->toBe('central-value');
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
// The store isn't prefixed, so the cache isn't separated the values persist from one context to another
// Also assert that the value of 'key' is set correctly inside SpecificCacheStoreService according to the current context
expect(cache()->store($cacheStore)->get('key'))->toBe('central-value');
$this->app->make(SpecificCacheStoreService::class)->handle();
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant1->getTenantKey());
tenancy()->initialize($tenant2);
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant1->getTenantKey());
$this->app->make(SpecificCacheStoreService::class)->handle();
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant2->getTenantKey());
tenancy()->end();
// We last executed handle() in tenant2's context, so the value should persist as tenant2's id
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant2->getTenantKey());
});
test('only the stores specified in tenantCacheStores get prefixed', function () {
// Make sure the currently used store ('redis') is the only store in $tenantCacheStores
config(['tenancy.cache.stores' => [$prefixedStore = 'redis']]);
$centralValue = 'central-value';
$assertStoreIsNotPrefixed = function (string $unprefixedStore) use ($prefixedStore, $centralValue) {
// Switch to the unprefixed store
config(['cache.default' => $unprefixedStore]);
expect(cache('key'))->toBe($centralValue);
// Switch back to the prefixed store
config(['cache.default' => $prefixedStore]);
};
$this->app->singleton(CacheService::class);
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($centralValue);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
expect(cache('key'))->toBeNull();
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($tenant1->getTenantKey());
$assertStoreIsNotPrefixed('redis2');
tenancy()->initialize($tenant2);
expect(cache('key'))->toBeNull();
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($tenant2->getTenantKey());
$assertStoreIsNotPrefixed('redis2');
tenancy()->end();
expect(cache('key'))->toBe($centralValue);
$this->app->make(CacheService::class)->handle();
expect(cache('key'))->toBe($centralValue);
});
test('non default stores get prefixed too when specified in tenantCacheStores', function () {
// In beforeEach, we set $tenantCacheStores to ['redis', 'redis2']
// Make 'redis' the default cache driver
config(['cache.default' => 'redis']);
$tenant = Tenant::create();
$defaultPrefix = cache()->store()->getPrefix();
$bootstrapper = app(CacheTenancyBootstrapper::class);
// The prefix is the same for both drivers in the central context
expect(cache()->store('redis')->getPrefix())->toBe($defaultPrefix);
expect(cache()->store('redis2')->getPrefix())->toBe($defaultPrefix);
tenancy()->initialize($tenant);
// We didn't add a prefix generator for our 'redis2' driver, so we expect the prefix to be generated using the 'default' generator
expect($bootstrapper->generatePrefix($tenant))
->toBe(cache()->getPrefix())
->toBe(cache()->store('redis2')->getPrefix()); // Non-default store
tenancy()->end();
});
test('cache store prefix generation can be customized', function() {
// Use custom prefix generator
CacheTenancyBootstrapper::generatePrefixUsing($customPrefixGenerator = function (Tenant $tenant) {
return 'redis_tenant_cache_' . $tenant->getTenantKey();
});
expect(CacheTenancyBootstrapper::$prefixGenerator)->toBe($customPrefixGenerator);
expect(app(CacheTenancyBootstrapper::class)->generatePrefix($tenant = Tenant::create()))
->toBe($customPrefixGenerator($tenant));
tenancy()->initialize($tenant = Tenant::create());
// Expect the 'redis' store to use the prefix generated by the custom generator
expect($customPrefixGenerator($tenant))
->toBe(cache()->getPrefix())
->toBe(cache()->store('redis2')->getPrefix()) // Non-default cache stores specified in $tenantCacheStores are prefixed too
->toBe(app('cache')->getPrefix())
->toBe(app('cache.store')->getPrefix());
tenancy()->end();
});
test('stores get prefixed using the default way if no prefix generator is specified', function() {
$originalPrefix = config('cache.prefix');
$prefixBase = config('tenancy.cache.prefix_base');
$tenant = Tenant::create();
$defaultPrefix = $originalPrefix . $prefixBase . $tenant->getTenantKey();
// Don't specify a prefix generator
// Let the prefix get created using the default approach
tenancy()->initialize($tenant);
// All stores use the default way of generating the prefix when the prefix generator isn't specified
expect($defaultPrefix)
->toBe(app(CacheTenancyBootstrapper::class)->generatePrefix($tenant))
->toBe(cache()->getPrefix()) // Get prefix of the default store ('redis')
->toBe(cache()->store('redis2')->getPrefix());
tenancy()->end();
});