1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-06 17:04:04 +00:00

Merge branch 'master' into subdomain-check

This commit is contained in:
Samuel Štancl 2026-04-12 14:01:44 +02:00 committed by GitHub
commit 3976880dcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 171 additions and 23 deletions

View file

@ -103,6 +103,33 @@ test('central helper doesnt change tenancy state when called in central context'
expect(tenant())->toBeNull();
});
test('reinitialize method does nothing in the central context', function () {
expect(tenancy()->initialized)->toBe(false);
expect(fn () => tenancy()->reinitialize())->not()->toThrow(\Throwable::class);
expect(tenancy()->initialized)->toBe(false);
});
test('reinitialize method runs bootstrappers again for the current tenant', function () {
config(['tenancy.bootstrappers' => [
ReinitBootstrapper::class,
]]);
tenancy()->initialize($tenant = Tenant::create(['reinit_bootstrapper_key' => 'foo']));
expect(tenant()->getKey())->toBe($tenant->getKey());
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo');
$tenant->update(['reinit_bootstrapper_key' => 'bar']);
// Unchanged until we reinitialize...
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo');
tenancy()->reinitialize();
expect(tenant()->getKey())->toBe($tenant->getKey());
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('bar');
});
class MyBootstrapper implements TenancyBootstrapper
{
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
@ -115,3 +142,16 @@ class MyBootstrapper implements TenancyBootstrapper
app()->instance('tenancy_ended', true);
}
}
class ReinitBootstrapper implements TenancyBootstrapper
{
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
{
app()->instance('tenancy_reinit_bootstrapper_key', $tenant->getAttribute('reinit_bootstrapper_key'));
}
public function revert(): void
{
app()->instance('tenancy_reinit_bootstrapper_key', null);
}
}

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\URL;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
@ -25,12 +26,16 @@ beforeEach(function () {
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
TenancyUrlGenerator::$prefixRouteNames = false;
TenancyUrlGenerator::$passTenantParameterToRoutes = false;
TenancyUrlGenerator::$overrides = [];
TenancyUrlGenerator::$bypassParameter = 'central';
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
});
afterEach(function () {
TenancyUrlGenerator::$prefixRouteNames = false;
TenancyUrlGenerator::$passTenantParameterToRoutes = false;
TenancyUrlGenerator::$overrides = [];
TenancyUrlGenerator::$bypassParameter = 'central';
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
});
@ -359,3 +364,40 @@ test('both the name prefixing and the tenant parameter logic gets skipped when b
expect(route('home', ['bypassParameter' => false, 'tenant' => $tenant->getTenantKey()]))->toBe($tenantRouteUrl)
->not()->toContain('bypassParameter');
});
test('the temporarySignedRoute method can automatically prefix the passed route name', function() {
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
Route::get('/{tenant}/foo', fn () => 'foo')->name('tenant.foo')->middleware([InitializeTenancyByPath::class]);
TenancyUrlGenerator::$prefixRouteNames = true;
$tenant = Tenant::create();
tenancy()->initialize($tenant);
// Route name ('foo') gets prefixed automatically (will be 'tenant.foo')
$tenantSignedUrl = URL::temporarySignedRoute('foo', now()->addMinutes(2), ['tenant' => $tenantKey = $tenant->getTenantKey()]);
expect($tenantSignedUrl)->toContain("localhost/{$tenantKey}/foo");
});
test('the bypass parameter works correctly with temporarySignedRoute', function() {
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
Route::get('/foo', fn () => 'foo')->name('central.foo');
TenancyUrlGenerator::$prefixRouteNames = true;
TenancyUrlGenerator::$bypassParameter = 'central';
$tenant = Tenant::create();
tenancy()->initialize($tenant);
// Bypass parameter allows us to generate URL for the 'central.foo' route in tenant context
$centralSignedUrl = URL::temporarySignedRoute('central.foo', now()->addMinutes(2), ['central' => true]);
expect($centralSignedUrl)
->toContain('localhost/foo')
->not()->toContain('central='); // Bypass parameter gets removed from the generated URL
});

View file

@ -23,6 +23,7 @@ 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
@ -100,7 +101,7 @@ test('redis sessions are separated using the redis bootstrapper', function (bool
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_");
return str($key)->startsWith(formatLaravelCacheKey(prefix: "tenant_{$tenant->id}_"));
}))->toHaveCount($bootstrappedEnabled ? 1 : 0);
})->with([true, false]);
@ -118,13 +119,13 @@ test('redis sessions are separated using the cache bootstrapper', function (bool
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);
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey(prefix: 'foo', suffix: "tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
@ -148,14 +149,14 @@ test('memcached sessions are separated using the cache bootstrapper', function (
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);
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
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}");
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
Artisan::call('cache:clear memcached');
@ -177,13 +178,13 @@ test('dynamodb sessions are separated using the cache bootstrapper', function (b
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);
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
@ -202,13 +203,13 @@ test('apc sessions are separated using the cache bootstrapper', function (bool $
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);
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions);
tenancy()->end();
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey());
expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) {
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}"));
}))->toHaveCount($scopeSessions ? 1 : 0);
})->with([true, false]);
@ -250,3 +251,13 @@ test('database sessions are separated regardless of whether the session bootstra
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
[false, false],
]);
function formatLaravelCacheKey(string $suffix = '', string $prefix = ''): string
{
// todo@release if we drop Laravel 12 support we can just switch to - syntax everywhere
if (version_compare(app()->version(), '13.0.0') >= 0) {
return $prefix . 'laravel-cache-' . $suffix;
} else {
return $prefix . 'laravel_cache_' . $suffix;
}
}