From ec9b585e7063881d51603a0d0d94766826c511d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 30 May 2025 03:55:52 +0200 Subject: [PATCH] request data identification: add tenant_model_column configuration --- assets/config.php | 2 + src/Overrides/TenancyUrlGenerator.php | 1 + src/Resolvers/RequestDataTenantResolver.php | 9 ++- .../UrlGeneratorBootstrapperTest.php | 2 +- tests/RequestDataIdentificationTest.php | 67 ++++++++++++++----- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/assets/config.php b/assets/config.php index 3b073c12..73becdee 100644 --- a/assets/config.php +++ b/assets/config.php @@ -132,6 +132,8 @@ return [ 'cookie' => 'tenant', 'query_parameter' => 'tenant', + 'tenant_model_column' => null, // null = tenant key + 'cache' => false, 'cache_ttl' => 3600, // seconds 'cache_store' => null, // null = default diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index 4c6120a8..06ef7673 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -176,6 +176,7 @@ class TenancyUrlGenerator extends UrlGenerator { if (tenant() && static::$passTenantParameterToRoutes) { if (static::$defaultParameterNames) { + // todo0 this should be changed to something like static::$queryParameter and it should respect the configured tenant model column return array_merge($parameters, ['tenant' => tenant()->getTenantKey()]); } else { return array_merge($parameters, [PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue(tenant())]); diff --git a/src/Resolvers/RequestDataTenantResolver.php b/src/Resolvers/RequestDataTenantResolver.php index ad63d6f3..760b61f0 100644 --- a/src/Resolvers/RequestDataTenantResolver.php +++ b/src/Resolvers/RequestDataTenantResolver.php @@ -20,7 +20,9 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver { $payload = (string) $args[0]; - if ($payload && $tenant = tenancy()->find($payload, withRelations: true)) { + $column = static::tenantModelColumn(); + + if ($payload && $tenant = tenancy()->find($payload, $column, withRelations: true)) { return $tenant; } @@ -34,6 +36,11 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver ]; } + public static function tenantModelColumn(): string + { + return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName(); + } + /** * Returns the name of the header used for identification, or null if header identification is disabled. */ diff --git a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php index 0f41dd86..d1a0d1e3 100644 --- a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php +++ b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php @@ -119,7 +119,7 @@ test('request data identification route helper behavior', function (bool $addTen tenancy()->initialize($tenant); - // todo0 test changing tenancy.identification.resolvers..query_parameter + // todo0 test changing tenancy.identification.resolvers..query_parameter and tenant_model_column if ($passTenantParameterToRoutes) { expect(route('tenant.home'))->toBe("{$appUrl}/tenant/home?tenant={$tenantKey}"); diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index 1b76d57c..ec8ad65e 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; @@ -21,50 +22,80 @@ beforeEach(function () { }); }); -test('header identification works', function () { - $tenant = Tenant::create(); +test('header identification works', function (string|null $tenantModelColumn) { + if ($tenantModelColumn) { + Schema::table('tenants', function (Blueprint $table) use ($tenantModelColumn) { + $table->string($tenantModelColumn)->unique(); + }); + Tenant::$extraCustomColumns = [$tenantModelColumn]; + } + + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.tenant_model_column' => $tenantModelColumn]); + + $tenant = Tenant::create($tenantModelColumn ? [$tenantModelColumn => 'acme'] : []); + $payload = $tenantModelColumn ? 'acme' : $tenant->id; // Default header name - $this->withoutExceptionHandling()->withHeader('X-Tenant', $tenant->id)->get('test')->assertSee($tenant->id); + $this->withoutExceptionHandling()->withHeader('X-Tenant', $payload)->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); + $this->withoutExceptionHandling()->withHeader('X-Custom-Tenant', $payload)->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); -}); + expect(fn () => $this->withoutExceptionHandling()->withHeader('X-Tenant', $payload)->get('test'))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); +})->with([null, 'slug']); -test('query parameter identification works', function () { - $tenant = Tenant::create(); +test('query parameter identification works', function (string|null $tenantModelColumn) { + if ($tenantModelColumn) { + Schema::table('tenants', function (Blueprint $table) use ($tenantModelColumn) { + $table->string($tenantModelColumn)->unique(); + }); + Tenant::$extraCustomColumns = [$tenantModelColumn]; + } + + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.tenant_model_column' => $tenantModelColumn]); + + $tenant = Tenant::create($tenantModelColumn ? [$tenantModelColumn => 'acme'] : []); + $payload = $tenantModelColumn ? 'acme' : $tenant->id; // Default query parameter name - $this->withoutExceptionHandling()->get('test?tenant=' . $tenant->id)->assertSee($tenant->id); + $this->withoutExceptionHandling()->get('test?tenant=' . $payload)->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); + $this->withoutExceptionHandling()->get('test?custom_tenant=' . $payload)->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); -}); + expect(fn () => $this->withoutExceptionHandling()->get('test?tenant=' . $payload))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); +})->with([null, 'slug']); -test('cookie identification works', function () { - $tenant = Tenant::create(); +test('cookie identification works', function (string|null $tenantModelColumn) { + if ($tenantModelColumn) { + Schema::table('tenants', function (Blueprint $table) use ($tenantModelColumn) { + $table->string($tenantModelColumn)->unique(); + }); + Tenant::$extraCustomColumns = [$tenantModelColumn]; + } + + config(['tenancy.identification.resolvers.' . RequestDataTenantResolver::class . '.tenant_model_column' => $tenantModelColumn]); + + $tenant = Tenant::create($tenantModelColumn ? [$tenantModelColumn => 'acme'] : []); + $payload = $tenantModelColumn ? 'acme' : $tenant->id; // Default cookie name - $this->withoutExceptionHandling()->withUnencryptedCookie('tenant', $tenant->id)->get('test')->assertSee($tenant->id); + $this->withoutExceptionHandling()->withUnencryptedCookie('tenant', $payload)->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); + $this->withoutExceptionHandling()->withUnencryptedCookie('custom_tenant_id', $payload)->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); -}); + expect(fn () => $this->withoutExceptionHandling()->withUnencryptedCookie('tenant', $payload)->get('test'))->toThrow(TenantCouldNotBeIdentifiedByRequestDataException::class); +})->with([null, 'slug']); // todo@tests encrypted cookie