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']); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => $tenantDomain = $tenant->getTenantKey() . '.localhost', ]); pest()->get("http://localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantDomain}/foo") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); pest()->get("http://localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantDomain}/bar") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); })->with('domain identification types'); test('a route can be universal using subdomain 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._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); $tenant = Tenant::create(); $tenantKey = $tenant->getTenantKey(); $tenant->domains()->create([ 'domain' => $tenantKey, ]); pest()->get("http://localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantKey}.localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); pest()->get("http://localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantKey}.localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); })->with('subdomain identification types'); test('a route can be universal using domainOrSubdomain 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._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => $tenantDomain = 'tenant-domain.test', ]); $tenant->domains()->create([ 'domain' => $tenantSubdomain = 'tenant-subdomain', ]); pest()->get("http://localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); // Domain identification pest()->get("http://{$tenantDomain}/foo") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); // Subdomain identification pest()->get("http://{$tenantSubdomain}.localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); pest()->get("http://localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantDomain}/bar") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); pest()->get("http://{$tenantSubdomain}.localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); })->with('domainOrSubdomain identification types'); test('a route can be universal using request data 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._tests.static_identification_middleware' => $routeMiddleware]); RouteFacade::get('/bar', [HasMiddlewareController::class, 'index']); $tenantKey = Tenant::create()->getTenantKey(); pest()->get("http://localhost/foo") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://localhost/foo?tenant={$tenantKey}") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); pest()->get("http://localhost/bar") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://localhost/bar?tenant={$tenantKey}") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); })->with('request data 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') { 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); pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); $this->withoutExceptionHandling()->get('http://nonexistent_domain.localhost/foo'); })->with('domain identification types'); test('correct exception is thrown when route is universal and tenant could not be identified using subdomain 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); pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); $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 request data 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); pest()->expectException(TenantCouldNotBeIdentifiedByRequestDataException::class); $this->withoutExceptionHandling()->get('http://localhost/foo?tenant=nonexistent_tenant'); })->with('request data identification types'); test('route is made universal by adding the universal flag using request data identification', function () { app(Kernel::class)->pushMiddleware(InitializeTenancyByRequestData::class); $tenant = Tenant::create(); 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.'); }); test('a route can be flagged as universal in both route modes', function (RouteMode $defaultRouteMode) { app(Kernel::class)->pushMiddleware(InitializeTenancyBySubdomain::class); app(Kernel::class)->pushMiddleware(PreventAccessFromUnwantedDomains::class); config(['tenancy.default_route_mode' => $defaultRouteMode]); RouteFacade::get('/universal', fn () => tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.')->middleware('universal'); Tenant::create()->domains()->create(['domain' => $tenantSubdomain = 'tenant-subdomain']); pest()->get("http://localhost/universal") ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); pest()->get("http://{$tenantSubdomain}.localhost/universal") ->assertSuccessful() ->assertSee('Tenancy is initialized.'); })->with([ 'default to tenant routes' => RouteMode::TENANT, 'default to central routes' => RouteMode::CENTRAL, ]); test('tenant resolver methods return the correct names for configured values', function (string $configurableParameter, string $value) { $configurableParameterConfigKey = 'tenancy.identification.resolvers.' . PathTenantResolver::class . '.' . $configurableParameter; config([$configurableParameterConfigKey => $value]); // Note: The names of the methods are NOT dynamic (PathTenantResolver::tenantParameterName(), PathTenantResolver::tenantRouteNamePrefix()) $resolverMethodName = str($configurableParameter)->camel()->toString(); expect(PathTenantResolver::$resolverMethodName())->toBe($value); })->with([ ['tenant_parameter_name', 'parameter'], ['tenant_route_name_prefix', 'prefix'] ]); 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.'; // Route with the package's request data identification middleware – implements MiddlewareUsableWithUniversalRoutes RouteFacade::get('/universal-route', $routeAction)->middleware(['universal', InitializeTenancyByRequestData::class]); // Routes with custom request data identification middleware – does not implement MiddlewareUsableWithUniversalRoutes RouteFacade::get('/custom-mw-universal-route', $routeAction)->middleware(['universal', CustomMiddleware::class]); RouteFacade::get('/custom-mw-tenant-route', $routeAction)->middleware(['tenant', CustomMiddleware::class]); // Ensure the custom identification middleware works with non-universal routes // This is tested here because this is the only test where the custom MW is used // No exception is thrown for this request since the route uses the TENANT middleware, not the UNIVERSAL middleware pest()->get('http://localhost/custom-mw-tenant-route?tenant=' . $tenantKey)->assertOk()->assertSee($tenantKey); pest()->get('http://localhost/universal-route')->assertOk(); pest()->get('http://localhost/universal-route?tenant=' . $tenantKey)->assertOk()->assertSee($tenantKey); pest()->expectException(MiddlewareNotUsableWithUniversalRoutesException::class); $this->withoutExceptionHandling()->get('http://localhost/custom-mw-universal-route'); }); foreach ([ 'domain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class], 'subdomain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyBySubdomain::class], 'domainOrSubdomain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomainOrSubdomain::class], 'request data identification types' => [InitializeTenancyByRequestData::class], ] as $datasetName => $middleware) { dataset($datasetName, [ 'kernel identification' => [ 'route_middleware' => ['universal'], 'global_middleware' => $middleware, ], 'route-level identification' => [ 'route_middleware' => ['universal', ...$middleware], 'global_middleware' => [], ], 'kernel identification + defaulting to universal routes' => [ 'route_middleware' => [], 'global_middleware' => ['universal', ...$middleware], ], 'route-level identification + defaulting to universal routes' => [ 'route_middleware' => $middleware, 'global_middleware' => ['universal'], ], ]); } class CustomMiddleware extends IdentificationMiddleware { use UsableWithEarlyIdentification; public static string $header = 'X-Tenant'; public static string $cookie = 'X-Tenant'; public static string $queryParameter = 'tenant'; public function __construct( protected Tenancy $tenancy, protected RequestDataTenantResolver $resolver, ) { } /** @return \Illuminate\Http\Response|mixed */ public function handle(Request $request, Closure $next): mixed { if ($this->shouldBeSkipped(tenancy()->getRoute($request))) { // Allow accessing central route in kernel identification return $next($request); } return $this->initializeTenancy($request, $next, $this->getPayload($request)); } protected function getPayload(Request $request): string|array|null { if (static::$header && $request->hasHeader(static::$header)) { return $request->header(static::$header); } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { return $request->get(static::$queryParameter); } elseif (static::$cookie && $request->hasCookie(static::$cookie)) { return $request->cookie(static::$cookie); } return null; } } class Controller extends BaseController { public function __invoke() { return tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.'; } }