diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index 9ed9d9e..43d8531 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -188,15 +188,18 @@ } ], "url": { - "raw": "{{base_url}}/api/tenants/bootstrap/{{tenant_domain}}", + "raw": "{{base_url}}/api/tenants/bootstrap?dominio={{tenant_domain}}&path={{tenant_path}}", "host": [ "{{base_url}}" ], "path": [ "api", "tenants", - "bootstrap", - "{{tenant_domain}}" + "bootstrap" + ], + "query": [ + {"key": "dominio", "value": "{{tenant_domain}}"}, + {"key": "path", "value": "{{tenant_path}}"} ] } }, @@ -1402,6 +1405,11 @@ "value": "localhost", "type": "string" }, + { + "key": "tenant_path", + "value": "/", + "type": "string" + }, { "key": "tenant_id", "value": "1", diff --git a/Tenant_CRUD_Postman_Collection.json b/Tenant_CRUD_Postman_Collection.json index ceb7beb..4dbea2c 100644 --- a/Tenant_CRUD_Postman_Collection.json +++ b/Tenant_CRUD_Postman_Collection.json @@ -41,15 +41,18 @@ } ], "url": { - "raw": "{{base_url}}/api/tenants/bootstrap/acme.com", + "raw": "{{base_url}}/api/tenants/bootstrap?dominio=acme.com&path=/", "host": [ "{{base_url}}" ], "path": [ "api", "tenants", - "bootstrap", - "acme.com" + "bootstrap" + ], + "query": [ + {"key": "dominio", "value": "acme.com"}, + {"key": "path", "value": "/"} ] } }, diff --git a/app/Domains/Bootstrap/Controllers/TenantBootstrapController.php b/app/Domains/Bootstrap/Controllers/TenantBootstrapController.php index 001bc30..57406f1 100644 --- a/app/Domains/Bootstrap/Controllers/TenantBootstrapController.php +++ b/app/Domains/Bootstrap/Controllers/TenantBootstrapController.php @@ -14,7 +14,10 @@ class TenantBootstrapController extends Controller public function __invoke(TenantBootstrapRequest $request): TenantResource { return TenantResource::make( - $this->bootstrapService->get((string) $request->validated('dominio')) + $this->bootstrapService->get( + (string) $request->validated('dominio'), + (string) $request->validated('path'), + ) ); } } diff --git a/app/Domains/Bootstrap/Requests/TenantBootstrapRequest.php b/app/Domains/Bootstrap/Requests/TenantBootstrapRequest.php index 10d8bbf..e5529aa 100644 --- a/app/Domains/Bootstrap/Requests/TenantBootstrapRequest.php +++ b/app/Domains/Bootstrap/Requests/TenantBootstrapRequest.php @@ -10,6 +10,8 @@ class TenantBootstrapRequest extends FormRequest { protected bool $hasInvalidDomain = false; + protected bool $hasInvalidPath = false; + public function authorize(): bool { return true; @@ -17,13 +19,20 @@ class TenantBootstrapRequest extends FormRequest protected function prepareForValidation(): void { - $rawDomain = $this->route('dominio'); + $rawDomain = $this->query('dominio', $this->route('dominio')); + $rawPath = $this->query('path', '/'); $normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); + $normalizedPath = TenantDomainNormalizer::normalizePath($rawPath); $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) && $normalizedDomain === null; - $this->merge(['dominio' => $normalizedDomain]); + $this->hasInvalidPath = ! is_string($rawPath) || $normalizedPath === null; + + $this->merge([ + 'dominio' => $normalizedDomain, + 'path' => $normalizedPath, + ]); } /** @return array */ @@ -41,6 +50,17 @@ class TenantBootstrapRequest extends FormRequest 'string', 'max:255', ], + 'path' => [ + 'bail', + function (string $attribute, mixed $value, Closure $fail): void { + if ($this->hasInvalidPath) { + $fail("The {$attribute} field must contain a valid URL path."); + } + }, + 'required', + 'string', + 'max:2048', + ], ]; } } diff --git a/app/Domains/Bootstrap/Services/TenantBootstrapService.php b/app/Domains/Bootstrap/Services/TenantBootstrapService.php index 8dd6862..d4a9653 100644 --- a/app/Domains/Bootstrap/Services/TenantBootstrapService.php +++ b/app/Domains/Bootstrap/Services/TenantBootstrapService.php @@ -5,15 +5,31 @@ namespace App\Domains\Bootstrap\Services; use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Services\TenantInformationService; +use App\Domains\Tenant\Support\TenantDomainNormalizer; +use Illuminate\Database\Eloquent\ModelNotFoundException; class TenantBootstrapService { public function __construct(protected TenantInformationService $tenantInformationService) {} - public function get(string $domain): Tenant + public function get(string $domain, string $path = '/'): Tenant { + $candidateKeys = TenantDomainNormalizer::tenantKeyCandidates($domain, $path); + $tenantsByDomain = Tenant::query() + ->whereIn('dominio', $candidateKeys) + ->get() + ->keyBy('dominio'); + + $tenant = collect($candidateKeys) + ->map(fn (string $candidate): ?Tenant => $tenantsByDomain->get($candidate)) + ->first(fn (?Tenant $candidate): bool => $candidate !== null); + + if (! $tenant instanceof Tenant) { + throw (new ModelNotFoundException)->setModel(Tenant::class); + } + return $this->tenantInformationService->load( - Tenant::query()->where('dominio', $domain)->firstOrFail(), + $tenant, [ 'menues' => fn ($query) => $query->whereHas( 'roles', diff --git a/app/Domains/Bootstrap/documentacion/README.md b/app/Domains/Bootstrap/documentacion/README.md index 2d9c6a4..ad0592c 100644 --- a/app/Domains/Bootstrap/documentacion/README.md +++ b/app/Domains/Bootstrap/documentacion/README.md @@ -12,12 +12,12 @@ Entrega la configuración inicial que necesitan la tienda y el panel administrat ## Endpoints -- `GET /tenants/bootstrap/{dominio}`: bootstrap público de la tienda. +- `GET /tenants/bootstrap?dominio={hostname}&path={path}`: bootstrap público de la tienda. Resuelve la clave de tenant más específica que sea prefijo completo del path y usa el dominio raíz como fallback. - Endpoint de bootstrap bajo `/v1/adminapp`, protegido por `auth:sanctum` y `adminapp.tenant`. ## Validación -`TenantBootstrapRequest` valida el dominio recibido. `AdminAppBootstrapRequest` reutiliza ese contrato para el panel. +`TenantBootstrapRequest` valida y normaliza por separado el hostname y el path recibidos. `AdminAppBootstrapRequest` reutiliza ese contrato para el panel. ## Dependencias diff --git a/app/Domains/Bootstrap/routes/api.php b/app/Domains/Bootstrap/routes/api.php index ab36e67..bd4265c 100644 --- a/app/Domains/Bootstrap/routes/api.php +++ b/app/Domains/Bootstrap/routes/api.php @@ -3,8 +3,7 @@ use App\Domains\Bootstrap\Controllers\TenantBootstrapController; use Illuminate\Support\Facades\Route; -Route::get('tenants/bootstrap/{dominio}', TenantBootstrapController::class) - ->where('dominio', '.*'); +Route::get('tenants/bootstrap', TenantBootstrapController::class); require __DIR__.'/adminapp.php'; require __DIR__.'/scanner.php'; diff --git a/app/Domains/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Tenant/Requests/StoreTenantRequest.php index 8c2c058..2fc1e26 100644 --- a/app/Domains/Tenant/Requests/StoreTenantRequest.php +++ b/app/Domains/Tenant/Requests/StoreTenantRequest.php @@ -23,7 +23,7 @@ class StoreTenantRequest extends FormRequest protected function prepareForValidation(): void { $rawDomain = $this->input('dominio'); - $normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); + $normalizedDomain = TenantDomainNormalizer::normalizeTenantKey($rawDomain); $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) && $normalizedDomain === null; diff --git a/app/Domains/Tenant/Requests/UpdateTenantRequest.php b/app/Domains/Tenant/Requests/UpdateTenantRequest.php index 94319fd..bc289ba 100644 --- a/app/Domains/Tenant/Requests/UpdateTenantRequest.php +++ b/app/Domains/Tenant/Requests/UpdateTenantRequest.php @@ -24,7 +24,7 @@ class UpdateTenantRequest extends FormRequest { if ($this->has('dominio')) { $rawDomain = $this->input('dominio'); - $normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); + $normalizedDomain = TenantDomainNormalizer::normalizeTenantKey($rawDomain); $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) && $normalizedDomain === null; diff --git a/app/Domains/Tenant/Support/TenantDomainNormalizer.php b/app/Domains/Tenant/Support/TenantDomainNormalizer.php index 99450ce..de8ea62 100644 --- a/app/Domains/Tenant/Support/TenantDomainNormalizer.php +++ b/app/Domains/Tenant/Support/TenantDomainNormalizer.php @@ -33,4 +33,90 @@ class TenantDomainNormalizer return strtolower($host); } + + public static function normalizeTenantKey(mixed $domain, mixed $path = null): ?string + { + $host = self::normalize($domain); + + if ($host === null) { + return null; + } + + if ($path === null && is_string($domain)) { + $decodedDomain = trim(urldecode($domain)); + $candidate = str_contains($decodedDomain, '://') + ? $decodedDomain + : "//{$decodedDomain}"; + $path = parse_url($candidate, PHP_URL_PATH) ?: '/'; + } + + $normalizedPath = self::normalizePath($path); + + if ($normalizedPath === null) { + return null; + } + + return $host.($normalizedPath === '/' ? '' : $normalizedPath); + } + + public static function normalizePath(mixed $path): ?string + { + if (! is_string($path)) { + return null; + } + + $path = trim($path); + + if ($path === '' || $path === '/') { + return '/'; + } + + $path = parse_url(str_starts_with($path, '/') ? $path : "/{$path}", PHP_URL_PATH); + + if (! is_string($path)) { + return null; + } + + $path = preg_replace('#/+#', '/', $path); + + if (! is_string($path)) { + return null; + } + + $segments = array_filter(explode('/', $path), static fn (string $segment): bool => $segment !== ''); + + foreach ($segments as $segment) { + if ($segment === '.' || $segment === '..') { + return null; + } + } + + return '/'.implode('/', $segments); + } + + /** @return list */ + public static function tenantKeyCandidates(mixed $domain, mixed $path): array + { + $host = self::normalize($domain); + $normalizedPath = self::normalizePath($path); + + if ($host === null || $normalizedPath === null) { + return []; + } + + $segments = array_values(array_filter( + explode('/', $normalizedPath), + static fn (string $segment): bool => $segment !== '', + )); + $candidates = []; + + while ($segments !== []) { + $candidates[] = $host.'/'.implode('/', $segments); + array_pop($segments); + } + + $candidates[] = $host; + + return $candidates; + } } diff --git a/routes/api.php b/routes/api.php index b865e8a..9e83804 100644 --- a/routes/api.php +++ b/routes/api.php @@ -7,12 +7,12 @@ require __DIR__.'/../app/Domains/StorageTest/routes/api.php'; require __DIR__.'/../app/Domains/MailTest/routes/api.php'; require __DIR__.'/../app/Domains/Purchase/routes/api.php'; require __DIR__.'/../app/Domains/Sale/routes/api.php'; +require __DIR__.'/../app/Domains/Bootstrap/routes/api.php'; require __DIR__.'/../app/Domains/Tenant/routes/api.php'; require __DIR__.'/../app/Domains/Integration/routes/api.php'; require __DIR__.'/../app/Domains/Menu/routes/api.php'; require __DIR__.'/../app/Domains/Ticket/routes/api.php'; require __DIR__.'/../app/Domains/Event/routes/api.php'; -require __DIR__.'/../app/Domains/Bootstrap/routes/api.php'; require __DIR__.'/../app/Domains/Forms/routes/api.php'; require __DIR__.'/../app/Domains/Staff/routes/api.php'; require __DIR__.'/../app/Domains/FiestaFutbolInfantil/routes/api.php'; diff --git a/tests/Feature/Catalog/CatalogSearchTest.php b/tests/Feature/Catalog/CatalogSearchTest.php index 52aa2b6..7fd4b07 100644 --- a/tests/Feature/Catalog/CatalogSearchTest.php +++ b/tests/Feature/Catalog/CatalogSearchTest.php @@ -42,7 +42,10 @@ class CatalogSearchTest extends TestCase ->assertJsonPath('data.search_group_layout', GroupLayout::SimpleVertical->value) ->assertJsonPath('data.search_items_per_page', 24); - $this->getJson("/api/tenants/bootstrap/{$tenant->dominio}") + $this->getJson('/api/tenants/bootstrap?'.http_build_query([ + 'dominio' => $tenant->dominio, + 'path' => '/', + ])) ->assertOk() ->assertJsonPath('data.search_product_layout', ProductLayout::Row->value) ->assertJsonPath('data.search_group_layout', GroupLayout::SimpleVertical->value) diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index bacaf0c..a9d8630 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -74,7 +74,7 @@ class BootstrapTenantControllerTest extends TestCase 'display_cart' => false, ]); - $response = $this->getJson('/api/tenants/bootstrap/acme.com'); + $response = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); $response ->assertOk() @@ -117,9 +117,9 @@ class BootstrapTenantControllerTest extends TestCase 'dominio' => 'acme.com', ]); - $encodedDomain = urlencode('https://ACME.com:8080/path?foo=bar#frag'); + $encodedDomain = urlencode('https://ACME.com:8080'); - $response = $this->getJson("/api/tenants/bootstrap/{$encodedDomain}"); + $response = $this->getJson("/api/tenants/bootstrap?dominio={$encodedDomain}&path=%2F"); $response ->assertOk() @@ -127,6 +127,62 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.dominio', 'acme.com'); } + public function test_it_resolves_the_longest_tenant_prefix_for_deep_links(): void + { + $this->createTenant([ + 'codigo' => 'qa-root', + 'nombre' => 'QA Root', + 'dominio' => 'qa.onticket.com.ar', + ]); + $this->createTenant([ + 'codigo' => 'pura-tendencia', + 'nombre' => 'Pura Tendencia', + 'dominio' => 'qa.onticket.com.ar/puratendencia', + ]); + $this->createTenant([ + 'codigo' => 'pura-tendencia-vip', + 'nombre' => 'Pura Tendencia VIP', + 'dominio' => 'qa.onticket.com.ar/puratendencia/vip', + ]); + + $this->getJson('/api/tenants/bootstrap?dominio=qa.onticket.com.ar&path=%2Fpuratendencia%2Fproductos%2F123') + ->assertOk() + ->assertJsonPath('data.codigo', 'pura-tendencia') + ->assertJsonPath('data.dominio', 'qa.onticket.com.ar/puratendencia'); + + $this->getJson('/api/tenants/bootstrap?dominio=qa.onticket.com.ar&path=%2Fpuratendencia%2Fvip%2Fproductos%2F123') + ->assertOk() + ->assertJsonPath('data.codigo', 'pura-tendencia-vip') + ->assertJsonPath('data.dominio', 'qa.onticket.com.ar/puratendencia/vip'); + } + + public function test_it_uses_the_root_tenant_for_spa_paths_without_a_tenant_prefix(): void + { + $this->createTenant([ + 'codigo' => 'qa-root', + 'nombre' => 'QA Root', + 'dominio' => 'qa.onticket.com.ar', + ]); + $this->createTenant([ + 'codigo' => 'sonder', + 'nombre' => 'Sonder', + 'dominio' => 'qa.onticket.com.ar/sonder', + ]); + + $this->getJson('/api/tenants/bootstrap?dominio=qa.onticket.com.ar&path=%2Fproducto%2F123') + ->assertOk() + ->assertJsonPath('data.codigo', 'qa-root'); + + $this->getJson('/api/tenants/bootstrap?dominio=qa.onticket.com.ar&path=%2Fsonder-shop%2Fproducto%2F123') + ->assertOk() + ->assertJsonPath('data.codigo', 'qa-root'); + } + + public function test_the_legacy_bootstrap_route_is_not_available(): void + { + $this->getJson('/api/tenants/bootstrap/acme.com')->assertNotFound(); + } + public function test_it_resolves_browser_branding_from_the_tenant_before_the_website_type(): void { $typeFavicon = Attachment::query()->create([ @@ -153,7 +209,7 @@ class BootstrapTenantControllerTest extends TestCase 'website_type_code' => $websiteType->codigo, ]); - $typeResponse = $this->getJson('/api/tenants/bootstrap/acme.com'); + $typeResponse = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); $typeResponse ->assertOk() @@ -165,7 +221,7 @@ class BootstrapTenantControllerTest extends TestCase 'favicon_id' => $tenantFavicon->id, ]); - $tenantResponse = $this->getJson('/api/tenants/bootstrap/acme.com'); + $tenantResponse = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); $tenantResponse ->assertOk() @@ -177,7 +233,7 @@ class BootstrapTenantControllerTest extends TestCase { $this->createTenant(); - $this->getJson('/api/tenants/bootstrap/acme.com') + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') ->assertOk() ->assertJsonPath('data.site_title', 'ShopitFront') ->assertJsonPath('data.favicon', null); @@ -210,7 +266,7 @@ class BootstrapTenantControllerTest extends TestCase 'nombre' => 'Global', ]); - $this->getJson('/api/tenants/bootstrap/acme.com') + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') ->assertOk() ->assertJsonCount(1, 'data.categories') ->assertJsonPath('data.categories.0.nombre', 'Remeras') @@ -254,7 +310,7 @@ class BootstrapTenantControllerTest extends TestCase 'is_enabled' => false, ]); - $this->getJson('/api/tenants/bootstrap/acme.com') + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') ->assertOk() ->assertJsonMissingPath('data.website_type') ->assertJsonPath('data.extras.contact.phone', '+54 341 555 0101') @@ -263,7 +319,7 @@ class BootstrapTenantControllerTest extends TestCase public function test_it_returns_not_found_when_the_domain_does_not_exist(): void { - $response = $this->getJson('/api/tenants/bootstrap/missing.example'); + $response = $this->getJson('/api/tenants/bootstrap?dominio=missing.example&path=%2F'); $response->assertNotFound(); } @@ -317,7 +373,7 @@ class BootstrapTenantControllerTest extends TestCase $dynamicChild->code, ]); - $response = $this->getJson('/api/tenants/bootstrap/acme.com'); + $response = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); $response ->assertOk() @@ -375,7 +431,7 @@ class BootstrapTenantControllerTest extends TestCase $userRole->menus()->sync([$userMenu->code]); $adminRole->menus()->sync([$adminMenu->code]); - $this->getJson('/api/tenants/bootstrap/acme.com') + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') ->assertOk() ->assertJsonPath('data.menues.0.code', 'account') ->assertJsonMissing(['code' => 'admin.catalog']); @@ -416,7 +472,7 @@ class BootstrapTenantControllerTest extends TestCase $tenant->menues()->sync($menuCodes); $userRole->menus()->sync($menuCodes); - $this->getJson('/api/tenants/bootstrap/acme.com') + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') ->assertOk() ->assertJsonPath('data.menues.0.code', 'tree') ->assertJsonPath('data.menues.1.code', 'zeta') @@ -424,14 +480,14 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.menues.0.submenues.1.code', 'tree.fox'); } - public function test_it_rejects_duplicate_domains_after_normalization_when_storing(): void + public function test_it_allows_different_domain_paths_and_rejects_duplicate_tenant_keys(): void { $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; $firstResponse = $this->postJson('/api/tenants', [ 'codigo' => 'acme', 'nombre' => 'Acme', - 'dominio' => 'https://ACME.com/path', + 'dominio' => 'https://ACME.com/puratendencia/', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#333333', @@ -444,7 +500,7 @@ class BootstrapTenantControllerTest extends TestCase $firstResponse ->assertCreated() - ->assertJsonPath('data.dominio', 'acme.com') + ->assertJsonPath('data.dominio', 'acme.com/puratendencia') ->assertJsonPath('data.primary_color', '#111111') ->assertJsonPath('data.secondary_color', '#222222') ->assertJsonPath('data.danger_color', '#333333') @@ -480,10 +536,28 @@ class BootstrapTenantControllerTest extends TestCase str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') ); + $differentPathResponse = $this->postJson('/api/tenants', [ + 'codigo' => 'pura-tendencia', + 'nombre' => 'Pura Tendencia', + 'dominio' => 'acme.com/sonder', + 'primary_color' => '#111111', + 'secondary_color' => '#222222', + 'danger_color' => '#333333', + 'success_color' => '#555555', + 'header_bg_color' => '#444444', + 'footer_bg_color' => '#444444', + 'header_logo' => $base64Image, + 'footer_logo' => $base64Image, + ]); + + $differentPathResponse + ->assertCreated() + ->assertJsonPath('data.dominio', 'acme.com/sonder'); + $secondResponse = $this->postJson('/api/tenants', [ 'codigo' => 'globex', 'nombre' => 'Globex', - 'dominio' => 'acme.com', + 'dominio' => 'acme.com/puratendencia', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#333333', @@ -534,7 +608,7 @@ class BootstrapTenantControllerTest extends TestCase $successfulResponse = $this->putJson("/api/tenants/{$tenant->codigo}", [ 'codigo' => 'acme', 'nombre' => 'Acme Updated', - 'dominio' => 'https://ACME.com:443/admin', + 'dominio' => 'https://ACME.com:443/', 'primary_color' => '#555555', 'secondary_color' => '#666666', 'danger_color' => '#777777', @@ -582,7 +656,7 @@ class BootstrapTenantControllerTest extends TestCase $failingResponse = $this->putJson("/api/tenants/{$otherTenant->codigo}", [ 'codigo' => 'globex', 'nombre' => 'Globex', - 'dominio' => 'https://ACME.com/storefront', + 'dominio' => 'https://ACME.com/', ]); $failingResponse diff --git a/tests/Feature/Tenant/TenantBootstrapRouteTest.php b/tests/Feature/Tenant/TenantBootstrapRouteTest.php new file mode 100644 index 0000000..2faaebe --- /dev/null +++ b/tests/Feature/Tenant/TenantBootstrapRouteTest.php @@ -0,0 +1,19 @@ +getRoutes()->match( + Request::create('/api/tenants/bootstrap', 'GET') + ); + + $this->assertSame(TenantBootstrapController::class, $route->getActionName()); + } +} diff --git a/tests/Unit/Tenant/TenantDomainNormalizerTest.php b/tests/Unit/Tenant/TenantDomainNormalizerTest.php new file mode 100644 index 0000000..ce0037c --- /dev/null +++ b/tests/Unit/Tenant/TenantDomainNormalizerTest.php @@ -0,0 +1,76 @@ +assertSame( + $expected, + TenantDomainNormalizer::normalizeTenantKey($domain, $path), + ); + } + + /** @return iterable */ + public static function tenantKeys(): iterable + { + yield 'pura tendencia' => [ + 'QA.ONTICKET.COM.AR', + '/puratendencia', + 'qa.onticket.com.ar/puratendencia', + ]; + + yield 'sonder with trailing slash' => [ + 'qa.onticket.com.ar', + '/sonder/', + 'qa.onticket.com.ar/sonder', + ]; + + yield 'root tenant' => [ + 'fnfi.onticket.com.ar', + '/', + 'fnfi.onticket.com.ar', + ]; + + yield 'full tenant URL when storing' => [ + 'https://QA.ONTICKET.COM.AR/puratendencia/', + null, + 'qa.onticket.com.ar/puratendencia', + ]; + } + + public function test_it_builds_tenant_key_candidates_from_the_longest_path_to_the_root(): void + { + $this->assertSame([ + 'qa.onticket.com.ar/puratendencia/vip/productos/123', + 'qa.onticket.com.ar/puratendencia/vip/productos', + 'qa.onticket.com.ar/puratendencia/vip', + 'qa.onticket.com.ar/puratendencia', + 'qa.onticket.com.ar', + ], TenantDomainNormalizer::tenantKeyCandidates( + 'QA.ONTICKET.COM.AR', + '/puratendencia/vip/productos/123?ref=home', + )); + } + + public function test_tenant_key_candidates_only_include_complete_path_segments(): void + { + $this->assertSame([ + 'qa.onticket.com.ar/sonder-shop/productos', + 'qa.onticket.com.ar/sonder-shop', + 'qa.onticket.com.ar', + ], TenantDomainNormalizer::tenantKeyCandidates( + 'qa.onticket.com.ar', + '/sonder-shop/productos', + )); + } +}