mirror of
https://github.com/archtechx/tenancy.git
synced 2026-05-07 01:04:03 +00:00
The `CreateTenantStorage` and `DeleteTenantStorage` listeners were used alongside JobPipelines. When the `TenantCreated` JobPipeline had `shouldBeQueued(true)` and the `Listeners\CreateTenantStorage` was uncommented, the listener would throw an exception (`Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException Database tenantX.sqlite does not exist.`) because at the time of executing the listener, the tenant DB wasn't created yet. The same issue could likely also occur in the `DeleteTenantStorage` listener as it uses `tenancy()->run()` to resolve the tenant's storage path which wouldn't work if the tenant's database (or other resources) was already deleted, making initialization impossible. This PR changes `DeleteTenantStorage` into a job and puts it (commented) into the job pipeline, so that it can be queued with the rest of the jobs. It also removes `CreateTenantStorage` because it should be redundant with the FilesystemTenancyBootstrapper creating the same paths automatically when storage path is suffixed. The old classes are kept but deprecated for backwards compatibility. We've also added some edge case hardening to `DeleteTenantStorage` to make sure it never deletes the central storage path directory, which previously could in theory occur due to a misconfiguration if a user enabled this job/listener but disabled storage path suffixing. Co-authored-by: Samuel Štancl <samuel@archte.ch> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
300 lines
11 KiB
PHP
300 lines
11 KiB
PHP
<?php
|
|
|
|
use Stancl\JobPipeline\JobPipeline;
|
|
use Illuminate\Support\Facades\File;
|
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Stancl\Tenancy\Events\TenancyEnded;
|
|
use Stancl\Tenancy\Events\TenantCreated;
|
|
use Stancl\Tenancy\Events\TenantDeleted;
|
|
use Stancl\Tenancy\Events\DeletingTenant;
|
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
|
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
|
|
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|
use Stancl\Tenancy\Jobs\DeleteTenantStorage;
|
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
|
use function Stancl\Tenancy\Tests\pest;
|
|
|
|
beforeEach(function () {
|
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
});
|
|
|
|
test('local storage public urls are generated correctly', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
|
]);
|
|
|
|
$tenant1 = Tenant::create();
|
|
$tenant2 = Tenant::create();
|
|
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
|
|
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
|
|
|
|
tenancy()->initialize($tenant1);
|
|
|
|
$this->assertEquals(
|
|
$tenant1StorageUrl,
|
|
Storage::disk('public')->url('')
|
|
);
|
|
|
|
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
|
|
|
|
$this->assertEquals(
|
|
$tenant1StorageUrl . $tenant1FileName,
|
|
Storage::disk('public')->url($tenant1FileName)
|
|
);
|
|
|
|
tenancy()->initialize($tenant2);
|
|
|
|
$this->assertEquals(
|
|
$tenant2StorageUrl,
|
|
Storage::disk('public')->url('')
|
|
);
|
|
|
|
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
|
|
|
|
$this->assertEquals(
|
|
$tenant2StorageUrl . $tenant2FileName,
|
|
Storage::disk('public')->url($tenant2FileName)
|
|
);
|
|
});
|
|
|
|
test('files can get fetched using the storage url', function() {
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
|
]);
|
|
|
|
$tenant1 = Tenant::create();
|
|
$tenant2 = Tenant::create();
|
|
|
|
pest()->artisan('tenants:link');
|
|
|
|
// First tenant
|
|
tenancy()->initialize($tenant1);
|
|
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
|
|
|
|
$url = Storage::disk('public')->url($tenantFileName);
|
|
$tenantDiskName = str(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
|
$hostname = str($url)->before($tenantDiskName);
|
|
$parsedUrl = str($url)->after($hostname);
|
|
|
|
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
|
|
|
// Second tenant
|
|
tenancy()->initialize($tenant2);
|
|
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
|
|
|
|
$url = Storage::disk('public')->url($tenantFileName);
|
|
$tenantDiskName = str(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
|
$hostname = str($url)->before($tenantDiskName);
|
|
$parsedUrl = str($url)->after($hostname);
|
|
|
|
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
|
|
|
// Central
|
|
tenancy()->end();
|
|
Storage::disk('public')->put($centralFileName = 'central.txt', $centralFileContent = 'central');
|
|
|
|
pest()->artisan('storage:link');
|
|
$url = Storage::disk('public')->url($centralFileName);
|
|
|
|
expect(file_get_contents(public_path($url)))->toBe($centralFileContent);
|
|
});
|
|
|
|
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
|
$originalStoragePath = storage_path();
|
|
|
|
config([
|
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
|
'tenancy.filesystem.suffix_storage_path' => false,
|
|
]);
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
$this->assertEquals($originalStoragePath, storage_path());
|
|
});
|
|
|
|
test('links to storage disks with a configured root are suffixed if not overridden', function() {
|
|
config([
|
|
'filesystems.disks.public.root' => 'http://sample-s3-url.com/my-app',
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'tenancy.filesystem.root_override.public' => null,
|
|
'tenancy.filesystem.url_override.public' => null,
|
|
]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
$expectedStoragePath = storage_path() . '/tenant' . $tenant->getTenantKey(); // /tenant = suffix base
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
// Check suffixing logic
|
|
expect(storage_path())->toEqual($expectedStoragePath);
|
|
});
|
|
|
|
test('create and delete storage symlinks jobs work', function() {
|
|
Event::listen(
|
|
TenantCreated::class,
|
|
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
|
|
return $event->tenant;
|
|
})->toListener()
|
|
);
|
|
|
|
Event::listen(
|
|
TenantDeleted::class,
|
|
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
|
|
return $event->tenant;
|
|
})->toListener()
|
|
);
|
|
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'tenancy.filesystem.suffix_base' => 'tenant-',
|
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
|
]);
|
|
|
|
/** @var Tenant $tenant */
|
|
$tenant = Tenant::create();
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
$tenantKey = $tenant->getTenantKey();
|
|
|
|
$this->assertDirectoryExists(storage_path("app/public"));
|
|
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
|
|
|
|
$tenant->delete();
|
|
|
|
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
|
});
|
|
|
|
test('tenant storage gets deleted during tenant deletion when the DeletingTenant pipeline contains DeleteTenantStorage', function() {
|
|
Event::listen(DeletingTenant::class,
|
|
JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) {
|
|
return $event->tenant;
|
|
})->shouldBeQueued(false)->toListener()
|
|
);
|
|
|
|
$centralStoragePath = storage_path();
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
// FilesystemTenancyBootstrapper not enabled,
|
|
// tenant and central storage path is the same,
|
|
// the storage deletion will be skipped.
|
|
$tenantStoragePath = storage_path();
|
|
expect($tenantStoragePath)->toBe($centralStoragePath);
|
|
expect(File::isDirectory($centralStoragePath))->toBeTrue();
|
|
tenant()->delete();
|
|
|
|
expect(File::isDirectory($centralStoragePath))->toBeTrue();
|
|
|
|
config([
|
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
|
'tenancy.filesystem.suffix_storage_path' => false,
|
|
]);
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
$tenantStoragePath = storage_path();
|
|
|
|
// FilesystemTenancyBootstrapper enabled,
|
|
// but tenant and central storage path is still the same
|
|
// because suffix_storage_path is false.
|
|
// The storage deletion will be skipped.
|
|
expect($tenantStoragePath)->toBe($centralStoragePath);
|
|
expect(File::isDirectory($centralStoragePath))->toBeTrue();
|
|
tenant()->delete();
|
|
|
|
expect(File::isDirectory($centralStoragePath))->toBeTrue();
|
|
|
|
config([
|
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
|
'tenancy.filesystem.suffix_storage_path' => true,
|
|
]);
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
$tenantStoragePath = storage_path();
|
|
|
|
// FilesystemTenancyBootstrapper enabled,
|
|
// suffix_storage_path enabled, so the two paths are distinct.
|
|
// Tenant storage will be deleted.
|
|
expect($tenantStoragePath)->not()->toBe($centralStoragePath);
|
|
expect(File::isDirectory($tenantStoragePath))->toBeTrue();
|
|
|
|
tenant()->delete();
|
|
|
|
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
|
expect(File::isDirectory($centralStoragePath))->toBeTrue();
|
|
});
|
|
|
|
test('the framework/cache directory is created when storage_path is scoped', function (bool $suffixStoragePath) {
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'tenancy.filesystem.suffix_storage_path' => $suffixStoragePath
|
|
]);
|
|
|
|
$centralStoragePath = storage_path();
|
|
|
|
tenancy()->initialize($tenant = Tenant::create());
|
|
|
|
if ($suffixStoragePath) {
|
|
expect(storage_path('framework/cache'))->toBe($centralStoragePath . "/tenant{$tenant->id}/framework/cache");
|
|
expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeTrue();
|
|
} else {
|
|
expect(storage_path('framework/cache'))->toBe($centralStoragePath . '/framework/cache');
|
|
expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeFalse();
|
|
}
|
|
})->with([true, false]);
|
|
|
|
test('scoped disks are scoped per tenant', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
],
|
|
'filesystems.disks.scoped_disk' => [
|
|
'driver' => 'scoped',
|
|
'disk' => 'public',
|
|
'prefix' => 'scoped_disk_prefix',
|
|
],
|
|
]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
Storage::disk('scoped_disk')->put('foo.txt', 'central');
|
|
expect(Storage::disk('scoped_disk')->get('foo.txt'))->toBe('central');
|
|
expect(file_get_contents(storage_path() . "/app/public/scoped_disk_prefix/foo.txt"))->toBe('central');
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(Storage::disk('scoped_disk')->get('foo.txt'))->toBe(null);
|
|
Storage::disk('scoped_disk')->put('foo.txt', 'tenant');
|
|
expect(file_get_contents(storage_path() . "/app/public/scoped_disk_prefix/foo.txt"))->toBe('tenant');
|
|
expect(Storage::disk('scoped_disk')->get('foo.txt'))->toBe('tenant');
|
|
|
|
tenancy()->end();
|
|
|
|
expect(Storage::disk('scoped_disk')->get('foo.txt'))->toBe('central');
|
|
Storage::disk('scoped_disk')->put('foo.txt', 'central2');
|
|
expect(Storage::disk('scoped_disk')->get('foo.txt'))->toBe('central2');
|
|
|
|
expect(file_get_contents(storage_path() . "/app/public/scoped_disk_prefix/foo.txt"))->toBe('central2');
|
|
expect(file_get_contents(storage_path() . "/tenant{$tenant->id}/app/public/scoped_disk_prefix/foo.txt"))->toBe('tenant');
|
|
});
|