diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..33dbc6bf33 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,18 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + +status: + project: yes + patch: yes + changes: no + +comment: + layout: "reach, diff, flags, files, footer" + behavior: default + require_changes: no diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index aaed621374..c3e476232c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -45,7 +45,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - name: Execute tests with Local driver diff --git a/.gitignore b/.gitignore index f423e5bf06..a4753bd34e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor coverage .phpunit.result.cache .idea/ +database.sqlite diff --git a/composer.json b/composer.json index 276de5f921..f34b96d3e5 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "require-dev": { "mockery/mockery": "^1.3", - "orchestra/testbench": "3.8.*|^4.0|^5.0", + "orchestra/testbench-browser-kit": "^4.0|^5.0", "phpunit/phpunit": "^8.0|^9.0" }, "autoload": { diff --git a/src/Contracts/PushesToPusher.php b/src/Contracts/PushesToPusher.php new file mode 100644 index 0000000000..4c160b3548 --- /dev/null +++ b/src/Contracts/PushesToPusher.php @@ -0,0 +1,32 @@ +header('x-app-id')); - $broadcaster = new PusherBroadcaster(new Pusher( - $app->key, - $app->secret, - $app->id, - [] - )); + $broadcaster = $this->getPusherBroadcaster([ + 'key' => $app->key, + 'secret' => $app->secret, + 'id' =>$app->id, + ]); /* * Since the dashboard itself is already secured by the diff --git a/src/Dashboard/Http/Controllers/SendMessage.php b/src/Dashboard/Http/Controllers/SendMessage.php index 92777e4a0f..c8d84d85f8 100644 --- a/src/Dashboard/Http/Controllers/SendMessage.php +++ b/src/Dashboard/Http/Controllers/SendMessage.php @@ -2,13 +2,15 @@ namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers; +use BeyondCode\LaravelWebSockets\Contracts\PushesToPusher; use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId; -use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster; +use Exception; use Illuminate\Http\Request; -use Pusher\Pusher; class SendMessage { + use PushesToPusher; + /** * Send the message to the requested channel. * @@ -17,7 +19,7 @@ class SendMessage */ public function __invoke(Request $request) { - $validated = $request->validate([ + $request->validate([ 'appId' => ['required', new AppId], 'key' => 'required|string', 'secret' => 'required|string', @@ -26,30 +28,27 @@ public function __invoke(Request $request) 'data' => 'required|json', ]); - $this->getPusherBroadcaster($validated)->broadcast( - [$validated['channel']], - $validated['event'], - json_decode($validated['data'], true) - ); + $broadcaster = $this->getPusherBroadcaster([ + 'key' => $request->key, + 'secret' => $request->secret, + 'id' => $request->appId, + ]); - return 'ok'; - } + try { + $broadcaster->broadcast( + [$request->channel], + $request->event, + json_decode($request->data, true) + ); + } catch (Exception $e) { + return response()->json([ + 'ok' => false, + 'exception' => $e->getMessage(), + ]); + } - /** - * Get the pusher broadcaster for the current request. - * - * @param array $validated - * @return \Illuminate\Broadcasting\Broadcasters\PusherBroadcaster - */ - protected function getPusherBroadcaster(array $validated): PusherBroadcaster - { - $pusher = new Pusher( - $validated['key'], - $validated['secret'], - $validated['appId'], - config('broadcasting.connections.pusher.options', []) - ); - - return new PusherBroadcaster($pusher); + return response()->json([ + 'ok' => true, + ]); } } diff --git a/src/Dashboard/Http/Controllers/DashboardApiController.php b/src/Dashboard/Http/Controllers/ShowStatistics.php similarity index 81% rename from src/Dashboard/Http/Controllers/DashboardApiController.php rename to src/Dashboard/Http/Controllers/ShowStatistics.php index c240905b2d..134cb623eb 100644 --- a/src/Dashboard/Http/Controllers/DashboardApiController.php +++ b/src/Dashboard/Http/Controllers/ShowStatistics.php @@ -5,7 +5,7 @@ use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver; use Illuminate\Http\Request; -class DashboardApiController +class ShowStatistics { /** * Get statistics for an app ID. @@ -15,7 +15,7 @@ class DashboardApiController * @param mixed $appId * @return \Illuminate\Http\Response */ - public function getStatistics(Request $request, StatisticsDriver $driver, $appId) + public function __invoke(Request $request, StatisticsDriver $driver, $appId) { return $driver::get($appId, $request); } diff --git a/src/PubSub/Broadcasters/RedisPusherBroadcaster.php b/src/PubSub/Broadcasters/RedisPusherBroadcaster.php index c59f065bdf..1c7966135b 100644 --- a/src/PubSub/Broadcasters/RedisPusherBroadcaster.php +++ b/src/PubSub/Broadcasters/RedisPusherBroadcaster.php @@ -45,10 +45,10 @@ class RedisPusherBroadcaster extends Broadcaster /** * Create a new broadcaster instance. * - * @param Pusher $pusher - * @param $appId - * @param \Illuminate\Contracts\Redis\Factory $redis - * @param string|null $connection + * @param Pusher $pusher + * @param mixed $appId + * @param \Illuminate\Contracts\Redis\Factory $redis + * @param string|null $connection */ public function __construct(Pusher $pusher, $appId, Redis $redis, $connection = null) { @@ -63,7 +63,6 @@ public function __construct(Pusher $pusher, $appId, Redis $redis, $connection = * * @param \Illuminate\Http\Request $request * @return mixed - * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function auth($request) @@ -83,8 +82,8 @@ public function auth($request) /** * Return the valid authentication response. * - * @param \Illuminate\Http\Request $request - * @param mixed $result + * @param \Illuminate\Http\Request $request + * @param mixed $result * @return mixed * @throws \Pusher\PusherException */ @@ -144,7 +143,7 @@ public function broadcast(array $channels, $event, array $payload = []) ]); foreach ($this->formatChannels($channels) as $channel) { - $connection->publish("{$this->appId}:$channel", $payload); + $connection->publish("{$this->appId}:{$channel}", $payload); } } } diff --git a/src/PubSub/Drivers/RedisClient.php b/src/PubSub/Drivers/RedisClient.php index 255d826892..14b935798c 100644 --- a/src/PubSub/Drivers/RedisClient.php +++ b/src/PubSub/Drivers/RedisClient.php @@ -265,7 +265,7 @@ public function channelMemberCounts($appId, array $channelNames): PromiseInterfa * @param string $payload * @return void */ - protected function onMessage(string $redisChannel, string $payload) + public function onMessage(string $redisChannel, string $payload) { $payload = json_decode($payload); diff --git a/src/Statistics/DnsResolver.php b/src/Statistics/DnsResolver.php deleted file mode 100644 index 57cfdcb201..0000000000 --- a/src/Statistics/DnsResolver.php +++ /dev/null @@ -1,59 +0,0 @@ -resolveInternal($domain); - } - - /** - * Resolve all domains. - * - * @param string $domain - * @param string $type - * @return FulfilledPromise - */ - public function resolveAll($domain, $type) - { - return $this->resolveInternal($domain, $type); - } - - /** - * Resolve the internal domain. - * - * @param string $domain - * @param string $type - * @return FulfilledPromise - */ - private function resolveInternal($domain, $type = null) - { - return new FulfilledPromise($this->internalIp); - } - - /** - * {@inheritdoc} - */ - public function __toString() - { - return $this->internalIp; - } -} diff --git a/src/Statistics/Drivers/DatabaseDriver.php b/src/Statistics/Drivers/DatabaseDriver.php index cb5e353743..034e4d4831 100644 --- a/src/Statistics/Drivers/DatabaseDriver.php +++ b/src/Statistics/Drivers/DatabaseDriver.php @@ -92,10 +92,10 @@ public static function create(array $data): StatisticsDriver * Get the records to show to the dashboard. * * @param mixed $appId - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request|null $request * @return array */ - public static function get($appId, Request $request): array + public static function get($appId, ?Request $request): array { $class = config('websockets.statistics.database.model'); diff --git a/src/Statistics/Drivers/StatisticsDriver.php b/src/Statistics/Drivers/StatisticsDriver.php index 9b9cfb0452..fd77b2cf46 100644 --- a/src/Statistics/Drivers/StatisticsDriver.php +++ b/src/Statistics/Drivers/StatisticsDriver.php @@ -61,10 +61,10 @@ public static function create(array $data): StatisticsDriver; * Get the records to show to the dashboard. * * @param mixed $appId - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request|null $request * @return void */ - public static function get($appId, Request $request); + public static function get($appId, ?Request $request); /** * Delete statistics from the store, diff --git a/src/Statistics/Logger/MemoryStatisticsLogger.php b/src/Statistics/Logger/MemoryStatisticsLogger.php index fe0ac82e5d..a0bee8e2f0 100644 --- a/src/Statistics/Logger/MemoryStatisticsLogger.php +++ b/src/Statistics/Logger/MemoryStatisticsLogger.php @@ -6,7 +6,6 @@ use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver; use BeyondCode\LaravelWebSockets\Statistics\Statistic; use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager; -use Ratchet\ConnectionInterface; class MemoryStatisticsLogger implements StatisticsLogger { @@ -47,12 +46,12 @@ public function __construct(ChannelManager $channelManager, StatisticsDriver $dr /** * Handle the incoming websocket message. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function webSocketMessage(ConnectionInterface $connection) + public function webSocketMessage($appId) { - $this->findOrMakeStatisticForAppId($connection->app->id) + $this->findOrMakeStatisticForAppId($appId) ->webSocketMessage(); } @@ -71,24 +70,24 @@ public function apiMessage($appId) /** * Handle the new conection. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function connection(ConnectionInterface $connection) + public function connection($appId) { - $this->findOrMakeStatisticForAppId($connection->app->id) + $this->findOrMakeStatisticForAppId($appId) ->connection(); } /** * Handle disconnections. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function disconnection(ConnectionInterface $connection) + public function disconnection($appId) { - $this->findOrMakeStatisticForAppId($connection->app->id) + $this->findOrMakeStatisticForAppId($appId) ->disconnection(); } @@ -126,4 +125,14 @@ protected function findOrMakeStatisticForAppId($appId): Statistic return $this->statistics[$appId]; } + + /** + * Get the saved statistics. + * + * @return array + */ + public function getStatistics(): array + { + return $this->statistics; + } } diff --git a/src/Statistics/Logger/NullStatisticsLogger.php b/src/Statistics/Logger/NullStatisticsLogger.php index 94e35475af..1120c2e951 100644 --- a/src/Statistics/Logger/NullStatisticsLogger.php +++ b/src/Statistics/Logger/NullStatisticsLogger.php @@ -4,7 +4,6 @@ use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver; use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager; -use Ratchet\ConnectionInterface; class NullStatisticsLogger implements StatisticsLogger { @@ -38,10 +37,10 @@ public function __construct(ChannelManager $channelManager, StatisticsDriver $dr /** * Handle the incoming websocket message. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function webSocketMessage(ConnectionInterface $connection) + public function webSocketMessage($appId) { // } @@ -60,10 +59,10 @@ public function apiMessage($appId) /** * Handle the new conection. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function connection(ConnectionInterface $connection) + public function connection($appId) { // } @@ -71,10 +70,10 @@ public function connection(ConnectionInterface $connection) /** * Handle disconnections. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function disconnection(ConnectionInterface $connection) + public function disconnection($appId) { // } diff --git a/src/Statistics/Logger/StatisticsLogger.php b/src/Statistics/Logger/StatisticsLogger.php index 84b09dbef8..6f6fe0ce84 100644 --- a/src/Statistics/Logger/StatisticsLogger.php +++ b/src/Statistics/Logger/StatisticsLogger.php @@ -2,17 +2,15 @@ namespace BeyondCode\LaravelWebSockets\Statistics\Logger; -use Ratchet\ConnectionInterface; - interface StatisticsLogger { /** * Handle the incoming websocket message. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function webSocketMessage(ConnectionInterface $connection); + public function webSocketMessage($appId); /** * Handle the incoming API message. @@ -25,18 +23,18 @@ public function apiMessage($appId); /** * Handle the new conection. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function connection(ConnectionInterface $connection); + public function connection($appId); /** * Handle disconnections. * - * @param \Ratchet\ConnectionInterface $connection + * @param mixed $appId * @return void */ - public function disconnection(ConnectionInterface $connection); + public function disconnection($appId); /** * Save all the stored statistics. diff --git a/src/WebSockets/WebSocketHandler.php b/src/WebSockets/WebSocketHandler.php index 7a2537ec85..0f00342633 100644 --- a/src/WebSockets/WebSocketHandler.php +++ b/src/WebSockets/WebSocketHandler.php @@ -65,7 +65,7 @@ public function onMessage(ConnectionInterface $connection, MessageInterface $mes $message->respond(); - StatisticsLogger::webSocketMessage($connection); + StatisticsLogger::webSocketMessage($connection->app->id); } /** @@ -82,7 +82,7 @@ public function onClose(ConnectionInterface $connection) 'socketId' => $connection->socketId, ]); - StatisticsLogger::disconnection($connection); + StatisticsLogger::disconnection($connection->app->id); } /** @@ -200,7 +200,7 @@ protected function establishConnection(ConnectionInterface $connection) 'socketId' => $connection->socketId, ]); - StatisticsLogger::connection($connection); + StatisticsLogger::connection($connection->app->id); return $this; } diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php index 09db7784ac..4c687edb1c 100644 --- a/src/WebSocketsServiceProvider.php +++ b/src/WebSocketsServiceProvider.php @@ -4,9 +4,9 @@ use BeyondCode\LaravelWebSockets\Apps\AppManager; use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard; -use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\DashboardApiController; use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage; use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard; +use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics; use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard; use BeyondCode\LaravelWebSockets\PubSub\Broadcasters\RedisPusherBroadcaster; use BeyondCode\LaravelWebSockets\Server\Router; @@ -118,13 +118,15 @@ protected function configurePubSub() */ protected function registerDashboardRoutes() { - Route::prefix(config('websockets.dashboard.path'))->group(function () { - Route::middleware(config('websockets.dashboard.middleware', [AuthorizeDashboard::class]))->group(function () { - Route::get('/', ShowDashboard::class); - Route::get('/api/{appId}/statistics', [DashboardApiController::class, 'getStatistics']); - Route::post('auth', AuthenticateDashboard::class); - Route::post('event', SendMessage::class); - }); + Route::group([ + 'prefix' => config('websockets.dashboard.path'), + 'as' => 'laravel-websockets.', + 'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]), + ], function () { + Route::get('/', ShowDashboard::class)->name('dashboard'); + Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics'); + Route::post('/auth', AuthenticateDashboard::class)->name('auth'); + Route::post('/event', SendMessage::class)->name('event'); }); return $this; diff --git a/tests/Channels/ChannelReplicationTest.php b/tests/Channels/ChannelReplicationTest.php index 4480442742..adf1e9ac2f 100644 --- a/tests/Channels/ChannelReplicationTest.php +++ b/tests/Channels/ChannelReplicationTest.php @@ -22,12 +22,12 @@ public function replication_clients_can_subscribe_to_channels() { $connection = $this->getWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'channel' => 'basic-channel', ], - ])); + ]); $this->pusherServer->onOpen($connection); @@ -47,12 +47,12 @@ public function replication_clients_can_unsubscribe_from_channels() $this->assertTrue($channel->hasConnections()); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:unsubscribe', 'data' => [ 'channel' => 'test-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); @@ -67,7 +67,7 @@ public function replication_a_client_cannot_broadcast_to_other_clients_by_defaul $connection = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}'); + $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']); $this->pusherServer->onMessage($connection, $message); @@ -84,7 +84,7 @@ public function replication_a_client_can_be_enabled_to_broadcast_to_other_client $connection = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}'); + $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']); $this->pusherServer->onMessage($connection, $message); @@ -147,9 +147,9 @@ public function replication_it_responds_correctly_to_the_ping_message() { $connection = $this->getConnectedWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:ping', - ])); + ]); $this->pusherServer->onMessage($connection, $message); diff --git a/tests/Channels/ChannelTest.php b/tests/Channels/ChannelTest.php index a16a83d200..333a38d3f3 100644 --- a/tests/Channels/ChannelTest.php +++ b/tests/Channels/ChannelTest.php @@ -12,12 +12,12 @@ public function clients_can_subscribe_to_channels() { $connection = $this->getWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'channel' => 'basic-channel', ], - ])); + ]); $this->pusherServer->onOpen($connection); @@ -37,12 +37,12 @@ public function clients_can_unsubscribe_from_channels() $this->assertTrue($channel->hasConnections()); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:unsubscribe', 'data' => [ 'channel' => 'test-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); @@ -57,7 +57,7 @@ public function a_client_cannot_broadcast_to_other_clients_by_default() $connection = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}'); + $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']); $this->pusherServer->onMessage($connection, $message); @@ -74,7 +74,7 @@ public function a_client_can_be_enabled_to_broadcast_to_other_clients() $connection = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message('{"event": "client-test", "data": {}, "channel": "test-channel"}'); + $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']); $this->pusherServer->onMessage($connection, $message); @@ -137,9 +137,9 @@ public function it_responds_correctly_to_the_ping_message() { $connection = $this->getConnectedWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:ping', - ])); + ]); $this->pusherServer->onMessage($connection, $message); diff --git a/tests/Channels/PresenceChannelReplicationTest.php b/tests/Channels/PresenceChannelReplicationTest.php index 822ef4e1f1..4cbe2e07da 100644 --- a/tests/Channels/PresenceChannelReplicationTest.php +++ b/tests/Channels/PresenceChannelReplicationTest.php @@ -33,14 +33,14 @@ public function clients_with_valid_auth_signatures_can_join_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); @@ -67,14 +67,14 @@ public function clients_with_valid_auth_signatures_can_leave_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); @@ -89,13 +89,13 @@ public function clients_with_valid_auth_signatures_can_leave_presence_channels() $this->getPublishClient() ->resetAssertions(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:unsubscribe', 'data' => [ 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret), 'channel' => 'presence-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); @@ -117,14 +117,14 @@ public function clients_with_no_user_info_can_join_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); diff --git a/tests/Channels/PresenceChannelTest.php b/tests/Channels/PresenceChannelTest.php index a72d94f8ec..f6481af22d 100644 --- a/tests/Channels/PresenceChannelTest.php +++ b/tests/Channels/PresenceChannelTest.php @@ -15,13 +15,13 @@ public function clients_need_valid_auth_signatures_to_join_presence_channels() $connection = $this->getWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => 'invalid', 'channel' => 'presence-channel', ], - ])); + ]); $this->pusherServer->onOpen($connection); @@ -46,14 +46,14 @@ public function clients_with_valid_auth_signatures_can_join_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); @@ -77,14 +77,14 @@ public function clients_with_valid_auth_signatures_can_leave_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); @@ -92,13 +92,13 @@ public function clients_with_valid_auth_signatures_can_leave_presence_channels() 'channel' => 'presence-channel', ]); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:unsubscribe', 'data' => [ 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret), 'channel' => 'presence-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); } @@ -118,14 +118,14 @@ public function clients_with_no_user_info_can_join_presence_channels() $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ '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); @@ -150,13 +150,13 @@ public function clients_with_valid_auth_signatures_cannot_leave_channels_they_ar $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:unsubscribe', 'data' => [ 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret), 'channel' => 'presence-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); diff --git a/tests/Channels/PrivateChannelReplicationTest.php b/tests/Channels/PrivateChannelReplicationTest.php index cc4bab725a..3a1641228f 100644 --- a/tests/Channels/PrivateChannelReplicationTest.php +++ b/tests/Channels/PrivateChannelReplicationTest.php @@ -25,13 +25,13 @@ public function replication_clients_need_valid_auth_signatures_to_join_private_c $connection = $this->getWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => 'invalid', 'channel' => 'private-channel', ], - ])); + ]); $this->pusherServer->onOpen($connection); @@ -49,13 +49,13 @@ public function replication_clients_with_valid_auth_signatures_can_join_private_ $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => "{$connection->app->key}:{$hashedAppSecret}", 'channel' => 'private-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); diff --git a/tests/Channels/PrivateChannelTest.php b/tests/Channels/PrivateChannelTest.php index 6b8d9b644e..91f48d006b 100644 --- a/tests/Channels/PrivateChannelTest.php +++ b/tests/Channels/PrivateChannelTest.php @@ -15,13 +15,13 @@ public function clients_need_valid_auth_signatures_to_join_private_channels() $connection = $this->getWebSocketConnection(); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => 'invalid', 'channel' => 'private-channel', ], - ])); + ]); $this->pusherServer->onOpen($connection); @@ -39,13 +39,13 @@ public function clients_with_valid_auth_signatures_can_join_private_channels() $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => "{$connection->app->key}:{$hashedAppSecret}", 'channel' => 'private-channel', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); diff --git a/tests/Commands/CleanStatisticsTest.php b/tests/Commands/CleanStatisticsTest.php index 91f7790904..9e26a6dcd2 100644 --- a/tests/Commands/CleanStatisticsTest.php +++ b/tests/Commands/CleanStatisticsTest.php @@ -42,4 +42,34 @@ public function it_can_clean_the_statistics() $this->assertCount(0, WebSocketsStatisticsEntry::where('created_at', '<', $cutOffDate)->get()); } + + /** @test */ + public function it_can_clean_the_statistics_for_app_id_only() + { + Collection::times(60)->each(function (int $index) { + WebSocketsStatisticsEntry::create([ + 'app_id' => 'app_id', + 'peak_connection_count' => 1, + 'websocket_message_count' => 2, + 'api_message_count' => 3, + 'created_at' => Carbon::now()->subDays($index)->startOfDay(), + ]); + }); + + Collection::times(60)->each(function (int $index) { + WebSocketsStatisticsEntry::create([ + 'app_id' => 'app_id2', + 'peak_connection_count' => 1, + 'websocket_message_count' => 2, + 'api_message_count' => 3, + 'created_at' => Carbon::now()->subDays($index)->startOfDay(), + ]); + }); + + $this->assertCount(120, WebSocketsStatisticsEntry::all()); + + Artisan::call('websockets:clean', ['appId' => 'app_id']); + + $this->assertCount(91, WebSocketsStatisticsEntry::all()); + } } diff --git a/tests/Commands/StartWebSocketServerTest.php b/tests/Commands/StartWebSocketServerTest.php index 637c1c8183..00d0d329db 100644 --- a/tests/Commands/StartWebSocketServerTest.php +++ b/tests/Commands/StartWebSocketServerTest.php @@ -9,7 +9,7 @@ class StartWebSocketServerTest extends TestCase /** @test */ public function does_not_fail_if_building_up() { - $this->artisan('websockets:serve', ['--test' => true]); + $this->artisan('websockets:serve', ['--test' => true, '--debug' => true]); $this->assertTrue(true); } diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 0aba6eccf9..3e17566d4f 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -58,7 +58,7 @@ public function ping_returns_pong() { $connection = $this->getWebSocketConnection(); - $message = new Message('{"event": "pusher:ping"}'); + $message = new Message(['event' => 'pusher:ping']); $this->pusherServer->onOpen($connection); diff --git a/tests/Dashboard/AuthTest.php b/tests/Dashboard/AuthTest.php new file mode 100644 index 0000000000..cf73ac5b25 --- /dev/null +++ b/tests/Dashboard/AuthTest.php @@ -0,0 +1,100 @@ +getConnectedWebSocketConnection(['test-channel']); + + $this->pusherServer->onOpen($connection); + + $this->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.auth'), [ + 'socket_id' => $connection->socketId, + 'channel_name' => 'test-channel', + ], ['x-app-id' => '1234']) + ->seeJsonStructure([ + 'auth', + 'channel_data', + ]); + } + + /** @test */ + public function can_authenticate_dashboard_over_private_channel() + { + $connection = $this->getWebSocketConnection(); + + $this->pusherServer->onOpen($connection); + + $signature = "{$connection->socketId}:private-channel"; + + $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret); + + $message = new Message([ + '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', + ]); + + $this->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.auth'), [ + 'socket_id' => $connection->socketId, + 'channel_name' => 'private-test-channel', + ], ['x-app-id' => '1234']) + ->seeJsonStructure([ + 'auth', + ]); + } + + /** @test */ + public function can_authenticate_dashboard_over_presence_channel() + { + $connection = $this->getWebSocketConnection(); + + $this->pusherServer->onOpen($connection); + + $channelData = [ + 'user_id' => 1, + 'user_info' => [ + 'name' => 'Marcel', + ], + ]; + + $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData); + + $message = new Message([ + '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->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.auth'), [ + 'socket_id' => $connection->socketId, + 'channel_name' => 'presence-channel', + ], ['x-app-id' => '1234']) + ->seeJsonStructure([ + 'auth', + ]); + } +} diff --git a/tests/Dashboard/DashboardTest.php b/tests/Dashboard/DashboardTest.php new file mode 100644 index 0000000000..1d6716db76 --- /dev/null +++ b/tests/Dashboard/DashboardTest.php @@ -0,0 +1,25 @@ +get(route('laravel-websockets.dashboard')) + ->assertResponseStatus(403); + } + + /** @test */ + public function can_see_dashboard() + { + $this->actingAs(factory(User::class)->create()) + ->get(route('laravel-websockets.dashboard')) + ->assertResponseOk() + ->see('WebSockets Dashboard'); + } +} diff --git a/tests/Dashboard/SendMessageTest.php b/tests/Dashboard/SendMessageTest.php new file mode 100644 index 0000000000..65ee7fbaa6 --- /dev/null +++ b/tests/Dashboard/SendMessageTest.php @@ -0,0 +1,76 @@ +skipOnRedisReplication(); + + // Because the Pusher server is not active, + // we expect it to turn out ok: false. + + $this->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.event'), [ + 'appId' => '1234', + 'key' => 'TestKey', + 'secret' => 'TestSecret', + 'channel' => 'test-channel', + 'event' => 'some-event', + 'data' => json_encode(['data' => 'yes']), + ]) + ->seeJson([ + 'ok' => false, + ]); + } + + /** @test */ + public function can_send_message_on_redis_replication() + { + $this->skipOnLocalReplication(); + + // Because the Pusher server is not active, + // we expect it to turn out ok: false. + // However, the driver is set to redis, + // so Redis would take care of this + // and stream the message to all active servers instead. + + $this->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.event'), [ + 'appId' => '1234', + 'key' => 'TestKey', + 'secret' => 'TestSecret', + 'channel' => 'test-channel', + 'event' => 'some-event', + 'data' => json_encode(['data' => 'yes']), + ]) + ->seeJson([ + 'ok' => true, + ]); + } + + /** @test */ + public function cant_send_message_for_invalid_app() + { + $this->skipOnRedisReplication(); + + // Because the Pusher server is not active, + // we expect it to turn out ok: false. + + $this->actingAs(factory(User::class)->create()) + ->json('POST', route('laravel-websockets.event'), [ + 'appId' => '9999', + 'key' => 'TestKey', + 'secret' => 'TestSecret', + 'channel' => 'test-channel', + 'event' => 'some-event', + 'data' => json_encode(['data' => 'yes']), + ]) + ->assertResponseStatus(422); + } +} diff --git a/tests/Dashboard/StatisticsTest.php b/tests/Dashboard/StatisticsTest.php new file mode 100644 index 0000000000..94af6c592a --- /dev/null +++ b/tests/Dashboard/StatisticsTest.php @@ -0,0 +1,63 @@ +getConnectedWebSocketConnection(['channel-1']); + + $logger = new MemoryStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->actingAs(factory(User::class)->create()) + ->json('GET', route('laravel-websockets.statistics', ['appId' => '1234'])) + ->assertResponseOk() + ->seeJsonStructure([ + 'peak_connections' => ['x', 'y'], + 'websocket_message_count' => ['x', 'y'], + 'api_message_count' => ['x', 'y'], + ]); + } + + /** @test */ + public function cant_get_statistics_for_invalid_app_id() + { + $connection = $this->getConnectedWebSocketConnection(['channel-1']); + + $logger = new MemoryStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->actingAs(factory(User::class)->create()) + ->json('GET', route('laravel-websockets.statistics', ['appId' => 'not_found'])) + ->seeJson([ + 'peak_connections' => ['x' => [], 'y' => []], + 'websocket_message_count' => ['x' => [], 'y' => []], + 'api_message_count' => ['x' => [], 'y' => []], + ]); + } +} diff --git a/tests/Messages/PusherClientMessageTest.php b/tests/Messages/PusherClientMessageTest.php index a97aed70d4..fed8e98cbf 100644 --- a/tests/Messages/PusherClientMessageTest.php +++ b/tests/Messages/PusherClientMessageTest.php @@ -12,13 +12,13 @@ public function client_messages_do_not_work_when_disabled() { $connection = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'client-test', 'channel' => 'test-channel', 'data' => [ 'client-event' => 'test', ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); @@ -42,13 +42,13 @@ public function client_messages_get_broadcasted_when_enabled() $connection1 = $this->getConnectedWebSocketConnection(['test-channel']); $connection2 = $this->getConnectedWebSocketConnection(['test-channel']); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'client-test', 'channel' => 'test-channel', 'data' => [ 'client-event' => 'test', ], - ])); + ]); $this->pusherServer->onMessage($connection1, $message); diff --git a/tests/Mocks/Message.php b/tests/Mocks/Message.php index 3b0706c1ad..4a9be3234c 100644 --- a/tests/Mocks/Message.php +++ b/tests/Mocks/Message.php @@ -2,17 +2,35 @@ namespace BeyondCode\LaravelWebSockets\Tests\Mocks; -class Message extends \Ratchet\RFC6455\Messaging\Message +use Ratchet\RFC6455\Messaging\Message as BaseMessage; + +class Message extends BaseMessage { + /** + * The payload as array. + * + * @var array + */ protected $payload; - public function __construct($payload) + /** + * Create a new message instance. + * + * @param array $payload + * @return void + */ + public function __construct(array $payload) { $this->payload = $payload; } - public function getPayload() + /** + * Get the payload as json-encoded string. + * + * @return string + */ + public function getPayload(): string { - return $this->payload; + return json_encode($this->payload); } } diff --git a/tests/Models/User.php b/tests/Models/User.php new file mode 100644 index 0000000000..1f134fb717 --- /dev/null +++ b/tests/Models/User.php @@ -0,0 +1,16 @@ +getConnectedWebSocketConnection(['test-channel']); + + $this->pusherServer->onOpen($connection); + + $channelData = [ + 'user_id' => 1, + 'user_info' => [ + 'name' => 'Marcel', + ], + ]; + + $payload = json_encode([ + 'appId' => '1234', + 'event' => 'test', + 'data' => $channelData, + 'socket' => $connection->socketId, + ]); + + $client = (new RedisClient)->boot( + LoopFactory::create(), RedisFactory::class + ); + + $client->onMessage('1234:test-channel', $payload); + + $client->getSubscribeClient() + ->assertEventDispatched('message'); + } } diff --git a/tests/Statistics/Logger/StatisticsLoggerTest.php b/tests/Statistics/Logger/StatisticsLoggerTest.php index 49abd19f00..c7f2365d50 100644 --- a/tests/Statistics/Logger/StatisticsLoggerTest.php +++ b/tests/Statistics/Logger/StatisticsLoggerTest.php @@ -3,6 +3,9 @@ namespace BeyondCode\LaravelWebSockets\Tests\Statistics\Controllers; use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger; +use BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger; +use BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger; +use BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry; use BeyondCode\LaravelWebSockets\Tests\TestCase; class StatisticsLoggerTest extends TestCase @@ -43,4 +46,50 @@ public function it_counts_unique_connections_no_channel_subscriptions() $this->assertEquals(1, StatisticsLogger::getForAppId(1234)['peak_connection_count']); } + + /** @test */ + public function it_counts_connections_with_memory_logger() + { + $connection = $this->getConnectedWebSocketConnection(['channel-1']); + + $logger = new MemoryStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->assertCount(1, WebSocketsStatisticsEntry::all()); + + $entry = WebSocketsStatisticsEntry::first(); + + $this->assertEquals(1, $entry->peak_connection_count); + $this->assertEquals(1, $entry->websocket_message_count); + $this->assertEquals(1, $entry->api_message_count); + } + + /** @test */ + public function it_counts_connections_with_null_logger() + { + $connection = $this->getConnectedWebSocketConnection(['channel-1']); + + $logger = new NullStatisticsLogger( + $this->channelManager, + $this->statisticsDriver + ); + + $logger->webSocketMessage($connection->app->id); + $logger->apiMessage($connection->app->id); + $logger->connection($connection->app->id); + $logger->disconnection($connection->app->id); + + $logger->save(); + + $this->assertCount(0, WebSocketsStatisticsEntry::all()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index b0c7b7affa..a78739669e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,10 +12,11 @@ use BeyondCode\LaravelWebSockets\Tests\Statistics\Logger\FakeStatisticsLogger; use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager; use GuzzleHttp\Psr7\Request; +use Orchestra\Testbench\BrowserKit\TestCase as BaseTestCase; use Ratchet\ConnectionInterface; use React\EventLoop\Factory as LoopFactory; -abstract class TestCase extends \Orchestra\Testbench\TestCase +abstract class TestCase extends BaseTestCase { /** * A test Pusher server. @@ -31,6 +32,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase */ protected $channelManager; + /** + * The used statistics driver. + * + * @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver + */ + protected $statisticsDriver; + /** * {@inheritdoc} */ @@ -38,9 +46,17 @@ public function setUp(): void { parent::setUp(); - $this->pusherServer = app(config('websockets.handlers.websocket')); + $this->resetDatabase(); + + $this->loadLaravelMigrations(['--database' => 'sqlite']); + + $this->withFactories(__DIR__.'/database/factories'); - $this->channelManager = app(ChannelManager::class); + $this->pusherServer = $this->app->make(config('websockets.handlers.websocket')); + + $this->channelManager = $this->app->make(ChannelManager::class); + + $this->statisticsDriver = $this->app->make(StatisticsDriver::class); StatisticsLogger::swap(new FakeStatisticsLogger( $this->channelManager, @@ -59,6 +75,7 @@ protected function getPackageProviders($app) { return [ \BeyondCode\LaravelWebSockets\WebSocketsServiceProvider::class, + TestServiceProvider::class, ]; } @@ -67,6 +84,18 @@ protected function getPackageProviders($app) */ protected function getEnvironmentSetUp($app) { + $app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD'); + + $app['config']->set('auth.providers.users.model', Models\User::class); + + $app['config']->set('database.default', 'sqlite'); + + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => __DIR__.'/database.sqlite', + 'prefix' => '', + ]); + $app['config']->set('websockets.apps', [ [ 'name' => 'Test App', @@ -160,12 +189,12 @@ protected function getConnectedWebSocketConnection(array $channelsToJoin = [], s $this->pusherServer->onOpen($connection); foreach ($channelsToJoin as $channel) { - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'channel' => $channel, ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); } @@ -194,14 +223,14 @@ protected function joinPresenceChannel($channel): Connection $signature = "{$connection->socketId}:{$channel}:".json_encode($channelData); - $message = new Message(json_encode([ + $message = new Message([ 'event' => 'pusher:subscribe', 'data' => [ 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret), 'channel' => $channel, 'channel_data' => json_encode($channelData), ], - ])); + ]); $this->pusherServer->onMessage($connection, $message); @@ -295,4 +324,14 @@ protected function getPublishClient() ->make(ReplicationInterface::class) ->getPublishClient(); } + + /** + * Reset the database. + * + * @return void + */ + protected function resetDatabase() + { + file_put_contents(__DIR__.'/database.sqlite', null); + } } diff --git a/tests/TestServiceProvider.php b/tests/TestServiceProvider.php new file mode 100644 index 0000000000..958086e34e --- /dev/null +++ b/tests/TestServiceProvider.php @@ -0,0 +1,31 @@ +define(\BeyondCode\LaravelWebSockets\Tests\Models\User::class, function () { + return [ + 'name' => 'Name'.Str::random(5), + 'email' => Str::random(5).'@gmail.com', + 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret + 'remember_token' => Str::random(10), + ]; +});