Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit b7ae9ba

Browse files
committed
Add tests for replication, fix bugs in the implementation
1 parent 4baac7e commit b7ae9ba

17 files changed

+351
-11
lines changed

src/HttpApi/Controllers/FetchChannelController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ public function __invoke(Request $request)
1515
throw new HttpException(404, "Unknown channel `{$request->channelName}`.");
1616
}
1717

18-
return $channel->toArray();
18+
return $channel->toArray($request->appId);
1919
}
2020
}

src/PubSub/Fake/FakeReplication.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace BeyondCode\LaravelWebSockets\PubSub\Fake;
4+
5+
use stdClass;
6+
use React\EventLoop\LoopInterface;
7+
use React\Promise\FulfilledPromise;
8+
use React\Promise\PromiseInterface;
9+
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
10+
11+
class FakeReplication implements ReplicationInterface
12+
{
13+
protected $channels = [];
14+
15+
/**
16+
* Boot the pub/sub provider (open connections, initial subscriptions, etc).
17+
*
18+
* @param LoopInterface $loop
19+
* @return self
20+
*/
21+
public function boot(LoopInterface $loop) : ReplicationInterface
22+
{
23+
return $this;
24+
}
25+
26+
/**
27+
* Publish a payload on a specific channel, for a specific app.
28+
*
29+
* @param string $appId
30+
* @param string $channel
31+
* @param stdClass $payload
32+
* @return bool
33+
*/
34+
public function publish(string $appId, string $channel, stdClass $payload) : bool
35+
{
36+
return true;
37+
}
38+
39+
/**
40+
* Subscribe to receive messages for a channel.
41+
*
42+
* @param string $appId
43+
* @param string $channel
44+
* @return bool
45+
*/
46+
public function subscribe(string $appId, string $channel) : bool
47+
{
48+
return true;
49+
}
50+
51+
/**
52+
* Unsubscribe from a channel.
53+
*
54+
* @param string $appId
55+
* @param string $channel
56+
* @return bool
57+
*/
58+
public function unsubscribe(string $appId, string $channel) : bool
59+
{
60+
return true;
61+
}
62+
63+
/**
64+
* Add a member to a channel. To be called when they have
65+
* subscribed to the channel.
66+
*
67+
* @param string $appId
68+
* @param string $channel
69+
* @param string $socketId
70+
* @param string $data
71+
*/
72+
public function joinChannel(string $appId, string $channel, string $socketId, string $data)
73+
{
74+
$this->channels["$appId:$channel"][$socketId] = $data;
75+
}
76+
77+
/**
78+
* Remove a member from the channel. To be called when they have
79+
* unsubscribed from the channel.
80+
*
81+
* @param string $appId
82+
* @param string $channel
83+
* @param string $socketId
84+
*/
85+
public function leaveChannel(string $appId, string $channel, string $socketId)
86+
{
87+
unset($this->channels["$appId:$channel"][$socketId]);
88+
if (empty($this->channels["$appId:$channel"])) {
89+
unset($this->channels["$appId:$channel"]);
90+
}
91+
}
92+
93+
/**
94+
* Retrieve the full information about the members in a presence channel.
95+
*
96+
* @param string $appId
97+
* @param string $channel
98+
* @return PromiseInterface
99+
*/
100+
public function channelMembers(string $appId, string $channel) : PromiseInterface
101+
{
102+
$data = array_map(function ($user) {
103+
return json_decode($user);
104+
}, $this->channels["$appId:$channel"]);
105+
106+
return new FulfilledPromise($data);
107+
}
108+
109+
/**
110+
* Get the amount of users subscribed for each presence channel.
111+
*
112+
* @param string $appId
113+
* @param array $channelNames
114+
* @return PromiseInterface
115+
*/
116+
public function channelMemberCounts(string $appId, array $channelNames) : PromiseInterface
117+
{
118+
$data = [];
119+
120+
foreach ($channelNames as $channel) {
121+
$data[$channel] = count($this->channels["$appId:$channel"]);
122+
}
123+
124+
return new FulfilledPromise($data);
125+
}
126+
}

src/PubSub/Redis/RedisClient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,9 @@ public function channelMembers(string $appId, string $channel): PromiseInterface
223223
return $this->publishClient->__call('hgetall', ["$appId:$channel"])
224224
->then(function ($members) {
225225
// The data is expected as objects, so we need to JSON decode
226-
return array_walk($members, function ($user) {
226+
return array_map(function ($user) {
227227
return json_decode($user);
228-
});
228+
}, $members);
229229
});
230230
}
231231

src/PubSub/ReplicationInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function publish(string $appId, string $channel, stdClass $payload): bool
2929
/**
3030
* Subscribe to receive messages for a channel.
3131
*
32+
* @param string $appId
3233
* @param string $channel
3334
* @return bool
3435
*/
@@ -37,6 +38,7 @@ public function subscribe(string $appId, string $channel): bool;
3738
/**
3839
* Unsubscribe from a channel.
3940
*
41+
* @param string $appId
4042
* @param string $channel
4143
* @return bool
4244
*/

src/WebSockets/Channels/Channel.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public function broadcastToOthers(ConnectionInterface $connection, $payload)
115115
if (config('websockets.replication.enabled') === true) {
116116
// Also broadcast via the other websocket servers
117117
app(ReplicationInterface::class)
118-
->publish($connection->app->id, $payload);
118+
->publish($connection->app->id, $this->channelName, $payload);
119119
}
120120

121121
$this->broadcastToEveryoneExcept($payload, $connection->socketId);
@@ -139,7 +139,7 @@ public function broadcastToEveryoneExcept($payload, ?string $socketId = null)
139139
}
140140
}
141141

142-
public function toArray()
142+
public function toArray(string $appId = null)
143143
{
144144
return [
145145
'occupied' => count($this->subscribedConnections) > 0,

src/WebSockets/Channels/PresenceChannel.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload)
8282
]));
8383
}
8484

85-
$this->broadcastToOthers($connection, [
85+
$this->broadcastToOthers($connection, (object) [
8686
'event' => 'pusher_internal:member_added',
8787
'channel' => $this->channelName,
8888
'data' => json_encode($channelData),
@@ -107,7 +107,7 @@ public function unsubscribe(ConnectionInterface $connection)
107107
);
108108
}
109109

110-
$this->broadcastToOthers($connection, [
110+
$this->broadcastToOthers($connection, (object) [
111111
'event' => 'pusher_internal:member_removed',
112112
'channel' => $this->channelName,
113113
'data' => json_encode([
@@ -119,6 +119,7 @@ public function unsubscribe(ConnectionInterface $connection)
119119
}
120120

121121
/**
122+
* @param string|null $appId
122123
* @return PromiseInterface|array
123124
*/
124125
public function toArray(string $appId = null)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
4+
5+
use BeyondCode\LaravelWebSockets\Tests\TestsReplication;
6+
7+
class ChannelReplicationTest extends ChannelTest
8+
{
9+
use TestsReplication;
10+
11+
public function setUp() : void
12+
{
13+
parent::setUp();
14+
15+
$this->setupReplication();
16+
}
17+
}

tests/Channels/ChannelTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public function channels_can_broadcast_messages_to_all_connections_except_the_gi
123123

124124
$channel = $this->getChannel($connection1, 'test-channel');
125125

126-
$channel->broadcastToOthers($connection1, [
126+
$channel->broadcastToOthers($connection1, (object) [
127127
'event' => 'broadcasted-event',
128128
'channel' => 'test-channel',
129129
]);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
4+
5+
use BeyondCode\LaravelWebSockets\Tests\TestsReplication;
6+
7+
class PresenceChannelReplicationTest extends PresenceChannelTest
8+
{
9+
use TestsReplication;
10+
11+
public function setUp() : void
12+
{
13+
parent::setUp();
14+
15+
$this->setupReplication();
16+
}
17+
}

tests/Channels/PresenceChannelTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,75 @@ public function clients_with_valid_auth_signatures_can_join_presence_channels()
5959
'channel' => 'presence-channel',
6060
]);
6161
}
62+
63+
/** @test */
64+
public function clients_with_valid_auth_signatures_can_leave_presence_channels()
65+
{
66+
$connection = $this->getWebSocketConnection();
67+
68+
$this->pusherServer->onOpen($connection);
69+
70+
$channelData = [
71+
'user_id' => 1,
72+
'user_info' => [
73+
'name' => 'Marcel',
74+
],
75+
];
76+
77+
$signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
78+
79+
$message = new Message(json_encode([
80+
'event' => 'pusher:subscribe',
81+
'data' => [
82+
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
83+
'channel' => 'presence-channel',
84+
'channel_data' => json_encode($channelData),
85+
],
86+
]));
87+
88+
$this->pusherServer->onMessage($connection, $message);
89+
90+
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
91+
'channel' => 'presence-channel',
92+
]);
93+
94+
$message = new Message(json_encode([
95+
'event' => 'pusher:unsubscribe',
96+
'data' => [
97+
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
98+
'channel' => 'presence-channel',
99+
],
100+
]));
101+
102+
$this->pusherServer->onMessage($connection, $message);
103+
}
104+
105+
/** @test */
106+
public function clients_with_valid_auth_signatures_cannot_leave_channels_they_are_not_in()
107+
{
108+
$connection = $this->getWebSocketConnection();
109+
110+
$this->pusherServer->onOpen($connection);
111+
112+
$channelData = [
113+
'user_id' => 1,
114+
'user_info' => [
115+
'name' => 'Marcel',
116+
],
117+
];
118+
119+
$signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
120+
121+
$message = new Message(json_encode([
122+
'event' => 'pusher:unsubscribe',
123+
'data' => [
124+
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
125+
'channel' => 'presence-channel',
126+
],
127+
]));
128+
129+
$this->pusherServer->onMessage($connection, $message);
130+
131+
$this->markTestAsPassed();
132+
}
62133
}

0 commit comments

Comments
 (0)