mirror of
https://github.com/archtechx/tenancy.git
synced 2026-08-06 05:54:03 +00:00
The $broadcaster property essentially had no real purpose, and there's no reason to override the static properties in the constructor. Instead of that, just get the credentials map in setConfig
352 lines
16 KiB
PHP
352 lines
16 KiB
PHP
<?php
|
|
|
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Stancl\Tenancy\Events\TenancyEnded;
|
|
use Illuminate\Broadcasting\BroadcastManager;
|
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
|
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
|
use Illuminate\Support\Facades\Broadcast;
|
|
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
|
|
|
|
afterEach($cleanup = function () {
|
|
BroadcastingConfigBootstrapper::$credentialsMap = [];
|
|
BroadcastingConfigBootstrapper::$mapPresets = [
|
|
'pusher' => [
|
|
'broadcasting.connections.pusher.key' => 'pusher_key',
|
|
'broadcasting.connections.pusher.secret' => 'pusher_secret',
|
|
'broadcasting.connections.pusher.app_id' => 'pusher_app_id',
|
|
'broadcasting.connections.pusher.options.cluster' => 'pusher_cluster',
|
|
],
|
|
'reverb' => [
|
|
'broadcasting.connections.reverb.key' => 'reverb_key',
|
|
'broadcasting.connections.reverb.secret' => 'reverb_secret',
|
|
'broadcasting.connections.reverb.app_id' => 'reverb_app_id',
|
|
'broadcasting.connections.reverb.options.cluster' => 'reverb_cluster',
|
|
],
|
|
'ably' => [
|
|
'broadcasting.connections.ably.key' => 'ably_key',
|
|
'broadcasting.connections.ably.public' => 'ably_public',
|
|
],
|
|
];
|
|
});
|
|
|
|
beforeEach(function () use ($cleanup) {
|
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
|
|
$cleanup();
|
|
});
|
|
|
|
test('each context uses its own manager and broadcaster instances, and the bound broadcaster matches the manager default driver', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
'broadcasting.default' => 'testing',
|
|
'broadcasting.connections.testing.driver' => 'testing',
|
|
]);
|
|
|
|
app(BroadcastManager::class)->extend('testing', fn ($app, $config) => new TestingBroadcaster('testing', $config));
|
|
|
|
$originalManager = app(BroadcastManager::class);
|
|
$originalBroadcaster = app(BroadcasterContract::class);
|
|
|
|
// The bound broadcaster and the manager's default driver should be the same instance in every context
|
|
expect(app(BroadcasterContract::class))->toBe(app(BroadcastManager::class)->driver());
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
// BroadcastingConfigBootstrapper binds a fresh manager and a freshly resolved broadcaster
|
|
expect(app(BroadcastManager::class))->not()->toBe($originalManager);
|
|
expect(app(BroadcasterContract::class))->not()->toBe($originalBroadcaster);
|
|
|
|
// The bound broadcaster is the same instance as the tenant BroadcastManager's default driver
|
|
expect(app(BroadcasterContract::class))->toBe(app(BroadcastManager::class)->driver());
|
|
|
|
tenancy()->end();
|
|
|
|
// Ending tenancy reverts the bindings back to the original instances
|
|
expect(app(BroadcastManager::class))->toBe($originalManager);
|
|
expect(app(BroadcasterContract::class))->toBe($originalBroadcaster);
|
|
|
|
// The bound broadcaster is still the same as the central manager's default driver
|
|
expect(app(BroadcasterContract::class))->toBe(app(BroadcastManager::class)->driver());
|
|
});
|
|
|
|
test('BroadcastingConfigBootstrapper maps tenant properties to broadcaster credentials correctly', function (string $driver) {
|
|
config([
|
|
'broadcasting.default' => $driver,
|
|
"broadcasting.connections.{$driver}.key" => 'central_key',
|
|
'tenancy.bootstrappers' => [
|
|
BroadcastingConfigBootstrapper::class,
|
|
],
|
|
]);
|
|
|
|
if ($driver === 'custom') {
|
|
config(['broadcasting.connections.custom.driver' => 'custom']);
|
|
}
|
|
|
|
BroadcastingConfigBootstrapper::$credentialsMap["broadcasting.connections.{$driver}.key"] = 'testing_key';
|
|
|
|
app(BroadcastManager::class)->extend($driver, fn ($app, $config) => new TestingBroadcaster('testing', $config));
|
|
|
|
$tenant1 = Tenant::create(['testing_key' => 'tenant1_key']);
|
|
$tenant2 = Tenant::create(['testing_key' => 'tenant2_key']);
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('central_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('central_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('central_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('central_key');
|
|
|
|
tenancy()->initialize($tenant1);
|
|
|
|
// Tenant's testing_key property is mapped to the current broadcasting connection key
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('tenant1_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('tenant1_key');
|
|
// Switching to tenant context makes the currently bound Broadcaster instance use the tenant's config
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('tenant1_key');
|
|
// The Broadcast facade (used in BroadcastController::authenticate) uses the broadcaster with tenant config
|
|
// instead of the stale broadcaster instance resolved before tenancy was initialized
|
|
expect(Broadcast::driver()->config['key'])->toBe('tenant1_key');
|
|
|
|
tenancy()->initialize($tenant2);
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('tenant2_key');
|
|
// Switching to another tenant context makes the current broadcaster use the new tenant's config
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('tenant2_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('tenant2_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('tenant2_key');
|
|
|
|
$tenant2->update(['testing_key' => 'new_tenant2_key']);
|
|
|
|
// Reinitialize tenancy to apply the tenant property update to config
|
|
tenancy()->end();
|
|
tenancy()->initialize($tenant2);
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('new_tenant2_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('new_tenant2_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('new_tenant2_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('new_tenant2_key');
|
|
|
|
tenancy()->initialize($tenant1);
|
|
|
|
// Direct config changes aren't picked up by the broadcasters -- they get resolved
|
|
// using the config mapped from tenant properties at initialization and stay cached
|
|
// until tenancy is reinitialized.
|
|
config(["broadcasting.connections.{$driver}.key" => 'new_tenant1_key']);
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('new_tenant1_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('tenant1_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('tenant1_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('tenant1_key');
|
|
|
|
// Ending tenancy after a tenant overrode the credentials reverts the broadcasters back to the central credentials
|
|
tenancy()->end();
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('central_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('central_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('central_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('central_key');
|
|
|
|
// Initializing tenancy for a tenant without the mapped property keeps the central config value
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('central_key');
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('central_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('central_key');
|
|
|
|
tenancy()->end();
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('central_key');
|
|
// Ending tenancy reverts the broadcaster changes
|
|
expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('central_key');
|
|
expect(app(BroadcasterContract::class)->config['key'])->toBe('central_key');
|
|
expect(Broadcast::driver()->config['key'])->toBe('central_key');
|
|
})->with([
|
|
'pusher',
|
|
'ably',
|
|
'reverb',
|
|
'custom',
|
|
]);
|
|
|
|
test('tenant broadcast manager receives the custom driver creators of the central broadcast manager', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [
|
|
BroadcastingConfigBootstrapper::class,
|
|
],
|
|
]);
|
|
|
|
$tenant = Tenant::create();
|
|
$tenant2 = Tenant::create();
|
|
|
|
app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing', $config));
|
|
|
|
$originalDrivers = array_keys(invade(app(BroadcastManager::class))->customCreators);
|
|
|
|
expect($originalDrivers)->toContain('testing');
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
app(BroadcastManager::class)->extend(
|
|
'testing-tenant1',
|
|
fn($app, $config) => new TestingBroadcaster('testing-tenant1', $config)
|
|
);
|
|
|
|
// Current BroadcastManager instance has the original custom creators plus the newly registered testing-tenant1 creator
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->toContain('testing');
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->toEqualCanonicalizing([...$originalDrivers, 'testing-tenant1']);
|
|
|
|
tenancy()->initialize($tenant2);
|
|
|
|
// Current BroadcastManager only has the original custom creators,
|
|
// the creator added in the previous tenant's context doesn't persist.
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->toContain('testing');
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->not()->toContain('testing-tenant1');
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->toEqualCanonicalizing($originalDrivers);
|
|
|
|
tenancy()->end();
|
|
|
|
// Ending tenancy reverts the BroadcastManager binding back to the original state,
|
|
// the creator registered in the tenant context doesn't persist.
|
|
expect(array_keys(invade(app(BroadcastManager::class))->customCreators))->toEqualCanonicalizing($originalDrivers);
|
|
});
|
|
|
|
test('tenant broadcasters receive the auth properties of the broadcaster bound in central context', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
'broadcasting.default' => 'testing',
|
|
'broadcasting.connections.testing.driver' => 'testing',
|
|
]);
|
|
|
|
$tenant = Tenant::create();
|
|
|
|
app(BroadcastManager::class)->extend('testing', fn () => new TestingBroadcaster('testing'));
|
|
$getCurrentChannelsFromBoundBroadcaster = fn () => array_keys(invade(app(BroadcasterContract::class))->channels);
|
|
$getCurrentChannelsThroughManager = fn () => array_keys(invade(app(BroadcastManager::class)->driver())->channels);
|
|
$resolveUser = fn () => app(BroadcasterContract::class)->resolveAuthenticatedUser(request());
|
|
|
|
Broadcast::channel($channel = 'testing-channel', $callback = fn () => true, $options = ['guards' => ['web']]);
|
|
|
|
// resolveAuthenticatedUserUsing() sets the broadcaster's $authenticatedUserCallback, which should be
|
|
// copied to tenant broadcasters along with the channels. User authentication (/broadcasting/user-auth)
|
|
// only works when this callback is registered (403 otherwise), so an app that registers it in central
|
|
// context would get 403s from user auth in tenant context if the callback wasn't copied.
|
|
Broadcast::resolveAuthenticatedUserUsing(fn () => ['id' => 'central-user']);
|
|
|
|
expect($channel)
|
|
->toBeIn($getCurrentChannelsThroughManager())
|
|
->toBeIn($getCurrentChannelsFromBoundBroadcaster());
|
|
expect($resolveUser())->toBe(['id' => 'central-user']);
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect($channel)
|
|
->toBeIn($getCurrentChannelsThroughManager())
|
|
->toBeIn($getCurrentChannelsFromBoundBroadcaster());
|
|
|
|
// The channel auth closure, the channel options, and the authenticated user resolver
|
|
// are copied to the tenant broadcaster as-is
|
|
expect(invade(app(BroadcasterContract::class))->channels[$channel])->toBe($callback);
|
|
expect(invade(app(BroadcasterContract::class))->retrieveChannelOptions($channel))->toBe($options);
|
|
expect($resolveUser())->toBe(['id' => 'central-user']);
|
|
|
|
tenancy()->end();
|
|
|
|
expect($channel)
|
|
->toBeIn($getCurrentChannelsThroughManager())
|
|
->toBeIn($getCurrentChannelsFromBoundBroadcaster());
|
|
expect($resolveUser())->toBe(['id' => 'central-user']);
|
|
});
|
|
|
|
test('channels registered in tenant context persist within that context but do not leak into other contexts', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
'broadcasting.default' => 'testing',
|
|
'broadcasting.connections.testing.driver' => 'testing',
|
|
]);
|
|
|
|
app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing', $config));
|
|
|
|
Broadcast::channel('central-channel', fn () => true);
|
|
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
Broadcast::channel('tenant-channel', fn () => true);
|
|
|
|
// Retrieving the broadcaster again (e.g. on Broadcast::auth() during a /broadcasting/auth request)
|
|
// returns the cached broadcaster, so the channel registered in tenant context is still available
|
|
expect(array_keys(invade(Broadcast::driver())->channels))
|
|
->toContain('central-channel')
|
|
->toContain('tenant-channel');
|
|
|
|
// The channel registered in the previous tenant's context doesn't leak to another tenant's broadcaster
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
expect(array_keys(invade(Broadcast::driver())->channels))
|
|
->toContain('central-channel')
|
|
->not()->toContain('tenant-channel');
|
|
|
|
tenancy()->end();
|
|
|
|
// The channel registered in tenant context doesn't leak to the central broadcaster
|
|
expect(array_keys(invade(Broadcast::driver())->channels))
|
|
->toContain('central-channel')
|
|
->not()->toContain('tenant-channel');
|
|
});
|
|
|
|
test('mappings specified in credentialsMap override default mapPresets', function ($driver) {
|
|
config([
|
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
'broadcasting.default' => $driver,
|
|
]);
|
|
|
|
// The preset mapping for the tested broadcaster (this is the default, we only set it here for clarity)
|
|
BroadcastingConfigBootstrapper::$mapPresets[$driver]["broadcasting.connections.{$driver}.key"] = "{$driver}_key";
|
|
|
|
// Custom mapping specified in credentialsMap should override the preset mapping for the tested broadcaster
|
|
BroadcastingConfigBootstrapper::$credentialsMap["broadcasting.connections.{$driver}.key"] = 'broadcasting_key';
|
|
|
|
app(BroadcastManager::class)->extend($driver, fn($app, $config) => new TestingBroadcaster('testing'));
|
|
|
|
$tenant = Tenant::create([
|
|
"{$driver}_key" => 'preset_value',
|
|
'broadcasting_key' => 'custom_value',
|
|
]);
|
|
|
|
tenancy()->initialize($tenant);
|
|
|
|
expect(config("broadcasting.connections.{$driver}.key"))->toBe('custom_value');
|
|
})->with([
|
|
'pusher',
|
|
'ably',
|
|
'reverb',
|
|
]);
|
|
|
|
test('initializing tenancy does not fail when the broadcaster does not extend the abstract Broadcaster class', function () {
|
|
config([
|
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
'broadcasting.default' => 'contract',
|
|
'broadcasting.connections.contract.driver' => 'contract',
|
|
]);
|
|
|
|
$contractBroadcaster = new class implements BroadcasterContract {
|
|
public function auth($request) {}
|
|
public function validAuthenticationResponse($request, $result) {}
|
|
public function broadcast(array $channels, $event, array $payload = []) {}
|
|
};
|
|
|
|
app(BroadcastManager::class)->extend('contract', fn () => clone $contractBroadcaster);
|
|
|
|
$centralBroadcaster = app(BroadcasterContract::class);
|
|
|
|
// Channel auth closures only exist on broadcasters extending the abstract Broadcaster class,
|
|
// so the bootstrapper skips copying them instead of failing.
|
|
tenancy()->initialize(Tenant::create());
|
|
|
|
expect(app(BroadcasterContract::class))
|
|
->toBeInstanceOf(get_class($contractBroadcaster))
|
|
->not()->toBe($centralBroadcaster);
|
|
});
|
|
|