diff --git a/src/Channels/CacheChannel.php b/src/Channels/CacheChannel.php new file mode 100644 index 00000000..aae76be8 --- /dev/null +++ b/src/Channels/CacheChannel.php @@ -0,0 +1,47 @@ +payload = $payload; + + parent::broadcast($payload, $except); + } + + /** + * Broadcast a message triggered from an internal source. + */ + public function broadcastInternally(array $payload, Connection $except = null): void + { + parent::broadcast($payload, $except); + } + + /** + * Determine if the channel has a cached payload. + */ + public function hasCachedPayload(): bool + { + return $this->payload !== null; + } + + /** + * Get the cached payload. + */ + public function cachedPayload(): ?array + { + return $this->payload; + } +} diff --git a/src/Channels/Channel.php b/src/Channels/Channel.php index c5bf76d0..7d474525 100644 --- a/src/Channels/Channel.php +++ b/src/Channels/Channel.php @@ -107,6 +107,14 @@ public function broadcast(array $payload, Connection $except = null): void } } + /** + * Broadcast a message triggered from an internal source. + */ + public function broadcastInternally(array $payload, Connection $except = null): void + { + $this->broadcast($payload, $except); + } + /** * Get the data associated with the channel. */ diff --git a/src/Channels/ChannelBroker.php b/src/Channels/ChannelBroker.php index e1bf1a1a..e9d49cf5 100644 --- a/src/Channels/ChannelBroker.php +++ b/src/Channels/ChannelBroker.php @@ -11,9 +11,12 @@ class ChannelBroker */ public static function create(string $name): Channel { - return match (Str::before($name, '-')) { - 'private' => new PrivateChannel($name), - 'presence' => new PresenceChannel($name), + return match (true) { + Str::startsWith($name, 'private-cache-') => new PrivateCacheChannel($name), + Str::startsWith($name, 'presence-cache-') => new PresenceCacheChannel($name), + Str::startsWith($name, 'cache') => new CacheChannel($name), + Str::startsWith($name, 'private') => new PrivateChannel($name), + Str::startsWith($name, 'presence') => new PresenceChannel($name), default => new Channel($name), }; } diff --git a/src/Channels/Concerns/InteractsWithPresenceChannels.php b/src/Channels/Concerns/InteractsWithPresenceChannels.php new file mode 100644 index 00000000..e19e340d --- /dev/null +++ b/src/Channels/Concerns/InteractsWithPresenceChannels.php @@ -0,0 +1,81 @@ +verify($connection, $auth, $data); + + parent::subscribe($connection, $auth, $data); + + parent::broadcastInternally( + [ + 'event' => 'pusher_internal:member_added', + 'data' => $data ? json_decode($data, true) : [], + 'channel' => $this->name(), + ], + $connection + ); + } + + /** + * Unsubscribe from the given channel. + */ + public function unsubscribe(Connection $connection): void + { + if (! $subscription = $this->connections->find($connection)) { + parent::unsubscribe($connection); + + return; + } + + if ($userId = $subscription->data('user_id')) { + parent::broadcast( + [ + 'event' => 'pusher_internal:member_removed', + 'data' => ['user_id' => $userId], + 'channel' => $this->name(), + ], + $connection + ); + } + + parent::unsubscribe($connection); + } + + /** + * Get the data associated with the channel. + */ + public function data(): array + { + $connections = collect($this->connections->all()) + ->map(fn ($connection) => $connection->data()); + + if ($connections->contains(fn ($connection) => ! isset($connection['user_id']))) { + return [ + 'presence' => [ + 'count' => 0, + 'ids' => [], + 'hash' => [], + ], + ]; + } + + return [ + 'presence' => [ + 'count' => $connections->count() ?? 0, + 'ids' => $connections->map(fn ($connection) => $connection['user_id'])->all(), + 'hash' => $connections->keyBy('user_id')->map->user_info->toArray(), + ], + ]; + } +} diff --git a/src/Channels/Concerns/InteractsWithPrivateChannels.php b/src/Channels/Concerns/InteractsWithPrivateChannels.php new file mode 100644 index 00000000..c0356fd7 --- /dev/null +++ b/src/Channels/Concerns/InteractsWithPrivateChannels.php @@ -0,0 +1,45 @@ +verify($connection, $auth, $data); + + parent::subscribe($connection, $auth, $data); + } + + /** + * Deteremine whether the given auth token is valid. + */ + protected function verify(Connection $connection, string $auth, string $data = null): bool + { + $signature = "{$connection->id()}:{$this->name()}"; + + if ($data) { + $signature .= ":{$data}"; + } + + if (! hash_equals( + hash_hmac( + 'sha256', + $signature, + $connection->app()->secret(), + ), + Str::after($auth, ':') + )) { + throw new ConnectionUnauthorized; + } + + return true; + } +} diff --git a/src/Channels/PresenceCacheChannel.php b/src/Channels/PresenceCacheChannel.php new file mode 100644 index 00000000..07853460 --- /dev/null +++ b/src/Channels/PresenceCacheChannel.php @@ -0,0 +1,10 @@ +broadcast( - [ - 'event' => 'pusher_internal:member_added', - 'data' => $data ? json_decode($data, true) : [], - 'channel' => $this->name(), - ], - $connection - ); - } - - /** - * Unsubscribe from the given channel. - */ - public function unsubscribe(Connection $connection): void - { - if (! $subscription = $this->connections->find($connection)) { - parent::unsubscribe($connection); - - return; - } - - if ($userId = $subscription->data('user_id')) { - $this->broadcast( - [ - 'event' => 'pusher_internal:member_removed', - 'data' => ['user_id' => $userId], - 'channel' => $this->name(), - ], - $connection - ); - } - - parent::unsubscribe($connection); - } - - /** - * Get the data associated with the channel. - */ - public function data(): array - { - $connections = collect($this->connections->all()) - ->map(fn ($connection) => $connection->data()); - - if ($connections->contains(fn ($connection) => ! isset($connection['user_id']))) { - return [ - 'presence' => [ - 'count' => 0, - 'ids' => [], - 'hash' => [], - ], - ]; - } - - return [ - 'presence' => [ - 'count' => $connections->count() ?? 0, - 'ids' => $connections->map(fn ($connection) => $connection['user_id'])->all(), - 'hash' => $connections->keyBy('user_id')->map->user_info->toArray(), - ], - ]; - } + use InteractsWithPresenceChannels; } diff --git a/src/Channels/PrivateCacheChannel.php b/src/Channels/PrivateCacheChannel.php new file mode 100644 index 00000000..2d353f90 --- /dev/null +++ b/src/Channels/PrivateCacheChannel.php @@ -0,0 +1,10 @@ +verify($connection, $auth, $data); - - parent::subscribe($connection, $auth, $data); - } - - /** - * Deteremine whether the given auth token is valid. - */ - protected function verify(Connection $connection, string $auth, string $data = null): bool - { - $signature = "{$connection->id()}:{$this->name()}"; - - if ($data) { - $signature .= ":{$data}"; - } - - if (! hash_equals( - hash_hmac( - 'sha256', - $signature, - $connection->app()->secret(), - ), - Str::after($auth, ':') - )) { - throw new ConnectionUnauthorized; - } - - return true; - } + use InteractsWithPrivateChannels; } diff --git a/src/Pusher/Event.php b/src/Pusher/Event.php index 8d719ad2..3e79fcd3 100644 --- a/src/Pusher/Event.php +++ b/src/Pusher/Event.php @@ -4,6 +4,8 @@ use Exception; use Illuminate\Support\Str; +use Laravel\Reverb\Channels\CacheChannel; +use Laravel\Reverb\Channels\Channel; use Laravel\Reverb\Contracts\ChannelManager; use Laravel\Reverb\Contracts\Connection; @@ -56,7 +58,7 @@ public function subscribe(Connection $connection, string $channel, string $auth $channel->subscribe($connection, $auth, $data); - $this->sendInternally($connection, 'subscription_succeeded', $channel->name(), $channel->data()); + $this->afterSubscribe($channel, $connection); } /** @@ -70,6 +72,35 @@ public function unsubscribe(Connection $connection, string $channel): void ->unsubscribe($connection); } + /** + * Carry out any actions that should be performed after a subscription. + */ + protected function afterSubscribe(Channel $channel, Connection $connection): void + { + $this->sendInternally($connection, 'subscription_succeeded', $channel->data(), $channel->name()); + + match (true) { + $channel instanceof CacheChannel => $this->sendCachedPayload($channel, $connection), + default => null, + }; + } + + /** + * Send the cached payload for the given channel. + */ + protected function sendCachedPayload(CacheChannel $channel, Connection $connection): void + { + if ($channel->hasCachedPayload()) { + $connection->send( + json_encode($channel->cachedPayload()) + ); + + return; + } + + $this->send($connection, 'cache_miss', channel: $channel->name()); + } + /** * Respond to a ping. */ @@ -91,17 +122,17 @@ public function ping(Connection $connection): void /** * Send a response to the given connection. */ - public function send(Connection $connection, string $event, array $data = []): void + public function send(Connection $connection, string $event, array $data = [], string $channel = null): void { $connection->send( - static::formatPayload($event, $data) + static::formatPayload($event, $data, $channel) ); } /** * Send an internal response to the given connection. */ - public function sendInternally(Connection $connection, string $event, string $channel, array $data = []): void + public function sendInternally(Connection $connection, string $event, array $data = [], string $channel = null): void { $connection->send( static::formatInternalPayload($event, $data, $channel) diff --git a/tests/Connection.php b/tests/Connection.php index 3c776e89..a6175713 100644 --- a/tests/Connection.php +++ b/tests/Connection.php @@ -76,6 +76,11 @@ public function assertSent(array $message): void Assert::assertContains(json_encode($message), $this->messages); } + public function assertSendCount(int $count): void + { + Assert::assertCount($count, $this->messages); + } + public function assertNothingSent(): void { Assert::assertEmpty($this->messages); diff --git a/tests/Feature/Reverb/ServerTest.php b/tests/Feature/Reverb/ServerTest.php index b9b68333..e938ef4d 100644 --- a/tests/Feature/Reverb/ServerTest.php +++ b/tests/Feature/Reverb/ServerTest.php @@ -42,6 +42,26 @@ expect(Str::contains($response, '"hash\":{\"1\":{\"name\":\"Test User\"}}'))->toBeTrue(); }); +it('can subscribe to a cache channel', function () { + $response = $this->subscribe('cache-test-channel'); + + expect($response)->toBe('{"event":"pusher_internal:subscription_succeeded","channel":"cache-test-channel"}'); +}); + +it('can subscribe to a private cache channel', function () { + $response = $this->subscribe('private-cache-test-channel'); + + expect($response)->toBe('{"event":"pusher_internal:subscription_succeeded","channel":"private-cache-test-channel"}'); +}); + +it('can subscribe to a presence cache channel', function () { + $data = ['user_id' => 1, 'user_info' => ['name' => 'Test User']]; + $response = $this->subscribe('presence-cache-test-channel', data: $data); + + expect(Str::contains($response, 'pusher_internal:subscription_succeeded'))->toBeTrue(); + expect(Str::contains($response, '"hash\":{\"1\":{\"name\":\"Test User\"}}'))->toBeTrue(); +}); + it('can notify other subscribers of a presence channel when a new member joins', function () { $connectionOne = $this->connect(); $data = ['user_id' => 1, 'user_info' => ['name' => 'Test User 1']]; @@ -88,6 +108,75 @@ expect(await($promiseFour))->toBe('{"event":"pusher_internal:member_removed","data":{"user_id":3},"channel":"presence-test-channel"}'); }); +it('can receive a cached message when joining a cache channel', function () { + $connection = $this->connect(); + + $this->triggerEvent( + 'cache-test-channel', + 'App\\Events\\TestEvent', + ['foo' => 'bar'] + ); + + $this->subscribe('cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"App\\\\Events\\\\TestEvent","data":{"foo":"bar"},"channel":"cache-test-channel"}'); +}); + +it('can receive a cached message when joining a private cache channel', function () { + $connection = $this->connect(); + + $this->triggerEvent( + 'private-cache-test-channel', + 'App\\Events\\TestEvent', + ['foo' => 'bar'] + ); + + $this->subscribe('private-cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"App\\\\Events\\\\TestEvent","data":{"foo":"bar"},"channel":"private-cache-test-channel"}'); +}); + +it('can receive a cached message when joining a presence cache channel', function () { + $connection = $this->connect(); + + $this->triggerEvent( + 'presence-cache-test-channel', + 'App\\Events\\TestEvent', + ['foo' => 'bar'] + ); + + $this->subscribe('presence-cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"App\\\\Events\\\\TestEvent","data":{"foo":"bar"},"channel":"presence-cache-test-channel"}'); +}); + +it('can receive a cach missed message when joining a cache channel with an empty cache', function () { + $connection = $this->connect(); + $this->subscribe('cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"pusher:cache_miss","channel":"cache-test-channel"}'); +}); + +it('can receive a cach missed message when joining a private cache channel with an empty cache', function () { + $connection = $this->connect(); + $this->subscribe('private-cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"pusher:cache_miss","channel":"private-cache-test-channel"}'); +}); + +it('can receive a cach missed message when joining a presence cache channel with an empty cache', function () { + $connection = $this->connect(); + $this->subscribe('presence-cache-test-channel', connection: $connection); + $promise = $this->messagePromise($connection); + + expect(await($promise))->toBe('{"event":"pusher:cache_miss","channel":"presence-cache-test-channel"}'); +}); + it('can receive a message broadcast from the server', function () { $connectionOne = $this->connect(); $this->subscribe('test-channel', connection: $connectionOne); @@ -240,6 +329,18 @@ expect($response)->toBe('{"event":"pusher:error","data":"{\"code\":4009,\"message\":\"Connection is unauthorized\"}"}'); }); +it('fails to subscribe to a private cache channel with invalid auth signature', function () { + $response = $this->subscribe('private-cache-test-channel', auth: 'invalid-signature'); + + expect($response)->toBe('{"event":"pusher:error","data":"{\"code\":4009,\"message\":\"Connection is unauthorized\"}"}'); +}); + +it('fails to subscribe to a presence cache channel with invalid auth signature', function () { + $response = $this->subscribe('presence-cache-test-channel', auth: 'invalid-signature'); + + expect($response)->toBe('{"event":"pusher:error","data":"{\"code\":4009,\"message\":\"Connection is unauthorized\"}"}'); +}); + it('fails to connect when an invalid application is provided', function () { $promise = new Deferred(); diff --git a/tests/Feature/ServerTest.php b/tests/Feature/ServerTest.php index a0bf86d4..e7218f72 100644 --- a/tests/Feature/ServerTest.php +++ b/tests/Feature/ServerTest.php @@ -156,6 +156,58 @@ ]); }); +it('receives no data when no previous event triggered when joining a cache channel', function () { + $this->server->message( + $connection = new Connection, + json_encode([ + 'event' => 'pusher:subscribe', + 'data' => [ + 'channel' => 'cache-test-channel', + ], + ])); + + $connection->assertSent([ + 'event' => 'pusher_internal:subscription_succeeded', + 'channel' => 'cache-test-channel', + ]); + $connection->assertSent([ + 'event' => 'pusher:cache_miss', + 'channel' => 'cache-test-channel', + ]); + $connection->assertSendCount(2); +}); + +it('receives last triggered event when joining a cache channel', function () { + $this->server->message( + $connection = new Connection, + json_encode([ + 'event' => 'pusher:subscribe', + 'data' => [ + 'channel' => 'cache-test-channel', + ], + ])); + + $channel = app(ChannelManager::class)->find('cache-test-channel'); + + $channel->broadcast(['foo' => 'bar']); + + $this->server->message( + $connection = new Connection, + json_encode([ + 'event' => 'pusher:subscribe', + 'data' => [ + 'channel' => 'cache-test-channel', + ], + ])); + + $connection->assertSent([ + 'event' => 'pusher_internal:subscription_succeeded', + 'channel' => 'cache-test-channel', + ]); + $connection->assertSent(['foo' => 'bar']); + $connection->assertSendCount(2); +}); + it('unsubscribes a user from a channel on disconnection', function () { $channelManager = Mockery::spy(ChannelManager::class); $channelManager->shouldReceive('for') diff --git a/tests/Unit/Channels/CacheChannelTest.php b/tests/Unit/Channels/CacheChannelTest.php new file mode 100644 index 00000000..bf2a26e9 --- /dev/null +++ b/tests/Unit/Channels/CacheChannelTest.php @@ -0,0 +1,34 @@ +connection = new Connection(); + $this->channelConnectionManager = Mockery::spy(ChannelConnectionManager::class); + $this->app->instance(ChannelConnectionManager::class, $this->channelConnectionManager); +}); + +it('receives no data when no previous event triggered', function () { + $channel = ChannelBroker::create('cache-test-channel'); + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, []); + + $channel->subscribe($this->connection); + + $this->connection->assertNothingSent(); +}); + +it('stores last triggered event', function () { + $channel = new CacheChannel('cache-test-channel'); + + expect($channel->hasCachedPayload())->toBeFalse(); + + $channel->broadcast(['foo' => 'bar']); + + expect($channel->hasCachedPayload())->toBeTrue(); + expect($channel->cachedPayload())->toEqual(['foo' => 'bar']); +}); diff --git a/tests/Unit/Channels/ChannelBrokerTest.php b/tests/Unit/Channels/ChannelBrokerTest.php index e61a6115..a0cbd51e 100644 --- a/tests/Unit/Channels/ChannelBrokerTest.php +++ b/tests/Unit/Channels/ChannelBrokerTest.php @@ -1,5 +1,6 @@ toBeInstanceOf(PresenceChannel::class); }); + +it('can return a cache channel instance', function () { + expect(ChannelBroker::create('cache-foo')) + ->toBeInstanceOf(CacheChannel::class); +}); diff --git a/tests/Unit/Channels/PresenceCacheChannelTest.php b/tests/Unit/Channels/PresenceCacheChannelTest.php new file mode 100644 index 00000000..f81363c8 --- /dev/null +++ b/tests/Unit/Channels/PresenceCacheChannelTest.php @@ -0,0 +1,184 @@ +connection = new Connection(); + $this->channelConnectionManager = Mockery::spy(ChannelConnectionManager::class); + $this->app->instance(ChannelConnectionManager::class, $this->channelConnectionManager); +}); + +it('can subscribe a connection to a channel', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('add') + ->once($this->connection, []); + + $this->channelConnectionManager->shouldReceive('connections') + ->andReturn([]); + + $channel->subscribe($this->connection, validAuth($this->connection->id(), 'presence-cache-test-channel')); +}); + +it('can unsubscribe a connection from a channel', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('remove') + ->once() + ->with($this->connection); + + $channel->unsubscribe($this->connection); +}); + +it('can broadcast to all connections of a channel', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('subscribe'); + + $this->channelConnectionManager->shouldReceive('all') + ->once() + ->andReturn($connections = connections(3)); + + $channel->broadcast(['foo' => 'bar']); + + collect($connections)->each(fn ($connection) => $connection->assertSent(['foo' => 'bar'])); +}); + +it('fails to subscribe if the signature is invalid', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldNotReceive('subscribe'); + + $channel->subscribe($this->connection, 'invalid-signature'); +})->throws(ConnectionUnauthorized::class); + +it('can return data stored on the connection', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $connections = [ + connections(data: ['user_info' => ['name' => 'Joe'], 'user_id' => 1])[0], + connections(data: ['user_info' => ['name' => 'Joe'], 'user_id' => 2])[0], + ]; + + $this->channelConnectionManager->shouldReceive('all') + ->once() + ->andReturn($connections); + + expect($channel->data($this->connection->app()))->toBe([ + 'presence' => [ + 'count' => 2, + 'ids' => [1, 2], + 'hash' => [ + 1 => ['name' => 'Joe'], + 2 => ['name' => 'Joe'], + ], + ], + ]); +}); + +it('sends notification of subscription', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, []); + + $this->channelConnectionManager->shouldReceive('all') + ->andReturn($connections = connections(3)); + + $channel->subscribe($this->connection, validAuth($this->connection->id(), 'presence-cache-test-channel')); + + collect($connections)->each(fn ($connection) => $connection->assertSent([ + 'event' => 'pusher_internal:member_added', + 'data' => [], + 'channel' => 'presence-cache-test-channel', + ])); +}); + +it('sends notification of subscription with data', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + $data = json_encode(['name' => 'Joe']); + + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, ['name' => 'Joe']); + + $this->channelConnectionManager->shouldReceive('all') + ->andReturn($connections = connections(3)); + + $channel->subscribe( + $this->connection, + validAuth( + $this->connection->id(), + 'presence-cache-test-channel', + $data + ), + $data + ); + + collect($connections)->each(fn ($connection) => $connection->assertSent([ + 'event' => 'pusher_internal:member_added', + 'data' => ['name' => 'Joe'], + 'channel' => 'presence-cache-test-channel', + ])); +}); + +it('sends notification of an unsubscribe', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + $data = json_encode(['user_info' => ['name' => 'Joe'], 'user_id' => 1]); + + $channel->subscribe( + $this->connection, + validAuth( + $this->connection->id(), + 'presence-cache-test-channel', + $data + ), + $data + ); + + $this->channelConnectionManager->shouldReceive('find') + ->andReturn(new ChannelConnection($this->connection, ['user_info' => ['name' => 'Joe'], 'user_id' => 1])); + + $this->channelConnectionManager->shouldReceive('all') + ->andReturn($connections = connections(3)); + + $this->channelConnectionManager->shouldReceive('remove') + ->once() + ->with($this->connection); + + $channel->unsubscribe($this->connection); + + collect($connections)->each(fn ($connection) => $connection->assertSent([ + 'event' => 'pusher_internal:member_removed', + 'data' => ['user_id' => 1], + 'channel' => 'presence-cache-test-channel', + ])); +}); + +it('receives no data when no previous event triggered', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, []); + + $channel->subscribe($this->connection, validAuth($this->connection->id(), 'presence-cache-test-channel')); + + $this->connection->assertNothingSent(); +}); + +it('stores last triggered event', function () { + $channel = new PresenceCacheChannel('presence-cache-test-channel'); + + expect($channel->hasCachedPayload())->toBeFalse(); + + $channel->broadcast(['foo' => 'bar']); + + expect($channel->hasCachedPayload())->toBeTrue(); + expect($channel->cachedPayload())->toEqual(['foo' => 'bar']); +}); diff --git a/tests/Unit/Channels/PrivateCacheChannelTest.php b/tests/Unit/Channels/PrivateCacheChannelTest.php new file mode 100644 index 00000000..d68f4d7e --- /dev/null +++ b/tests/Unit/Channels/PrivateCacheChannelTest.php @@ -0,0 +1,77 @@ +connection = new Connection(); + $this->channelConnectionManager = Mockery::spy(ChannelConnectionManager::class); + $this->app->instance(ChannelConnectionManager::class, $this->channelConnectionManager); +}); + +it('can subscribe a connection to a channel', function () { + $channel = new PrivateCacheChannel('private-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, []); + + $channel->subscribe($this->connection, validAuth($this->connection->id(), 'private-cache-test-channel')); +}); + +it('can unsubscribe a connection from a channel', function () { + $channel = new PrivateCacheChannel('private-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('remove') + ->once() + ->with($this->connection); + + $channel->unsubscribe($this->connection); +}); + +it('can broadcast to all connections of a channel', function () { + $channel = new PrivateCacheChannel('test-channel'); + + $this->channelConnectionManager->shouldReceive('add'); + + $this->channelConnectionManager->shouldReceive('all') + ->once() + ->andReturn($connections = connections(3)); + + $channel->broadcast(['foo' => 'bar']); + + collect($connections)->each(fn ($connection) => $connection->assertSent(['foo' => 'bar'])); +}); + +it('fails to subscribe if the signature is invalid', function () { + $channel = new PrivateCacheChannel('presence-test-channel'); + + $this->channelConnectionManager->shouldNotReceive('subscribe'); + + $channel->subscribe($this->connection, 'invalid-signature'); +})->throws(ConnectionUnauthorized::class); + +it('receives no data when no previous event triggered', function () { + $channel = new PrivateCacheChannel('private-cache-test-channel'); + + $this->channelConnectionManager->shouldReceive('add') + ->once() + ->with($this->connection, []); + + $channel->subscribe($this->connection, validAuth($this->connection->id(), 'private-cache-test-channel')); + + $this->connection->assertNothingSent(); +}); + +it('stores last triggered event', function () { + $channel = new PrivateCacheChannel('presence-test-channel'); + + expect($channel->hasCachedPayload())->toBeFalse(); + + $channel->broadcast(['foo' => 'bar']); + + expect($channel->hasCachedPayload())->toBeTrue(); + expect($channel->cachedPayload())->toEqual(['foo' => 'bar']); +}); diff --git a/tests/Unit/Channels/PrivateChannelTest.php b/tests/Unit/Channels/PrivateChannelTest.php index dc3879b5..7f6a836c 100644 --- a/tests/Unit/Channels/PrivateChannelTest.php +++ b/tests/Unit/Channels/PrivateChannelTest.php @@ -46,7 +46,7 @@ }); it('fails to subscribe if the signature is invalid', function () { - $channel = new PrivateChannel('presence-test-channel'); + $channel = new PrivateChannel('private-test-channel'); $this->channelConnectionManager->shouldNotReceive('subscribe');