From c312156c18fce53e7c708fa1cc8c772817c83d84 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 25 Jan 2024 15:27:17 +0100 Subject: [PATCH] Give universal flag highest priority (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Give universal flag the highest priority (wip) * Stop forgetting tenant parameter when route-level path ID is used * Fix PHPStan errors * Simplify annotation * Fix comment * Correct annotations * Improve requestHasTenant comment * Make cloning logic only clone universal routes, delete the universal flag from the new (tenant) route * Delete APP_DEBUG * make if condition easier to read * Update DealsWithRouteContexts.php * Fix test * Fix code style (php-cs-fixer) * Move tests * Delete incorrectly committed file * Cloning routes update wip * Route cloning rework WIP * Add todo to clone routes * Fix code style (php-cs-fixer) * Route cloning fix WIP * Set CloneRoutesAsTenant::$tenantMiddleware to ID MW * Revert CloneRoutesAsTenant::$tenantMiddleware-related changes * Simplify requestHasTenant * Add and test 'ckone' flag * Delete setting $skippedRoutes from CloneRoutesAsTenant * Fix code style (php-cs-fixer) * make config key used for testing distinct from normal tenancy config keys * Update src/Actions/CloneRoutesAsTenant.php * Move 'path identification types' dataset to CloneActionTest --------- Co-authored-by: Samuel Štancl Co-authored-by: Samuel Štancl Co-authored-by: PHP CS Fixer --- src/Actions/CloneRoutesAsTenant.php | 14 +- src/Concerns/DealsWithRouteContexts.php | 31 ++- src/Database/DatabaseConfig.php | 2 +- src/Listeners/ForgetTenantParameter.php | 5 +- src/Middleware/InitializeTenancyByPath.php | 13 +- src/TenancyServiceProvider.php | 1 + tests/CloneActionTest.php | 269 +++++++++++++++++++++ tests/EarlyIdentificationTest.php | 12 - tests/Etc/HasMiddlewareController.php | 2 +- tests/UniversalRouteTest.php | 262 ++------------------ 10 files changed, 315 insertions(+), 296 deletions(-) create mode 100644 tests/CloneActionTest.php diff --git a/src/Actions/CloneRoutesAsTenant.php b/src/Actions/CloneRoutesAsTenant.php index 97ad817c..22627d2d 100644 --- a/src/Actions/CloneRoutesAsTenant.php +++ b/src/Actions/CloneRoutesAsTenant.php @@ -118,10 +118,10 @@ class CloneRoutesAsTenant $pathIdentificationUsed = (! $routeHasNonPathIdentificationMiddleware) && ($routeHasPathIdentificationMiddleware || $pathIdentificationMiddlewareInGlobalStack); - $routeMode = tenancy()->getRouteMode($route); - $routeIsUniversalOrTenant = $routeMode === RouteMode::TENANT || $routeMode === RouteMode::UNIVERSAL; - - if ($pathIdentificationUsed && $routeIsUniversalOrTenant) { + if ( + $pathIdentificationUsed && + (tenancy()->getRouteMode($route) === RouteMode::UNIVERSAL || tenancy()->routeHasMiddleware($route, 'clone')) + ) { return true; } @@ -167,7 +167,11 @@ class CloneRoutesAsTenant // Add original route middleware to ensure there's no duplicate middleware unset($newRoute->action['middleware']); - $newRoute->middleware(tenancy()->getRouteMiddleware($route)); + // Exclude `universal` and `clone` middleware from the new route -- it should specifically be a tenant route + $newRoute->middleware(array_filter( + tenancy()->getRouteMiddleware($route), + fn (string $middleware) => ! in_array($middleware, ['universal', 'clone']) + )); if ($routeName && ! $route->named($tenantRouteNamePrefix . '*')) { // Clear the route name so that `name()` sets the route name instead of suffixing it diff --git a/src/Concerns/DealsWithRouteContexts.php b/src/Concerns/DealsWithRouteContexts.php index e015848c..75b56fbe 100644 --- a/src/Concerns/DealsWithRouteContexts.php +++ b/src/Concerns/DealsWithRouteContexts.php @@ -20,46 +20,43 @@ trait DealsWithRouteContexts * Get route's middleware context (tenant, central or universal). * The context is determined by the route's middleware. * + * If the route has the 'universal' middleware, the context is universal, + * and the route is accessible from both contexts. + * The universal flag has the highest priority. + * * If the route has the 'central' middleware, the context is central. - * If the route has the 'tenant' middleware, or any tenancy identification middleware (and the route isn't flagged as universal), the context is tenant. + * If the route has the 'tenant' middleware, or any tenancy identification middleware, the context is tenant. * * If the route doesn't have any of the mentioned middleware, * the context is determined by the `tenancy.default_route_mode` config. */ public static function getRouteMode(Route $route): RouteMode { + // If the route is universal, you have to determine its actual context using + // the identification middleware's determineUniversalRouteContextFromRequest + if (static::routeIsUniversal($route)) { + return RouteMode::UNIVERSAL; + } + if (static::routeHasMiddleware($route, 'central')) { return RouteMode::CENTRAL; } - $routeIsUniversal = static::routeIsUniversal($route); - - // If the route is flagged as tenant, consider it tenant - // If the route has an identification middleware and the route is not universal, consider it tenant - if ( - static::routeHasMiddleware($route, 'tenant') || - (static::routeHasIdentificationMiddleware($route) && ! $routeIsUniversal) - ) { + // If the route is flagged as tenant or it has identification middleware, consider it tenant + if (static::routeHasMiddleware($route, 'tenant') || static::routeHasIdentificationMiddleware($route)) { return RouteMode::TENANT; } - // If the route is universal, you have to determine its actual context using - // The identification middleware's determineUniversalRouteContextFromRequest - if ($routeIsUniversal) { - return RouteMode::UNIVERSAL; - } - return config('tenancy.default_route_mode'); } public static function routeIsUniversal(Route $route): bool { - $routeFlaggedAsTenantOrCentral = static::routeHasMiddleware($route, 'tenant') || static::routeHasMiddleware($route, 'central'); $routeFlaggedAsUniversal = static::routeHasMiddleware($route, 'universal'); $universalFlagUsedInGlobalStack = app(Kernel::class)->hasMiddleware('universal'); $defaultRouteModeIsUniversal = config('tenancy.default_route_mode') === RouteMode::UNIVERSAL; - return ! $routeFlaggedAsTenantOrCentral && ($routeFlaggedAsUniversal || $universalFlagUsedInGlobalStack || $defaultRouteModeIsUniversal); + return $routeFlaggedAsUniversal || $universalFlagUsedInGlobalStack || $defaultRouteModeIsUniversal; } /** diff --git a/src/Database/DatabaseConfig.php b/src/Database/DatabaseConfig.php index 41e14a81..b0bda3e5 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -231,7 +231,7 @@ class DatabaseConfig $databaseManagers = config('tenancy.database.managers'); if (! array_key_exists($driver, $databaseManagers)) { - throw new Exceptions\DatabaseManagerNotRegisteredException($driver); + throw new DatabaseManagerNotRegisteredException($driver); } return app($databaseManagers[$driver]); diff --git a/src/Listeners/ForgetTenantParameter.php b/src/Listeners/ForgetTenantParameter.php index 19738615..72e36180 100644 --- a/src/Listeners/ForgetTenantParameter.php +++ b/src/Listeners/ForgetTenantParameter.php @@ -27,9 +27,10 @@ class ForgetTenantParameter public function handle(RouteMatched $event): void { $kernelPathIdentificationUsed = PathIdentificationManager::pathIdentificationInGlobalStack() && ! tenancy()->routeHasIdentificationMiddleware($event->route); - $routeModeIsTenant = tenancy()->getRouteMode($event->route) === RouteMode::TENANT; + $routeMode = tenancy()->getRouteMode($event->route); + $routeModeIsTenantOrUniversal = $routeMode === RouteMode::TENANT || ($routeMode === RouteMode::UNIVERSAL && $event->route->hasParameter(PathIdentificationManager::getTenantParameterName())); - if ($kernelPathIdentificationUsed && $routeModeIsTenant) { + if ($kernelPathIdentificationUsed && $routeModeIsTenantOrUniversal) { $event->route->forgetParameter(PathIdentificationManager::getTenantParameterName()); } } diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 6f8ca405..2d44b3ed 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -8,8 +8,8 @@ use Closure; use Illuminate\Http\Request; use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification; use Stancl\Tenancy\Concerns\UsableWithUniversalRoutes; -use Stancl\Tenancy\Enums\RouteMode; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; +use Stancl\Tenancy\PathIdentificationManager; use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tenancy; @@ -52,17 +52,10 @@ class InitializeTenancyByPath extends IdentificationMiddleware implements Usable } /** - * Path identification request has a tenant if the middleware context is tenant. - * - * With path identification, we can just check the MW context because we're cloning the universal routes, - * and the routes are flagged with the 'tenant' MW group (= their MW context is tenant). - * - * With other identification middleware, we have to determine the context differently because we only have one - * truly universal route available ('truly universal' because with path identification, applying 'universal' to a route just means that - * it should get cloned, whereas with other ID MW, it means that the route you apply the 'universal' flag to will be accessible in both contexts). + * Request has tenant if the request's route has the tenant parameter. */ public function requestHasTenant(Request $request): bool { - return tenancy()->getRouteMode(tenancy()->getRoute($request)) === RouteMode::TENANT; + return tenancy()->getRoute($request)->hasParameter(PathIdentificationManager::getTenantParameterName()); } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index dc65f720..cfbeb170 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -138,6 +138,7 @@ class TenancyServiceProvider extends ServiceProvider return $instance; }); + Route::middlewareGroup('clone', []); Route::middlewareGroup('universal', []); Route::middlewareGroup('tenant', []); Route::middlewareGroup('central', []); diff --git a/tests/CloneActionTest.php b/tests/CloneActionTest.php new file mode 100644 index 00000000..71b5feb6 --- /dev/null +++ b/tests/CloneActionTest.php @@ -0,0 +1,269 @@ + RouteMode::UNIVERSAL]); + } else { + app(Kernel::class)->pushMiddleware($middleware); + } + } + + RouteFacade::get('/foo', function () { + return tenancy()->initialized + ? 'Tenancy is initialized.' + : 'Tenancy is not initialized.'; + })->middleware($routeMiddleware); + + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); + + RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); + + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + + $cloneRoutesAction->handle(); + + $tenantKey = Tenant::create()->getTenantKey(); + + pest()->get("http://localhost/foo") + ->assertSuccessful() + ->assertSee('Tenancy is not initialized.'); + + pest()->get("http://localhost/{$tenantKey}/foo") + ->assertSuccessful() + ->assertSee('Tenancy is initialized.'); + + tenancy()->end(); + + pest()->get("http://localhost/bar") + ->assertSuccessful() + ->assertSee('Tenancy is not initialized.'); + + pest()->get("http://localhost/{$tenantKey}/bar") + ->assertSuccessful() + ->assertSee('Tenancy is initialized.'); +})->with('path identification types'); + +test('CloneRoutesAsTenant registers prefixed duplicates of universal routes correctly', function (bool $kernelIdentification, bool $useController, string $tenantMiddleware) { + $routeMiddleware = ['universal']; + config(['tenancy.identification.path_identification_middleware' => [$tenantMiddleware]]); + + if ($kernelIdentification) { + app(Kernel::class)->pushMiddleware($tenantMiddleware); + } else { + $routeMiddleware[] = $tenantMiddleware; + } + + config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => $tenantParameterName = 'team']); + config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_route_name_prefix' => $tenantRouteNamePrefix = 'team-route.']); + + // Test that routes with controllers as well as routes with closure actions get cloned correctly + $universalRoute = RouteFacade::get('/home', $useController ? Controller::class : fn () => tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.')->middleware($routeMiddleware)->name('home'); + $centralRoute = RouteFacade::get('/central', fn () => true)->name('central'); + + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); + + $universalRoute2 = RouteFacade::get('/bar', [HasMiddlewareController::class, 'index'])->name('second-home'); + + expect($routes = RouteFacade::getRoutes()->get())->toContain($universalRoute) + ->toContain($universalRoute2) + ->toContain($centralRoute); + + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + + $cloneRoutesAction->handle(); + + expect($routesAfterRegisteringDuplicates = RouteFacade::getRoutes()->get()) + ->toContain($universalRoute) + ->toContain($centralRoute); + + $newRoutes = collect($routesAfterRegisteringDuplicates)->filter(fn ($route) => ! in_array($route, $routes)); + + expect($newRoutes->first()->uri())->toBe('{' . $tenantParameterName . '}' . '/' . $universalRoute->uri()); + expect($newRoutes->last()->uri())->toBe('{' . $tenantParameterName . '}' . '/' . $universalRoute2->uri()); + + // Universal flag is excluded from the route middleware + expect(tenancy()->getRouteMiddleware($newRoutes->first())) + ->toEqualCanonicalizing( + array_filter(array_merge(tenancy()->getRouteMiddleware($universalRoute), ['tenant']), + fn($middleware) => $middleware !== 'universal') + ); + + // Universal flag is provided statically in the route's controller, so we cannot exclude it + expect(tenancy()->getRouteMiddleware($newRoutes->last())) + ->toEqualCanonicalizing( + array_merge(tenancy()->getRouteMiddleware($universalRoute2), ['tenant']) + ); + + $tenant = Tenant::create(); + + pest()->get(route($centralRouteName = $universalRoute->getName()))->assertSee('Tenancy is not initialized.'); + pest()->get(route($centralRouteName2 = $universalRoute2->getName()))->assertSee('Tenancy is not initialized.'); + pest()->get(route($tenantRouteName = $newRoutes->first()->getName(), [$tenantParameterName => $tenant->getTenantKey()]))->assertSee('Tenancy is initialized.'); + tenancy()->end(); + pest()->get(route($tenantRouteName2 = $newRoutes->last()->getName(), [$tenantParameterName => $tenant->getTenantKey()]))->assertSee('Tenancy is initialized.'); + + expect($tenantRouteName)->toBe($tenantRouteNamePrefix . $universalRoute->getName()); + expect($tenantRouteName2)->toBe($tenantRouteNamePrefix . $universalRoute2->getName()); + expect($centralRouteName)->toBe($universalRoute->getName()); + expect($centralRouteName2)->toBe($universalRoute2->getName()); +})->with([ + 'kernel identification' => true, + 'route-level identification' => false, +// Creates a matrix (multiple with()) +])->with([ + 'use controller' => true, + 'use closure' => false +])->with([ + 'path identification middleware' => InitializeTenancyByPath::class, + 'custom path identification middleware' => CustomInitializeTenancyByPath::class, +]); + +test('CloneRoutesAsTenant only clones routes with path identification by default', function () { + app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class); + + $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); + + $initialRouteCount = $currentRouteCount(); + + // Path identification is used globally, and this route doesn't use a specific identification middleware, meaning path identification is used and the route should get cloned + RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name('home'); + // The route uses a specific identification middleware other than InitializeTenancyByPath – the route shouldn't get cloned + RouteFacade::get('/home-domain-id', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware(['universal', InitializeTenancyByDomain::class])->name('home-domain-id'); + + expect($currentRouteCount())->toBe($newRouteCount = $initialRouteCount + 2); + + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + + $cloneRoutesAction->handle(); + + // Only one of the two routes gets cloned + expect($currentRouteCount())->toBe($newRouteCount + 1); +}); + +test('custom callbacks can be used for cloning universal routes', function () { + RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware(['universal', InitializeTenancyByPath::class])->name($routeName = 'home'); + + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); + $initialRouteCount = $currentRouteCount(); + + $cloneRoutesAction; + + // Skip cloning the 'home' route + $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { + return; + })->handle(); + + // Expect route count to stay the same because the 'home' route cloning gets skipped + expect($initialRouteCount)->toEqual($currentRouteCount()); + + // Modify the 'home' route cloning so that a different route is cloned + $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { + RouteFacade::get('/cloned-route', fn () => true)->name('new.home'); + })->handle(); + + expect($currentRouteCount())->toEqual($initialRouteCount + 1); +}); + +test('cloning of specific routes can get skipped', function () { + RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name($routeName = 'home'); + + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); + $initialRouteCount = $currentRouteCount(); + + // Skip cloning the 'home' route + $cloneRoutesAction->skipRoute($routeName); + + $cloneRoutesAction->handle(); + + // Expect route count to stay the same because the 'home' route cloning gets skipped + expect($initialRouteCount)->toEqual($currentRouteCount()); +}); + +test('routes except nonuniversal routes with path id mw are given the tenant flag after cloning', function (array $routeMiddleware, array $globalMiddleware) { + foreach ($globalMiddleware as $middleware) { + if ($middleware === 'universal') { + config(['tenancy.default_route_mode' => RouteMode::UNIVERSAL]); + } else { + app(Kernel::class)->pushMiddleware($middleware); + } + } + + $route = RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.') + ->middleware($routeMiddleware) + ->name($routeName = 'home'); + + app(CloneRoutesAsTenant::class)->handle(); + + $clonedRoute = RouteFacade::getRoutes()->getByName('tenant.' . $routeName); + + // Non-universal routes with identification middleware are already considered tenant, so they don't get the tenant flag + if (! tenancy()->routeIsUniversal($route) && tenancy()->routeHasIdentificationMiddleware($clonedRoute)) { + expect($clonedRoute->middleware())->not()->toContain('tenant'); + } else { + expect($clonedRoute->middleware())->toContain('tenant'); + } +})->with('path identification types'); + +test('routes with the clone flag get cloned without making the routes universal', function ($identificationMiddleware) { + config(['tenancy.identification.path_identification_middleware' => [$identificationMiddleware]]); + + RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.') + ->middleware(['clone', $identificationMiddleware]) + ->name($routeName = 'home'); + + $tenant = Tenant::create(); + + app(CloneRoutesAsTenant::class)->handle(); + + $clonedRoute = RouteFacade::getRoutes()->getByName('tenant.' . $routeName); + + expect($clonedRoute->middleware())->toEqualCanonicalizing(['tenant', $identificationMiddleware]); + + // The original route is not accessible + pest()->get(route($routeName))->assertServerError(); + pest()->get(route($routeName, ['tenant' => $tenant]))->assertServerError(); + // The cloned route is a tenant route + pest()->get(route('tenant.' . $routeName, ['tenant' => $tenant]))->assertSee('Tenancy initialized.'); +})->with([InitializeTenancyByPath::class, CustomInitializeTenancyByPath::class]); + +class CustomInitializeTenancyByPath extends InitializeTenancyByPath +{ + +} + +dataset('path identification types', [ + 'kernel identification' => [ + 'route_middleware' => ['universal'], + 'global_middleware' => [InitializeTenancyByPath::class], + ], + 'route-level identification' => [ + 'route_middleware' => ['universal', InitializeTenancyByPath::class], + 'global_middleware' => [], + ], + 'kernel identification + defaulting to universal routes' => [ + 'route_middleware' => [], + 'global_middleware' => ['universal', InitializeTenancyByPath::class], + ], + 'route-level identification + defaulting to universal routes' => [ + 'route_middleware' => [InitializeTenancyByPath::class], + 'global_middleware' => ['universal'], + ], +]); diff --git a/tests/EarlyIdentificationTest.php b/tests/EarlyIdentificationTest.php index c0e6b577..c9bc80bc 100644 --- a/tests/EarlyIdentificationTest.php +++ b/tests/EarlyIdentificationTest.php @@ -430,18 +430,6 @@ test('route level identification is prioritized over kernel identification', fun 'default to central routes' => RouteMode::CENTRAL, ]); -test('routes with path identification middleware can get prefixed using the clone action', function() { - $tenantKey = Tenant::create()->getTenantKey(); - - RouteFacade::get('/home', fn () => tenant()?->getTenantKey())->name('home')->middleware(InitializeTenancyByPath::class); - - pest()->get("http://localhost/$tenantKey/home")->assertNotFound(); - - app(CloneRoutesAsTenant::class)->handle(); - - pest()->get("http://localhost/$tenantKey/home")->assertOk(); -}); - function assertTenancyInitializedInEarlyIdentificationRequest(bool $expect = true): void { expect(app()->make('additionalMiddlewareRunsInTenantContext'))->toBe($expect); // Assert that middleware added in the controller constructor runs in tenant context diff --git a/tests/Etc/HasMiddlewareController.php b/tests/Etc/HasMiddlewareController.php index 27fb82a8..6a61a7ca 100644 --- a/tests/Etc/HasMiddlewareController.php +++ b/tests/Etc/HasMiddlewareController.php @@ -9,7 +9,7 @@ class HasMiddlewareController implements HasMiddleware { public static function middleware() { - return array_map(fn (string $middleware) => new Middleware($middleware), config('tenancy.static_identification_middleware')); + return array_map(fn (string $middleware) => new Middleware($middleware), config('tenancy._tests.static_identification_middleware')); } public function index() diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index 81d3171a..ce10ccfe 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -22,7 +22,6 @@ use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; -use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\MiddlewareNotUsableWithUniversalRoutesException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; @@ -43,7 +42,7 @@ test('a route can be universal using domain identification', function (array $ro : 'Tenancy is not initialized.'; })->middleware($routeMiddleware); - config(['tenancy.static_identification_middleware' => $routeMiddleware]); + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); @@ -87,7 +86,7 @@ test('a route can be universal using subdomain identification', function (array : 'Tenancy is not initialized.'; })->middleware($routeMiddleware); - config(['tenancy.static_identification_middleware' => $routeMiddleware]); + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); @@ -132,7 +131,7 @@ test('a route can be universal using domainOrSubdomain identification', function : 'Tenancy is not initialized.'; })->middleware($routeMiddleware); - config(['tenancy.static_identification_middleware' => $routeMiddleware]); + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); @@ -194,7 +193,7 @@ test('a route can be universal using request data identification', function (arr : 'Tenancy is not initialized.'; })->middleware($routeMiddleware); - config(['tenancy.static_identification_middleware' => $routeMiddleware]); + config(['tenancy._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); @@ -219,51 +218,6 @@ test('a route can be universal using request data identification', function (arr ->assertSee('Tenancy is initialized.'); })->with('request data identification types'); -test('a route can be universal using path identification', function (array $routeMiddleware, array $globalMiddleware) { - foreach ($globalMiddleware as $middleware) { - if ($middleware === 'universal') { - config(['tenancy.default_route_mode' => RouteMode::UNIVERSAL]); - } else { - app(Kernel::class)->pushMiddleware($middleware); - } - } - - RouteFacade::get('/foo', function () { - return tenancy()->initialized - ? 'Tenancy is initialized.' - : 'Tenancy is not initialized.'; - })->middleware($routeMiddleware); - - config(['tenancy.static_identification_middleware' => $routeMiddleware]); - - RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - - $cloneRoutesAction->handle(); - - $tenantKey = Tenant::create()->getTenantKey(); - - pest()->get("http://localhost/foo") - ->assertSuccessful() - ->assertSee('Tenancy is not initialized.'); - - pest()->get("http://localhost/{$tenantKey}/foo") - ->assertSuccessful() - ->assertSee('Tenancy is initialized.'); - - tenancy()->end(); - - pest()->get("http://localhost/bar") - ->assertSuccessful() - ->assertSee('Tenancy is not initialized.'); - - pest()->get("http://localhost/{$tenantKey}/bar") - ->assertSuccessful() - ->assertSee('Tenancy is initialized.'); -})->with('path identification types'); - test('correct exception is thrown when route is universal and tenant could not be identified using domain identification', function (array $routeMiddleware, array $globalMiddleware) { foreach ($globalMiddleware as $middleware) { if ($middleware === 'universal') { @@ -302,26 +256,6 @@ test('correct exception is thrown when route is universal and tenant could not b $this->withoutExceptionHandling()->get('http://nonexistent_subdomain.localhost/foo'); })->with('subdomain identification types'); -test('correct exception is thrown when route is universal and tenant could not be identified using path identification', function (array $routeMiddleware, array $globalMiddleware) { - foreach ($globalMiddleware as $middleware) { - if ($middleware === 'universal') { - config(['tenancy.default_route_mode' => RouteMode::UNIVERSAL]); - } else { - app(Kernel::class)->pushMiddleware($middleware); - } - } - - RouteFacade::get('/foo', fn () => tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.')->middleware($routeMiddleware)->name('foo'); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - - $cloneRoutesAction->handle(); - - pest()->expectException(TenantCouldNotBeIdentifiedByPathException::class); - $this->withoutExceptionHandling()->get('http://localhost/non_existent/foo'); -})->with('path identification types'); - test('correct exception is thrown when route is universal and tenant could not be identified using request data identification', function (array $routeMiddleware, array $globalMiddleware) { foreach ($globalMiddleware as $middleware) { if ($middleware === 'universal') { @@ -341,29 +275,15 @@ test('correct exception is thrown when route is universal and tenant could not b $this->withoutExceptionHandling()->get('http://localhost/foo?tenant=nonexistent_tenant'); })->with('request data identification types'); -test('tenant and central flags override the universal flag', function () { +test('route is made universal by adding the universal flag using request data identification', function () { app(Kernel::class)->pushMiddleware(InitializeTenancyByRequestData::class); $tenant = Tenant::create(); - $route = RouteFacade::get('/route', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal'); + RouteFacade::get('/route', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal'); // Route is universal pest()->get('/route')->assertOk()->assertSee('Tenancy not initialized.'); pest()->get('/route?tenant=' . $tenant->getTenantKey())->assertOk()->assertSee('Tenancy initialized.'); - tenancy()->end(); - - // Route is in tenant context - $route->action['middleware'] = ['universal', 'tenant']; - - pest()->get('/route')->assertServerError(); // "Tenant could not be identified by request data with payload..." - pest()->get('/route?tenant=' . $tenant->getTenantKey())->assertOk()->assertSee('Tenancy initialized.'); - tenancy()->end(); - - // Route is in central context - $route->action['middleware'] = ['universal', 'central']; - - pest()->get('/route')->assertOk()->assertSee('Tenancy not initialized.'); - pest()->get('/route?tenant=' . $tenant->getTenantKey())->assertOk()->assertSee('Tenancy not initialized.'); // Route is accessible, but the context is central }); test('a route can be flagged as universal in both route modes', function (RouteMode $defaultRouteMode) { @@ -388,67 +308,6 @@ test('a route can be flagged as universal in both route modes', function (RouteM 'default to central routes' => RouteMode::CENTRAL, ]); -test('CloneRoutesAsTenant registers prefixed duplicates of universal routes correctly', function (bool $kernelIdentification, bool $useController) { - $routeMiddleware = ['universal']; - - if ($kernelIdentification) { - app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class); - } else { - $routeMiddleware[] = InitializeTenancyByPath::class; - } - - config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => $tenantParameterName = 'team']); - config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_route_name_prefix' => $tenantRouteNamePrefix = 'team-route.']); - - // Test that routes with controllers as well as routes with closure actions get cloned correctly - $universalRoute = RouteFacade::get('/home', $useController ? Controller::class : fn () => tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.')->middleware($routeMiddleware)->name('home'); - $centralRoute = RouteFacade::get('/central', fn () => true)->name('central'); - - config(['tenancy.static_identification_middleware' => $routeMiddleware]); - - $universalRoute2 = RouteFacade::get('/bar', [HasMiddlewareController::class, 'index'])->name('second-home'); - - expect($routes = RouteFacade::getRoutes()->get())->toContain($universalRoute) - ->toContain($universalRoute2) - ->toContain($centralRoute); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - - $cloneRoutesAction->handle(); - - expect($routesAfterRegisteringDuplicates = RouteFacade::getRoutes()->get()) - ->toContain($universalRoute) - ->toContain($centralRoute); - - $newRoutes = collect($routesAfterRegisteringDuplicates)->filter(fn ($route) => ! in_array($route, $routes)); - - expect($newRoutes->first()->uri())->toBe('{' . $tenantParameterName . '}' . '/' . $universalRoute->uri()); - expect($newRoutes->last()->uri())->toBe('{' . $tenantParameterName . '}' . '/' . $universalRoute2->uri()); - - expect(tenancy()->getRouteMiddleware($newRoutes->first()))->toBe(array_merge(tenancy()->getRouteMiddleware($universalRoute), ['tenant'])); - expect(tenancy()->getRouteMiddleware($newRoutes->last()))->toBe(array_merge(tenancy()->getRouteMiddleware($universalRoute2), ['tenant'])); - - $tenant = Tenant::create(); - - pest()->get(route($centralRouteName = $universalRoute->getName()))->assertSee('Tenancy is not initialized.'); - pest()->get(route($centralRouteName2 = $universalRoute2->getName()))->assertSee('Tenancy is not initialized.'); - pest()->get(route($tenantRouteName = $newRoutes->first()->getName(), [$tenantParameterName => $tenant->getTenantKey()]))->assertSee('Tenancy is initialized.'); - tenancy()->end(); - pest()->get(route($tenantRouteName2 = $newRoutes->last()->getName(), [$tenantParameterName => $tenant->getTenantKey()]))->assertSee('Tenancy is initialized.'); - - expect($tenantRouteName)->toBe($tenantRouteNamePrefix . $universalRoute->getName()); - expect($tenantRouteName2)->toBe($tenantRouteNamePrefix . $universalRoute2->getName()); - expect($centralRouteName)->toBe($universalRoute->getName()); - expect($centralRouteName2)->toBe($universalRoute2->getName()); -})->with([ - 'kernel identification' => true, - 'route-level identification' => false, -// Creates a matrix (multiple with()) -])->with([ - 'use controller' => true, - 'use closure' => false -]); test('tenant resolver methods return the correct names for configured values', function (string $configurableParameter, string $value) { $configurableParameterConfigKey = 'tenancy.identification.resolvers.' . PathTenantResolver::class . '.' . $configurableParameter; @@ -464,73 +323,6 @@ test('tenant resolver methods return the correct names for configured values', f ['tenant_route_name_prefix', 'prefix'] ]); -test('CloneRoutesAsTenant only clones routes with path identification by default', function () { - app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class); - - $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); - - $initialRouteCount = $currentRouteCount(); - - // Path identification is used globally, and this route doesn't use a specific identification middleware, meaning path identification is used and the route should get cloned - RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name('home'); - // The route uses a specific identification middleware other than InitializeTenancyByPath – the route shouldn't get cloned - RouteFacade::get('/home-domain-id', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware(['universal', InitializeTenancyByDomain::class])->name('home-domain-id'); - - expect($currentRouteCount())->toBe($newRouteCount = $initialRouteCount + 2); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - - $cloneRoutesAction->handle(); - - // Only one of the two routes gets cloned - expect($currentRouteCount())->toBe($newRouteCount + 1); -}); - -test('custom callbacks can be used for cloning universal routes', function () { - RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware(['universal', InitializeTenancyByPath::class])->name($routeName = 'home'); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); - $initialRouteCount = $currentRouteCount(); - - $cloneRoutesAction; - - // Skip cloning the 'home' route - $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { - return; - })->handle(); - - // Expect route count to stay the same because the 'home' route cloning gets skipped - expect($initialRouteCount)->toEqual($currentRouteCount()); - - // Modify the 'home' route cloning so that a different route is cloned - $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { - RouteFacade::get('/cloned-route', fn () => true)->name('new.home'); - })->handle(); - - expect($currentRouteCount())->toEqual($initialRouteCount + 1); -}); - -test('cloning of specific routes can get skipped', function () { - RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name($routeName = 'home'); - - /** @var CloneRoutesAsTenant $cloneRoutesAction */ - $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); - $initialRouteCount = $currentRouteCount(); - - // Skip cloning the 'home' route - $cloneRoutesAction->skipRoute($routeName); - - $cloneRoutesAction->handle(); - - // Expect route count to stay the same because the 'home' route cloning gets skipped - expect($initialRouteCount)->toEqual($currentRouteCount()); -}); - - test('identification middleware works with universal routes only when it implements MiddlewareUsableWithUniversalRoutes', function () { $tenantKey = Tenant::create()->getTenantKey(); $routeAction = fn () => tenancy()->initialized ? $tenantKey : 'Tenancy is not initialized.'; @@ -554,36 +346,10 @@ test('identification middleware works with universal routes only when it impleme $this->withoutExceptionHandling()->get('http://localhost/custom-mw-universal-route'); }); -test('routes except nonuniversal routes with path id mw are given the tenant flag after cloning', function (array $routeMiddleware, array $globalMiddleware) { - foreach ($globalMiddleware as $middleware) { - if ($middleware === 'universal') { - config(['tenancy.default_route_mode' => RouteMode::UNIVERSAL]); - } else { - app(Kernel::class)->pushMiddleware($middleware); - } - } - - $route = RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.') - ->middleware($routeMiddleware) - ->name($routeName = 'home'); - - app(CloneRoutesAsTenant::class)->handle(); - - $clonedRoute = RouteFacade::getRoutes()->getByName('tenant.' . $routeName); - - // Non-universal routes with identification middleware are already considered tenant, so they don't get the tenant flag - if (! tenancy()->routeIsUniversal($route) && tenancy()->routeHasIdentificationMiddleware($clonedRoute)) { - expect($clonedRoute->middleware())->not()->toContain('tenant'); - } else { - expect($clonedRoute->middleware())->toContain('tenant'); - } -})->with('path identification types'); - foreach ([ 'domain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class], 'subdomain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyBySubdomain::class], 'domainOrSubdomain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomainOrSubdomain::class], - 'path identification types' => [InitializeTenancyByPath::class], 'request data identification types' => [InitializeTenancyByRequestData::class], ] as $datasetName => $middleware) { dataset($datasetName, [ @@ -606,14 +372,6 @@ foreach ([ ]); } -class Controller extends BaseController -{ - public function __invoke() - { - return tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.'; - } -} - class CustomMiddleware extends IdentificationMiddleware { use UsableWithEarlyIdentification; @@ -652,3 +410,11 @@ class CustomMiddleware extends IdentificationMiddleware return null; } } + +class Controller extends BaseController +{ + public function __invoke() + { + return tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.'; + } +}