mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 12:44:02 +00:00
* Give universal flag the highest priority (wip) * Stop forgetting tenant parameter when route-level path ID is used * Fix PHPStan errors * Simplify annotation * Fix comment * Correct annotations * Improve requestHasTenant comment * Make cloning logic only clone universal routes, delete the universal flag from the new (tenant) route * Delete APP_DEBUG * make if condition easier to read * Update DealsWithRouteContexts.php * Fix test * Fix code style (php-cs-fixer) * Move tests * Delete incorrectly committed file * Cloning routes update wip * Route cloning rework WIP * Add todo to clone routes * Fix code style (php-cs-fixer) * Route cloning fix WIP * Set CloneRoutesAsTenant::$tenantMiddleware to ID MW * Revert CloneRoutesAsTenant::$tenantMiddleware-related changes * Simplify requestHasTenant * Add and test 'ckone' flag * Delete setting $skippedRoutes from CloneRoutesAsTenant * Fix code style (php-cs-fixer) * make config key used for testing distinct from normal tenancy config keys * Update src/Actions/CloneRoutesAsTenant.php * Move 'path identification types' dataset to CloneActionTest --------- Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> Co-authored-by: Samuel Štancl <samuel@archte.ch> Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
420 lines
16 KiB
PHP
420 lines
16 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Stancl\Tenancy\Tenancy;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Routing\Route;
|
||
use Stancl\Tenancy\Enums\RouteMode;
|
||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||
use Illuminate\Contracts\Http\Kernel;
|
||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||
use Illuminate\Routing\Controller as BaseController;
|
||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
|
||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||
use Stancl\Tenancy\Middleware\IdentificationMiddleware;
|
||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||
use Stancl\Tenancy\Exceptions\MiddlewareNotUsableWithUniversalRoutesException;
|
||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
||
|
||
test('a route can be universal using domain identification', function (array $routeMiddleware, array $globalMiddleware) {
|
||
foreach ($globalMiddleware as $middleware) {
|
||
// Instead of a global 'universal' MW, we use the default_route_mode config key to make routes universal by default
|
||
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->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.';
|
||
}
|
||
}
|