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

Merge branch 'master' into fix-url-bootstrappers

This commit is contained in:
Samuel Štancl 2024-11-25 04:51:57 +01:00
commit 58cdae908a
44 changed files with 584 additions and 260 deletions

View file

@ -48,10 +48,6 @@ test('BroadcastChannelPrefixBootstrapper prefixes the channels events are broadc
$table->timestamps();
});
universal_channel('users.{userId}', function ($user, $userId) {
return User::find($userId)->is($user);
});
$broadcaster = app(BroadcastManager::class)->driver();
$tenant = Tenant::create();

View file

@ -136,14 +136,18 @@ test('broadcasting channel helpers register channels correctly', function() {
// Tenant channel registered its name is correctly prefixed ("{tenant}.user.{userId}")
$tenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName");
expect($tenantChannelClosure)
->not()->toBeNull() // Channel registered
->not()->toBe($centralChannelClosure); // The tenant channel closure is different after the auth user, it accepts the tenant ID
expect($tenantChannelClosure)->toBe($centralChannelClosure);
// The tenant channels are prefixed with '{tenant}.'
// They accept the tenant key, but their closures only run in tenant context when tenancy is initialized
// The regular channels don't accept the tenant key, but they also respect the current context
// The tenant key is used solely for the name prefixing the closures can still run in the central context
tenant_channel($channelName, $tenantChannelClosure = function ($user, $tenant, $userName) {
return User::firstWhere('name', $userName)?->is($user) ?? false;
});
expect($tenantChannelClosure)->not()->toBe($centralChannelClosure);
expect($tenantChannelClosure($centralUser, $tenant->getTenantKey(), $centralUser->name))->toBeTrue();
expect($tenantChannelClosure($centralUser, $tenant->getTenantKey(), $tenantUser->name))->toBeFalse();
@ -160,25 +164,6 @@ test('broadcasting channel helpers register channels correctly', function() {
expect($getChannels())->toBeEmpty();
// universal_channel helper registers both the unprefixed and the prefixed broadcasting channel correctly
// Using the tenant_channel helper + basic channel registration (Broadcast::channel())
universal_channel($channelName, $channelClosure);
// Regular channel registered correctly
$centralChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === $channelName);
expect($centralChannelClosure)->not()->toBeNull();
// Tenant channel registered correctly
$tenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName");
expect($tenantChannelClosure)
->not()->toBeNull() // Channel registered
->not()->toBe($centralChannelClosure); // The tenant channel callback is different after the auth user, it accepts the tenant ID
$broadcastManager->purge($driver);
$broadcastManager->extend($driver, fn () => new NullBroadcaster);
expect($getChannels())->toBeEmpty();
// Global channel helper prefixes the channel name with 'global__'
global_channel($channelName, $channelClosure);

View file

@ -6,62 +6,56 @@ use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () {
Route::group([
'middleware' => InitializeTenancyByDomainOrSubdomain::class,
], function () {
Route::get('/foo/{a}/{b}', function ($a, $b) {
return "$a + $b";
Route::get('/test', function () {
return tenant('id');
});
});
config(['tenancy.models.tenant' => CombinedTenant::class]);
});
test('tenant can be identified by subdomain', function () {
config(['tenancy.identification.central_domains' => ['localhost']]);
$tenant = CombinedTenant::create([
'id' => 'acme',
]);
$tenant->domains()->create([
'domain' => 'foo',
]);
$tenant = Tenant::create(['id' => 'acme']);
$tenant->domains()->create(['domain' => 'foo']);
expect(tenancy()->initialized)->toBeFalse();
pest()
->get('http://foo.localhost/foo/abc/xyz')
->assertSee('abc + xyz');
expect(tenancy()->initialized)->toBeTrue();
expect(tenant('id'))->toBe('acme');
pest()->get('http://foo.localhost/test')->assertSee('acme');
});
test('tenant can be identified by domain', function () {
config(['tenancy.identification.central_domains' => []]);
$tenant = CombinedTenant::create([
'id' => 'acme',
]);
$tenant->domains()->create([
'domain' => 'foobar.localhost',
]);
$tenant = Tenant::create(['id' => 'acme']);
$tenant->domains()->create(['domain' => 'foobar.localhost']);
expect(tenancy()->initialized)->toBeFalse();
pest()
->get('http://foobar.localhost/foo/abc/xyz')
->assertSee('abc + xyz');
expect(tenancy()->initialized)->toBeTrue();
expect(tenant('id'))->toBe('acme');
pest()->get('http://foobar.localhost/test')->assertSee('acme');
});
class CombinedTenant extends Models\Tenant
{
use HasDomains;
}
test('domain records can be either in domain syntax or subdomain syntax', function () {
config(['tenancy.identification.central_domains' => ['localhost']]);
$foo = Tenant::create(['id' => 'foo']);
$foo->domains()->create(['domain' => 'foo']);
$bar = Tenant::create(['id' => 'bar']);
$bar->domains()->create(['domain' => 'bar.localhost']);
expect(tenancy()->initialized)->toBeFalse();
// Subdomain format
pest()->get('http://foo.localhost/test')->assertSee('foo');
tenancy()->end();
// Domain format
pest()->get('http://bar.localhost/test')->assertSee('bar');
});

View file

@ -38,6 +38,12 @@ test('origin identification works', function () {
});
test('tenant routes are not accessible on central domains while using origin identification', function () {
$tenant = Tenant::create();
$tenant->domains()->create([
'domain' => 'foo',
]);
pest()
->withHeader('Origin', 'localhost')
->post('home')
@ -54,3 +60,50 @@ test('onfail logic can be customized', function() {
->post('home')
->assertSee('onFail message');
});
test('origin identification can be used with universal routes', function () {
$tenant = Tenant::create();
$tenant->domains()->create([
'domain' => 'foo',
]);
Route::post('/universal', function () {
return response(tenant('id') ?? 'central');
})->middleware([InitializeTenancyByOriginHeader::class, 'universal'])->name('universal');
pest()
->withHeader('Origin', 'foo.localhost')
->post('universal')
->assertSee($tenant->id);
tenancy()->end();
pest()
->withHeader('Origin', 'localhost')
->post('universal')
->assertSee('central');
pest()
// no header
->post('universal')
->assertSee('central');
});
test('origin identification can be used with both domains and subdomains', function () {
$foo = Tenant::create();
$foo->domains()->create(['domain' => 'foo']);
$bar = Tenant::create();
$bar->domains()->create(['domain' => 'bar.localhost']);
pest()
->withHeader('Origin', 'foo.localhost')
->post('home')
->assertSee($foo->id);
pest()
->withHeader('Origin', 'bar.localhost')
->post('home')
->assertSee($bar->id);
});

View file

@ -145,6 +145,36 @@ test('db name is prefixed with db path when sqlite is used', function () {
expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
});
test('sqlite databases use the WAL journal mode by default', function (bool|null $wal) {
$expected = $wal ? 'wal' : 'delete';
if ($wal !== null) {
SQLiteDatabaseManager::$WAL = $wal;
} else {
// default behavior
$expected = 'wal';
}
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
$tenant = Tenant::create([
'tenancy_db_connection' => 'sqlite',
]);
$dbPath = database_path($tenant->database()->getName());
expect(file_exists($dbPath))->toBeTrue();
$db = new PDO('sqlite:' . $dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($expected);
// cleanup
SQLiteDatabaseManager::$WAL = true;
})->with([true, false, null]);
test('schema manager uses schema to separate tenant dbs', function () {
config([
'tenancy.database.managers.pgsql' => PostgreSQLSchemaManager::class,
@ -332,7 +362,7 @@ test('database credentials can be provided to PermissionControlledMySQLDatabaseM
/** @var PermissionControlledMySQLDatabaseManager $manager */
$manager = $tenant->database()->manager();
expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection
expect($manager->connection()->getConfig('username'))->toBe($username); // user created for the HOST connection
expect($manager->userExists($usernameForNewDB))->toBeTrue();
expect($manager->databaseExists($name))->toBeTrue();
});
@ -371,7 +401,7 @@ test('tenant database can be created by using the username and password from ten
/** @var MySQLDatabaseManager $manager */
$manager = $tenant->database()->manager();
expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection
expect($manager->connection()->getConfig('username'))->toBe($username); // user created for the HOST connection
expect($manager->databaseExists($name))->toBeTrue();
});
@ -417,7 +447,7 @@ test('the tenant connection template can be specified either by name or as a con
/** @var MySQLDatabaseManager $manager */
$manager = $tenant->database()->manager();
expect($manager->databaseExists($name))->toBeTrue();
expect($manager->database()->getConfig('host'))->toBe('mysql');
expect($manager->connection()->getConfig('host'))->toBe('mysql');
config([
'tenancy.database.template_tenant_connection' => [
@ -446,7 +476,7 @@ test('the tenant connection template can be specified either by name or as a con
/** @var MySQLDatabaseManager $manager */
$manager = $tenant->database()->manager();
expect($manager->databaseExists($name))->toBeTrue(); // tenant connection works
expect($manager->database()->getConfig('host'))->toBe('mysql2');
expect($manager->connection()->getConfig('host'))->toBe('mysql2');
});
test('partial tenant connection templates get merged into the central connection template', function () {
@ -471,8 +501,8 @@ test('partial tenant connection templates get merged into the central connection
/** @var MySQLDatabaseManager $manager */
$manager = $tenant->database()->manager();
expect($manager->databaseExists($name))->toBeTrue(); // tenant connection works
expect($manager->database()->getConfig('host'))->toBe('mysql2');
expect($manager->database()->getConfig('url'))->toBeNull();
expect($manager->connection()->getConfig('host'))->toBe('mysql2');
expect($manager->connection()->getConfig('url'))->toBeNull();
});
// Datasets