mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-04 12:34:03 +00:00
Merge branch 'master' into url-generator-bootstrapper-https
This commit is contained in:
commit
e0478cb828
68 changed files with 1336 additions and 367 deletions
|
|
@ -18,8 +18,6 @@ beforeEach(function () {
|
|||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
// todo@move move these to be in the same file as the other tests from this PR (#909) rather than generic "action tests"
|
||||
|
||||
test('create storage symlinks action works', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ test('tags separate cache properly', function () {
|
|||
$tenant1 = Tenant::create();
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
cache()->put('foo', 'bar', 1);
|
||||
cache()->put('foo', 'bar');
|
||||
expect(cache()->get('foo'))->toBe('bar');
|
||||
|
||||
$tenant2 = Tenant::create();
|
||||
|
|
@ -64,7 +64,7 @@ test('tags separate cache properly', function () {
|
|||
|
||||
expect(cache('foo'))->not()->toBe('bar');
|
||||
|
||||
cache()->put('foo', 'xyz', 1);
|
||||
cache()->put('foo', 'xyz');
|
||||
expect(cache()->get('foo'))->toBe('xyz');
|
||||
});
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ test('invoking the cache helper works', function () {
|
|||
$tenant1 = Tenant::create();
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
cache(['foo' => 'bar'], 1);
|
||||
cache(['foo' => 'bar']);
|
||||
expect(cache('foo'))->toBe('bar');
|
||||
|
||||
$tenant2 = Tenant::create();
|
||||
|
|
@ -80,7 +80,7 @@ test('invoking the cache helper works', function () {
|
|||
|
||||
expect(cache('foo'))->not()->toBe('bar');
|
||||
|
||||
cache(['foo' => 'xyz'], 1);
|
||||
cache(['foo' => 'xyz']);
|
||||
expect(cache('foo'))->toBe('xyz');
|
||||
});
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ test('cache is persisted', function () {
|
|||
$tenant1 = Tenant::create();
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
cache(['foo' => 'bar'], 10);
|
||||
cache(['foo' => 'bar']);
|
||||
expect(cache('foo'))->toBe('bar');
|
||||
|
||||
tenancy()->end();
|
||||
|
|
@ -102,7 +102,7 @@ test('cache is persisted when reidentification is used', function () {
|
|||
$tenant2 = Tenant::create();
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
cache(['foo' => 'bar'], 10);
|
||||
cache(['foo' => 'bar']);
|
||||
expect(cache('foo'))->toBe('bar');
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
|
|
|||
|
|
@ -115,8 +115,6 @@ test('files can get fetched using the storage url', function() {
|
|||
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
||||
$originalStoragePath = storage_path();
|
||||
|
||||
// todo@tests https://github.com/tenancy-for-laravel/v4/pull/44#issue-2228530362
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||
'tenancy.filesystem.suffix_storage_path' => false,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ use Illuminate\Support\Facades\Route as RouteFacade;
|
|||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
|
|
@ -23,6 +25,8 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||
|
||||
beforeEach($cleanup = function () {
|
||||
Tenant::$extraCustomColumns = [];
|
||||
|
|
@ -112,11 +116,19 @@ test('cache is invalidated when the tenant is updated', function (string $resolv
|
|||
// Only testing update here - presumably if this works, deletes (and other things we test here)
|
||||
// will work as well. The main unique thing about this test is that it makes the change from
|
||||
// *within* the tenant context.
|
||||
test('cache is invalidated when tenant is updated from within the tenant context', function (string $cacheBootstrapper) {
|
||||
config(['tenancy.bootstrappers' => [$cacheBootstrapper]]);
|
||||
test('cache is invalidated when tenant is updated from within the tenant context', function (string $cacheStore, array $bootstrappers) {
|
||||
config([
|
||||
'cache.default' => $cacheStore,
|
||||
'tenancy.bootstrappers' => $bootstrappers,
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
if ($cacheStore === 'database') {
|
||||
withCacheTables();
|
||||
withTenantDatabases();
|
||||
}
|
||||
|
||||
$resolver = PathTenantResolver::class;
|
||||
|
||||
$tenant = Tenant::create([$tenantModelColumn = tenantModelColumn(true) => 'acme']);
|
||||
|
|
@ -150,9 +162,9 @@ test('cache is invalidated when tenant is updated from within the tenant context
|
|||
|
||||
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the tenant was retrieved from the DB
|
||||
})->with([
|
||||
// todo@samuel test this with the database cache bootstrapper too?
|
||||
CacheTenancyBootstrapper::class,
|
||||
CacheTagsBootstrapper::class,
|
||||
['redis', [CacheTenancyBootstrapper::class]],
|
||||
['redis', [CacheTagsBootstrapper::class]],
|
||||
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||
]);
|
||||
|
||||
test('cache is invalidated when the tenant is deleted', function (string $resolver, bool $configureTenantModelColumn) {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ test('the clone action can clone specific routes either using name or route inst
|
|||
false,
|
||||
]);
|
||||
|
||||
test('the clone action prefixes already prefixed routes correctly', function () {
|
||||
test('the clone action prefixes already prefixed routes correctly', function (bool $tenantParameterBeforePrefix) {
|
||||
$routes = [
|
||||
RouteFacade::get('/home', fn () => true)
|
||||
->middleware(['clone'])
|
||||
|
|
@ -195,7 +195,12 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
|||
->prefix('prefix/'),
|
||||
];
|
||||
|
||||
app(CloneRoutesAsTenant::class)->handle();
|
||||
$cloneAction = app(CloneRoutesAsTenant::class);
|
||||
$cloneAction
|
||||
->tenantParameterBeforePrefix($tenantParameterBeforePrefix)
|
||||
->handle();
|
||||
|
||||
$expectedPrefix = $tenantParameterBeforePrefix ? '{tenant}/prefix' : 'prefix/{tenant}';
|
||||
|
||||
$clonedRoutes = [
|
||||
RouteFacade::getRoutes()->getByName('tenant.home'),
|
||||
|
|
@ -206,9 +211,10 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
|||
|
||||
// The cloned route is prefixed correctly
|
||||
foreach ($clonedRoutes as $key => $route) {
|
||||
expect($route->getPrefix())->toBe("prefix/{tenant}");
|
||||
expect($route->getPrefix())->toBe($expectedPrefix);
|
||||
|
||||
$clonedRouteUrl = route($route->getName(), ['tenant' => $tenant = Tenant::create()]);
|
||||
$expectedPrefixInUrl = $tenantParameterBeforePrefix ? "{$tenant->id}/prefix" : "prefix/{$tenant->id}";
|
||||
|
||||
expect($clonedRouteUrl)
|
||||
// Original prefix does not occur in the cloned route's URL
|
||||
|
|
@ -216,14 +222,14 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
|||
->not()->toContain("//prefix")
|
||||
->not()->toContain("prefix//")
|
||||
// Instead, the route is prefixed correctly
|
||||
->toBe("http://localhost/prefix/{$tenant->id}/{$routes[$key]->getName()}");
|
||||
->toBe("http://localhost/{$expectedPrefixInUrl}/{$routes[$key]->getName()}");
|
||||
|
||||
// The cloned route is accessible
|
||||
pest()->get($clonedRouteUrl)->assertOk();
|
||||
}
|
||||
});
|
||||
})->with([true, false]);
|
||||
|
||||
test('clone action trims trailing slashes from prefixes given to nested route groups', function () {
|
||||
test('clone action trims trailing slashes from prefixes given to nested route groups', function (bool $tenantParameterBeforePrefix) {
|
||||
RouteFacade::prefix('prefix')->group(function () {
|
||||
RouteFacade::prefix('')->group(function () {
|
||||
// This issue seems to only happen when there's a group with a prefix, then a group with an empty prefix, and then a / route
|
||||
|
|
@ -237,7 +243,10 @@ test('clone action trims trailing slashes from prefixes given to nested route gr
|
|||
});
|
||||
});
|
||||
|
||||
app(CloneRoutesAsTenant::class)->handle();
|
||||
$cloneAction = app(CloneRoutesAsTenant::class);
|
||||
$cloneAction
|
||||
->tenantParameterBeforePrefix($tenantParameterBeforePrefix)
|
||||
->handle();
|
||||
|
||||
$clonedLandingUrl = route('tenant.landing', ['tenant' => $tenant = Tenant::create()]);
|
||||
$clonedHomeRouteUrl = route('tenant.home', ['tenant' => $tenant]);
|
||||
|
|
@ -245,17 +254,20 @@ test('clone action trims trailing slashes from prefixes given to nested route gr
|
|||
$landingRoute = RouteFacade::getRoutes()->getByName('tenant.landing');
|
||||
$homeRoute = RouteFacade::getRoutes()->getByName('tenant.home');
|
||||
|
||||
expect($landingRoute->uri())->toBe('prefix/{tenant}');
|
||||
expect($homeRoute->uri())->toBe('prefix/{tenant}/home');
|
||||
$expectedPrefix = $tenantParameterBeforePrefix ? '{tenant}/prefix' : 'prefix/{tenant}';
|
||||
$expectedPrefixInUrl = $tenantParameterBeforePrefix ? "{$tenant->id}/prefix" : "prefix/{$tenant->id}";
|
||||
|
||||
expect($landingRoute->uri())->toBe($expectedPrefix);
|
||||
expect($homeRoute->uri())->toBe("{$expectedPrefix}/home");
|
||||
|
||||
expect($clonedLandingUrl)
|
||||
->not()->toContain("prefix//")
|
||||
->toBe("http://localhost/prefix/{$tenant->id}");
|
||||
->toBe("http://localhost/{$expectedPrefixInUrl}");
|
||||
|
||||
expect($clonedHomeRouteUrl)
|
||||
->not()->toContain("prefix//")
|
||||
->toBe("http://localhost/prefix/{$tenant->id}/home");
|
||||
});
|
||||
->toBe("http://localhost/{$expectedPrefixInUrl}/home");
|
||||
})->with([true, false]);
|
||||
|
||||
test('tenant routes are ignored from cloning and clone middleware in groups causes no issues', function () {
|
||||
// Should NOT be cloned, already has tenant parameter
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
|||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||
|
||||
beforeEach(function () {
|
||||
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||
|
|
@ -95,6 +96,60 @@ test('migrate command works with tenants option', function () {
|
|||
expect(Schema::hasTable('users'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('migrate command uses the passed database option as the template tenant connection', function () {
|
||||
$originalTemplateConnection = config('tenancy.database.template_tenant_connection');
|
||||
|
||||
// Add a custom connection that will be used as the template for the tenant connection
|
||||
// Identical to the default (mysql), just with different charset and collation
|
||||
config(['database.connections.custom_connection' => [
|
||||
"driver" => "mysql",
|
||||
"url" => "",
|
||||
"host" => "mysql",
|
||||
"port" => "3306",
|
||||
"database" => "main",
|
||||
"username" => "root",
|
||||
"password" => "password",
|
||||
"unix_socket" => "",
|
||||
"charset" => "latin1", // Different from the default (utf8mb4)
|
||||
"collation" => "latin1_swedish_ci", // Different from the default (utf8mb4_unicode_ci)
|
||||
"prefix" => "",
|
||||
"prefix_indexes" => true,
|
||||
"strict" => true,
|
||||
"engine" => null,
|
||||
"options" => []
|
||||
]]);
|
||||
|
||||
$templateConnectionDuringMigration = null;
|
||||
$tenantConnectionDuringMigration = null;
|
||||
|
||||
Event::listen(MigratingDatabase::class, function() use (&$templateConnectionDuringMigration, &$tenantConnectionDuringMigration) {
|
||||
$templateConnectionDuringMigration = config('tenancy.database.template_tenant_connection');
|
||||
$tenantConnectionDuringMigration = DB::connection('tenant')->getConfig();
|
||||
});
|
||||
|
||||
// The original tenant template connection config remains default
|
||||
expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection);
|
||||
|
||||
Tenant::create();
|
||||
|
||||
// The original template connection is used when the --database option is not passed
|
||||
pest()->artisan('tenants:migrate');
|
||||
expect($templateConnectionDuringMigration)->toBe($originalTemplateConnection);
|
||||
|
||||
Tenant::create();
|
||||
|
||||
// The migrate command temporarily uses the connection passed in the --database option
|
||||
pest()->artisan('tenants:migrate', ['--database' => 'custom_connection']);
|
||||
expect($templateConnectionDuringMigration)->toBe('custom_connection');
|
||||
|
||||
// The tenant connection during migration actually used custom_connection's config
|
||||
expect($tenantConnectionDuringMigration['charset'])->toBe('latin1');
|
||||
expect($tenantConnectionDuringMigration['collation'])->toBe('latin1_swedish_ci');
|
||||
|
||||
// The tenant template connection config is restored to the original after migrating
|
||||
expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection);
|
||||
});
|
||||
|
||||
test('migrate command only throws exceptions if skip-failing is not passed', function() {
|
||||
Tenant::create();
|
||||
|
||||
|
|
@ -311,6 +366,21 @@ test('migrate fresh command works', function () {
|
|||
expect(DB::table('users')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('migrate fresh command respects force option in production', function () {
|
||||
// Set environment to production
|
||||
app()->detectEnvironment(fn() => 'production');
|
||||
|
||||
Tenant::create();
|
||||
|
||||
// Without --force in production, command should prompt for confirmation
|
||||
pest()->artisan('tenants:migrate-fresh')
|
||||
->expectsConfirmation('Are you sure you want to run this command?');
|
||||
|
||||
// With --force, command should succeed without prompting
|
||||
pest()->artisan('tenants:migrate-fresh', ['--force' => true])
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('run command with array of tenants works', function () {
|
||||
$tenantId1 = Tenant::create()->getTenantKey();
|
||||
$tenantId2 = Tenant::create()->getTenantKey();
|
||||
|
|
|
|||
193
tests/DatabaseCacheBootstrapperTest.php
Normal file
193
tests/DatabaseCacheBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||
|
||||
beforeEach(function () {
|
||||
withBootstrapping();
|
||||
withCacheTables();
|
||||
withTenantDatabases(true);
|
||||
|
||||
DatabaseCacheBootstrapper::$stores = null;
|
||||
|
||||
config([
|
||||
'cache.stores.database.connection' => 'central', // Explicitly set cache DB connection name in config
|
||||
'cache.stores.database.lock_connection' => 'central', // Also set lock connection name
|
||||
'cache.default' => 'database',
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
DatabaseCacheBootstrapper::class, // Used instead of CacheTenancyBootstrapper
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
DatabaseCacheBootstrapper::$stores = null;
|
||||
});
|
||||
|
||||
test('DatabaseCacheBootstrapper switches the database cache store connections correctly', function () {
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.database.lock_connection'))->toBe('central');
|
||||
expect(Cache::store()->getConnection()->getName())->toBe('central');
|
||||
expect(Cache::lock('foo')->getConnectionName())->toBe('central');
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
expect(config('cache.stores.database.connection'))->toBe('tenant');
|
||||
expect(config('cache.stores.database.lock_connection'))->toBe('tenant');
|
||||
expect(Cache::store()->getConnection()->getName())->toBe('tenant');
|
||||
expect(Cache::lock('foo')->getConnectionName())->toBe('tenant');
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.database.lock_connection'))->toBe('central');
|
||||
expect(Cache::store()->getConnection()->getName())->toBe('central');
|
||||
expect(Cache::lock('foo')->getConnectionName())->toBe('central');
|
||||
});
|
||||
|
||||
test('cache is separated correctly when using DatabaseCacheBootstrapper', function() {
|
||||
// We need the prefix later for lower-level assertions. Let's store it
|
||||
// once now and reuse this variable rather than re-fetching it to make
|
||||
// it clear that the scoping does NOT come from a prefix change.
|
||||
|
||||
$cachePrefix = config('cache.prefix');
|
||||
$getCacheUsingDbQuery = fn (string $cacheKey) =>
|
||||
DB::selectOne("SELECT * FROM `cache` WHERE `key` = '{$cachePrefix}{$cacheKey}'")?->value;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
// Write to cache in central context
|
||||
cache()->set('foo', 'central');
|
||||
expect(Cache::get('foo'))->toBe('central');
|
||||
// The value retrieved by the DB query is formatted like "s:7:"central";".
|
||||
// We use toContain() because of this formatting instead of just toBe().
|
||||
expect($getCacheUsingDbQuery('foo'))->toContain('central');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// Central cache doesn't leak to tenant context
|
||||
expect(Cache::has('foo'))->toBeFalse();
|
||||
expect($getCacheUsingDbQuery('foo'))->toBeNull();
|
||||
|
||||
cache()->set('foo', 'bar');
|
||||
expect(Cache::get('foo'))->toBe('bar');
|
||||
expect($getCacheUsingDbQuery('foo'))->toContain('bar');
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
// Assert one tenant's cache doesn't leak to another tenant
|
||||
expect(Cache::has('foo'))->toBeFalse();
|
||||
expect($getCacheUsingDbQuery('foo'))->toBeNull();
|
||||
|
||||
cache()->set('foo', 'xyz');
|
||||
expect(Cache::get('foo'))->toBe('xyz');
|
||||
expect($getCacheUsingDbQuery('foo'))->toContain('xyz');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// Assert cache didn't leak to the original tenant
|
||||
expect(Cache::get('foo'))->toBe('bar');
|
||||
expect($getCacheUsingDbQuery('foo'))->toContain('bar');
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Assert central 'foo' cache is still the same ('central')
|
||||
expect(Cache::get('foo'))->toBe('central');
|
||||
expect($getCacheUsingDbQuery('foo'))->toContain('central');
|
||||
});
|
||||
|
||||
test('DatabaseCacheBootstrapper auto-detects all database driver stores by default', function() {
|
||||
config([
|
||||
'cache.stores.database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => 'central',
|
||||
'table' => 'cache',
|
||||
],
|
||||
'cache.stores.sessions' => [
|
||||
'driver' => 'database',
|
||||
'connection' => 'central',
|
||||
'table' => 'sessions_cache',
|
||||
],
|
||||
'cache.stores.redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
],
|
||||
'cache.stores.file' => [
|
||||
'driver' => 'file',
|
||||
'path' => '/foo/bar',
|
||||
],
|
||||
]);
|
||||
|
||||
// Here, we're using auto-detection (default behavior)
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||
expect(config('cache.stores.file.path'))->toBe('/foo/bar');
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
// Using auto-detection (default behavior),
|
||||
// all database driver stores should be configured,
|
||||
// and stores with non-database drivers are ignored.
|
||||
expect(config('cache.stores.database.connection'))->toBe('tenant');
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('tenant');
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default'); // unchanged
|
||||
expect(config('cache.stores.file.path'))->toBe('/foo/bar'); // unchanged
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// All database stores should be reverted, others unchanged
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||
expect(config('cache.stores.file.path'))->toBe('/foo/bar');
|
||||
});
|
||||
|
||||
test('manual $stores configuration takes precedence over auto-detection', function() {
|
||||
// Configure multiple database stores
|
||||
config([
|
||||
'cache.stores.sessions' => [
|
||||
'driver' => 'database',
|
||||
'connection' => 'central',
|
||||
'table' => 'sessions_cache',
|
||||
],
|
||||
'cache.stores.redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
],
|
||||
]);
|
||||
|
||||
// Specific store overrides (including non-database stores)
|
||||
DatabaseCacheBootstrapper::$stores = ['sessions', 'redis']; // Note: excludes 'database'
|
||||
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
// Manual config takes precedence: only 'sessions' is configured
|
||||
// - redis filtered out by driver check
|
||||
// - database store not included in $stores
|
||||
expect(config('cache.stores.database.connection'))->toBe('central'); // Excluded in manual config
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('tenant'); // Included and is database driver
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default'); // Included but filtered out (not database driver)
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Only the manually configured stores' config will be reverted
|
||||
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Contracts\Http\Kernel;
|
|||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
|
|
@ -120,7 +121,7 @@ test('early identification works with path identification', function (bool $useK
|
|||
RouteFacade::get('/{post}/comment/{comment}/edit', [$controller, 'computePost']);
|
||||
});
|
||||
|
||||
$tenant = Tenant::create(['tenancy_db_name' => pest()->randomString()]);
|
||||
$tenant = Tenant::create(['tenancy_db_name' => Str::random(10)]);
|
||||
|
||||
// Migrate users and comments tables on tenant connection
|
||||
pest()->artisan('tenants:migrate', [
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
|
|||
return json_encode(DB::select(request('q2')));
|
||||
});
|
||||
|
||||
tenancy(); // trigger features: todo@samuel remove after feature refactor
|
||||
tenancy()->bootstrapFeatures();
|
||||
|
||||
if ($disallow) {
|
||||
expect(fn () => pest()->post('/central-sqli', [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ test('tenant redirect macro replaces only the hostname', function () {
|
|||
'tenancy.features' => [CrossDomainRedirect::class],
|
||||
]);
|
||||
|
||||
tenancy()->bootstrapFeatures();
|
||||
|
||||
Route::get('/foobar', function () {
|
||||
return 'Foo';
|
||||
})->name('home');
|
||||
|
|
|
|||
|
|
@ -2,29 +2,27 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Features\TenantConfig;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\TenantConfigBootstrapper;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [TenantConfigBootstrapper::class],
|
||||
]);
|
||||
|
||||
withBootstrapping();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
TenantConfig::$storageToConfigMap = [];
|
||||
TenantConfigBootstrapper::$storageToConfigMap = [];
|
||||
});
|
||||
|
||||
test('nested tenant values are merged', function () {
|
||||
expect(config('whitelabel.theme'))->toBeNull();
|
||||
config([
|
||||
'tenancy.features' => [TenantConfig::class],
|
||||
'tenancy.bootstrappers' => [],
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
TenantConfig::$storageToConfigMap = [
|
||||
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||
'whitelabel.config.theme' => 'whitelabel.theme',
|
||||
];
|
||||
|
||||
|
|
@ -39,14 +37,8 @@ test('nested tenant values are merged', function () {
|
|||
|
||||
test('config is merged and removed', function () {
|
||||
expect(config('services.paypal'))->toBe(null);
|
||||
config([
|
||||
'tenancy.features' => [TenantConfig::class],
|
||||
'tenancy.bootstrappers' => [],
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
TenantConfig::$storageToConfigMap = [
|
||||
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||
'paypal_api_public' => 'services.paypal.public',
|
||||
'paypal_api_private' => 'services.paypal.private',
|
||||
];
|
||||
|
|
@ -68,14 +60,8 @@ test('config is merged and removed', function () {
|
|||
|
||||
test('the value can be set to multiple config keys', function () {
|
||||
expect(config('services.paypal'))->toBe(null);
|
||||
config([
|
||||
'tenancy.features' => [TenantConfig::class],
|
||||
'tenancy.bootstrappers' => [],
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
TenantConfig::$storageToConfigMap = [
|
||||
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||
'paypal_api_public' => [
|
||||
'services.paypal.public1',
|
||||
'services.paypal.public2',
|
||||
|
|
|
|||
|
|
@ -3,27 +3,42 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Vite;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Overrides\Vite as StanclVite;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Features\ViteBundler;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
test('vite helper uses our custom class', function() {
|
||||
$vite = app(Vite::class);
|
||||
|
||||
expect($vite)->toBeInstanceOf(Vite::class);
|
||||
expect($vite)->not()->toBeInstanceOf(StanclVite::class);
|
||||
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'tenancy.features' => [ViteBundler::class],
|
||||
'tenancy.filesystem.asset_helper_override' => true,
|
||||
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
app()->forgetInstance(Vite::class);
|
||||
|
||||
$vite = app(Vite::class);
|
||||
|
||||
expect($vite)->toBeInstanceOf(StanclVite::class);
|
||||
File::ensureDirectoryExists(dirname($manifestPath = public_path('build/manifest.json')));
|
||||
File::put($manifestPath, json_encode([
|
||||
'foo' => [
|
||||
'file' => 'assets/foo-AbC123.js',
|
||||
'src' => 'js/foo.js',
|
||||
],
|
||||
]));
|
||||
});
|
||||
|
||||
test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () {
|
||||
config(['tenancy.features' => [ViteBundler::class]]);
|
||||
tenancy()->bootstrapFeatures();
|
||||
|
||||
withBootstrapping();
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
// Not what we want
|
||||
expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
|
||||
|
||||
$viteAssetUrl = app(Vite::class)->asset('foo');
|
||||
$expectedGlobalUrl = global_asset('build/assets/foo-AbC123.js');
|
||||
|
||||
expect($viteAssetUrl)->toBe($expectedGlobalUrl);
|
||||
expect($viteAssetUrl)->toBe('http://localhost/build/assets/foo-AbC123.js');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
|
||||
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
|
|
@ -20,26 +25,40 @@ beforeEach(function () {
|
|||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
withCacheTables();
|
||||
});
|
||||
|
||||
test('global cache manager stores data in global cache', function (string $bootstrapper) {
|
||||
config(['tenancy.bootstrappers' => [$bootstrapper]]);
|
||||
test('global cache manager stores data in global cache', function (string $store, array $bootstrappers) {
|
||||
config([
|
||||
'cache.default' => $store,
|
||||
'tenancy.bootstrappers' => $bootstrappers,
|
||||
]);
|
||||
|
||||
if ($store === 'database') withTenantDatabases(true);
|
||||
|
||||
expect(cache('foo'))->toBe(null);
|
||||
GlobalCache::put(['foo' => 'bar'], 1);
|
||||
GlobalCache::put('foo', 'bar');
|
||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
tenancy()->initialize($tenant1);
|
||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||
|
||||
GlobalCache::put(['abc' => 'xyz'], 1);
|
||||
cache(['def' => 'ghi'], 10);
|
||||
GlobalCache::put('abc', 'xyz');
|
||||
cache(['def' => 'ghi']);
|
||||
expect(cache('def'))->toBe('ghi');
|
||||
|
||||
// different stores, same underlying connection. the prefix is set ON THE STORE
|
||||
expect(cache()->store()->getStore() === GlobalCache::store()->getStore())->toBeFalse();
|
||||
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
|
||||
// different stores
|
||||
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
|
||||
if ($store === 'redis') {
|
||||
// same underlying connection. the prefix is set ON THE STORE
|
||||
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
|
||||
} else {
|
||||
// different connections
|
||||
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
|
||||
expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
}
|
||||
|
||||
tenancy()->end();
|
||||
expect(GlobalCache::get('abc'))->toBe('xyz');
|
||||
|
|
@ -51,25 +70,129 @@ test('global cache manager stores data in global cache', function (string $boots
|
|||
expect(GlobalCache::get('abc'))->toBe('xyz');
|
||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||
expect(cache('def'))->toBe(null);
|
||||
cache(['def' => 'xxx'], 1);
|
||||
cache(['def' => 'xxx']);
|
||||
expect(cache('def'))->toBe('xxx');
|
||||
|
||||
tenancy()->initialize($tenant1);
|
||||
expect(cache('def'))->toBe('ghi');
|
||||
})->with([
|
||||
CacheTagsBootstrapper::class,
|
||||
CacheTenancyBootstrapper::class,
|
||||
['redis', [CacheTagsBootstrapper::class]],
|
||||
['redis', [CacheTenancyBootstrapper::class]],
|
||||
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||
]);
|
||||
|
||||
test('the global_cache helper supports the same syntax as the cache helper', function (string $bootstrapper) {
|
||||
config(['tenancy.bootstrappers' => [$bootstrapper]]);
|
||||
test('global cache facade is not persistent', function () {
|
||||
$oldId = spl_object_id(GlobalCache::getFacadeRoot());
|
||||
|
||||
$_ = new class {};
|
||||
|
||||
expect(spl_object_id(GlobalCache::getFacadeRoot()))->not()->toBe($oldId);
|
||||
});
|
||||
|
||||
test('global cache is always central', function (string $store, array $bootstrappers, string $initialCentralCall) {
|
||||
config([
|
||||
'cache.default' => $store,
|
||||
'tenancy.bootstrappers' => $bootstrappers,
|
||||
]);
|
||||
|
||||
if ($store === 'database') {
|
||||
withTenantDatabases(true);
|
||||
}
|
||||
|
||||
// This tells us which "accessor" for the global cache should be instantiated first, before we go
|
||||
// into the tenant context. We make sure to not touch the other one here. This tests that whether
|
||||
// a particular accessor is used "early" makes no difference in the later behavior.
|
||||
if ($initialCentralCall === 'helper') {
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
global_cache()->put('central-helper', true);
|
||||
} else if ($initialCentralCall === 'facade') {
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
GlobalCache::put('central-facade', true);
|
||||
} else if ($initialCentralCall === 'both') {
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
global_cache()->put('central-helper', true);
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
GlobalCache::put('central-facade', true);
|
||||
}
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant->enter();
|
||||
|
||||
// different stores, same underlying connection. the prefix is set ON THE STORE
|
||||
expect(cache()->store()->getStore() === global_cache()->store()->getStore())->toBeFalse();
|
||||
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
|
||||
// Here we use both the helper and the facade to ensure the value is accessible via either one
|
||||
if ($initialCentralCall === 'helper') {
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
expect(global_cache('central-helper'))->toBe(true);
|
||||
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||
} else if ($initialCentralCall === 'facade') {
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
expect(global_cache('central-facade'))->toBe(true);
|
||||
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||
} else if ($initialCentralCall === 'both') {
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
expect(global_cache('central-helper'))->toBe(true);
|
||||
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||
expect(global_cache('central-facade'))->toBe(true);
|
||||
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||
}
|
||||
|
||||
global_cache()->put('tenant-helper', true);
|
||||
GlobalCache::put('tenant-facade', true);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
|
||||
expect(global_cache('tenant-helper'))->toBe(true);
|
||||
expect(GlobalCache::get('tenant-helper'))->toBe(true);
|
||||
expect(global_cache('tenant-facade'))->toBe(true);
|
||||
expect(GlobalCache::get('tenant-facade'))->toBe(true);
|
||||
|
||||
if ($initialCentralCall === 'helper') {
|
||||
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||
} else if ($initialCentralCall === 'facade') {
|
||||
expect(global_cache('central-facade'))->toBe(true);
|
||||
} else if ($initialCentralCall === 'both') {
|
||||
expect(global_cache('central-helper'))->toBe(true);
|
||||
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||
expect(global_cache('central-facade'))->toBe(true);
|
||||
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||
}
|
||||
})->with([
|
||||
['redis', [CacheTagsBootstrapper::class]],
|
||||
['redis', [CacheTenancyBootstrapper::class]],
|
||||
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||
])->with([
|
||||
'helper',
|
||||
'facade',
|
||||
'both',
|
||||
'none',
|
||||
]);
|
||||
|
||||
test('the global_cache helper supports the same syntax as the cache helper', function (string $store, array $bootstrappers) {
|
||||
config([
|
||||
'cache.default' => $store,
|
||||
'tenancy.bootstrappers' => $bootstrappers,
|
||||
]);
|
||||
|
||||
if ($store === 'database') withTenantDatabases(true);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant->enter();
|
||||
|
||||
// different stores
|
||||
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
|
||||
if ($store === 'redis') {
|
||||
// same underlying connection. the prefix is set ON THE STORE
|
||||
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
|
||||
} else {
|
||||
// different connections
|
||||
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
|
||||
expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||
}
|
||||
|
||||
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
||||
|
||||
|
|
@ -81,6 +204,7 @@ test('the global_cache helper supports the same syntax as the cache helper', fun
|
|||
|
||||
expect(cache('foo'))->toBe(null); // tenant cache is not affected
|
||||
})->with([
|
||||
CacheTagsBootstrapper::class,
|
||||
CacheTenancyBootstrapper::class,
|
||||
['redis', [CacheTagsBootstrapper::class]],
|
||||
['redis', [CacheTenancyBootstrapper::class]],
|
||||
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use Illuminate\Support\Facades\Artisan;
|
|||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled;
|
||||
use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled;
|
||||
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
|
|
@ -38,18 +40,46 @@ test('tenants can be in maintenance mode', function () {
|
|||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
});
|
||||
|
||||
test('maintenance mode events are fired', function () {
|
||||
$tenant = MaintenanceTenant::create();
|
||||
test('maintenance mode middleware can be used with universal routes', function () {
|
||||
Route::get('/foo', function () {
|
||||
return 'bar';
|
||||
})->middleware(['universal', InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
|
||||
|
||||
Event::fake();
|
||||
$tenant = MaintenanceTenant::create();
|
||||
$tenant->domains()->create([
|
||||
'domain' => 'acme.localhost',
|
||||
]);
|
||||
|
||||
// Revert to central context after each request so that the tenant context
|
||||
// from the request doesn't persist
|
||||
$run = function (Closure $callback) { $callback(); tenancy()->end(); };
|
||||
|
||||
$run(fn () => pest()->get('http://acme.localhost/foo')->assertStatus(200));
|
||||
$run(fn () => pest()->get('http://localhost/foo')->assertStatus(200));
|
||||
|
||||
$tenant->putDownForMaintenance();
|
||||
|
||||
Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeEnabled::class);
|
||||
$run(fn () => pest()->get('http://acme.localhost/foo')->assertStatus(503));
|
||||
$run(fn () => pest()->get('http://localhost/foo')->assertStatus(200)); // Not affected by a tenant's maintenance mode
|
||||
|
||||
$tenant->bringUpFromMaintenance();
|
||||
|
||||
Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeDisabled::class);
|
||||
$run(fn () => pest()->get('http://acme.localhost/foo')->assertStatus(200));
|
||||
$run(fn () => pest()->get('http://localhost/foo')->assertStatus(200));
|
||||
});
|
||||
|
||||
test('maintenance mode events are fired', function () {
|
||||
$tenant = MaintenanceTenant::create();
|
||||
|
||||
Event::fake([TenantMaintenanceModeEnabled::class, TenantMaintenanceModeDisabled::class]);
|
||||
|
||||
$tenant->putDownForMaintenance();
|
||||
|
||||
Event::assertDispatched(TenantMaintenanceModeEnabled::class);
|
||||
|
||||
$tenant->bringUpFromMaintenance();
|
||||
|
||||
Event::assertDispatched(TenantMaintenanceModeDisabled::class);
|
||||
});
|
||||
|
||||
test('tenants can be put into maintenance mode using artisan commands', function() {
|
||||
|
|
|
|||
|
|
@ -2,21 +2,52 @@
|
|||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
|
||||
uses(TestCase::class)->in(__DIR__);
|
||||
|
||||
function withTenantDatabases()
|
||||
function withBootstrapping()
|
||||
{
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
}
|
||||
|
||||
function withTenantDatabases(bool $migrate = false)
|
||||
{
|
||||
Event::listen(TenantCreated::class, JobPipeline::make($migrate
|
||||
? [CreateDatabase::class, MigrateDatabase::class]
|
||||
: [CreateDatabase::class]
|
||||
)->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
}
|
||||
|
||||
function withCacheTables()
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
function pest(): TestCase
|
||||
{
|
||||
return \Pest\TestSuite::getInstance()->test;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
|
|
@ -44,7 +45,7 @@ test('asset can be accessed using the url returned by the tenant asset helper',
|
|||
$tenant = Tenant::create();
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$filename = 'testfile' . pest()->randomString(10);
|
||||
$filename = 'testfile' . Str::random(8);
|
||||
Storage::disk('public')->put($filename, 'bar');
|
||||
$path = storage_path("app/public/$filename");
|
||||
|
||||
|
|
@ -136,7 +137,7 @@ test('TenantAssetController headers are configurable', function () {
|
|||
tenancy()->initialize($tenant);
|
||||
$tenant->createDomain('foo.localhost');
|
||||
|
||||
$filename = 'testfile' . pest()->randomString(10);
|
||||
$filename = 'testfile' . Str::random(10);
|
||||
Storage::disk('public')->put($filename, 'bar');
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQ
|
|||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||
|
||||
beforeEach(function () {
|
||||
SQLiteDatabaseManager::$path = null;
|
||||
|
|
@ -43,7 +45,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager
|
|||
"tenancy.database.managers.$driver" => $databaseManager,
|
||||
]);
|
||||
|
||||
$name = 'db' . pest()->randomString();
|
||||
$name = 'db' . Str::random(10);
|
||||
|
||||
$manager = app($databaseManager);
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ test('dbs can be created when another driver is used for the central db', functi
|
|||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$database = 'db' . pest()->randomString();
|
||||
$database = 'db' . Str::random(10);
|
||||
|
||||
$mysqlmanager = app(MySQLDatabaseManager::class);
|
||||
$mysqlmanager->setConnection('mysql');
|
||||
|
|
@ -86,7 +88,7 @@ test('dbs can be created when another driver is used for the central db', functi
|
|||
$postgresManager = app(PostgreSQLDatabaseManager::class);
|
||||
$postgresManager->setConnection('pgsql');
|
||||
|
||||
$database = 'db' . pest()->randomString();
|
||||
$database = 'db' . Str::random(10);
|
||||
expect($postgresManager->databaseExists($database))->toBeFalse();
|
||||
|
||||
Tenant::create([
|
||||
|
|
@ -146,18 +148,15 @@ test('db name is prefixed with db path when sqlite is used', function () {
|
|||
expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
|
||||
});
|
||||
|
||||
test('sqlite databases use the WAL journal mode by default', function (bool|null $wal) {
|
||||
$expected = $wal ? 'wal' : 'delete';
|
||||
if ($wal !== null) {
|
||||
SQLiteDatabaseManager::$WAL = $wal;
|
||||
} else {
|
||||
// default behavior
|
||||
$expected = 'wal';
|
||||
}
|
||||
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
test('sqlite databases respect the template journal_mode config', function (string $journal_mode) {
|
||||
withTenantDatabases();
|
||||
withBootstrapping();
|
||||
config([
|
||||
'database.connections.sqlite.journal_mode' => $journal_mode,
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
],
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'tenancy_db_connection' => 'sqlite',
|
||||
|
|
@ -170,11 +169,18 @@ test('sqlite databases use the WAL journal mode by default', function (bool|null
|
|||
$db = new PDO('sqlite:' . $dbPath);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($expected);
|
||||
// Before we connect to the DB using Laravel, it will be in default delete mode
|
||||
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe('delete');
|
||||
|
||||
// cleanup
|
||||
SQLiteDatabaseManager::$WAL = true;
|
||||
})->with([true, false, null]);
|
||||
// This will trigger the logic in Laravel's SQLiteConnector
|
||||
$tenant->run(fn () => DB::select('select 1'));
|
||||
|
||||
$db = new PDO('sqlite:' . $dbPath);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Once we connect to the DB, it will be in the configured journal mode
|
||||
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($journal_mode);
|
||||
})->with(['delete', 'wal']);
|
||||
|
||||
test('schema manager uses schema to separate tenant dbs', function () {
|
||||
config([
|
||||
|
|
@ -239,9 +245,6 @@ test('tenant database can be created and deleted on a foreign server', function
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -287,9 +290,6 @@ test('tenant database can be created on a foreign server by using the host from
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -327,9 +327,6 @@ test('database credentials can be provided to PermissionControlledMySQLDatabaseM
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ beforeEach(function () {
|
|||
],
|
||||
]);
|
||||
|
||||
tenancy()->bootstrapFeatures();
|
||||
|
||||
Event::listen(
|
||||
TenantCreated::class,
|
||||
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
|||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\TenantConfigBootstrapper;
|
||||
|
||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||
{
|
||||
|
|
@ -38,6 +40,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
|
||||
ini_set('memory_limit', '1G');
|
||||
|
||||
TenancyServiceProvider::$registerForgetTenantParameterListener = true;
|
||||
TenancyServiceProvider::$migrateFreshOverride = true;
|
||||
TenancyServiceProvider::$adjustCacheManagerUsing = null;
|
||||
|
||||
Redis::connection('default')->flushdb();
|
||||
Redis::connection('cache')->flushdb();
|
||||
Artisan::call('cache:clear memcached'); // flush memcached
|
||||
|
|
@ -138,9 +144,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
'database.connections.sqlite.database' => ':memory:',
|
||||
'database.connections.mysql.charset' => 'utf8mb4',
|
||||
|
|
@ -180,6 +183,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
// to manually register bootstrappers as singletons here.
|
||||
$app->singleton(RedisTenancyBootstrapper::class);
|
||||
$app->singleton(CacheTenancyBootstrapper::class);
|
||||
$app->singleton(DatabaseCacheBootstrapper::class);
|
||||
$app->singleton(BroadcastingConfigBootstrapper::class);
|
||||
$app->singleton(BroadcastChannelPrefixBootstrapper::class);
|
||||
$app->singleton(PostgresRLSBootstrapper::class);
|
||||
|
|
@ -187,6 +191,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
$app->singleton(RootUrlBootstrapper::class);
|
||||
$app->singleton(UrlGeneratorBootstrapper::class);
|
||||
$app->singleton(FilesystemTenancyBootstrapper::class);
|
||||
$app->singleton(TenantConfigBootstrapper::class);
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
|
|
@ -230,11 +235,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class);
|
||||
}
|
||||
|
||||
public function randomString(int $length = 10)
|
||||
{
|
||||
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (ceil($length / strlen($x))))), 1, $length);
|
||||
}
|
||||
|
||||
public function assertArrayIsSubset($subset, $array, string $message = ''): void
|
||||
{
|
||||
parent::assertTrue(array_intersect($subset, $array) == $subset, $message);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue