diff --git a/assets/config.php b/assets/config.php index 37306806..13dc416e 100644 --- a/assets/config.php +++ b/assets/config.php @@ -119,7 +119,7 @@ return [ Resolvers\PathTenantResolver::class => [ 'tenant_parameter_name' => 'tenant', // todo0 test changing this 'tenant_model_column' => null, // null = tenant key - 'tenant_route_name_prefix' => null, // null = 'tenant.' + 'tenant_route_name_prefix' => 'tenant.', 'allowed_extra_model_columns' => [], // used with binding route fields 'cache' => false, @@ -127,8 +127,9 @@ return [ 'cache_store' => null, // null = default ], Resolvers\RequestDataTenantResolver::class => [ + // Set any of these to null to disable that method of identification 'header' => 'X-Tenant', - 'cookie' => 'tenant', // todo0 test in url generator + 'cookie' => 'tenant', 'query_parameter' => 'tenant', 'cache' => false, diff --git a/src/Bootstrappers/UrlGeneratorBootstrapper.php b/src/Bootstrappers/UrlGeneratorBootstrapper.php index 6ead7cfd..09259145 100644 --- a/src/Bootstrappers/UrlGeneratorBootstrapper.php +++ b/src/Bootstrappers/UrlGeneratorBootstrapper.php @@ -73,7 +73,6 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper foreach (PathTenantResolver::allowedExtraModelColumns() as $column) { // todo0 should this be tenantParameterName() concatenated to :$column? - // add tests $defaultParameters["tenant:$column"] = $tenant->getAttribute($column); } } diff --git a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php index eeedd05a..b4b9d40e 100644 --- a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php +++ b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php @@ -15,6 +15,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Illuminate\Routing\Exceptions\UrlGenerationException; use Illuminate\Support\Facades\Schema; use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; +use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use function Stancl\Tenancy\Tests\pest; @@ -47,80 +48,87 @@ test('url generator bootstrapper swaps the url generator instance correctly', fu }); test('tenancy url generator 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'); + config([ + 'tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_route_name_prefix' => 'custom_prefix.', + ]); + + Route::get('/central/home', fn () => '')->name('home'); + Route::get('/tenant/home', fn () => '')->name('custom_prefix.home'); $tenant = Tenant::create(); - $centralRouteUrl = route('home'); - $tenantRouteUrl = route('tenant.home'); config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); tenancy()->initialize($tenant); // Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false (default) - expect(route('home'))->toBe($centralRouteUrl); + expect(route('home'))->toBe('http://localhost/central/home'); - // When $prefixRouteNames is true, the route name passed to the route() helper ('home') gets prefixed with 'tenant.' automatically. + // When $prefixRouteNames is true, the route name passed to the route() helper ('home') gets prefixed automatically. TenancyUrlGenerator::$prefixRouteNames = true; - expect(route('home'))->toBe($tenantRouteUrl); + expect(route('home'))->toBe('http://localhost/tenant/home'); - // The 'tenant.home' route name doesn't get prefixed -- it is already prefixed with 'tenant.' - expect(route('tenant.home'))->toBe($tenantRouteUrl); + // The 'custom_prefix.home' route name doesn't get prefixed -- it is already prefixed with 'custom_prefix.' + expect(route('custom_prefix.home'))->toBe('http://localhost/tenant/home'); // Ending tenancy reverts route() behavior changes tenancy()->end(); - expect(route('home'))->toBe($centralRouteUrl); + expect(route('home'))->toBe('http://localhost/central/home'); }); -test('the route helper can receive the tenant parameter automatically', function ( - string $identification, - bool $addTenantParameterToDefaults, - bool $passTenantParameterToRoutes, -) { +test('path identification route helper behavior', function (bool $addTenantParameterToDefaults, bool $passTenantParameterToRoutes) { config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); $appUrl = config('app.url'); - UrlGeneratorBootstrapper::$addTenantParameterToDefaults = $addTenantParameterToDefaults; - - // When the tenant parameter isn't added to defaults, the tenant parameter has to be passed "manually" - // by setting $passTenantParameterToRoutes to true. This is only preferable with query string identification. - // With path identification, this ultimately doesn't have any effect - // if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is true, - // but TenancyUrlGenerator::$passTenantParameterToRoutes can still be used instead. TenancyUrlGenerator::$passTenantParameterToRoutes = $passTenantParameterToRoutes; $tenant = Tenant::create(); $tenantKey = $tenant->getTenantKey(); - Route::get('/central/home', fn () => route('home'))->name('home'); - - $tenantRoute = $identification === InitializeTenancyByPath::class ? "/{tenant}/home" : "/tenant/home"; - - Route::get($tenantRoute, fn () => route('tenant.home')) + Route::get('/{tenant}/home', fn () => '') ->name('tenant.home') - ->middleware(['tenant', $identification]); + ->middleware(['tenant', InitializeTenancyByPath::class]); tenancy()->initialize($tenant); - $expectedUrl = match (true) { - $identification === InitializeTenancyByRequestData::class && $passTenantParameterToRoutes => "{$appUrl}/tenant/home?tenant={$tenantKey}", - $identification === InitializeTenancyByRequestData::class => "{$appUrl}/tenant/home", // $passTenantParameterToRoutes is false - $identification === InitializeTenancyByPath::class && ($addTenantParameterToDefaults || $passTenantParameterToRoutes) => "{$appUrl}/{$tenantKey}/home", - $identification === InitializeTenancyByPath::class => null, // Should throw an exception -- route() doesn't receive the tenant parameter in this case - }; - - if ($expectedUrl === null) { + if (! $addTenantParameterToDefaults && ! $passTenantParameterToRoutes) { expect(fn () => route('tenant.home'))->toThrow(UrlGenerationException::class, 'Missing parameter: tenant'); } else { - expect(route('tenant.home'))->toBe($expectedUrl); + // If at least *one* of the approaches was used, the parameter will make its way to the route + expect(route('tenant.home'))->toBe("{$appUrl}/{$tenantKey}/home"); } -})->with([InitializeTenancyByPath::class, InitializeTenancyByRequestData::class]) - ->with([true, false]) // UrlGeneratorBootstrapper::$addTenantParameterToDefaults +})->with([true, false]) // UrlGeneratorBootstrapper::$addTenantParameterToDefaults + ->with([true, false]); // TenancyUrlGenerator::$passTenantParameterToRoutes + +test('request data identification route helper behavior', function (bool $addTenantParameterToDefaults, bool $passTenantParameterToRoutes) { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + $appUrl = config('app.url'); + UrlGeneratorBootstrapper::$addTenantParameterToDefaults = $addTenantParameterToDefaults; + TenancyUrlGenerator::$passTenantParameterToRoutes = $passTenantParameterToRoutes; + + $tenant = Tenant::create(); + $tenantKey = $tenant->getTenantKey(); + + Route::get('/tenant/home', fn () => tenant('id')) + ->name('tenant.home') + ->middleware(['tenant', InitializeTenancyByRequestData::class]); + + tenancy()->initialize($tenant); + + // todo0 test changing tenancy.identification.resolvers..query_parameter + + if ($passTenantParameterToRoutes) { + expect(route('tenant.home'))->toBe("{$appUrl}/tenant/home?tenant={$tenantKey}"); + pest()->get(route('tenant.home'))->assertSee($tenant->id); + } else { + expect(route('tenant.home'))->toBe("{$appUrl}/tenant/home"); + expect(fn () => $this->withoutExceptionHandling()->get(route('tenant.home')))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); + } +})->with([true, false]) // UrlGeneratorBootstrapper::$addTenantParameterToDefaults ->with([true, false]); // TenancyUrlGenerator::$passTenantParameterToRoutes test('setting extra model columns sets additional URL defaults', function () { diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index 365b652c..1b76d57c 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -14,12 +14,9 @@ beforeEach(function () { 'tenancy.identification.central_domains' => [ 'localhost', ], - 'tenancy.identification.' . RequestDataTenantResolver::class . '.header' => 'X-Tenant', - 'tenancy.identification.' . RequestDataTenantResolver::class . '.query_parameter' => 'tenant', - 'tenancy.identification.' . RequestDataTenantResolver::class . '.cookie' => 'tenant', ]); - Route::middleware(['tenant', InitializeTenancyByRequestData::class])->get('/test', function () { + Route::middleware([InitializeTenancyByRequestData::class])->get('/test', function () { return 'Tenant id: ' . tenant('id'); }); }); @@ -27,35 +24,52 @@ beforeEach(function () { test('header identification works', function () { $tenant = Tenant::create(); - $this - ->withoutExceptionHandling() - ->withHeader('X-Tenant', $tenant->id) - ->get('test') - ->assertSee($tenant->id); + // Default header name + $this->withoutExceptionHandling()->withHeader('X-Tenant', $tenant->id)->get('test')->assertSee($tenant->id); + + // Custom header name + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.header' => 'X-Custom-Tenant']); + $this->withoutExceptionHandling()->withHeader('X-Custom-Tenant', $tenant->id)->get('test')->assertSee($tenant->id); + + // Setting the header to null disables header identification + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.header' => null]); + expect(fn () => $this->withoutExceptionHandling()->withHeader('X-Tenant', $tenant->id)->get('test'))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); }); test('query parameter identification works', function () { $tenant = Tenant::create(); - $this - ->withoutExceptionHandling() - ->get('test?tenant=' . $tenant->id) - ->assertSee($tenant->id); + // Default query parameter name + $this->withoutExceptionHandling()->get('test?tenant=' . $tenant->id)->assertSee($tenant->id); + + // Custom query parameter name + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.query_parameter' => 'custom_tenant']); + $this->withoutExceptionHandling()->get('test?custom_tenant=' . $tenant->id)->assertSee($tenant->id); + + // Setting the query parameter to null disables query parameter identification + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.query_parameter' => null]); + expect(fn () => $this->withoutExceptionHandling()->get('test?tenant=' . $tenant->id))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); }); test('cookie identification works', function () { $tenant = Tenant::create(); - $this - ->withoutExceptionHandling() - ->withUnencryptedCookie('tenant', $tenant->id) - ->get('test') - ->assertSee($tenant->id); + // Default cookie name + $this->withoutExceptionHandling()->withUnencryptedCookie('tenant', $tenant->id)->get('test')->assertSee($tenant->id); + + // Custom cookie name + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.cookie' => 'custom_tenant_id']); + $this->withoutExceptionHandling()->withUnencryptedCookie('custom_tenant_id', $tenant->id)->get('test')->assertSee($tenant->id); + + // Setting the cookie to null disables cookie identification + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.cookie' => null]); + expect(fn () => $this->withoutExceptionHandling()->withUnencryptedCookie('tenant', $tenant->id)->get('test'))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); }); // todo@tests encrypted cookie -test('middleware throws exception when tenant data is not provided in the request', function () { +test('an exception is thrown when no tenant data is not provided in the request', function () { pest()->expectException(TenantCouldNotBeIdentifiedByRequestDataException::class); $this->withoutExceptionHandling()->get('test'); }); +