diff --git a/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php b/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php index 785430f5..40bac461 100644 --- a/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php +++ b/tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php @@ -16,6 +16,9 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; 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 @@ test('BroadcastChannelPrefixBootstrapper prefixes the channels events are broadc 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 5eb987db..f311bee4 100644 --- a/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php +++ b/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php @@ -10,6 +10,8 @@ use Stancl\Tenancy\Tests\Etc\TestingBroadcaster; 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); @@ -38,68 +40,135 @@ test('BroadcastingConfigBootstrapper binds TenancyBroadcastManager to BroadcastM 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() { config([ - 'broadcasting.connections.testing.driver' => 'testing', - 'broadcasting.connections.testing.message' => $defaultMessage = 'default', - 'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class], + 'broadcasting.default' => $driver = 'testing', + 'broadcasting.connections.testing.driver' => $driver, + 'broadcasting.connections.testing.key' => 'central_key', + 'tenancy.bootstrappers' => [ + BroadcastingConfigBootstrapper::class, + ], ]); - BroadcastingConfigBootstrapper::$credentialsMap = [ - 'broadcasting.connections.testing.message' => 'testing_broadcaster_message', - ]; + BroadcastingConfigBootstrapper::$credentialsMap['broadcasting.connections.testing.key'] = 'testing_key'; - $tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']); - $tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']); + // Register the testing broadcaster + app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing', $config)); - tenancy()->initialize($tenant); + $tenant1 = Tenant::create(['testing_key' => 'tenant1_key']); + $tenant2 = Tenant::create(['testing_key' => 'tenant2_key']); - expect(array_key_exists('testing_broadcaster_message', tenant()->getAttributes()))->toBeTrue(); - expect(config('broadcasting.connections.testing.message'))->toBe($tenantMessage); + expect(config('broadcasting.connections.testing.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); + + expect(array_key_exists('testing_key', tenant()->getAttributes()))->toBeTrue(); + // Tenant's testing_key property is mapped to broadcasting.connections.testing.key config value + expect(config('broadcasting.connections.testing.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.testing.message'))->toBe($secondTenantMessage); + expect(array_key_exists('testing_key', tenant()->getAttributes()))->toBeTrue(); + expect(config('broadcasting.connections.testing.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'); tenancy()->end(); - expect(config('broadcasting.connections.testing.message'))->toBe($defaultMessage); + expect(config('broadcasting.connections.testing.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'); }); -test('BroadcastingConfigBootstrapper makes the app use broadcasters with the correct credentials', function() { +test('tenant broadcast manager receives the custom driver creators of the central broadcast manager', function() { config([ - 'broadcasting.default' => 'testing', - 'broadcasting.connections.testing.driver' => 'testing', - 'broadcasting.connections.testing.message' => $defaultMessage = 'default', - 'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class], + 'tenancy.bootstrappers' => [ + BroadcastingConfigBootstrapper::class, + ], ]); - TenancyBroadcastManager::$tenantBroadcasters[] = 'testing'; - BroadcastingConfigBootstrapper::$credentialsMap = [ - 'broadcasting.connections.testing.message' => 'testing_broadcaster_message', - ]; + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); - $registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn ($app, $config) => new TestingBroadcaster($config['message'])); + app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing', $config)); - $registerTestingBroadcaster(); + $originalDrivers = array_keys(invade(app(BroadcastManager::class))->customCreators); - expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage); - - $tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']); - $tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']); + expect($originalDrivers)->toContain('testing'); tenancy()->initialize($tenant); - $registerTestingBroadcaster(); - expect(invade(app(BroadcastManager::class)->driver())->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); - $registerTestingBroadcaster(); - expect(invade(app(BroadcastManager::class)->driver())->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(); - $registerTestingBroadcaster(); - expect(invade(app(BroadcastManager::class)->driver())->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('tenant broadcasters receive the channels from the broadcaster bound in central context', function() { + config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]); + config([ + 'broadcasting.default' => $driver = 'testing', + 'broadcasting.connections.testing.driver' => $driver, + ]); + + TenancyBroadcastManager::$tenantBroadcasters[] = $driver; + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + app(BroadcastManager::class)->extend('testing', 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); + + Broadcast::channel($channel = 'testing-channel', fn() => true); + + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); + + tenancy()->initialize($tenant1); + + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); + + tenancy()->initialize($tenant2); + + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); + + tenancy()->end(); + + expect($channel) + ->toBeIn($getCurrentChannelsThroughManager()) + ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); }); diff --git a/tests/BroadcastingTest.php b/tests/BroadcastingTest.php deleted file mode 100644 index 305bf44b..00000000 --- a/tests/BroadcastingTest.php +++ /dev/null @@ -1,272 +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('broadcasting config bootstrapper maps the config to broadcaster credentials correctly', function() { - config([ - 'broadcasting.default' => $driver = 'testing', - 'broadcasting.connections.testing.driver' => $driver, - 'broadcasting.connections.testing.key' => 'central_key', - 'tenancy.bootstrappers' => [ - BroadcastingConfigBootstrapper::class, - ], - ]); - - BroadcastingConfigBootstrapper::$credentialsMap['broadcasting.connections.testing.key'] = 'testing_key'; - - // Register the testing broadcaster - app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing', $config)); - - $tenant1 = Tenant::create(['testing_key' => 'tenant1_key']); - $tenant2 = Tenant::create(['testing_key' => 'tenant2_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); - - 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); - - // 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'); - - tenancy()->end(); - - // 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'); -}); - -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))->toBe([...$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))->toBe($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))->toBe($originalDrivers); -}); - -test('tenant broadcasters receive the channels from the broadcaster bound in central context', function() { - config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]); - config([ - 'broadcasting.default' => $driver = 'testing', - 'broadcasting.connections.testing.driver' => $driver, - ]); - - TenancyBroadcastManager::$tenantBroadcasters[] = $driver; - - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - app(BroadcastManager::class)->extend('testing', 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); - - Broadcast::channel($channel = 'testing-channel', fn() => true); - - expect($channel) - ->toBeIn($getCurrentChannelsThroughManager()) - ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); - - tenancy()->initialize($tenant1); - - expect($channel) - ->toBeIn($getCurrentChannelsThroughManager()) - ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); - - tenancy()->initialize($tenant2); - - expect($channel) - ->toBeIn($getCurrentChannelsThroughManager()) - ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); - - tenancy()->end(); - - expect($channel) - ->toBeIn($getCurrentChannelsThroughManager()) - ->toBeIn($getCurrentChannelsFromBoundBroadcaster()); -}); - -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(); -});