diff --git a/src/Bootstrappers/BroadcastingConfigBootstrapper.php b/src/Bootstrappers/BroadcastingConfigBootstrapper.php index 66fee7043..6b90435d5 100644 --- a/src/Bootstrappers/BroadcastingConfigBootstrapper.php +++ b/src/Bootstrappers/BroadcastingConfigBootstrapper.php @@ -7,11 +7,19 @@ use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Config\Repository; use Illuminate\Contracts\Broadcasting\Broadcaster; +use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory; use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Broadcast; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Overrides\TenancyBroadcastManager; +/** + * Maps tenant properties to broadcasting config and overrides + * the BroadcastManager binding with TenancyBroadcastManager. + * + * @see TenancyBroadcastManager + */ class BroadcastingConfigBootstrapper implements TenancyBootstrapper { /** @@ -54,7 +62,7 @@ public function __construct( protected Application $app ) { static::$broadcaster ??= $config->get('broadcasting.default'); - static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$broadcaster] ?? []); + static::$credentialsMap = array_merge(static::$mapPresets[static::$broadcaster] ?? [], static::$credentialsMap); } public function bootstrap(Tenant $tenant): void @@ -64,10 +72,35 @@ public function bootstrap(Tenant $tenant): void $this->setConfig($tenant); - // Make BroadcastManager resolve to a custom BroadcastManager which makes the broadcasters use the tenant credentials + // Make BroadcastManager resolve to TenancyBroadcastManager which always re-resolves the used broadcasters so that + // the credentials used by broadcasters are always up-to-date with the config when retrieving the broadcasters using + // the manager and gives the channels of the broadcaster from central context to the newly resolved broadcasters in tenant context. $this->app->extend(BroadcastManager::class, function (BroadcastManager $broadcastManager) { - return new TenancyBroadcastManager($this->app); + $originalCustomCreators = invade($broadcastManager)->customCreators; + $tenantBroadcastManager = new TenancyBroadcastManager($this->app); + + // TenancyBroadcastManager inherits the custom driver creators registered in the central context so that + // custom drivers work in tenant context without having to re-register the creators manually. + foreach ($originalCustomCreators as $driver => $closure) { + $tenantBroadcastManager->extend($driver, $closure); + } + + return $tenantBroadcastManager; }); + + // Swap currently bound Broadcaster instance for one that's resolved through the tenant BroadcastManager. + // Note that updating broadcasting config (credentials) in tenant context doesn't update the credentials + // used by the bound Broadcaster instance. If you need to e.g. send a notification in response to + // updating tenant's broadcasting credentials in tenant context, it's recommended to + // reinitialize tenancy after updating the credentials. + $this->app->extend(Broadcaster::class, function (Broadcaster $broadcaster) { + return $this->app->make(BroadcastManager::class)->connection(); + }); + + // Clear the resolved Broadcast facade's Illuminate\Contracts\Broadcasting\Factory instance + // so that it gets re-resolved as TenancyBroadcastManager instead of the central BroadcastManager + // when used. E.g. the Broadcast::auth() call in BroadcastController::authenticate (/broadcasting/auth). + Broadcast::clearResolvedInstance(BroadcastingFactory::class); } public function revert(): void @@ -76,6 +109,9 @@ public function revert(): void $this->app->singleton(BroadcastManager::class, fn (Application $app) => $this->originalBroadcastManager); $this->app->singleton(Broadcaster::class, fn (Application $app) => $this->originalBroadcaster); + // Clear the resolved Broadcast facade instance so that it gets re-resolved as the central BroadcastManager + Broadcast::clearResolvedInstance(BroadcastingFactory::class); + $this->unsetConfig(); } diff --git a/src/Overrides/TenancyBroadcastManager.php b/src/Overrides/TenancyBroadcastManager.php index 16e44a400..6454156b6 100644 --- a/src/Overrides/TenancyBroadcastManager.php +++ b/src/Overrides/TenancyBroadcastManager.php @@ -7,26 +7,37 @@ use Illuminate\Broadcasting\Broadcasters\Broadcaster; use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; -use Illuminate\Contracts\Foundation\Application; +/** + * BroadcastManager override that always re-resolves the broadcasters in static::$tenantBroadcasters + * when attempting to retrieve them and passes the channels of the original (central) broadcaster + * to the newly resolved (tenant) broadcasters. + * + * Affects calls that use app(BroadcastManager::class)->get(). + * + * @see Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper + */ class TenancyBroadcastManager extends BroadcastManager { /** - * Names of broadcasters to always recreate using $this->resolve() (even when they're - * cached and available in the $broadcasters property). - * - * The reason for recreating the broadcasters is - * to make your app use the correct broadcaster credentials when tenancy is initialized. + * Names of broadcasters that + * - should always be recreated using $this->resolve(), even when they're cached and available + * in $this->drivers so that when you update broadcasting config in the tenant context, + * the updated config/credentials will be used for broadcasting immediately. + * Note that in cases like this, only direct config changes are reflected right away. + * For the broadcasters to reflect tenant property changes made in tenant context, + * you still have to reinitialize tenancy after updating the tenant properties intended + * to be mapped to broadcasting config, since the properties are only mapped to config + * on BroadcastingConfigBootstrapper::bootstrap(). + * - should inherit the original broadcaster's channels (= the channels registered in + * the central context, e.g. in routes/channels.php, before this manager overrides the bound BroadcastManager). */ - public static array $tenantBroadcasters = ['pusher', 'ably']; + public static array $tenantBroadcasters = ['pusher', 'ably', 'reverb']; /** - * Override the get method so that the broadcasters in $tenantBroadcasters - * always get freshly resolved even when they're cached and available in the $broadcasters property, - * and that the resolved broadcaster will override the BroadcasterContract::class singleton. - * - * If there's a cached broadcaster with the same name as $name, - * give its channels to the newly resolved bootstrapper. + * Override the get method so that the broadcasters in static::$tenantBroadcasters + * - receive the original (central) broadcaster's channels + * - always get freshly resolved. */ protected function get($name) { @@ -35,24 +46,27 @@ protected function get($name) $originalBroadcaster = $this->app->make(BroadcasterContract::class); $newBroadcaster = $this->resolve($name); - // If there is a current broadcaster, give its channels to the newly resolved one + // Give the channels of the original (central) broadcaster to the newly resolved one. + // // Broadcasters only have to implement the Illuminate\Contracts\Broadcasting\Broadcaster contract - // Which doesn't require the channels property - // So passing the channels is only needed for Illuminate\Broadcasting\Broadcasters\Broadcaster instances + // which doesn't require the channels property, so passing the channels is only needed for + // Illuminate\Broadcasting\Broadcasters\Broadcaster instances (= all the default broadcasters, e.g. PusherBroadcaster). if ($originalBroadcaster instanceof Broadcaster && $newBroadcaster instanceof Broadcaster) { $this->passChannelsFromOriginalBroadcaster($originalBroadcaster, $newBroadcaster); } - $this->app->singleton(BroadcasterContract::class, fn (Application $app) => $newBroadcaster); - return $newBroadcaster; } return parent::get($name); } - // Because, unlike the original broadcaster, the newly resolved broadcaster won't have the channels registered using routes/channels.php - // Using it for broadcasting won't work, unless we make it have the original broadcaster's channels + /** + * The newly resolved broadcasters don't automatically receive the channels registered + * in central context (e.g. Broadcast::channel() in routes/channels.php), so the channels + * have to be obtained from the original (central) broadcaster and manually passed to the new broadcasters + * (broadcasting using a broadcaster with no channels results in a 403 error on Broadcast::auth()). + */ protected function passChannelsFromOriginalBroadcaster(Broadcaster $originalBroadcaster, Broadcaster $newBroadcaster): void { // invade() because channels can't be retrieved through any of the broadcaster's public methods diff --git a/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php b/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php index 785430f52..40bac461a 100644 --- a/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php +++ b/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php @@ -16,6 +16,9 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use function Stancl\Tenancy\Tests\pest; +use Illuminate\Broadcasting\Broadcasters\NullBroadcaster; +use Illuminate\Support\Facades\Broadcast; +use Illuminate\Support\Collection; beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); @@ -137,3 +140,96 @@ protected function formatChannels(array $channels) expect(app(BroadcastManager::class)->driver())->toBe($broadcaster); expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames); }); + +test('broadcasting channel helpers register channels correctly', function() { + config([ + 'broadcasting.default' => $driver = 'testing', + 'broadcasting.connections.testing.driver' => $driver, + ]); + + config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); + + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + $centralUser = User::create(['name' => 'central', 'email' => 'test@central.cz', 'password' => 'test']); + $tenant = Tenant::create(); + + migrateTenants(); + + tenancy()->initialize($tenant); + + // Same ID as $centralUser + $tenantUser = User::create(['name' => 'tenant', 'email' => 'test@tenant.cz', 'password' => 'test']); + + tenancy()->end(); + + /** @var BroadcastManager $broadcastManager */ + $broadcastManager = app(BroadcastManager::class); + + // Use a driver with no channels + $broadcastManager->extend($driver, fn () => new NullBroadcaster); + + $getChannels = fn (): Collection => $broadcastManager->driver($driver)->getChannels(); + + expect($getChannels())->toBeEmpty(); + + // Basic channel registration + Broadcast::channel($channelName = 'user.{userName}', $channelClosure = function ($user, $userName) { + return User::firstWhere('name', $userName)?->is($user) ?? false; + }); + + // Check if the channel is registered + $centralChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === $channelName); + expect($centralChannelClosure)->not()->toBeNull(); + + // Channel closures work as expected (running in central context) + expect($centralChannelClosure($centralUser, $centralUser->name))->toBeTrue(); + expect($centralChannelClosure($centralUser, $tenantUser->name))->toBeFalse(); + + // Register a tenant broadcasting channel (almost identical to the original channel, just able to accept the tenant key) + tenant_channel($channelName, $channelClosure); + + // Tenant channel registered – its name is correctly prefixed ("{tenant}.user.{userId}") + $tenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName"); + 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(); + + tenancy()->initialize($tenant); + + // The channel closure runs in the central context + // Only the central user is available + expect($tenantChannelClosure($centralUser, $tenant->getTenantKey(), $tenantUser->name))->toBeFalse(); + expect($tenantChannelClosure($tenantUser, $tenant->getTenantKey(), $tenantUser->name))->toBeTrue(); + + // Use a new channel instance to delete the previously registered channels before testing the universal_channel helper + $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); + + // Channel prefixed with 'global__' found + $foundChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === 'global__' . $channelName); + expect($foundChannelClosure)->not()->toBeNull(); +}); diff --git a/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php b/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php index 5eb987db6..0ced35e20 100644 --- a/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php +++ b/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php @@ -10,18 +10,20 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Overrides\TenancyBroadcastManager; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; +use Illuminate\Support\Facades\Broadcast; +use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); BroadcastingConfigBootstrapper::$credentialsMap = []; - TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; + TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably', 'reverb']; }); afterEach(function () { BroadcastingConfigBootstrapper::$credentialsMap = []; - TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; + TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably', 'reverb']; }); test('BroadcastingConfigBootstrapper binds TenancyBroadcastManager to BroadcastManager and reverts the binding when tenancy is ended', function() { @@ -38,68 +40,174 @@ expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class); }); -test('BroadcastingConfigBootstrapper maps tenant broadcaster credentials to config as specified in the $credentialsMap property and reverts the config after ending tenancy', function() { +test('BroadcastingConfigBootstrapper maps tenant properties to broadcaster credentials correctly', function(string $driver) { config([ - 'broadcasting.connections.testing.driver' => 'testing', - 'broadcasting.connections.testing.message' => $defaultMessage = 'default', - 'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class], + 'broadcasting.default' => $driver, + "broadcasting.connections.{$driver}.key" => 'central_key', + 'tenancy.bootstrappers' => [ + BroadcastingConfigBootstrapper::class, + ], ]); - BroadcastingConfigBootstrapper::$credentialsMap = [ - 'broadcasting.connections.testing.message' => 'testing_broadcaster_message', - ]; + if ($driver === 'custom') { + config(['broadcasting.connections.custom.driver' => 'custom']); + + // Custom driver, not included in TenancyBroadcastManager::$tenantBroadcasters by default + TenancyBroadcastManager::$tenantBroadcasters = ['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 broadcasting.connections.testing.key config value + 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'); - $tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']); - $tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']); + tenancy()->initialize($tenant1); + + // When updating tenant properties without reinitializing, the tenant property update doesn't update the config, + // so the config has to be modified manually. Only methods that use TenancyBroadcastManager::get() + // will use the updated credentials without needing to reinitialize tenancy (e.g. the bound + // BroadcasterContract instance will still the original credentials, even after config gets updated directly). + config(["broadcasting.connections.{$driver}.key" => 'new_tenant1_key']); + + expect(app(BroadcastManager::class)->driver()->config['key'])->toBe('new_tenant1_key'); + expect(Broadcast::driver()->config['key'])->toBe('new_tenant1_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', // Except for this custom driver, assume that the drivers are included in TenancyBroadcastManager::$tenantBroadcasters by default +]); + +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); - expect(array_key_exists('testing_broadcaster_message', tenant()->getAttributes()))->toBeTrue(); - expect(config('broadcasting.connections.testing.message'))->toBe($tenantMessage); + 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))->toBe([...$originalDrivers, 'testing-tenant1']); tenancy()->initialize($tenant2); - expect(config('broadcasting.connections.testing.message'))->toBe($secondTenantMessage); + // 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))->toBe($originalDrivers); tenancy()->end(); - expect(config('broadcasting.connections.testing.message'))->toBe($defaultMessage); + // 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))->toBe($originalDrivers); }); -test('BroadcastingConfigBootstrapper makes the app use broadcasters with the correct credentials', function() { +test('tenant broadcasters receive the channels from the broadcaster bound in central context', function(string $driver) { config([ - 'broadcasting.default' => 'testing', - 'broadcasting.connections.testing.driver' => 'testing', - 'broadcasting.connections.testing.message' => $defaultMessage = 'default', 'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class], + 'broadcasting.default' => $driver, ]); - TenancyBroadcastManager::$tenantBroadcasters[] = 'testing'; - BroadcastingConfigBootstrapper::$credentialsMap = [ - 'broadcasting.connections.testing.message' => 'testing_broadcaster_message', - ]; + if ($driver === 'custom') { + config(['broadcasting.connections.custom.driver' => 'custom']); - $registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn ($app, $config) => new TestingBroadcaster($config['message'])); + // Custom driver, not included in TenancyBroadcastManager::$tenantBroadcasters by default + TenancyBroadcastManager::$tenantBroadcasters = ['custom']; + } - $registerTestingBroadcaster(); + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); - expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage); + app(BroadcastManager::class)->extend($driver, fn($app, $config) => new TestingBroadcaster('testing')); + $getCurrentChannelsFromBoundBroadcaster = fn() => array_keys(invade(app(BroadcasterContract::class))->channels); + $getCurrentChannelsThroughManager = fn() => array_keys(invade(app(BroadcastManager::class)->driver())->channels); - $tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']); - $tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']); + Broadcast::channel($channel = 'testing-channel', fn() => true); - tenancy()->initialize($tenant); - $registerTestingBroadcaster(); + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); + + tenancy()->initialize($tenant1); - expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($tenantMessage); + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); tenancy()->initialize($tenant2); - $registerTestingBroadcaster(); - expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($secondTenantMessage); + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); tenancy()->end(); - $registerTestingBroadcaster(); - expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage); -}); + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); +})->with([ + 'pusher', + 'ably', + 'reverb', + 'custom', // Except for this custom driver, assume that the drivers are included in TenancyBroadcastManager::$tenantBroadcasters by default +]); diff --git a/tests/BroadcastingTest.php b/tests/BroadcastingTest.php deleted file mode 100644 index c3509426b..000000000 --- a/tests/BroadcastingTest.php +++ /dev/null @@ -1,174 +0,0 @@ - [BroadcastingConfigBootstrapper::class]]); - config(['broadcasting.default' => 'null']); - TenancyBroadcastManager::$tenantBroadcasters[] = 'null'; - - $originalBroadcaster = app(BroadcasterContract::class); - - tenancy()->initialize(Tenant::create()); - - // TenancyBroadcastManager binds new broadcaster - $tenantBroadcaster = app(BroadcastManager::class)->driver(); - - expect($tenantBroadcaster)->not()->toBe($originalBroadcaster); - - tenancy()->end(); - - expect($originalBroadcaster)->toBe(app(BroadcasterContract::class)); -}); - -test('new broadcasters get the channels from the previously bound broadcaster', function() { - config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]); - config([ - 'broadcasting.default' => $driver = 'testing', - 'broadcasting.connections.testing.driver' => $driver, - ]); - - TenancyBroadcastManager::$tenantBroadcasters[] = $driver; - - $registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing')); - $getCurrentChannels = fn() => array_keys(invade(app(BroadcastManager::class)->driver())->channels); - - $registerTestingBroadcaster(); - Broadcast::channel($channel = 'testing-channel', fn() => true); - - expect($channel)->toBeIn($getCurrentChannels()); - - tenancy()->initialize(Tenant::create()); - $registerTestingBroadcaster(); - - expect($channel)->toBeIn($getCurrentChannels()); - - tenancy()->end(); - $registerTestingBroadcaster(); - - expect($channel)->toBeIn($getCurrentChannels()); -}); - -test('broadcasting channel helpers register channels correctly', function() { - config([ - 'broadcasting.default' => $driver = 'testing', - 'broadcasting.connections.testing.driver' => $driver, - ]); - - config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); - - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->string('email')->unique(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - $centralUser = User::create(['name' => 'central', 'email' => 'test@central.cz', 'password' => 'test']); - $tenant = Tenant::create(); - - migrateTenants(); - - tenancy()->initialize($tenant); - - // Same ID as $centralUser - $tenantUser = User::create(['name' => 'tenant', 'email' => 'test@tenant.cz', 'password' => 'test']); - - tenancy()->end(); - - /** @var BroadcastManager $broadcastManager */ - $broadcastManager = app(BroadcastManager::class); - - // Use a driver with no channels - $broadcastManager->extend($driver, fn () => new NullBroadcaster); - - $getChannels = fn (): Collection => $broadcastManager->driver($driver)->getChannels(); - - expect($getChannels())->toBeEmpty(); - - // Basic channel registration - Broadcast::channel($channelName = 'user.{userName}', $channelClosure = function ($user, $userName) { - return User::firstWhere('name', $userName)?->is($user) ?? false; - }); - - // Check if the channel is registered - $centralChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === $channelName); - expect($centralChannelClosure)->not()->toBeNull(); - - // Channel closures work as expected (running in central context) - expect($centralChannelClosure($centralUser, $centralUser->name))->toBeTrue(); - expect($centralChannelClosure($centralUser, $tenantUser->name))->toBeFalse(); - - // Register a tenant broadcasting channel (almost identical to the original channel, just able to accept the tenant key) - tenant_channel($channelName, $channelClosure); - - // Tenant channel registered – its name is correctly prefixed ("{tenant}.user.{userId}") - $tenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName"); - 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(); - - tenancy()->initialize($tenant); - - // The channel closure runs in the central context - // Only the central user is available - expect($tenantChannelClosure($centralUser, $tenant->getTenantKey(), $tenantUser->name))->toBeFalse(); - expect($tenantChannelClosure($tenantUser, $tenant->getTenantKey(), $tenantUser->name))->toBeTrue(); - - // Use a new channel instance to delete the previously registered channels before testing the univeresal_channel helper - $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); - - // Channel prefixed with 'global__' found - $foundChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === 'global__' . $channelName); - expect($foundChannelClosure)->not()->toBeNull(); -}); diff --git a/tests/Etc/TestingBroadcaster.php b/tests/Etc/TestingBroadcaster.php index 1a2258875..0b923f8fb 100644 --- a/tests/Etc/TestingBroadcaster.php +++ b/tests/Etc/TestingBroadcaster.php @@ -6,7 +6,8 @@ class TestingBroadcaster extends Broadcaster { public function __construct( - public string $message = 'nothing' + public string $message = 'nothing', + public array $config = [], ) {} public function auth($request)