This commit is contained in:
Alex Renoki 2020-08-17 11:47:50 +03:00
parent 38b2e4d404
commit 04ff39d75e
7 changed files with 673 additions and 34 deletions

View File

@ -2,6 +2,7 @@
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
use BeyondCode\LaravelWebSockets\Tests\TestCase;
class ChannelReplicationTest extends TestCase
@ -16,10 +17,142 @@ class ChannelReplicationTest extends TestCase
$this->runOnlyOnRedisReplication();
}
public function test_not_implemented()
{
$this->markTestIncomplete(
'Not yet implemented tests.'
);
}
/** @test */
public function replication_clients_can_subscribe_to_channels()
{
$connection = $this->getWebSocketConnection();
$message = new Message(json_encode([
'event' => 'pusher:subscribe',
'data' => [
'channel' => 'basic-channel',
],
]));
$this->pusherServer->onOpen($connection);
$this->pusherServer->onMessage($connection, $message);
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
'channel' => 'basic-channel',
]);
}
/** @test */
public function replication_clients_can_unsubscribe_from_channels()
{
$connection = $this->getConnectedWebSocketConnection(['test-channel']);
$channel = $this->getChannel($connection, 'test-channel');
$this->assertTrue($channel->hasConnections());
$message = new Message(json_encode([
'event' => 'pusher:unsubscribe',
'data' => [
'channel' => 'test-channel',
],
]));
$this->pusherServer->onMessage($connection, $message);
$this->assertFalse($channel->hasConnections());
}
/** @test */
public function replication_a_client_cannot_broadcast_to_other_clients_by_default()
{
// One connection inside channel "test-channel".
$existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
$connection = $this->getConnectedWebSocketConnection(['test-channel']);
$message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}');
$this->pusherServer->onMessage($connection, $message);
$existingConnection->assertNotSentEvent('client-test');
}
/** @test */
public function replication_a_client_can_be_enabled_to_broadcast_to_other_clients()
{
config()->set('websockets.apps.0.enable_client_messages', true);
// One connection inside channel "test-channel".
$existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
$connection = $this->getConnectedWebSocketConnection(['test-channel']);
$message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}');
$this->pusherServer->onMessage($connection, $message);
$existingConnection->assertSentEvent('client-test');
}
/** @test */
public function replication_closed_connections_get_removed_from_all_connected_channels()
{
$connection = $this->getConnectedWebSocketConnection(['test-channel-1', 'test-channel-2']);
$channel1 = $this->getChannel($connection, 'test-channel-1');
$channel2 = $this->getChannel($connection, 'test-channel-2');
$this->assertTrue($channel1->hasConnections());
$this->assertTrue($channel2->hasConnections());
$this->pusherServer->onClose($connection);
$this->assertFalse($channel1->hasConnections());
$this->assertFalse($channel2->hasConnections());
}
/** @test */
public function replication_channels_can_broadcast_messages_to_all_connections()
{
$connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
$connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
$channel = $this->getChannel($connection1, 'test-channel');
$channel->broadcast([
'event' => 'broadcasted-event',
'channel' => 'test-channel',
]);
$connection1->assertSentEvent('broadcasted-event');
$connection2->assertSentEvent('broadcasted-event');
}
/** @test */
public function replication_channels_can_broadcast_messages_to_all_connections_except_the_given_connection()
{
$connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
$connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
$channel = $this->getChannel($connection1, 'test-channel');
$channel->broadcastToOthers($connection1, (object) [
'event' => 'broadcasted-event',
'channel' => 'test-channel',
]);
$connection1->assertNotSentEvent('broadcasted-event');
$connection2->assertSentEvent('broadcasted-event');
}
/** @test */
public function replication_it_responds_correctly_to_the_ping_message()
{
$connection = $this->getConnectedWebSocketConnection();
$message = new Message(json_encode([
'event' => 'pusher:ping',
]));
$this->pusherServer->onMessage($connection, $message);
$connection->assertSentEvent('pusher:pong');
}
}

View File

@ -50,20 +50,87 @@ class PresenceChannelReplicationTest extends TestCase
$connection->socketId,
json_encode($channelData),
])
->assertCalledWithArgs('hgetall', [
'1234:presence-channel',
]);
// TODO: This fails somehow
// Debugging shows the exact same pattern as good.
/* ->assertCalledWithArgs('publish', [
'1234:presence-channel',
json_encode([
'event' => 'pusher_internal:member_added',
'channel' => 'presence-channel',
'data' => $channelData,
'appId' => '1234',
'serverId' => $this->app->make(ReplicationInterface::class)->getServerId(),
]),
]) */
->assertCalledWithArgs('hgetall', ['1234:presence-channel'])
->assertCalled('publish');
}
/** @test */
public function clients_with_valid_auth_signatures_can_leave_presence_channels()
{
$connection = $this->getWebSocketConnection();
$this->pusherServer->onOpen($connection);
$channelData = [
'user_id' => 1,
];
$signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
$message = new Message(json_encode([
'event' => 'pusher:subscribe',
'data' => [
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
'channel' => 'presence-channel',
'channel_data' => json_encode($channelData),
],
]));
$this->pusherServer->onMessage($connection, $message);
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertCalled('hset')
->assertCalledWithArgs('hgetall', ['1234:presence-channel'])
->assertCalled('publish');
$this->getPublishClient()
->resetAssertions();
$message = new Message(json_encode([
'event' => 'pusher:unsubscribe',
'data' => [
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
'channel' => 'presence-channel',
],
]));
$this->pusherServer->onMessage($connection, $message);
$this->getPublishClient()
->assertCalled('hdel')
->assertCalled('publish');
}
/** @test */
public function clients_with_no_user_info_can_join_presence_channels()
{
$connection = $this->getWebSocketConnection();
$this->pusherServer->onOpen($connection);
$channelData = [
'user_id' => 1,
];
$signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
$message = new Message(json_encode([
'event' => 'pusher:subscribe',
'data' => [
'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
'channel' => 'presence-channel',
'channel_data' => json_encode($channelData),
],
]));
$this->pusherServer->onMessage($connection, $message);
$this->getPublishClient()
->assertCalled('hset')
->assertcalledWithArgs('hgetall', ['1234:presence-channel'])
->assertCalled('publish');
}
}

View File

@ -2,7 +2,10 @@
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
use BeyondCode\LaravelWebSockets\Tests\TestCase;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
class PrivateChannelReplicationTest extends TestCase
{
@ -16,10 +19,49 @@ class PrivateChannelReplicationTest extends TestCase
$this->runOnlyOnRedisReplication();
}
public function test_not_implemented()
/** @test */
public function replication_clients_need_valid_auth_signatures_to_join_private_channels()
{
$this->markTestIncomplete(
'Not yet implemented tests.'
);
$this->expectException(InvalidSignature::class);
$connection = $this->getWebSocketConnection();
$message = new Message(json_encode([
'event' => 'pusher:subscribe',
'data' => [
'auth' => 'invalid',
'channel' => 'private-channel',
],
]));
$this->pusherServer->onOpen($connection);
$this->pusherServer->onMessage($connection, $message);
}
/** @test */
public function replication_clients_with_valid_auth_signatures_can_join_private_channels()
{
$connection = $this->getWebSocketConnection();
$this->pusherServer->onOpen($connection);
$signature = "{$connection->socketId}:private-channel";
$hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
$message = new Message(json_encode([
'event' => 'pusher:subscribe',
'data' => [
'auth' => "{$connection->app->key}:{$hashedAppSecret}",
'channel' => 'private-channel',
],
]));
$this->pusherServer->onMessage($connection, $message);
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
'channel' => 'private-channel',
]);
}
}

View File

@ -103,7 +103,8 @@ class FetchChannelReplicationTest extends TestCase
/** @var JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->getSubscribeClient()->assertNothingCalled();
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertCalled('hset')

View File

@ -2,7 +2,13 @@
namespace BeyondCode\LaravelWebSockets\Tests\HttpApi;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
use BeyondCode\LaravelWebSockets\Tests\TestCase;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\HttpException;
class FetchChannelsReplicationTest extends TestCase
{
@ -16,10 +22,161 @@ class FetchChannelsReplicationTest extends TestCase
$this->runOnlyOnRedisReplication();
}
public function test_not_implemented()
/** @test */
public function replication_it_returns_the_channel_information()
{
$this->markTestIncomplete(
'Not yet implemented tests.'
);
$this->joinPresenceChannel('presence-channel');
$connection = new Connection();
$requestPath = '/apps/1234/channels';
$routeParams = [
'appId' => '1234',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchChannelsController::class);
$controller->onOpen($connection, $request);
/** @var JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertCalled('hset')
->assertCalledWithArgs('hgetall', ['1234:presence-channel'])
->assertCalled('publish')
->assertCalled('multi')
->assertCalledWithArgs('hlen', ['1234:presence-channel'])
->assertCalled('exec');
}
/** @test */
public function replication_it_returns_the_channel_information_for_prefix()
{
$this->joinPresenceChannel('presence-global.1');
$this->joinPresenceChannel('presence-global.1');
$this->joinPresenceChannel('presence-global.2');
$this->joinPresenceChannel('presence-notglobal.2');
$connection = new Connection();
$requestPath = '/apps/1234/channels';
$routeParams = [
'appId' => '1234',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
'filter_by_prefix' => 'presence-global',
]);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchChannelsController::class);
$controller->onOpen($connection, $request);
/** @var JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertCalled('hset')
->assertCalledWithArgs('hgetall', ['1234:presence-global.1'])
->assertCalledWithArgs('hgetall', ['1234:presence-global.2'])
->assertCalledWithArgs('hgetall', ['1234:presence-notglobal.2'])
->assertCalled('publish')
->assertCalled('multi')
->assertCalledWithArgs('hlen', ['1234:presence-global.1'])
->assertCalledWithArgs('hlen', ['1234:presence-global.2'])
->assertNotCalledWithArgs('hlen', ['1234:presence-notglobal.2'])
->assertCalled('exec');
}
/** @test */
public function replication_it_returns_the_channel_information_for_prefix_with_user_count()
{
$this->joinPresenceChannel('presence-global.1');
$this->joinPresenceChannel('presence-global.1');
$this->joinPresenceChannel('presence-global.2');
$this->joinPresenceChannel('presence-notglobal.2');
$connection = new Connection();
$requestPath = '/apps/1234/channels';
$routeParams = [
'appId' => '1234',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
'filter_by_prefix' => 'presence-global',
'info' => 'user_count',
]);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchChannelsController::class);
$controller->onOpen($connection, $request);
/** @var JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertCalled('hset')
->assertCalledWithArgs('hgetall', ['1234:presence-global.1'])
->assertCalledWithArgs('hgetall', ['1234:presence-global.2'])
->assertCalledWithArgs('hgetall', ['1234:presence-notglobal.2'])
->assertCalled('publish')
->assertCalled('multi')
->assertCalledWithArgs('hlen', ['1234:presence-global.1'])
->assertCalledWithArgs('hlen', ['1234:presence-global.2'])
->assertNotCalledWithArgs('hlen', ['1234:presence-notglobal.2'])
->assertCalled('exec');
}
/** @test */
public function replication_it_returns_empty_object_for_no_channels_found()
{
$connection = new Connection();
$requestPath = '/apps/1234/channels';
$routeParams = [
'appId' => '1234',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchChannelsController::class);
$controller->onOpen($connection, $request);
/** @var JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient()
->assertNotCalled('hset')
->assertNotCalled('hgetall')
->assertNotCalled('publish')
->assertCalled('multi')
->assertNotCalled('hlen')
->assertCalled('exec');
}
}

View File

@ -2,7 +2,12 @@
namespace BeyondCode\LaravelWebSockets\Tests\HttpApi;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
use BeyondCode\LaravelWebSockets\Tests\TestCase;
use GuzzleHttp\Psr7\Request;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\HttpException;
class FetchUsersReplicationTest extends TestCase
{
@ -16,10 +21,111 @@ class FetchUsersReplicationTest extends TestCase
$this->runOnlyOnRedisReplication();
}
public function test_not_implemented()
/** @test */
public function test_invalid_signatures_can_not_access_the_api()
{
$this->markTestIncomplete(
'Not yet implemented tests.'
);
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Invalid auth signature provided.');
$connection = new Connection();
$requestPath = '/apps/1234/channel/my-channel';
$routeParams = [
'appId' => '1234',
'channelName' => 'my-channel',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchUsersController::class);
$controller->onOpen($connection, $request);
}
/** @test */
public function test_it_only_returns_data_for_presence_channels()
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Invalid presence channel');
$this->getConnectedWebSocketConnection(['my-channel']);
$connection = new Connection();
$requestPath = '/apps/1234/channel/my-channel/users';
$routeParams = [
'appId' => '1234',
'channelName' => 'my-channel',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchUsersController::class);
$controller->onOpen($connection, $request);
}
/** @test */
public function test_it_returns_404_for_invalid_channels()
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Unknown channel');
$this->getConnectedWebSocketConnection(['my-channel']);
$connection = new Connection();
$requestPath = '/apps/1234/channel/invalid-channel/users';
$routeParams = [
'appId' => '1234',
'channelName' => 'invalid-channel',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchUsersController::class);
$controller->onOpen($connection, $request);
}
/** @test */
public function test_it_returns_connected_user_information()
{
$this->skipOnRedisReplication();
$this->joinPresenceChannel('presence-channel');
$connection = new Connection();
$requestPath = '/apps/1234/channel/presence-channel/users';
$routeParams = [
'appId' => '1234',
'channelName' => 'presence-channel',
];
$queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
$controller = app(FetchUsersController::class);
$controller->onOpen($connection, $request);
/** @var \Illuminate\Http\JsonResponse $response */
$response = array_pop($connection->sentRawData);
$this->assertSame([
'users' => [
[
'id' => 1,
],
],
], json_decode($response->getContent(), true));
}
}

View File

@ -14,6 +14,13 @@ class LazyClient extends BaseLazyClient
*/
protected $calls = [];
/**
* A list of called events for the connector.
*
* @var array
*/
protected $events = [];
/**
* {@inheritdoc}
*/
@ -24,6 +31,16 @@ class LazyClient extends BaseLazyClient
return parent::__call($name, $args);
}
/**
* {@inheritdoc}
*/
public function on($event, callable $listener)
{
$this->events[] = $event;
return parent::on($event, $listener);
}
/**
* Check if the method got called.
*
@ -71,6 +88,53 @@ class LazyClient extends BaseLazyClient
return $this;
}
/**
* Check if the method didn't call.
*
* @param string $name
* @return $this
*/
public function assertNotCalled($name)
{
foreach ($this->getCalledFunctions() as $function) {
[$calledName, ] = $function;
if ($calledName === $name) {
PHPUnit::assertFalse(true);
return $this;
}
}
PHPUnit::assertTrue(true);
return $this;
}
/**
* Check if the method got not called with specific args.
*
* @param string $name
* @param array $args
* @return $this
*/
public function assertNotCalledWithArgs($name, array $args)
{
foreach ($this->getCalledFunctions() as $function) {
[$calledName, $calledArgs] = $function;
if ($calledName === $name && $calledArgs === $args) {
PHPUnit::assertFalse(true);
return $this;
}
}
PHPUnit::assertTrue(true);
return $this;
}
/**
* Check if no function got called.
*
@ -83,6 +147,39 @@ class LazyClient extends BaseLazyClient
return $this;
}
/**
* Check if the event got dispatched.
*
* @param string $event
* @return $this
*/
public function assertEventDispatched($event)
{
foreach ($this->getCalledEvents() as $dispatchedEvent) {
if ($dispatchedEvent === $event) {
PHPUnit::assertTrue(true);
return $this;
}
}
PHPUnit::assertFalse(true);
return $this;
}
/**
* Check if no function got called.
*
* @return $this
*/
public function assertNothingDispatched()
{
PHPUnit::assertEquals([], $this->getCalledEvents());
return $this;
}
/**
* Get the list of all calls.
*
@ -92,4 +189,40 @@ class LazyClient extends BaseLazyClient
{
return $this->calls;
}
/**
* Get the list of events.
*
* @return array
*/
public function getCalledEvents()
{
return $this->events;
}
/**
* Dump the assertions.
*
* @return void
*/
public function dd()
{
dd([
'functions' => $this->getCalledFunctions(),
'events' => $this->getCalledEvents(),
]);
}
/**
* Reset the assertions.
*
* @return $this
*/
public function resetAssertions()
{
$this->calls = [];
$this->events = [];
return $this;
}
}