feat(bootstrap): update tenant bootstrap endpoint to accept domain and path as query parameters, enhancing tenant resolution logic
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "/"}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, mixed> */
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string> */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
19
tests/Feature/Tenant/TenantBootstrapRouteTest.php
Normal file
19
tests/Feature/Tenant/TenantBootstrapRouteTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Domains\Bootstrap\Controllers\TenantBootstrapController;
|
||||
use Illuminate\Http\Request;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantBootstrapRouteTest extends TestCase
|
||||
{
|
||||
public function test_the_static_bootstrap_route_takes_precedence_over_the_tenant_resource(): void
|
||||
{
|
||||
$route = app('router')->getRoutes()->match(
|
||||
Request::create('/api/tenants/bootstrap', 'GET')
|
||||
);
|
||||
|
||||
$this->assertSame(TenantBootstrapController::class, $route->getActionName());
|
||||
}
|
||||
}
|
||||
76
tests/Unit/Tenant/TenantDomainNormalizerTest.php
Normal file
76
tests/Unit/Tenant/TenantDomainNormalizerTest.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Tenant;
|
||||
|
||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TenantDomainNormalizerTest extends TestCase
|
||||
{
|
||||
#[DataProvider('tenantKeys')]
|
||||
public function test_it_normalizes_a_hostname_and_path_as_a_single_tenant_key(
|
||||
string $domain,
|
||||
?string $path,
|
||||
string $expected,
|
||||
): void {
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
TenantDomainNormalizer::normalizeTenantKey($domain, $path),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string, ?string, string}> */
|
||||
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',
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user