1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 11:14:04 +00:00

Update path identification and Fortify integration-related logic (#13)

* Add commented UrlBinding + FortifyRouteTenancy bootstrappers to the config

* Improve FortifyRoute bootstrapper docblock

* Rename bootstrappers

* Complete renaming

* Pass defaults of the original URL generator to the new one

* Fix URL generator-related test (query string id test WIP)

* Fix code style (php-cs-fixer)

* Make Fortify bootstrapper not depend on the UrlGenerator bootstrapper, update comments

* Fix testing UrlGenerator bootstrapper

* Update TenancyUrlGenerator annotations

* Pass tenant parameter manually in Fortify bootstrapper

* Properly test TenancyUrlGenerator functionality

* Get rid of query string in Fortify bootstrapper

* Fix code style (php-cs-fixer)

* Delete outdated comment

* Improve comment

* Improve before/afterEach

* Encourage passing parameters using TenancyUrlGenerator instead of URL::defaults()

* Delete rest of defaulting logic

* Fix code style (php-cs-fixer)

* Delete test group

* Update ForgetTenantParameter docblock

* Update passTenantParameterToRoutes annotation

* Complete todo in test

* Improve test

* Update comment

* Improve comment

* Add keepQueryParameters bool to Fortify bootstrapper

* Test keepQueryParameters

* minor docblock update

* minor docblock changes

* Delete extra import

* Update src/Overrides/TenancyUrlGenerator.php

Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>

* Improve comment

* Rename test

* Update bypass parameter-related test comments

* Fix merge

* Rename $keepQueryParameters

* Add docblock

* Add comment

* Refactor Fortify bootstrapper

* Fix code style (php-cs-fixer)

* Fix comment

* Skip Fortify bootstrapper test

* minor code improvements

* Improve fortify bootstrapper test

* Add Fortify bootstrapper annotation, improve code

* Fix code style (php-cs-fixer)

* Add commenet

* Complete resource syncing todo (cleanup not needed)

* Delete incorrect namespace

* Complete route context trait name todo

* Fix code style (php-cs-fixer)

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
lukinovec 2023-11-26 21:08:41 +01:00 committed by GitHub
parent c043661318
commit 4953c69fd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 255 additions and 141 deletions

View file

@ -3,10 +3,10 @@
declare(strict_types=1);
use Illuminate\Support\Str;
use Stancl\Tenancy\Enums\Context;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\URL;
use Stancl\JobPipeline\JobPipeline;
use Illuminate\Broadcasting\Channel;
use Illuminate\Support\Facades\File;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Cache;
@ -20,9 +20,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Events\TenantDeleted;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Broadcast;
use Stancl\Tenancy\Events\DeletingTenant;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Broadcasting\BroadcastManager;
use Stancl\Tenancy\Events\TenancyInitialized;
@ -30,25 +28,26 @@ use Illuminate\Contracts\Routing\UrlGenerator;
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
use Stancl\Tenancy\Overrides\TenancyBroadcastManager;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlBindingBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper;
use Illuminate\Routing\Exceptions\UrlGenerationException;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
use Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteTenancyBootstrapper;
beforeEach(function () {
@ -59,7 +58,7 @@ beforeEach(function () {
// Reset static properties of classes used in this test file to their default values
BroadcastingConfigBootstrapper::$credentialsMap = [];
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
UrlTenancyBootstrapper::$rootUrlOverride = null;
RootUrlBootstrapper::$rootUrlOverride = null;
Event::listen(
TenantCreated::class,
@ -74,11 +73,12 @@ beforeEach(function () {
afterEach(function () {
// Reset static properties of classes used in this test file to their default values
UrlTenancyBootstrapper::$rootUrlOverride = null;
RootUrlBootstrapper::$rootUrlOverride = null;
PrefixCacheTenancyBootstrapper::$tenantCacheStores = [];
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
BroadcastingConfigBootstrapper::$credentialsMap = [];
TenancyUrlGenerator::$prefixRouteNames = false;
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
});
test('database data is separated', function () {
@ -501,7 +501,7 @@ test('MailTenancyBootstrapper reverts the config and mailer credentials to defau
});
test('url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() {
config(['tenancy.bootstrappers' => [UrlTenancyBootstrapper::class]]);
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
Route::group([
'middleware' => InitializeTenancyBySubdomain::class,
@ -521,7 +521,7 @@ test('url bootstrapper overrides the root url when tenancy gets initialized and
return $scheme . '://' . $tenant->getTenantKey() . '.' . $hostname;
};
UrlTenancyBootstrapper::$rootUrlOverride = $rootUrlOverride;
RootUrlBootstrapper::$rootUrlOverride = $rootUrlOverride;
$tenant = Tenant::create();
$tenantUrl = $rootUrlOverride($tenant);
@ -546,36 +546,43 @@ test('url bootstrapper overrides the root url when tenancy gets initialized and
});
test('url binding tenancy bootstrapper swaps the url generator instance correctly', function() {
config(['tenancy.bootstrappers' => [UrlBindingBootstrapper::class]]);
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
tenancy()->initialize(Tenant::create());
expect(app('url'))->toBeInstanceOf(TenancyUrlGenerator::class);
expect(url())->toBeInstanceOf(TenancyUrlGenerator::class);
tenancy()->end();
expect(app('url'))->toBeInstanceOf(UrlGenerator::class);
expect(url())->toBeInstanceOf(UrlGenerator::class);
expect(app('url'))->toBeInstanceOf(UrlGenerator::class)
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
expect(url())->toBeInstanceOf(UrlGenerator::class)
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
});
test('url binding tenancy bootstrapper changes route helper behavior correctly', function() {
test('url generator bootstrapper can prefix route names passed to the route helper', function() {
Route::get('/central/home', fn () => route('home'))->name('home');
// Tenant route name prefix is 'tenant.' by default
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
Route::get('/query-string', fn () => route('query-string'))->name('query-string')->middleware(['tenant', InitializeTenancyByRequestData::class]);
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
$centralRouteUrl = route('home');
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]);
$queryStringCentralUrl = route('query-string');
$queryStringTenantUrl = route('query-string', ['tenant' => $tenantKey]);
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
$bypassParameter = TenancyUrlGenerator::$bypassParameter;
config(['tenancy.bootstrappers' => [UrlBindingBootstrapper::class]]);
TenancyUrlGenerator::$prefixRouteNames = true;
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
tenancy()->initialize($tenant);
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false
expect(route('home'))->not()->toBe($centralRouteUrl);
// When TenancyUrlGenerator::$passTenantParameterToRoutes is true (default)
// The route helper receives the tenant parameter
// So in order to generate central URL, we have to pass the bypass parameter
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl);
TenancyUrlGenerator::$prefixRouteNames = true;
// The $prefixRouteNames property is true
// The route name passed to the route() helper ('home') gets prefixed prefixed with 'tenant.' automatically
expect(route('home'))->toBe($tenantRouteUrl);
@ -584,58 +591,149 @@ test('url binding tenancy bootstrapper changes route helper behavior correctly',
// Also, the route receives the tenant parameter automatically
expect(route('tenant.home'))->toBe($tenantRouteUrl);
// The $bypassParameter parameter ('central' by default) can bypass the route name prefixing
// When the bypass parameter is true, the generated route URL points to the route named 'home'
// Also, check if the bypass parameter gets removed from the generated URL query string
expect(route('home', [$bypassParameter => true]))->toBe($centralRouteUrl)
->not()->toContain($bypassParameter);
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
expect(route('home', [$bypassParameter => false]))->toBe($tenantRouteUrl)
->not()->toContain($bypassParameter);
TenancyUrlGenerator::$prefixRouteNames = false;
// Route names don't get prefixed TenancyUrlGenerator::$prefixRouteNames is false
expect(route('home', [$bypassParameter => true]))->toBe($centralRouteUrl);
expect(route('query-string'))->toBe($queryStringTenantUrl);
TenancyUrlGenerator::$passTenantParameterToRoutes = false;
expect(route('query-string'))->toBe($queryStringCentralUrl);
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
expect(route('query-string'))->toBe($queryStringTenantUrl);
// Ending tenancy reverts route() behavior changes
tenancy()->end();
expect(route('home'))->toBe($centralRouteUrl);
expect(route('query-string'))->toBe($queryStringCentralUrl);
expect(route('tenant.home', ['tenant' => $tenantKey]))->toBe($tenantRouteUrl);
});
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
$tenantParameterName = PathTenantResolver::tenantParameterName();
Route::get('/central/home', fn () => route('home'))->name('home');
// Tenant route name prefix is 'tenant.' by default
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
$tenant = Tenant::create();
$centralRouteUrl = route('home');
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenant->getTenantKey()]);
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
TenancyUrlGenerator::$prefixRouteNames = true;
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
tenancy()->initialize($tenant);
// The $bypassParameter parameter ('central' by default) can bypass the route name prefixing
// When the bypass parameter is true, the generated route URL points to the route named 'home'
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl)
// Bypass parameter prevents passing the tenant parameter directly
->not()->toContain($tenantParameterName . '=')
// Bypass parameter gets removed from the generated URL automatically
->not()->toContain('bypassParameter');
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl)
->not()->toContain('bypassParameter');
});
test('url generator bootstrapper can make route helper generate links with the tenant parameter', function() {
Route::get('/query_string', fn () => route('query_string'))->name('query_string')->middleware(['universal', InitializeTenancyByRequestData::class]);
Route::get('/path', fn () => route('path'))->name('path');
Route::get('/{tenant}/path', fn () => route('tenant.path'))->name('tenant.path')->middleware([InitializeTenancyByPath::class]);
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
$queryStringCentralUrl = route('query_string');
$queryStringTenantUrl = route('query_string', ['tenant' => $tenantKey]);
$pathCentralUrl = route('path');
$pathTenantUrl = route('tenant.path', ['tenant' => $tenantKey]);
// Makes the route helper receive the tenant parameter whenever available
// Unless the bypass parameter is true
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
expect(route('path'))->toBe($pathCentralUrl);
// Tenant parameter required, but not passed since tenancy wasn't initialized
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
tenancy()->initialize($tenant);
// Tenant parameter is passed automatically
expect(route('path'))->not()->toBe($pathCentralUrl); // Parameter added as query string bypassParameter needed
expect(route('path', ['bypassParameter' => true]))->toBe($pathCentralUrl);
expect(route('tenant.path'))->toBe($pathTenantUrl);
expect(route('query_string'))->toBe($queryStringTenantUrl)->toContain('tenant=');
expect(route('query_string', ['bypassParameter' => 'true']))->toBe($queryStringCentralUrl)->not()->toContain('tenant=');
tenancy()->end();
expect(route('query_string'))->toBe($queryStringCentralUrl);
// Tenant parameter required, but shouldn't be passed since tenancy isn't initialized
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
// Route-level identification
pest()->get("http://localhost/central/home")->assertSee($centralRouteUrl);
pest()->get("http://localhost/$tenantKey/home")->assertSee($tenantRouteUrl);
pest()->get("http://localhost/query-string?tenant=$tenantKey")->assertSee($queryStringTenantUrl);
pest()->get("http://localhost/query_string")->assertSee($queryStringCentralUrl);
pest()->get("http://localhost/query_string?tenant=$tenantKey")->assertSee($queryStringTenantUrl);
pest()->get("http://localhost/path")->assertSee($pathCentralUrl);
pest()->get("http://localhost/$tenantKey/path")->assertSee($pathTenantUrl);
});
test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
config(['tenancy.bootstrappers' => [FortifyRouteTenancyBootstrapper::class]]);
Route::get('/', function () {
return true;
})->name($tenantHomeRouteName = 'tenant.home');
FortifyRouteTenancyBootstrapper::$fortifyHome = $tenantHomeRouteName;
FortifyRouteTenancyBootstrapper::$fortifyRedirectTenantMap = ['logout' => FortifyRouteTenancyBootstrapper::$fortifyHome];
$originalFortifyHome = config('fortify.home');
$originalFortifyRedirects = config('fortify.redirects');
tenancy()->initialize(Tenant::create());
expect(config('fortify.home'))->toBe($homeUrl = route($tenantHomeRouteName));
expect(config('fortify.redirects'))->toBe(['logout' => $homeUrl]);
Route::get('/home', function () {
return true;
})->name($homeRouteName = 'home');
Route::get('/{tenant}/home', function () {
return true;
})->name($pathIdHomeRouteName = 'tenant.home');
Route::get('/welcome', function () {
return true;
})->name($welcomeRouteName = 'welcome');
Route::get('/{tenant}/welcome', function () {
return true;
})->name($pathIdWelcomeRouteName = 'path.welcome');
FortifyRouteTenancyBootstrapper::$fortifyHome = $homeRouteName;
// Make login redirect to the central welcome route
FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login'] = [
'route_name' => $welcomeRouteName,
'context' => Context::CENTRAL,
];
tenancy()->initialize($tenant = Tenant::create());
// The bootstraper makes fortify.home always receive the tenant parameter
expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey());
// The login redirect route has the central context specified, so it doesn't receive the tenant parameter
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']);
tenancy()->end();
expect(config('fortify.home'))->toBe($originalFortifyHome);
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
// Making a route's context will pass the tenant parameter to the route
FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT;
tenancy()->initialize($tenant);
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
// Make the home and login route accept the tenant as a route parameter
// To confirm that tenant route parameter gets filled automatically too (path identification works as well as query string)
FortifyRouteTenancyBootstrapper::$fortifyHome = $pathIdHomeRouteName;
FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login']['route_name'] = $pathIdWelcomeRouteName;
tenancy()->end();
tenancy()->initialize($tenant);
expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home");
expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]);
});
test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) {