mirror of
https://github.com/archtechx/tenancy.git
synced 2026-06-21 03:04:04 +00:00
Merge branch 'master' into validate-sql-parameters
This commit is contained in:
commit
9055b61a04
20 changed files with 546 additions and 98 deletions
|
|
@ -401,3 +401,47 @@ test('the bypass parameter works correctly with temporarySignedRoute', function(
|
|||
->toContain('localhost/foo')
|
||||
->not()->toContain('central='); // Bypass parameter gets removed from the generated URL
|
||||
});
|
||||
|
||||
test('toRoute can automatically prefix the passed route name', function () {
|
||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||
|
||||
Route::get('/central/home', fn () => 'central')->name('home');
|
||||
Route::get('/tenant/home', fn () => 'tenant')->name('tenant.home');
|
||||
|
||||
TenancyUrlGenerator::$prefixRouteNames = true;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$centralRoute = Route::getRoutes()->getByName('home');
|
||||
|
||||
// url()->toRoute() prefixes the name of the passed route ('home') with the tenant prefix
|
||||
// and generates the URL for the tenant route (as if the 'tenant.home' route was passed to the method)
|
||||
expect(url()->toRoute($centralRoute, [], true))->toBe('http://localhost/tenant/home');
|
||||
|
||||
// Passing the bypass parameter skips the name prefixing, so the method returns the central route URL
|
||||
expect(url()->toRoute($centralRoute, ['central' => true], true))->toBe('http://localhost/central/home');
|
||||
});
|
||||
|
||||
test('toRoute modifies parameters even when the route has no name', function () {
|
||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||
|
||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
||||
|
||||
$unnamedRoute = Route::get('/unnamed', fn () => 'unnamed');
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// The tenant parameter is added to the URL even for unnamed routes
|
||||
expect(url()->toRoute($unnamedRoute, [], true))
|
||||
->toBe("http://localhost/unnamed?tenant={$tenant->getTenantKey()}");
|
||||
|
||||
// The bypass parameter prevents passing the tenant parameter and is stripped from the URL
|
||||
expect(url()->toRoute($unnamedRoute, ['central' => true], true))
|
||||
->toBe("http://localhost/unnamed")
|
||||
->not()->toContain('tenant=')
|
||||
->not()->toContain('central=');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,17 +2,27 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Jobs\DeleteDatabase;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
use Stancl\Tenancy\Jobs\SeedDatabase;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Foundation\Auth\User as Authenticable;
|
||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||
|
||||
beforeEach($cleanup = function () {
|
||||
DeleteDatabase::$ignoreFailures = false;
|
||||
DeleteDatabase::$skipWhenCreateDatabaseIsFalse = true;
|
||||
});
|
||||
|
||||
afterEach($cleanup);
|
||||
|
||||
test('database can be created after tenant creation', function () {
|
||||
config(['tenancy.database.template_tenant_connection' => 'mysql']);
|
||||
|
||||
|
|
@ -82,6 +92,73 @@ test('custom job can be added to the pipeline', function () {
|
|||
});
|
||||
});
|
||||
|
||||
test('database can be deleted after tenant deletion', function () {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
Event::listen(TenantDeleted::class, JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
|
||||
|
||||
$tenant->delete();
|
||||
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeFalse();
|
||||
});
|
||||
|
||||
test('database deletion is skipped when create_database is false', function (bool $skipWhenCreateDatabaseIsFalse) {
|
||||
Event::listen(TenantDeleted::class, JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
// create_database=false means no DB is created (e.g. tenant uses a pre-existing DB)
|
||||
// On deletion, DeleteDatabase should skip rather than attempting DROP DATABASE on a non-existent DB
|
||||
$tenant = Tenant::create(['tenancy_create_database' => false, 'tenancy_db_name' => 'non_existing_db']);
|
||||
|
||||
$manager = $tenant->database()->manager();
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeFalse();
|
||||
|
||||
DeleteDatabase::$skipWhenCreateDatabaseIsFalse = $skipWhenCreateDatabaseIsFalse;
|
||||
|
||||
if ($skipWhenCreateDatabaseIsFalse) {
|
||||
$tenant->delete(); // no exception
|
||||
} else {
|
||||
expect(fn () => $tenant->delete())->toThrow(QueryException::class, "database doesn't exist");
|
||||
}
|
||||
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeFalse();
|
||||
})->with([true, false]);
|
||||
|
||||
test('database deletion failure is ignored when ignoreFailures is true', function (bool $ignoreFailures) {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
Event::listen(TenantDeleted::class, JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
DeleteDatabase::$ignoreFailures = $ignoreFailures;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$manager = $tenant->database()->manager();
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
|
||||
|
||||
$manager->deleteDatabase($tenant); // manually delete so the job fails
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeFalse();
|
||||
|
||||
if ($ignoreFailures) {
|
||||
$tenant->delete(); // no exception
|
||||
} else {
|
||||
expect(fn () => $tenant->delete())->toThrow(QueryException::class, "database doesn't exist");
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
class User extends Authenticable
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ test('using different default route modes works with global domain identificatio
|
|||
$exception = match ($middleware) {
|
||||
InitializeTenancyByDomain::class => TenantCouldNotBeIdentifiedOnDomainException::class,
|
||||
InitializeTenancyBySubdomain::class => NotASubdomainException::class,
|
||||
InitializeTenancyByDomainOrSubdomain::class => NotASubdomainException::class,
|
||||
InitializeTenancyByDomainOrSubdomain::class => TenantCouldNotBeIdentifiedOnDomainException::class,
|
||||
};
|
||||
|
||||
expect(fn () => $this->withoutExceptionHandling()->get('http://localhost/central-route'))->toThrow($exception);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,25 @@ use Stancl\Tenancy\Events\PendingTenantPulled;
|
|||
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
use Stancl\Tenancy\Jobs\SeedDatabase;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
|
||||
beforeEach($cleanup = function () {
|
||||
Tenant::$extraCustomColumns = [];
|
||||
Tenant::$getPendingAttributesUsing = null;
|
||||
|
||||
MigrateDatabase::$includePending = true;
|
||||
SeedDatabase::$includePending = true;
|
||||
});
|
||||
|
||||
afterEach($cleanup);
|
||||
|
|
@ -154,8 +169,8 @@ test('pending events are dispatched', function () {
|
|||
Event::assertDispatched(PendingTenantPulled::class);
|
||||
});
|
||||
|
||||
test('commands do not run for pending tenants if tenancy.pending.include_in_queries is false and the with pending option does not get passed', function() {
|
||||
config(['tenancy.pending.include_in_queries' => false]);
|
||||
test('commands include tenants based on the include_in_queries config when --with-pending is not passed', function (bool $includeInQueries) {
|
||||
config(['tenancy.pending.include_in_queries' => $includeInQueries]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
|
|
@ -164,21 +179,21 @@ test('commands do not run for pending tenants if tenancy.pending.include_in_quer
|
|||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
$command = pest()->artisan("tenants:run 'bar testing testing@test.test password foo'");
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||
$tenants->each(function ($tenant) use ($command, $includeInQueries) {
|
||||
if ($tenant->pending() && ! $includeInQueries) {
|
||||
$command->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}");
|
||||
} else {
|
||||
$command->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}");
|
||||
}
|
||||
});
|
||||
|
||||
$pendingTenants = $tenants->filter->pending();
|
||||
$readyTenants = $tenants->reject->pending();
|
||||
$command->assertSuccessful();
|
||||
})->with([true, false]);
|
||||
|
||||
$pendingTenants->each(fn ($tenant) => $artisan->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
$readyTenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('commands run for pending tenants too if tenancy.pending.include_in_queries is true', function() {
|
||||
config(['tenancy.pending.include_in_queries' => true]);
|
||||
test('commands include pending tenants when truthy --with-pending is passed', function (bool $includeInQueries) {
|
||||
config(['tenancy.pending.include_in_queries' => $includeInQueries]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
|
|
@ -187,17 +202,22 @@ test('commands run for pending tenants too if tenancy.pending.include_in_queries
|
|||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
foreach ([
|
||||
'--with-pending',
|
||||
'--with-pending=true',
|
||||
'--with-pending=1'
|
||||
] as $option) {
|
||||
$command = pest()->artisan("tenants:run 'bar testing testing@test.test password foo' {$option}");
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||
// Pending tenants are included regardless of tenancy.pending.include_in_queries
|
||||
$tenants->each(fn ($tenant) => $command->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
$command->assertSuccessful();
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('commands run for pending tenants too if the with pending option is passed', function() {
|
||||
config(['tenancy.pending.include_in_queries' => false]);
|
||||
test('commands exclude pending tenants when falsy --with-pending is passed', function (bool $includeInQueries) {
|
||||
config(['tenancy.pending.include_in_queries' => $includeInQueries]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
|
|
@ -206,14 +226,25 @@ test('commands run for pending tenants too if the with pending option is passed'
|
|||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
foreach ([
|
||||
'--with-pending=false',
|
||||
'--with-pending=0',
|
||||
'--with-pending=foo' // Invalid values are treated as false
|
||||
] as $option) {
|
||||
$command = pest()->artisan("tenants:run 'bar testing testing@test.test password foo' {$option}");
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz' --with-pending");
|
||||
$tenants->each(function ($tenant) use ($command) {
|
||||
if ($tenant->pending()) {
|
||||
// Pending tenants are excluded regardless of tenancy.pending.include_in_queries
|
||||
$command->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}");
|
||||
} else {
|
||||
$command->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}");
|
||||
}
|
||||
});
|
||||
|
||||
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
$command->assertSuccessful();
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
test('pending tenants can have default attributes for non-nullable columns', function (bool $withPendingAttributes) {
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
|
|
@ -236,3 +267,105 @@ test('pending tenants can have default attributes for non-nullable columns', fun
|
|||
else
|
||||
expect($fn)->toThrow(QueryException::class);
|
||||
})->with([true, false]);
|
||||
|
||||
test('pending tenant databases can be migrated using a job unless configured otherwise', function (bool $includeInQueries, ?bool $migrateWithPending) {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
|
||||
'tenancy.pending.include_in_queries' => $includeInQueries,
|
||||
]);
|
||||
|
||||
MigrateDatabase::$includePending = $migrateWithPending;
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([
|
||||
CreateDatabase::class,
|
||||
MigrateDatabase::class,
|
||||
])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$pendingTenant = Tenant::createPending();
|
||||
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
|
||||
tenancy()->initialize($pendingTenant);
|
||||
|
||||
// MigrateDatabase includes/excludes pending tenants based on its $includePending property,
|
||||
// regardless of the tenancy.pending.include_in_queries config.
|
||||
expect(Schema::hasTable('users'))->toBe($migrateWithPending ?? $includeInQueries);
|
||||
})->with([
|
||||
'include pending in queries' => [true],
|
||||
'exclude pending from queries' => [false],
|
||||
])->with([
|
||||
'migrate with pending' => [true],
|
||||
'migrate without pending' => [false],
|
||||
'default to config' => [null],
|
||||
]);
|
||||
|
||||
test('pending tenant databases can be seeded using a job unless configured otherwise', function (bool $includeInQueries, ?bool $seedWithPending) {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
|
||||
'tenancy.pending.include_in_queries' => $includeInQueries,
|
||||
'tenancy.seeder_parameters.--class' => TestSeeder::class,
|
||||
]);
|
||||
|
||||
MigrateDatabase::$includePending = true;
|
||||
SeedDatabase::$includePending = $seedWithPending;
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([
|
||||
CreateDatabase::class,
|
||||
MigrateDatabase::class,
|
||||
SeedDatabase::class,
|
||||
])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$pendingTenant = Tenant::createPending();
|
||||
|
||||
tenancy()->initialize($pendingTenant);
|
||||
|
||||
// SeedDatabase includes/excludes pending tenants based on its $includePending property,
|
||||
// regardless of the tenancy.pending.include_in_queries config.
|
||||
expect(User::where('email', 'seeded@user')->exists())->toBe($seedWithPending ?? $includeInQueries);
|
||||
})->with([
|
||||
'include pending in queries' => [true],
|
||||
'exclude pending from queries' => [false],
|
||||
])->with([
|
||||
'seed with pending' => [true],
|
||||
'seed without pending' => [false],
|
||||
'default to config' => [null],
|
||||
]);
|
||||
|
||||
test('jobs that run before tenants get fully created recognize pending tenants', function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([
|
||||
CreateDatabase::class,
|
||||
PendingTenantJob::class,
|
||||
])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
Tenant::createPending();
|
||||
|
||||
expect(app('tenant_is_pending'))->toBeTrue();
|
||||
});
|
||||
|
||||
class PendingTenantJob
|
||||
{
|
||||
public function __construct(
|
||||
public Tenant $tenant,
|
||||
) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
app()->instance('tenant_is_pending', $this->tenant->pending());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains;
|
|||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
use function Stancl\Tenancy\Tests\pest;
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
@ -108,6 +109,14 @@ test('we cant use a subdomain that doesnt belong to our central domains', functi
|
|||
->get('http://foo.localhost/foo/abc/xyz');
|
||||
});
|
||||
|
||||
test('domain resolver correctly determines if string is a subdomain', function() {
|
||||
config(['tenancy.identification.central_domains' => ['site.com', 'blog.site.com']]);
|
||||
|
||||
expect(DomainTenantResolver::isSubdomain('blog.site.com'))->toBeFalse();
|
||||
expect(DomainTenantResolver::isSubdomain('tenant.site.com'))->toBeTrue();
|
||||
expect(DomainTenantResolver::isSubdomain('tenantsite.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
class SubdomainTenant extends Models\Tenant
|
||||
{
|
||||
use HasDomains;
|
||||
|
|
|
|||
|
|
@ -89,13 +89,14 @@ test('tenant user can be impersonated on a tenant domain', function () {
|
|||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
expect(session('tenancy_impersonation_guard'))->toBe('web');
|
||||
expect($token->auth_guard)->toBe('web');
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::stopImpersonating();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
expect(session('tenancy_impersonation_guard'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('http://foo.localhost/dashboard')
|
||||
|
|
@ -135,19 +136,113 @@ test('tenant user can be impersonated on a tenant path', function () {
|
|||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
expect(session('tenancy_impersonation_guard'))->toBe('web');
|
||||
expect($token->auth_guard)->toBe('web');
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::stopImpersonating();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
expect(session('tenancy_impersonation_guard'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('/acme/dashboard')
|
||||
->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('stopImpersonating can keep the user authenticated', function () {
|
||||
makeLoginRoute();
|
||||
|
||||
Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false));
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'id' => 'acme',
|
||||
'tenancy_db_name' => 'db' . Str::random(16),
|
||||
]);
|
||||
|
||||
migrateTenants();
|
||||
|
||||
$user = $tenant->run(function () {
|
||||
return ImpersonationUser::create([
|
||||
'name' => 'Joe',
|
||||
'email' => 'joe@local',
|
||||
'password' => bcrypt('secret'),
|
||||
]);
|
||||
});
|
||||
|
||||
// Impersonate the user
|
||||
$token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard');
|
||||
|
||||
pest()->get('/acme/impersonate/' . $token->token)
|
||||
->assertRedirect('/acme/dashboard');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
|
||||
// Stop impersonating without logging out
|
||||
UserImpersonation::stopImpersonating(false);
|
||||
|
||||
// The impersonation session key should be cleared
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonation_guard'))->toBeNull();
|
||||
|
||||
// The user should still be authenticated
|
||||
pest()->get('/acme/dashboard')
|
||||
->assertSuccessful()
|
||||
->assertSee('You are logged in as Joe');
|
||||
});
|
||||
|
||||
test('stopImpersonating logs out the user from the impersonation guard stored in session', function () {
|
||||
Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false));
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'id' => 'acme',
|
||||
'tenancy_db_name' => 'db' . Str::random(16),
|
||||
]);
|
||||
|
||||
migrateTenants();
|
||||
|
||||
$user = $tenant->run(function () {
|
||||
return ImpersonationUser::create([
|
||||
'name' => 'Joe',
|
||||
'email' => 'joe@local',
|
||||
'password' => bcrypt('secret'),
|
||||
]);
|
||||
});
|
||||
|
||||
// Impersonate the user
|
||||
$token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard');
|
||||
|
||||
pest()->get('/acme/impersonate/' . $token->token)
|
||||
->assertRedirect('/acme/dashboard');
|
||||
|
||||
expect(session('tenancy_impersonation_guard'))->toBe('web');
|
||||
|
||||
// Impersonation logged in the user using the current guard ('web')
|
||||
expect(auth('web')->check())->toBeTrue();
|
||||
|
||||
config(['auth.guards.test' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
]]);
|
||||
|
||||
// Manually log the user in through the 'test' guard
|
||||
auth('test')->loginUsingId($user->id);
|
||||
|
||||
// Should log the user out from the guard used for impersonation ('web')
|
||||
UserImpersonation::stopImpersonating();
|
||||
|
||||
expect(auth('web')->check())->toBeFalse();
|
||||
expect(auth('test')->check())->toBeTrue();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
|
||||
// tenancy_impersonation_guard isn't in the session anymore,
|
||||
// stopImpersonating should throw an exception instead of logging out
|
||||
expect(fn() => UserImpersonation::stopImpersonating())->toThrow(Exception::class);
|
||||
|
||||
expect(auth('test')->check())->toBeTrue();
|
||||
});
|
||||
|
||||
test('tokens have a limited ttl', function () {
|
||||
Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes());
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue