mirror of
https://github.com/archtechx/tenancy.git
synced 2026-09-20 14:34:03 +00:00
Test that tenant assets can be served from scoped disks, and that tenant asset roots respect the disk's configured prefix.
Currently, the tests fail because TenantAssetController grabs the root from the config instead of resolving it via Storage::disk(...)->path('').
439 lines
16 KiB
PHP
439 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
use Illuminate\Contracts\Http\Kernel;
|
|
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;
|
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
|
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
|
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
|
use Stancl\Tenancy\Controllers\TenantAssetController;
|
|
use Stancl\Tenancy\Events\TenancyEnded;
|
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
|
use function Stancl\Tenancy\Tests\pest;
|
|
|
|
beforeEach(function () {
|
|
config(['tenancy.bootstrappers' => [
|
|
FilesystemTenancyBootstrapper::class,
|
|
]]);
|
|
|
|
TenancyUrlGenerator::$prefixRouteNames = false;
|
|
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
|
TenantAssetController::$headers = [];
|
|
TenantAssetController::$publicDisk = null;
|
|
InitializeTenancyByRequestData::$onFail = null;
|
|
|
|
/** @var CloneRoutesAsTenant $cloneAction */
|
|
$cloneAction = app(CloneRoutesAsTenant::class);
|
|
$cloneAction->handle(Route::getRoutes()->getByName('stancl.tenancy.asset'));
|
|
|
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
});
|
|
|
|
afterEach(function () {
|
|
TenantAssetController::$headers = [];
|
|
TenantAssetController::$publicDisk = null;
|
|
InitializeTenancyByRequestData::$onFail = null;
|
|
});
|
|
|
|
test('asset can be accessed using the url returned by the tenant asset helper', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('public')->put($filename, 'bar');
|
|
$path = storage_path("app/public/$filename");
|
|
|
|
// response()->file() returns BinaryFileResponse whose content is
|
|
// inaccessible via getContent, so ->assertSee() can't be used
|
|
expect($path)->toBeFile();
|
|
$response = pest()->get(tenant_asset($filename), [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
|
|
$response->assertSuccessful();
|
|
|
|
$f = fopen($path, 'r');
|
|
$content = fread($f, filesize($path));
|
|
fclose($f);
|
|
|
|
expect($content)->toBe('bar');
|
|
});
|
|
|
|
test('tenant assets are served even when the suffix_storage_path config is set to false', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
'tenancy.filesystem.suffix_storage_path' => false,
|
|
]);
|
|
|
|
// With suffix_storage_path disabled, storage_path() stays central in tenant context
|
|
$centralStoragePath = storage_path();
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('public')->put($filename, 'bar');
|
|
|
|
$response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]);
|
|
|
|
// The asset is served from the tenant's storage directory, not from the central storage path
|
|
$response->assertSuccessful();
|
|
expect($response->getFile()->getPathname())
|
|
->toBe("$centralStoragePath/tenant{$tenant->id}/app/public/$filename");
|
|
});
|
|
|
|
test('the disk used for serving tenant assets is configurable', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
// This is tenancy's default override for the local disk's root, set it here for clarity
|
|
'tenancy.filesystem.root_override.local' => '%storage_path%/app/',
|
|
]);
|
|
|
|
// The local disk's root is overridden to '%storage_path%/app/' (so it does not use 'app/public')
|
|
TenantAssetController::$publicDisk = 'local';
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('local')->put($filename, 'bar');
|
|
$path = Storage::disk('local')->path($filename);
|
|
|
|
$response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]);
|
|
|
|
// The asset is served from the disk's root instead of 'app/public'
|
|
$response->assertSuccessful();
|
|
expect($response->getFile()->getPathname())->toBe($path);
|
|
});
|
|
|
|
test('tenant asset controller throws when the configured disk is not local', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
// Add a disk that uses the s3 driver (= non-local disk).
|
|
// Use dummy credentials so that the s3 disk can be resolved without throwing an AWS exception.
|
|
'filesystems.disks.remote' => [
|
|
'driver' => 's3',
|
|
'region' => 'us-east-1',
|
|
'key' => 'key',
|
|
'secret' => 'secret',
|
|
'bucket' => 'bucket',
|
|
],
|
|
]);
|
|
|
|
TenantAssetController::$publicDisk = 'remote';
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage('Disk [remote] is not a local disk.');
|
|
|
|
pest()->get(tenant_asset('foo.txt'), ['X-Tenant' => $tenant->id]);
|
|
});
|
|
|
|
test('tenant assets are served from the resolved root of a scoped disk', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
// A scoped disk has no configured root -- it inherits the root of its parent disk
|
|
// and appends its prefix to it, both of which happens when the disk is resolved.
|
|
'filesystems.disks.scoped_disk' => [
|
|
'driver' => 'scoped',
|
|
'disk' => 'public',
|
|
'prefix' => 'scoped_disk_prefix',
|
|
],
|
|
// Default tenancy config, set it here for clarity
|
|
'tenancy.filesystem.disks' => ['local', 'public'],
|
|
]);
|
|
|
|
TenantAssetController::$publicDisk = 'scoped_disk';
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('scoped_disk')->put($filename, 'bar');
|
|
$path = Storage::disk('scoped_disk')->path($filename);
|
|
|
|
// The parent disk is tenant-aware, so the scoped disk's root is inside the tenant's storage directory
|
|
expect($path)->toBe(storage_path("app/public/scoped_disk_prefix/$filename"));
|
|
|
|
$response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]);
|
|
|
|
$response->assertSuccessful();
|
|
expect($response->getFile()->getPathname())->toBe($path);
|
|
});
|
|
|
|
test('tenant assets are served from the resolved root of a disk with a configured prefix', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
// A prefix is part of the disk's root path, so it has to be included in the asset root
|
|
'filesystems.disks.prefixed' => [
|
|
'driver' => 'local',
|
|
'root' => storage_path('app/media'),
|
|
'prefix' => 'foo-prefix',
|
|
],
|
|
'tenancy.filesystem.disks' => ['prefixed'],
|
|
'tenancy.filesystem.root_override.prefixed' => '%storage_path%/app/media/',
|
|
]);
|
|
|
|
TenantAssetController::$publicDisk = 'prefixed';
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('prefixed')->put($filename, 'bar');
|
|
$path = Storage::disk('prefixed')->path($filename);
|
|
|
|
expect($path)->toBe(storage_path("app/media/foo-prefix/$filename"));
|
|
|
|
$response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]);
|
|
|
|
$response->assertSuccessful();
|
|
expect($response->getFile()->getPathname())->toBe($path);
|
|
});
|
|
|
|
test('tenant assets are served from the central storage path in central context', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
// Mimic the universal route setup (let the request through even though no tenant is identified)
|
|
InitializeTenancyByRequestData::$onFail = fn ($e, $request, $next) => $next($request);
|
|
|
|
$filename = 'testfile' . Str::random(8);
|
|
Storage::disk('public')->put($filename, 'bar');
|
|
|
|
$response = pest()->get(tenant_asset($filename));
|
|
|
|
$response->assertSuccessful();
|
|
expect($response->getFile()->getPathname())->toBe(storage_path("app/public/$filename"));
|
|
});
|
|
|
|
test('asset helper returns a link to tenant asset controller when asset url is null', function () {
|
|
config(['app.asset_url' => null]);
|
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
|
|
|
|
tenancy()->end();
|
|
expect(asset('foo'))->toBe('http://localhost/foo');
|
|
});
|
|
|
|
test('asset helper returns a link to an external url when asset url is not null', function () {
|
|
config(['app.asset_url' => 'https://an-s3-bucket']);
|
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(asset('foo'))->toBe("https://an-s3-bucket/tenant{$tenant->id}/foo");
|
|
|
|
tenancy()->end();
|
|
expect(asset('foo'))->toBe('https://an-s3-bucket/foo');
|
|
});
|
|
|
|
test('asset helper works correctly with path identification', function (bool $kernelIdentification) {
|
|
TenancyUrlGenerator::$prefixRouteNames = true;
|
|
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
|
|
|
config(['tenancy.filesystem.asset_helper_override' => true]);
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]);
|
|
config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]);
|
|
|
|
$tenantAssetRoute = Route::prefix('{tenant}')->get('/tenant_helper', function () {
|
|
return tenant_asset('foo');
|
|
})->name('tenant.helper.tenant');
|
|
|
|
$assetRoute = Route::prefix('{tenant}')->get('/asset_helper', function () {
|
|
return asset('foo');
|
|
})->name('tenant.helper.asset');
|
|
|
|
if ($kernelIdentification) {
|
|
app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class);
|
|
} else {
|
|
$assetRoute->middleware(InitializeTenancyByPath::class);
|
|
$tenantAssetRoute->middleware(InitializeTenancyByPath::class);
|
|
}
|
|
|
|
/** @var CloneRoutesAsTenant $cloneAction */
|
|
$cloneAction = app(CloneRoutesAsTenant::class);
|
|
|
|
$cloneAction->handle();
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
expect(pest()->get(route('tenant.helper.asset'))->getContent())->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
|
|
expect(pest()->get(route('tenant.helper.tenant'))->getContent())->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
|
|
})->with([
|
|
'kernel identification' => true,
|
|
'route-level identification' => false,
|
|
]);
|
|
|
|
test('TenantAssetController headers are configurable', function () {
|
|
TenantAssetController::$headers = function (Request $request) {
|
|
return ['X-Foo' => 'Bar'];
|
|
};
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
$tenant->createDomain('foo.localhost');
|
|
|
|
$filename = 'testfile' . Str::random(10);
|
|
Storage::disk('public')->put($filename, 'bar');
|
|
|
|
$this->withoutExceptionHandling();
|
|
|
|
$response = pest()->get("http://foo.localhost/tenancy/assets/$filename", [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertHeader('X-Foo', 'Bar');
|
|
});
|
|
|
|
test('global asset helper returns the same url regardless of tenancy initialization', function () {
|
|
$original = global_asset('foobar');
|
|
expect(global_asset('foobar'))->toBe(asset('foobar'));
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(global_asset('foobar'))->toBe($original);
|
|
});
|
|
|
|
test('asset helper tenancy can be disabled', function () {
|
|
$original = asset('foo');
|
|
|
|
config([
|
|
'app.asset_url' => null,
|
|
'tenancy.filesystem.asset_helper_override' => false,
|
|
]);
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(asset('foo'))->toBe($original);
|
|
});
|
|
|
|
test('test asset controller returns a 404 when no path is provided', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage('Empty path'); // outside tests this is a 404
|
|
|
|
pest()->get(tenant_asset(null), [
|
|
'X-Tenant' => $tenant->id,
|
|
])->assertNotFound();
|
|
});
|
|
|
|
test('tenant asset controller returns a 404 when the storage root doesnt exist', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
$storageRoot = storage_path("app/public");
|
|
|
|
if (is_dir($storageRoot)) {
|
|
rmdir(storage_path("app/public"));
|
|
}
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage("Storage root doesn't exist"); // outside tests this is a 404
|
|
|
|
pest()->get(tenant_asset('foo.txt'), [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
});
|
|
|
|
test('tenant asset controller returns a 404 when accessing a nonexistent file', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
$storageRoot = storage_path("app/public");
|
|
|
|
if (! is_dir($storageRoot)) {
|
|
mkdir(storage_path("app/public"), recursive: true);
|
|
}
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage("Accessing a nonexistent file"); // outside tests this is a 404
|
|
|
|
pest()->get(tenant_asset('foo.txt'), [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
});
|
|
|
|
test('tenant asset controller throws an exception when accessing a file in a directory whose name starts with the name of the asset root', function () {
|
|
config([
|
|
'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class,
|
|
// Disk used for serving the assets -- its root is 'app/media' in the tenant's storage directory
|
|
'filesystems.disks.media' => ['driver' => 'local', 'root' => storage_path('app/media')],
|
|
'tenancy.filesystem.disks' => array_merge(config('tenancy.filesystem.disks'), ['media']),
|
|
'tenancy.filesystem.root_override.media' => '%storage_path%/app/media/',
|
|
]);
|
|
|
|
TenantAssetController::$publicDisk = 'media';
|
|
|
|
$tenant = Tenant::create();
|
|
tenancy()->initialize($tenant);
|
|
|
|
Storage::disk('media')->put('photo.jpg', 'public file');
|
|
|
|
// A directory next to the asset root, e.g. one holding files that shouldn't be served
|
|
mkdir($privateDirectory = storage_path('app/media-originals'), recursive: true);
|
|
file_put_contents($privateDirectory . '/photo.jpg', 'private file');
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage('Accessing a file outside the storage root'); // outside tests this is a 404
|
|
|
|
pest()->get(tenant_asset('../media-originals/photo.jpg'), [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
});
|
|
|
|
test('test asset controller returns a 404 when accessing a file outside the storage root', function () {
|
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
$storageRoot = storage_path("app/public");
|
|
|
|
if (! is_dir($storageRoot)) {
|
|
mkdir(storage_path("app/public"), recursive: true);
|
|
file_put_contents(storage_path('app/foo.txt'), 'bar');
|
|
}
|
|
|
|
$this->withoutExceptionHandling();
|
|
pest()->expectExceptionMessage('Accessing a file outside the storage root'); // outside tests this is a 404
|
|
|
|
pest()->get(tenant_asset('../foo.txt'), [
|
|
'X-Tenant' => $tenant->id,
|
|
]);
|
|
});
|