Merge pull request #464 from beyondcode/tests/redis-connector

[2.x] Redis connector tests
This commit is contained in:
rennokki 2020-08-17 13:19:45 +03:00 committed by GitHub
commit 99a48d501c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 669 additions and 32 deletions

View File

@ -27,6 +27,7 @@
"cboden/ratchet": "^0.4.1", "cboden/ratchet": "^0.4.1",
"clue/buzz-react": "^2.5", "clue/buzz-react": "^2.5",
"clue/redis-react": "^2.3", "clue/redis-react": "^2.3",
"evenement/evenement": "^2.0|^3.0",
"facade/ignition-contracts": "^1.0", "facade/ignition-contracts": "^1.0",
"guzzlehttp/psr7": "^1.5", "guzzlehttp/psr7": "^1.5",
"illuminate/broadcasting": "^6.0|^7.0", "illuminate/broadcasting": "^6.0|^7.0",

View File

@ -2,6 +2,7 @@
namespace BeyondCode\LaravelWebSockets\Tests\Channels; namespace BeyondCode\LaravelWebSockets\Tests\Channels;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
use BeyondCode\LaravelWebSockets\Tests\TestCase; use BeyondCode\LaravelWebSockets\Tests\TestCase;
class ChannelReplicationTest extends TestCase class ChannelReplicationTest extends TestCase
@ -16,10 +17,142 @@ class ChannelReplicationTest extends TestCase
$this->runOnlyOnRedisReplication(); $this->runOnlyOnRedisReplication();
} }
public function test_not_implemented() /** @test */
public function replication_clients_can_subscribe_to_channels()
{ {
$this->markTestIncomplete( $connection = $this->getWebSocketConnection();
'Not yet implemented tests.'
); $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, $connection->socketId,
json_encode($channelData), json_encode($channelData),
]) ])
->assertCalledWithArgs('hgetall', [ ->assertCalledWithArgs('hgetall', ['1234:presence-channel'])
'1234:presence-channel', ->assertCalled('publish');
]); }
// TODO: This fails somehow
// Debugging shows the exact same pattern as good. /** @test */
/* ->assertCalledWithArgs('publish', [ public function clients_with_valid_auth_signatures_can_leave_presence_channels()
'1234:presence-channel', {
json_encode([ $connection = $this->getWebSocketConnection();
'event' => 'pusher_internal:member_added',
'channel' => 'presence-channel', $this->pusherServer->onOpen($connection);
'data' => $channelData,
'appId' => '1234', $channelData = [
'serverId' => $this->app->make(ReplicationInterface::class)->getServerId(), '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,9 @@
namespace BeyondCode\LaravelWebSockets\Tests\Channels; namespace BeyondCode\LaravelWebSockets\Tests\Channels;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
use BeyondCode\LaravelWebSockets\Tests\TestCase; use BeyondCode\LaravelWebSockets\Tests\TestCase;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
class PrivateChannelReplicationTest extends TestCase class PrivateChannelReplicationTest extends TestCase
{ {
@ -16,10 +18,49 @@ class PrivateChannelReplicationTest extends TestCase
$this->runOnlyOnRedisReplication(); $this->runOnlyOnRedisReplication();
} }
public function test_not_implemented() /** @test */
public function replication_clients_need_valid_auth_signatures_to_join_private_channels()
{ {
$this->markTestIncomplete( $this->expectException(InvalidSignature::class);
'Not yet implemented tests.'
); $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 */ /** @var JsonResponse $response */
$response = array_pop($connection->sentRawData); $response = array_pop($connection->sentRawData);
$this->getSubscribeClient()->assertNothingCalled(); $this->getSubscribeClient()
->assertEventDispatched('message');
$this->getPublishClient() $this->getPublishClient()
->assertCalled('hset') ->assertCalled('hset')

View File

@ -2,7 +2,12 @@
namespace BeyondCode\LaravelWebSockets\Tests\HttpApi; namespace BeyondCode\LaravelWebSockets\Tests\HttpApi;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
use BeyondCode\LaravelWebSockets\Tests\TestCase; use BeyondCode\LaravelWebSockets\Tests\TestCase;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
use Pusher\Pusher;
class FetchChannelsReplicationTest extends TestCase class FetchChannelsReplicationTest extends TestCase
{ {
@ -16,10 +21,160 @@ class FetchChannelsReplicationTest extends TestCase
$this->runOnlyOnRedisReplication(); $this->runOnlyOnRedisReplication();
} }
public function test_not_implemented() /** @test */
public function replication_it_returns_the_channel_information()
{ {
$this->markTestIncomplete( $this->joinPresenceChannel('presence-channel');
'Not yet implemented tests.'
); $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; namespace BeyondCode\LaravelWebSockets\Tests\HttpApi;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController;
use BeyondCode\LaravelWebSockets\Tests\Mocks\Connection;
use BeyondCode\LaravelWebSockets\Tests\TestCase; use BeyondCode\LaravelWebSockets\Tests\TestCase;
use GuzzleHttp\Psr7\Request;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\HttpException;
class FetchUsersReplicationTest extends TestCase class FetchUsersReplicationTest extends TestCase
{ {
@ -16,10 +21,111 @@ class FetchUsersReplicationTest extends TestCase
$this->runOnlyOnRedisReplication(); $this->runOnlyOnRedisReplication();
} }
public function test_not_implemented() /** @test */
public function test_invalid_signatures_can_not_access_the_api()
{ {
$this->markTestIncomplete( $this->expectException(HttpException::class);
'Not yet implemented tests.' $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 = []; protected $calls = [];
/**
* A list of called events for the connector.
*
* @var array
*/
protected $events = [];
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -24,6 +31,16 @@ class LazyClient extends BaseLazyClient
return parent::__call($name, $args); 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. * Check if the method got called.
* *
@ -71,6 +88,53 @@ class LazyClient extends BaseLazyClient
return $this; 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. * Check if no function got called.
* *
@ -83,6 +147,39 @@ class LazyClient extends BaseLazyClient
return $this; 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. * Get the list of all calls.
* *
@ -92,4 +189,40 @@ class LazyClient extends BaseLazyClient
{ {
return $this->calls; 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;
}
} }