1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 12:54:05 +00:00

Early identification support (#1)

* wip

* Improve tests

* rename class

* wip

* improve tests

* introduce early identification middlewares

* Update PreventAccessFromCentralDomains.php

* method rename

* method rename

* Update UniversalRouteTest.php

* Update UniversalRouteTest.php

* Update EarlyIdentificationTest.php

* remove early classes and add check in existing classes

* MWs improvements

* Update UniversalRouteTest.php

* Fix code style (php-cs-fixer)

* trigger ci

* fix failing test after merge

* add test for universal route in early identification

* Update InitializeTenancyByDomain.php

* Update UniversalRouteTest.php

* remove `routeHasMiddleware` method from MW and use the UniversalRoutes class method

* helper dockblock

* add test

* handle universal routes in early identification

* remove UniversalRoute class because we are not using it anymore

* rename class from PreventAccessFromCentralDomains to PreventAccessFromUnwantedDomains

* improvements after self review

* Update PreventAccessFromUnwantedDomains.php

* remove inline class namespaces

* remove DomainTenant class and use the Tenant class

* update comment

* removed custom expection and add method

* Update tests/EarlyIdentificationTest.php

Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>

* use ltrim and simplify the comment

* remove comments and typhint

* dataset and keys rename

* rename $route parameter

* removed helper functions

* fix style

* Update InitializeTenancyByPath.php

* Update tests/EarlyIdentificationTest.php

* code style

* improve subdomain test

* use TenancyInitialized event and remove DomainTenant alias

* remove comment and move expectException below

* code style

* use TenancyInitialized event

* improve test

* improve datasets

* Initialized -> Initializing

* Update InitializeTenancyByPath.php

* remove todo

* Fix code style (php-cs-fixer)

* refactor helper method

* Update UniversalRouteTest.php

* add note above test

* remove after each hook

* renamed universal_middleware to global_middleware

* remove helper and improve url names

* change check position

* Revert "change check position"

This reverts commit e4371d2f3aa8ad7ae5e5b7d15781b72a5f1be03c.

* repositioned central check

* add comment

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
Abrar Ahmad 2022-11-20 06:31:37 +05:00 committed by GitHub
parent 9520cbc811
commit ff46bcfe20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 330 additions and 163 deletions

View file

@ -163,17 +163,17 @@ test('rollback command works', function () {
expect(Schema::hasTable('users'))->toBeFalse();
});
test('seed command works', function (){
test('seed command works', function () {
$tenant = Tenant::create();
Artisan::call('tenants:migrate');
$tenant->run(function (){
$tenant->run(function () {
expect(DB::table('users')->count())->toBe(0);
});
Artisan::call('tenants:seed', ['--class' => TestSeeder::class]);
$tenant->run(function (){
$tenant->run(function () {
$user = DB::table('users');
expect($user->count())->toBe(1)
->and($user->first()->email)->toBe('seeded@user');

View file

@ -9,7 +9,7 @@ beforeEach(function () {
config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]);
});
test('job delete domains successfully', function (){
test('job delete domains successfully', function () {
$tenant = DatabaseAndDomainTenant::create();
$tenant->domains()->create([

View file

@ -8,7 +8,6 @@ use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Features\UniversalRoutes;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
@ -95,7 +94,6 @@ test('throw correct exception when onFail is null and universal routes are enabl
// Enable UniversalRoute feature
Route::middlewareGroup('universal', []);
config(['tenancy.features' => [UniversalRoutes::class]]);
$this->withoutExceptionHandling()->get('http://foo.localhost/foo/abc/xyz');
})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);;

View file

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\Controller;
use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () {
config()->set([
'tenancy.token' => 'central-abc123',
]);
Event::listen(TenancyInitialized::class, function (TenancyInitialized $event) {
config()->set([
'tenancy.token' => $event->tenancy->tenant->getTenantKey() . '-abc123',
]);
});
});
test('early identification works with path identification', function () {
app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class);
Route::group([
'prefix' => '/{tenant}',
], function () {
Route::get('/foo', [Controller::class, 'index'])->name('foo');
});
Tenant::create([
'id' => 'acme',
]);
$response = pest()->get('/acme/foo')->assertOk();
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
// check if default parameter feature is working fine by asserting that the route WITHOUT the tenant parameter
// matches the route WITH the tenant parameter
expect(route('foo'))->toBe(route('foo', ['tenant' => 'acme']));
});
test('early identification works with request data identification', function (string $type) {
app(Kernel::class)->pushMiddleware(InitializeTenancyByRequestData::class);
Route::get('/foo', [Controller::class, 'index'])->name('foo');
$tenant = Tenant::create([
'id' => 'acme',
]);
if ($type === 'header') {
$response = pest()->get('/foo', ['X-Tenant' => $tenant->id])->assertOk();
} elseif ($type === 'queryParameter') {
$response = pest()->get("/foo?tenant=$tenant->id")->assertOk();
}
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
})->with([
'using request header parameter' => 'header',
'using request query parameter' => 'queryParameter'
]);
// The name of this test is suffixed by the dataset — domain / subdomain / domainOrSubdomain identification
test('early identification works', function (string $middleware, string $domain, string $url) {
app(Kernel::class)->pushMiddleware($middleware);
config(['tenancy.tenant_model' => Tenant::class]);
Route::get('/foo', [Controller::class, 'index'])
->middleware(PreventAccessFromUnwantedDomains::class)
->name('foo');
$tenant = Tenant::create();
$tenant->domains()->create([
'domain' => $domain,
]);
$response = pest()->get($url)->assertOk();
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
})->with([
'domain identification' => ['middleware' => InitializeTenancyByDomain::class, 'domain' => 'foo.test', 'url' => 'http://foo.test/foo'],
'subdomain identification' => ['middleware' => InitializeTenancyBySubdomain::class, 'domain' => 'foo', 'url' => 'http://foo.localhost/foo'],
'domainOrSubdomain identification using domain' => ['middleware' => InitializeTenancyByDomainOrSubdomain::class, 'domain' => 'foo.test', 'url' => 'http://foo.test/foo'],
'domainOrSubdomain identification using subdomain' => ['middleware' => InitializeTenancyByDomainOrSubdomain::class, 'domain' => 'foo', 'url' => 'http://foo.localhost/foo'],
]);
function assertTenancyInitializedInEarlyIdentificationRequest(string|false $string): void
{
expect($string)->toBe(tenant()->getTenantKey() . '-abc123'); // Assert that the service class returns tenant value
expect(app()->make('additionalMiddlewareRunsInTenantContext'))->toBeTrue(); // Assert that middleware added in the controller constructor runs in tenant context
expect(app()->make('controllerRunsInTenantContext'))->toBeTrue(); // Assert that tenancy is initialized in the controller constructor
}

View file

@ -0,0 +1,16 @@
<?php
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
use Closure;
use Illuminate\Http\Request;
class AdditionalMiddleware
{
public function handle(Request $request, Closure $next): mixed
{
app()->instance('additionalMiddlewareRunsInTenantContext', tenancy()->initialized);
return $next($request);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
public function __construct(public Service $service)
{
app()->instance('controllerRunsInTenantContext', tenancy()->initialized);
$this->middleware(AdditionalMiddleware::class);
}
public function index(): string
{
return $this->service->token;
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
class Service
{
public string $token;
public function __construct()
{
$this->token = config('tenancy.token');
}
}

View file

@ -52,12 +52,13 @@ test('onfail logic can be customized', function () {
->assertSee('foo');
});
test('localhost is not a valid subdomain', function () {
test('archte.ch is not a valid subdomain', function () {
pest()->expectException(NotASubdomainException::class);
// This gets routed to the app, but with a request domain of 'archte.ch'
$this
->withoutExceptionHandling()
->get('http://localhost/foo/abc/xyz');
->get('http://archte.ch/foo/abc/xyz');
});
test('ip address is not a valid subdomain', function () {
@ -65,7 +66,7 @@ test('ip address is not a valid subdomain', function () {
$this
->withoutExceptionHandling()
->get('http://127.0.0.1/foo/abc/xyz');
->get('http://127.0.0.2/foo/abc/xyz');
});
test('oninvalidsubdomain logic can be customized', function () {
@ -81,7 +82,7 @@ test('oninvalidsubdomain logic can be customized', function () {
$this
->withoutExceptionHandling()
->get('http://127.0.0.1/foo/abc/xyz')
->get('http://127.0.0.2/foo/abc/xyz')
->assertSee('foo custom invalid subdomain handler');
});
@ -106,26 +107,6 @@ test('we cant use a subdomain that doesnt belong to our central domains', functi
->get('http://foo.localhost/foo/abc/xyz');
});
test('central domain is not a subdomain', function () {
config(['tenancy.central_domains' => [
'localhost',
]]);
$tenant = SubdomainTenant::create([
'id' => 'acme',
]);
$tenant->domains()->create([
'domain' => 'acme',
]);
pest()->expectException(NotASubdomainException::class);
$this
->withoutExceptionHandling()
->get('http://localhost/foo/abc/xyz');
});
class SubdomainTenant extends Models\Tenant
{
use HasDomains;

View file

@ -3,27 +3,24 @@
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Features\UniversalRoutes;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Contracts\Http\Kernel;
afterEach(function () {
InitializeTenancyByDomain::$onFail = null;
});
test('a route can work in both central and tenant context', function (array $routeMiddleware, string|null $globalMiddleware) {
if ($globalMiddleware) {
app(Kernel::class)->pushMiddleware($globalMiddleware);
}
test('a route can work in both central and tenant context', function () {
Route::middlewareGroup('universal', []);
config(['tenancy.features' => [UniversalRoutes::class]]);
Route::get('/foo', function () {
return tenancy()->initialized
? 'Tenancy is initialized.'
: 'Tenancy is not initialized.';
})->middleware(['universal', InitializeTenancyByDomain::class]);
pest()->get('http://localhost/foo')
->assertSuccessful()
->assertSee('Tenancy is not initialized.');
})->middleware($routeMiddleware);
$tenant = Tenant::create([
'id' => 'acme',
@ -32,28 +29,33 @@ test('a route can work in both central and tenant context', function () {
'domain' => 'acme.localhost',
]);
pest()->get('http://acme.localhost/foo')
pest()->get("http://localhost/foo")
->assertSuccessful()
->assertSee('Tenancy is not initialized.');
pest()->get("http://acme.localhost/foo")
->assertSuccessful()
->assertSee('Tenancy is initialized.');
});
})->with('identification types');
test('making one route universal doesnt make all routes universal', function () {
Route::get('/bar', function () {
return tenant('id');
})->middleware(InitializeTenancyByDomain::class);
test('making one route universal doesnt make all routes universal', function (array $routeMiddleware, string|null $globalMiddleware) {
if ($globalMiddleware) {
app(Kernel::class)->pushMiddleware($globalMiddleware);
}
Route::middlewareGroup('universal', []);
config(['tenancy.features' => [UniversalRoutes::class]]);
Route::get('/foo', function () {
return tenancy()->initialized
? 'Tenancy is initialized.'
: 'Tenancy is not initialized.';
})->middleware(['universal', InitializeTenancyByDomain::class]);
Route::middleware($routeMiddleware)->group(function () {
Route::get('/nonuniversal', function () {
return tenant('id');
});
pest()->get('http://localhost/foo')
->assertSuccessful()
->assertSee('Tenancy is not initialized.');
Route::get('/universal', function () {
return tenancy()->initialized
? 'Tenancy is initialized.'
: 'Tenancy is not initialized.';
})->middleware('universal');
});
$tenant = Tenant::create([
'id' => 'acme',
@ -62,16 +64,57 @@ test('making one route universal doesnt make all routes universal', function ()
'domain' => 'acme.localhost',
]);
pest()->get('http://acme.localhost/foo')
pest()->get("http://localhost/universal")
->assertSuccessful()
->assertSee('Tenancy is not initialized.');
pest()->get("http://acme.localhost/universal")
->assertSuccessful()
->assertSee('Tenancy is initialized.');
tenancy()->end();
pest()->get('http://localhost/bar')
->assertStatus(500);
pest()->get('http://localhost/nonuniversal')
->assertStatus(404);
pest()->get('http://acme.localhost/bar')
pest()->get('http://acme.localhost/nonuniversal')
->assertSuccessful()
->assertSee('acme');
});
})->with([
'early identification' => [
'route_middleware' => [PreventAccessFromUnwantedDomains::class],
'global_middleware' => InitializeTenancyByDomain::class,
],
'route-level identification' => [
'route_middleware' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class],
'global_middleware' => null,
]
]);
test('it throws correct exception when route is universal and tenant does not exist', function (array $routeMiddleware, string|null $globalMiddleware) {
if ($globalMiddleware) {
app(Kernel::class)->pushMiddleware($globalMiddleware);
}
Route::middlewareGroup('universal', []);
Route::get('/foo', function () {
return tenancy()->initialized
? 'Tenancy is initialized.'
: 'Tenancy is not initialized.';
})->middleware($routeMiddleware);
pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class);
$this->withoutExceptionHandling()->get('http://acme.localhost/foo');
})->with('identification types');
dataset('identification types', [
'early identification' => [
'route_middleware' => ['universal', PreventAccessFromUnwantedDomains::class],
'global_middleware' => InitializeTenancyByDomain::class,
],
'route-level identification' => [
'route_middleware' => ['universal', PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class],
'global_middleware' => null,
]
]);