From 093bbe3a44dd3f2a12c960088022f75d35fd418c Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Thu, 2 Apr 2026 12:44:16 +0200 Subject: [PATCH] A tests, documentation for helpers and lifecycle - Introduced `helpers-and-testing.md` to document global helpers and WebsocketService class usage. - Created `HandlerLifecycleTest.php` to test the full WebSocket handler lifecycle, including connection management, channel subscriptions, and message routing. - Added `WebsocketServiceTest.php` to validate state tracking methods in WebsocketService, covering user authentication, channel tracking, and broadcast functionality. --- README.md | 129 ++- config/websockets.php | 8 - docs/advanced-usage/_index.md | 9 + docs/advanced-usage/helpers-and-testing.md | 114 +++ src/API/Controller.php | 296 ------ src/API/FetchChannel.php | 52 - src/API/FetchChannels.php | 77 -- src/API/FetchUsers.php | 35 - src/API/TriggerEvent.php | 78 -- src/Apps/MysqlAppManager.php | 171 ---- src/Apps/SQLiteAppManager.php | 167 ---- src/Cache/ArrayLock.php | 55 -- src/Cache/Lock.php | 46 - src/Cache/RedisLock.php | 69 -- src/ChannelManagers/LocalChannelManager.php | 2 +- src/ChannelManagers/RedisChannelManager.php | 891 ------------------ src/Channels/Channel.php | 22 +- src/Channels/PresenceChannel.php | 33 +- src/Concerns/PushesToPusher.php | 27 - src/Console/Commands/CleanStatistics.php | 44 - .../Commands/FlushCollectedStatistics.php | 37 - src/Console/Commands/StartServer.php | 20 +- src/Contracts/StatisticsCollector.php | 78 -- src/Contracts/StatisticsStore.php | 55 -- .../Controllers/AuthenticateDashboard.php | 42 - .../Http/Controllers/SendMessage.php | 56 -- src/Dashboard/Http/Controllers/ShowApps.php | 27 - .../Http/Controllers/ShowDashboard.php | 31 - .../Http/Controllers/ShowStatistics.php | 33 - src/Dashboard/Http/Controllers/StoreApp.php | 37 - src/Dashboard/Http/Middleware/Authorize.php | 22 - .../Http/Requests/StoreAppRequest.php | 20 - src/DashboardLogger.php | 107 --- src/Events/ConnectionPonged.php | 38 - src/Events/SubscribedToChannel.php | 57 -- src/Events/UnsubscribedFromChannel.php | 57 -- src/Events/WebSocketMessageReceived.php | 56 -- src/Facades/StatisticsCollector.php | 19 - src/Facades/StatisticsStore.php | 19 - src/Models/WebSocketsStatisticsEntry.php | 18 - src/Queue/AsyncRedisConnector.php | 24 - src/Queue/AsyncRedisQueue.php | 25 - src/Rules/AppId.php | 36 - src/Server/Exceptions/WebSocketException.php | 4 +- .../Messages/PusherChannelProtocolMessage.php | 90 -- src/Server/Messages/PusherClientMessage.php | 79 -- src/Server/Messages/PusherMessageFactory.php | 46 - src/Server/Router.php | 4 - src/Server/WebSocketHandler.php | 86 +- src/Services/WebsocketService.php | 2 +- src/Statistics/Collectors/MemoryCollector.php | 191 ---- src/Statistics/Collectors/RedisCollector.php | 374 -------- src/Statistics/Statistic.php | 220 ----- src/Statistics/Stores/DatabaseStore.php | 140 --- src/WebSocketsServiceProvider.php | 197 ---- src/Websocket/ChannelManager.php | 19 - src/Websocket/Controller.php | 19 +- src/Websocket/Handler.php | 73 +- tests/Apps/MysqlAppManagerTest.php | 117 --- tests/Apps/SqliteAppManagerTest.php | 104 -- tests/Cache/IpcCacheTest.php | 2 +- tests/Commands/StatisticsCleanTest.php | 45 - tests/ConnectionTest.php | 38 +- tests/Dashboard/AppsTest.php | 38 - tests/Dashboard/AuthTest.php | 89 -- tests/Dashboard/DashboardTest.php | 22 - tests/Dashboard/SendMessageTest.php | 43 - tests/Dashboard/StatisticsTest.php | 42 - tests/FetchChannelTest.php | 109 --- tests/FetchChannelsTest.php | 178 ---- tests/FetchUsersTest.php | 117 --- tests/HandlerLifecycleTest.php | 472 ++++++++++ tests/HealthTest.php | 4 +- .../SocketPairIpcWebsocketWorkflowTest.php | 52 +- tests/PingTest.php | 6 +- tests/PresenceChannelTest.php | 224 +---- tests/PrivateChannelTest.php | 195 +--- tests/PublicChannelTest.php | 168 +--- tests/ReplicationTest.php | 2 +- tests/StatisticsStoreTest.php | 95 -- tests/TestCase.php | 75 +- tests/TriggerEventTest.php | 26 - tests/Unit/ControllerResolverTest.php | 14 +- tests/Websocket/HandlerForkPathTest.php | 34 +- .../HandlerSocketPairIntegrationTest.php | 32 +- tests/Websocket/HandlerStabilityTest.php | 225 ++--- tests/WebsocketServiceTest.php | 210 +++++ 87 files changed, 1295 insertions(+), 6266 deletions(-) create mode 100644 docs/advanced-usage/helpers-and-testing.md delete mode 100644 src/API/Controller.php delete mode 100644 src/API/FetchChannel.php delete mode 100644 src/API/FetchChannels.php delete mode 100644 src/API/FetchUsers.php delete mode 100644 src/API/TriggerEvent.php delete mode 100644 src/Apps/MysqlAppManager.php delete mode 100644 src/Apps/SQLiteAppManager.php delete mode 100644 src/Cache/ArrayLock.php delete mode 100644 src/Cache/Lock.php delete mode 100644 src/Cache/RedisLock.php delete mode 100644 src/ChannelManagers/RedisChannelManager.php delete mode 100644 src/Concerns/PushesToPusher.php delete mode 100644 src/Console/Commands/CleanStatistics.php delete mode 100644 src/Console/Commands/FlushCollectedStatistics.php delete mode 100644 src/Contracts/StatisticsCollector.php delete mode 100644 src/Contracts/StatisticsStore.php delete mode 100644 src/Dashboard/Http/Controllers/AuthenticateDashboard.php delete mode 100644 src/Dashboard/Http/Controllers/SendMessage.php delete mode 100644 src/Dashboard/Http/Controllers/ShowApps.php delete mode 100644 src/Dashboard/Http/Controllers/ShowDashboard.php delete mode 100644 src/Dashboard/Http/Controllers/ShowStatistics.php delete mode 100644 src/Dashboard/Http/Controllers/StoreApp.php delete mode 100644 src/Dashboard/Http/Middleware/Authorize.php delete mode 100644 src/Dashboard/Http/Requests/StoreAppRequest.php delete mode 100644 src/DashboardLogger.php delete mode 100644 src/Events/ConnectionPonged.php delete mode 100644 src/Events/SubscribedToChannel.php delete mode 100644 src/Events/UnsubscribedFromChannel.php delete mode 100644 src/Events/WebSocketMessageReceived.php delete mode 100644 src/Facades/StatisticsCollector.php delete mode 100644 src/Facades/StatisticsStore.php delete mode 100644 src/Models/WebSocketsStatisticsEntry.php delete mode 100644 src/Queue/AsyncRedisConnector.php delete mode 100644 src/Queue/AsyncRedisQueue.php delete mode 100644 src/Rules/AppId.php delete mode 100644 src/Server/Messages/PusherChannelProtocolMessage.php delete mode 100644 src/Server/Messages/PusherClientMessage.php delete mode 100644 src/Server/Messages/PusherMessageFactory.php delete mode 100644 src/Statistics/Collectors/MemoryCollector.php delete mode 100644 src/Statistics/Collectors/RedisCollector.php delete mode 100644 src/Statistics/Statistic.php delete mode 100644 src/Statistics/Stores/DatabaseStore.php delete mode 100644 tests/Apps/MysqlAppManagerTest.php delete mode 100644 tests/Apps/SqliteAppManagerTest.php delete mode 100644 tests/Commands/StatisticsCleanTest.php delete mode 100644 tests/Dashboard/AppsTest.php delete mode 100644 tests/Dashboard/AuthTest.php delete mode 100644 tests/Dashboard/DashboardTest.php delete mode 100644 tests/Dashboard/SendMessageTest.php delete mode 100644 tests/Dashboard/StatisticsTest.php delete mode 100644 tests/FetchChannelTest.php delete mode 100644 tests/FetchChannelsTest.php delete mode 100644 tests/FetchUsersTest.php create mode 100644 tests/HandlerLifecycleTest.php delete mode 100644 tests/StatisticsStoreTest.php delete mode 100644 tests/TriggerEventTest.php create mode 100644 tests/WebsocketServiceTest.php diff --git a/README.md b/README.md index 056e048..8084b10 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,128 @@ -# Laravel WebSockets 🛰 +# Laravel WebSockets -> [!NOTE] -> This is a fork from [https://github.com/beyondcode/laravel-websockets](beyondcode/laravel-websockets) which has been abandoned on Feb 7, 2024 due to the release of Laravel Reverb. -> -Bring the power of WebSockets to your Laravel application. Drop-in Pusher replacement, SSL support, Laravel Echo support and a debug dashboard are just some of its features. +> [!NOTE] +> This package is actively maintained as a fork of beyondcode/laravel-websockets. + +Plug-and-play WebSockets for Laravel with a Pusher-compatible protocol, a fast fork-based handler, and practical helpers for broadcasting and testing. + +## Why this package + +- Drop-in broadcasting backend for Laravel apps that already use Echo/Pusher-compatible clients +- Fast local handler with async processing via `pcntl_fork` +- Protocol compatibility for both modern `websocket.*` and legacy `pusher:*` action formats +- Built-in developer ergonomics: helper functions, service methods, and rich test helpers + +## Install in 2 minutes + +1. Install package + +```bash +composer require blax-software/laravel-websockets +``` + +2. Publish config + +```bash +php artisan vendor:publish --provider="BlaxSoftware\\LaravelWebSockets\\WebSocketsServiceProvider" --tag="config" +``` + +3. Start server + +```bash +php artisan websockets:serve +``` + +Default server URL is `ws://127.0.0.1:6001`. + +## Helper functions (broadcast from anywhere) + +The package ships with global helpers in `src/helpers_global.php`. + +```php +// Broadcast to everyone on a channel +ws_broadcast('chat.message', ['text' => 'Hello'], 'chat'); + +// Whisper to specific socket IDs only +ws_whisper('chat.typing', ['typing' => true], ['1234.1', '1234.2'], 'chat'); + +// Broadcast to everyone except listed sockets +ws_broadcast_except('chat.message', ['text' => 'Server msg'], ['1234.1'], 'chat'); + +// Check if local unix-socket broadcaster is available +if (ws_available()) { + ws_broadcast('app.health', ['ok' => true]); +} + +// Build protocol auth payload for private/presence channels +$auth = wsSession('private-updates', ['user_id' => 7, 'user_info' => ['name' => 'Jane']]); +``` + +## Service API + +Use the service directly when you prefer explicit class calls over helpers. + +```php +use BlaxSoftware\LaravelWebSockets\Services\WebsocketService; + +WebsocketService::send('metrics.tick', ['count' => 1], 'websocket'); +WebsocketService::whisper('chat.typing', ['typing' => true], ['1234.1'], 'chat'); +WebsocketService::broadcastExcept('chat.message', ['text' => 'Hi'], ['1234.1'], 'chat'); + +// Optional in-process tracking helpers +WebsocketService::setUserAuthed($socketId, $userId); +$authed = WebsocketService::getAuthedUsers(); +``` + +## Testing experience + +The test suite includes helper-first patterns so WebSocket tests stay short and readable. + +### Test helpers + +- `newConnection()` +- `newActiveConnection(['channel'])` +- `newPrivateConnection('private-channel')` +- `newPresenceConnection('presence-channel', ['user_id' => 1, 'user_info' => [...]])` + +### Example + +```php +$connection = $this->newActiveConnection(['chat']); + +$this->wsHandler->onMessage($connection, new Message([ + 'event' => 'websocket.ping', + 'data' => new stdClass(), +])); + +$connection->assertSentEvent('websocket.pong'); +``` + +Run tests: + +```bash +vendor/bin/phpunit --exclude-group=stability,stress,integration,requires-server +``` ## Documentation -### Features -* Laravel native event broadcasting -* Async with pcntl_fork -* SSL support -* Laravel Echo support -* Docker & Traefik capable +- Main docs: [docs](docs) +- Getting started: [docs/getting-started/introduction.md](docs/getting-started/introduction.md) +- Helper & testing guide: [docs/advanced-usage/helpers-and-testing.md](docs/advanced-usage/helpers-and-testing.md) -### Changelog +## Changelog -Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. +See [CHANGELOG](CHANGELOG.md). -### Security +## Security -If you discover any security related issues, please email office@blax.at or the the issue tracker. +Please report vulnerabilities via issue tracker or by email: office@blax.at. ## Credits -- [Marcel Pociot (beyondco.de)](https://github.com/mpociot) +- [Marcel Pociot](https://github.com/mpociot) - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +MIT. See [LICENSE.md](LICENSE.md). diff --git a/config/websockets.php b/config/websockets.php index a391805..48b884c 100644 --- a/config/websockets.php +++ b/config/websockets.php @@ -287,14 +287,6 @@ return [ 'health' => \BlaxSoftware\LaravelWebSockets\Server\HealthHandler::class, - 'trigger_event' => \BlaxSoftware\LaravelWebSockets\API\TriggerEvent::class, - - 'fetch_channels' => \BlaxSoftware\LaravelWebSockets\API\FetchChannels::class, - - 'fetch_channel' => \BlaxSoftware\LaravelWebSockets\API\FetchChannel::class, - - 'fetch_users' => \BlaxSoftware\LaravelWebSockets\API\FetchUsers::class, - ], /* diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md index 0297feb..b257333 100644 --- a/docs/advanced-usage/_index.md +++ b/docs/advanced-usage/_index.md @@ -2,3 +2,12 @@ title: Advanced Usage order: 4 --- + +# Advanced Usage + +- [Custom WebSocket handlers](custom-websocket-handlers.md) +- [Helpers and testing](helpers-and-testing.md) +- [Dispatched events](dispatched-events.md) +- [App providers](app-providers.md) +- [Webhooks](webhooks.md) +- [Non-blocking queue driver](non-blocking-queue-driver.md) diff --git a/docs/advanced-usage/helpers-and-testing.md b/docs/advanced-usage/helpers-and-testing.md new file mode 100644 index 0000000..441c2d0 --- /dev/null +++ b/docs/advanced-usage/helpers-and-testing.md @@ -0,0 +1,114 @@ +--- +title: Helpers and Testing +order: 2 +--- + +# Helpers and Testing + +This package is designed to be easy to use from both app code and test code. + +## Global helpers + +Global helpers live in `src/helpers_global.php` and call into the same core service used by the server. + +### Broadcast to channel + +```php +ws_broadcast('chat.message', ['text' => 'Hello world'], 'chat'); +``` + +### Whisper to specific sockets + +```php +ws_whisper('chat.typing', ['typing' => true], ['1234.1', '1234.2'], 'chat'); +``` + +### Broadcast except sockets + +```php +ws_broadcast_except('chat.message', ['text' => 'Server update'], ['1234.1'], 'chat'); +``` + +### Runtime availability check + +```php +if (ws_available()) { + ws_broadcast('system.heartbeat', ['ok' => true]); +} +``` + +### Generate auth payload + +```php +$auth = wsSession('presence-room', [ + 'user_id' => 42, + 'user_info' => ['name' => 'Amelia'], +]); +``` + +Use this when you need to produce channel auth payloads in custom flows. + +## WebsocketService class + +If you prefer explicit classes over helpers: + +```php +use BlaxSoftware\LaravelWebSockets\Services\WebsocketService; + +WebsocketService::send('chat.message', ['text' => 'Hello'], 'chat'); +WebsocketService::whisper('chat.typing', ['typing' => true], ['1234.1'], 'chat'); +WebsocketService::broadcastExcept('chat.message', ['text' => 'Hi'], ['1234.1'], 'chat'); +``` + +### Tracking helpers + +`WebsocketService` also exposes lightweight in-process tracking helpers: + +- `setUserAuthed($socketId, $userId)` +- `clearUserAuthed($socketId)` +- `getAuth()` +- `getAuthedUsers()` +- `isUserConnected($userId)` +- `getUserSocketIds($userId)` +- `getActiveChannels()` +- `getChannelConnections($channel)` +- `resetAllTracking()` + +## Testing with package helpers + +The package test base class includes high-level helpers that make tests concise: + +- `newConnection()` +- `newActiveConnection(['channel'])` +- `newPrivateConnection('private-channel')` +- `newPresenceConnection('presence-channel', ['user_id' => 1, 'user_info' => [...]])` + +### Example ping/pong test + +```php +$connection = $this->newActiveConnection(['websocket']); +$connection->resetEvents(); + +$this->wsHandler->onMessage($connection, new Message([ + 'event' => 'websocket.ping', + 'data' => new stdClass(), +])); + +$connection->assertSentEvent('websocket.pong'); +``` + +### Example subscription test + +```php +$connection = $this->newPrivateConnection('private-chat'); +$connection->assertSentEvent('websocket_internal.subscription_succeeded'); +``` + +## New reference test files + +For complete examples, see: + +- `tests/WebsocketServiceTest.php` +- `tests/HandlerLifecycleTest.php` + +These files cover helper-first testing patterns and full handler lifecycle behavior. diff --git a/src/API/Controller.php b/src/API/Controller.php deleted file mode 100644 index 339c3ec..0000000 --- a/src/API/Controller.php +++ /dev/null @@ -1,296 +0,0 @@ -channelManager = $channelManager; - } - - /** - * Handle the opened socket connection. - * - * @param \Ratchet\ConnectionInterface $connection - * @param \Psr\Http\Message\RequestInterface $request - * @return void - */ - public function onOpen(ConnectionInterface $connection, RequestInterface $request = null) - { - $this->request = $request; - - $this->contentLength = $this->findContentLength($request->getHeaders()); - - $this->requestBuffer = (string) $request->getBody(); - - if (! $this->verifyContentLength()) { - return; - } - - $this->handleRequest($connection); - } - - /** - * Handle the oncoming message and add it to buffer. - * - * @param \Ratchet\ConnectionInterface $from - * @param mixed $msg - * @return void - */ - public function onMessage(ConnectionInterface $from, $msg) - { - $this->requestBuffer .= $msg; - - if (! $this->verifyContentLength()) { - return; - } - - $this->handleRequest($from); - } - - /** - * Handle the socket closing. - * - * @param \Ratchet\ConnectionInterface $connection - * @return void - */ - public function onClose(ConnectionInterface $connection) - { - // - } - - /** - * Handle the errors. - * - * @param \Ratchet\ConnectionInterface $connection - * @param Exception $exception - * @return void - */ - public function onError(ConnectionInterface $connection, Exception $exception) - { - if (! $exception instanceof HttpException) { - return; - } - - $response = new Response($exception->getStatusCode(), [ - 'Content-Type' => 'application/json', - ], json_encode([ - 'error' => $exception->getMessage(), - ])); - - tap($connection)->send(Message::toString($response))->close(); - } - - /** - * Get the content length from the headers. - * - * @param array $headers - * @return int - */ - protected function findContentLength(array $headers): int - { - return Collection::make($headers)->first(function ($values, $header) { - return strtolower($header) === 'content-length'; - })[0] ?? 0; - } - - /** - * Check the content length. - * - * @return bool - */ - protected function verifyContentLength() - { - return strlen($this->requestBuffer) === $this->contentLength; - } - - /** - * Handle the oncoming connection. - * - * @param \Ratchet\ConnectionInterface $connection - * @return void - */ - protected function handleRequest(ConnectionInterface $connection) - { - $serverRequest = (new ServerRequest( - $this->request->getMethod(), - $this->request->getUri(), - $this->request->getHeaders(), - $this->requestBuffer, - $this->request->getProtocolVersion() - ))->withQueryParams(QueryParameters::create($this->request)->all()); - - $laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest)); - - $this - ->ensureValidAppId($laravelRequest->appId) - ->then(function ($app) use ($laravelRequest, $connection) { - try { - $this->ensureValidSignature($app, $laravelRequest); - } catch (HttpException $exception) { - $this->onError($connection, $exception); - - return; - } - - // Invoke the controller action - try { - $response = $this($laravelRequest); - } catch (HttpException $exception) { - $this->onError($connection, $exception); - - return; - } - - // Allow for async IO in the controller action - if ($response instanceof PromiseInterface) { - $response->then(function ($response) use ($connection) { - $this->sendAndClose($connection, $response); - }); - - return; - } - - if ($response instanceof HttpException) { - $this->onError($connection, $response); - - return; - } - - $this->sendAndClose($connection, $response); - }); - } - - /** - * Send the response and close the connection. - * - * @param \Ratchet\ConnectionInterface $connection - * @param mixed $response - * @return void - */ - protected function sendAndClose(ConnectionInterface $connection, $response) - { - tap($connection)->send(new JsonResponse($response))->close(); - } - - /** - * Ensure app existence. - * - * @param mixed $appId - * @return PromiseInterface - * - * @throws \Symfony\Component\HttpKernel\Exception\HttpException - */ - public function ensureValidAppId($appId) - { - $deferred = new Deferred(); - - App::findById($appId) - ->then(function ($app) use ($appId, $deferred) { - if (! $app) { - throw new HttpException(401, "Unknown app id `{$appId}` provided."); - } - $deferred->resolve($app); - }); - - return $deferred->promise(); - } - - /** - * Ensure signature integrity coming from an - * authorized application. - * - * @param App $app - * @param Request $request - * @return $this - */ - protected function ensureValidSignature(App $app, Request $request) - { - // The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value. - // The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client. - - $params = Arr::except($request->query(), [ - 'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName', - ]); - - if ($request->getContent() !== '') { - $params['body_md5'] = md5($request->getContent()); - } - - ksort($params); - - $signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params); - - $authSignature = hash_hmac('sha256', $signature, $app->secret); - - if ($authSignature !== $request->get('auth_signature')) { - throw new HttpException(401, 'Invalid auth signature provided.'); - } - - return $this; - } - - /** - * Handle the incoming request. - * - * @param \Illuminate\Http\Request $request - * @return void - */ - abstract public function __invoke(Request $request); -} diff --git a/src/API/FetchChannel.php b/src/API/FetchChannel.php deleted file mode 100644 index 1c2d74f..0000000 --- a/src/API/FetchChannel.php +++ /dev/null @@ -1,52 +0,0 @@ -channelManager->find( - $request->appId, $request->channelName - ); - - if (is_null($channel)) { - return new HttpException(404, "Unknown channel `{$request->channelName}`."); - } - - return $this->channelManager - ->getGlobalConnectionsCount($request->appId, $request->channelName) - ->then(function ($connectionsCount) use ($request) { - // For the presence channels, we need a slightly different response - // that need an additional call. - if (Str::startsWith($request->channelName, 'presence-')) { - return $this->channelManager - ->getChannelsMembersCount($request->appId, [$request->channelName]) - ->then(function ($channelMembers) use ($connectionsCount, $request) { - return [ - 'occupied' => $connectionsCount > 0, - 'subscription_count' => $connectionsCount, - 'user_count' => $channelMembers[$request->channelName] ?? 0, - ]; - }); - } - - // For the rest of the channels, we might as well - // send the basic response with the subscriptions count. - return [ - 'occupied' => $connectionsCount > 0, - 'subscription_count' => $connectionsCount, - ]; - }); - } -} diff --git a/src/API/FetchChannels.php b/src/API/FetchChannels.php deleted file mode 100644 index db44c2b..0000000 --- a/src/API/FetchChannels.php +++ /dev/null @@ -1,77 +0,0 @@ -has('info')) { - $attributes = explode(',', trim($request->info)); - - if (in_array('user_count', $attributes) && ! Str::startsWith($request->filter_by_prefix, 'presence-')) { - throw new HttpException(400, 'Request must be limited to presence channels in order to fetch user_count'); - } - } - - return $this->channelManager - ->getGlobalChannels($request->appId) - ->then(function ($channels) use ($request, $attributes) { - $channels = collect($channels)->keyBy(function ($channel) { - return $channel instanceof Channel - ? $channel->getName() - : $channel; - }); - - if ($request->has('filter_by_prefix')) { - $channels = $channels->filter(function ($channel, $channelName) use ($request) { - return Str::startsWith($channelName, $request->filter_by_prefix); - }); - } - - $channelNames = $channels->map(function ($channel) { - return $channel instanceof Channel - ? $channel->getName() - : $channel; - })->toArray(); - - return $this->channelManager - ->getChannelsMembersCount($request->appId, $channelNames) - ->then(function ($counts) use ($channels, $attributes) { - $channels = $channels->map(function ($channel) use ($counts, $attributes) { - $info = new stdClass; - - $channelName = $channel instanceof Channel - ? $channel->getName() - : $channel; - - if (in_array('user_count', $attributes)) { - $info->user_count = $counts[$channelName]; - } - - return $info; - })->sortBy(function ($content, $name) { - return $name; - })->all(); - - return [ - 'channels' => $channels ?: new stdClass, - ]; - }); - }); - } -} diff --git a/src/API/FetchUsers.php b/src/API/FetchUsers.php deleted file mode 100644 index 75fcccf..0000000 --- a/src/API/FetchUsers.php +++ /dev/null @@ -1,35 +0,0 @@ -channelName, 'presence-')) { - return new HttpException(400, "Invalid presence channel `{$request->channelName}`"); - } - - return $this->channelManager - ->getChannelMembers($request->appId, $request->channelName) - ->then(function ($members) { - $users = collect($members)->map(function ($user) { - return ['id' => $user->user_id]; - })->values()->toArray(); - - return [ - 'users' => $users, - ]; - }); - } -} diff --git a/src/API/TriggerEvent.php b/src/API/TriggerEvent.php deleted file mode 100644 index b0ca1a6..0000000 --- a/src/API/TriggerEvent.php +++ /dev/null @@ -1,78 +0,0 @@ -has('channel')) { - $channels = [$request->get('channel')]; - } else { - $channels = $request->channels ?: []; - - if (is_string($channels)) { - $channels = [$channels]; - } - } - - foreach ($channels as $channelName) { - // Here you can use the ->find(), even if the channel - // does not exist on the server. If it does not exist, - // then the message simply will get broadcasted - // across the other servers. - $channel = $this->channelManager->find( - $request->appId, $channelName - ); - - $payload = [ - 'event' => $request->name, - 'channel' => $channelName, - 'data' => $request->data, - ]; - - if ($channel) { - $channel->broadcastLocallyToEveryoneExcept( - (object) $payload, - $request->socket_id, - $request->appId - ); - } - - $this->channelManager->broadcastAcrossServers( - $request->appId, $request->socket_id, $channelName, (object) $payload - ); - - $deferred = new Deferred(); - - $this->ensureValidAppId($request->appId) - ->then(function ($app) use ($request, $channelName, $deferred) { - if ($app->statisticsEnabled) { - StatisticsCollector::apiMessage($request->appId); - } - - DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [ - 'event' => $request->name, - 'channel' => $channelName, - 'payload' => $request->data, - ]); - - $deferred->resolve((object) []); - }); - } - - return $deferred->promise(); - } -} diff --git a/src/Apps/MysqlAppManager.php b/src/Apps/MysqlAppManager.php deleted file mode 100644 index 3286748..0000000 --- a/src/Apps/MysqlAppManager.php +++ /dev/null @@ -1,171 +0,0 @@ -database = $database; - } - - protected function getTableName(): string - { - return config('websockets.managers.mysql.table'); - } - - /** - * Get all apps. - * - * @return PromiseInterface - */ - public function all(): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * FROM `'.$this->getTableName().'`') - ->then(function (QueryResult $result) use ($deferred) { - $deferred->resolve($result->resultRows); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by id. - * - * @param string|int $appId - * @return PromiseInterface - */ - public function findById($appId): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId]) - ->then(function (QueryResult $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->resultRows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by app key. - * - * @param string $appKey - * @return PromiseInterface - */ - public function findByKey($appKey): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey]) - ->then(function (QueryResult $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->resultRows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by secret. - * - * @param string $appSecret - * @return PromiseInterface - */ - public function findBySecret($appSecret): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret]) - ->then(function (QueryResult $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->resultRows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Map the app into an App instance. - * - * @param array|null $app - * @return \BlaxSoftware\LaravelWebSockets\Apps\App|null - */ - protected function convertIntoApp(?array $appAttributes): ?App - { - if (! $appAttributes) { - return null; - } - - $app = new App( - $appAttributes['id'], - $appAttributes['key'], - $appAttributes['secret'] - ); - - if (isset($appAttributes['name'])) { - $app->setName($appAttributes['name']); - } - - if (isset($appAttributes['host'])) { - $app->setHost($appAttributes['host']); - } - - if (isset($appAttributes['path'])) { - $app->setPath($appAttributes['path']); - } - - $app - ->enableClientMessages((bool) $appAttributes['enable_client_messages']) - ->enableStatistics((bool) $appAttributes['enable_statistics']) - ->setCapacity($appAttributes['capacity'] ?? null) - ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins']))); - - return $app; - } - - /** - * @inheritDoc - */ - public function createApp($appData): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query( - 'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null]) - ->then(function () use ($deferred) { - $deferred->resolve(); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } -} diff --git a/src/Apps/SQLiteAppManager.php b/src/Apps/SQLiteAppManager.php deleted file mode 100644 index 4d7de25..0000000 --- a/src/Apps/SQLiteAppManager.php +++ /dev/null @@ -1,167 +0,0 @@ -database = $database; - } - - /** - * Get all apps. - * - * @return PromiseInterface - */ - public function all(): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * FROM `apps`') - ->then(function (Result $result) use ($deferred) { - $deferred->resolve($result->rows); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by id. - * - * @param string|int $appId - * @return PromiseInterface - */ - public function findById($appId): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId]) - ->then(function (Result $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->rows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by app key. - * - * @param string $appKey - * @return PromiseInterface - */ - public function findByKey($appKey): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey]) - ->then(function (Result $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->rows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Get app by secret. - * - * @param string $appSecret - * @return PromiseInterface - */ - public function findBySecret($appSecret): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret]) - ->then(function (Result $result) use ($deferred) { - $deferred->resolve($this->convertIntoApp($result->rows[0])); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } - - /** - * Map the app into an App instance. - * - * @param array|null $app - * @return \BlaxSoftware\LaravelWebSockets\Apps\App|null - */ - protected function convertIntoApp(?array $appAttributes): ?App - { - if (! $appAttributes) { - return null; - } - - $app = new App( - $appAttributes['id'], - $appAttributes['key'], - $appAttributes['secret'] - ); - - if (isset($appAttributes['name'])) { - $app->setName($appAttributes['name']); - } - - if (isset($appAttributes['host'])) { - $app->setHost($appAttributes['host']); - } - - if (isset($appAttributes['path'])) { - $app->setPath($appAttributes['path']); - } - - $app - ->enableClientMessages((bool) $appAttributes['enable_client_messages']) - ->enableStatistics((bool) $appAttributes['enable_statistics']) - ->setCapacity($appAttributes['capacity'] ?? null) - ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins']))); - - return $app; - } - - /** - * @inheritDoc - */ - public function createApp($appData): PromiseInterface - { - $deferred = new Deferred(); - - $this->database->query(' - INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins) - VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins) - ', $appData) - ->then(function (Result $result) use ($deferred) { - $deferred->resolve(); - }, function ($error) use ($deferred) { - $deferred->reject($error); - }); - - return $deferred->promise(); - } -} diff --git a/src/Cache/ArrayLock.php b/src/Cache/ArrayLock.php deleted file mode 100644 index 6a6a231..0000000 --- a/src/Cache/ArrayLock.php +++ /dev/null @@ -1,55 +0,0 @@ -lock = new LaravelLock($store, $name, $seconds, $owner); - } - - public function acquire(): PromiseInterface - { - return Helpers::createFulfilledPromise($this->lock->acquire()); - } - - public function get($callback = null): PromiseInterface - { - return $this->lock->get($callback); - } - - public function release(): PromiseInterface - { - return Helpers::createFulfilledPromise($this->lock->release()); - } -} diff --git a/src/Cache/Lock.php b/src/Cache/Lock.php deleted file mode 100644 index d2a3f21..0000000 --- a/src/Cache/Lock.php +++ /dev/null @@ -1,46 +0,0 @@ -name = $name; - $this->seconds = $seconds; - $this->owner = $owner; - } - - abstract public function acquire(): PromiseInterface; - - abstract public function get($callback = null): PromiseInterface; - - abstract public function release(): PromiseInterface; -} diff --git a/src/Cache/RedisLock.php b/src/Cache/RedisLock.php deleted file mode 100644 index ebdbf98..0000000 --- a/src/Cache/RedisLock.php +++ /dev/null @@ -1,69 +0,0 @@ -redis = $redis; - } - - public function acquire(): PromiseInterface - { - $promise = new Deferred(); - - if ($this->seconds > 0) { - $this->redis - ->set($this->name, $this->owner, 'EX', $this->seconds, 'NX') - ->then(function ($result) use ($promise) { - $promise->resolve($result === true); - }); - } else { - $this->redis - ->setnx($this->name, $this->owner) - ->then(function ($result) use ($promise) { - $promise->resolve($result === 1); - }); - } - - return $promise->promise(); - } - - public function get($callback = null): PromiseInterface - { - $promise = new Deferred(); - - $this->acquire() - ->then(function ($result) use ($callback, $promise) { - if ($result) { - try { - $callback(); - } finally { - $promise->resolve($this->release()); - } - } - }); - - return $promise->promise(); - } - - public function release(): PromiseInterface - { - return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner); - } -} diff --git a/src/ChannelManagers/LocalChannelManager.php b/src/ChannelManagers/LocalChannelManager.php index eb23e7c..dda246b 100644 --- a/src/ChannelManagers/LocalChannelManager.php +++ b/src/ChannelManagers/LocalChannelManager.php @@ -2,7 +2,7 @@ namespace BlaxSoftware\LaravelWebSockets\ChannelManagers; -use BlaxSoftware\LaravelWebSockets\Cache\ArrayLock; +use Illuminate\Cache\ArrayLock; use BlaxSoftware\LaravelWebSockets\Channels\Channel; use BlaxSoftware\LaravelWebSockets\Channels\OpenPresenceChannel; use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel; diff --git a/src/ChannelManagers/RedisChannelManager.php b/src/ChannelManagers/RedisChannelManager.php deleted file mode 100644 index 4bed831..0000000 --- a/src/ChannelManagers/RedisChannelManager.php +++ /dev/null @@ -1,891 +0,0 @@ -loop = $loop; - - $this->redis = Redis::connection( - config('websockets.replication.modes.redis.connection', 'default') - ); - - $connectionUri = $this->getConnectionUri(); - - $factoryClass = $factoryClass ?: Factory::class; - $factory = new $factoryClass($this->loop); - - $this->publishClient = $factory->createLazyClient($connectionUri); - $this->subscribeClient = $factory->createLazyClient($connectionUri); - - $this->subscribeClient->on('message', function ($channel, $payload) { - $this->onMessage($channel, $payload); - }); - } - - /** - * Get all channels for a specific app - * across multiple servers. - * - * @param string|int $appId - * @return \React\Promise\PromiseInterface[array] - */ - public function getGlobalChannels($appId): PromiseInterface - { - return $this->publishClient->smembers( - $this->getChannelsRedisHash($appId) - ); - } - - /** - * Remove connection from all channels. - * - * @param \Ratchet\ConnectionInterface $connection - * @return PromiseInterface[bool] - */ - public function unsubscribeFromAllChannels(ConnectionInterface $connection): PromiseInterface - { - return $this->getGlobalChannels($connection->app->id) - ->then(function ($channels) use ($connection) { - $promises = []; - foreach ($channels as $channel) { - $promises[] = $this->unsubscribeFromChannel($connection, $channel, new stdClass); - } - - return all($promises); - }) - ->then(function () use ($connection) { - return parent::unsubscribeFromAllChannels($connection); - }); - } - - /** - * Subscribe the connection to a specific channel. - * - * @param \Ratchet\ConnectionInterface $connection - * @param string $channelName - * @param stdClass $payload - * @return PromiseInterface[bool] - */ - public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface - { - return $this->subscribeToTopic($connection->app->id, $channelName) - ->then(function () use ($connection) { - return $this->addConnectionToSet($connection, Carbon::now()); - }) - ->then(function () use ($connection, $channelName) { - return $this->addChannelToSet($connection->app->id, $channelName); - }) - ->then(function () use ($connection, $channelName) { - return $this->incrementSubscriptionsCount($connection->app->id, $channelName, 1); - }) - ->then(function () use ($connection, $channelName, $payload) { - return parent::subscribeToChannel($connection, $channelName, $payload); - }); - } - - /** - * Unsubscribe the connection from the channel. - * - * @param \Ratchet\ConnectionInterface $connection - * @param string $channelName - * @param stdClass $payload - * @return PromiseInterface[bool] - */ - public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface - { - return $this->getGlobalConnectionsCount($connection->app->id, $channelName) - ->then(function ($count) use ($connection, $channelName) { - if ($count === 0) { - // Make sure to not stay subscribed to the PubSub topic - // if there are no connections. - return $this->unsubscribeFromTopic($connection->app->id, $channelName); - } - - return Helpers::createFulfilledPromise(null); - }) - ->then(function () use ($connection, $channelName) { - return $this->decrementSubscriptionsCount($connection->app->id, $channelName) - ->then(function ($count) use ($connection, $channelName) { - // If the total connections count gets to 0 after unsubscribe, - // try again to check & unsubscribe from the PubSub topic if needed. - if ($count < 1) { - $promises = []; - - $promises[] = $this->unsubscribeFromTopic($connection->app->id, $channelName); - $promises[] = $this->removeChannelFromSet($connection->app->id, $channelName); - - return all($promises); - } - }); - }) - ->then(function () use ($connection) { - return $this->removeConnectionFromSet($connection); - }) - ->then(function () use ($connection, $channelName, $payload) { - return parent::unsubscribeFromChannel($connection, $channelName, $payload); - }); - } - - /** - * Subscribe the connection to a specific channel, returning - * a promise containing the amount of connections. - * - * @param string|int $appId - * @return PromiseInterface[int] - */ - public function subscribeToApp($appId): PromiseInterface - { - return $this->subscribeToTopic($appId) - ->then(function () use ($appId) { - return $this->incrementSubscriptionsCount($appId); - }); - } - - /** - * Unsubscribe the connection from the channel, returning - * a promise containing the amount of connections after decrement. - * - * @param string|int $appId - * @return PromiseInterface[int] - */ - public function unsubscribeFromApp($appId): PromiseInterface - { - return $this->unsubscribeFromTopic($appId) - ->then(function () use ($appId) { - return $this->decrementSubscriptionsCount($appId); - }); - } - - /** - * Get the connections count - * across multiple servers. - * - * @param string|int $appId - * @param string|null $channelName - * @return PromiseInterface[int] - */ - public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface - { - return $this->publishClient - ->hget($this->getStatsRedisHash($appId, $channelName), 'connections') - ->then(function ($count) { - return is_null($count) ? 0 : (int) $count; - }); - } - - /** - * Broadcast the message across multiple servers. - * - * @param string|int $appId - * @param string|null $socketId - * @param string $channel - * @param stdClass $payload - * @param string|null $serverId - * @return PromiseInterface[bool] - */ - public function broadcastAcrossServers($appId, ?string $socketId, string $channel, stdClass $payload, string $serverId = null): PromiseInterface - { - $payload->appId = $appId; - $payload->socketId = $socketId; - $payload->serverId = $serverId ?: $this->getServerId(); - - return $this->publishClient - ->publish($this->getRedisTopicName($appId, $channel), json_encode($payload)) - ->then(function () use ($appId, $socketId, $channel, $payload, $serverId) { - return parent::broadcastAcrossServers($appId, $socketId, $channel, $payload, $serverId); - }); - } - - /** - * Handle the user when it joined a presence channel. - * - * @param \Ratchet\ConnectionInterface $connection - * @param stdClass $user - * @param string $channel - * @param stdClass $payload - * @return PromiseInterface - */ - public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload): PromiseInterface - { - return $this->storeUserData($connection->app->id, $channel, $connection->socketId, json_encode($user)) - ->then(function () use ($connection, $channel, $user) { - return $this->addUserSocket($connection->app->id, $channel, $user, $connection->socketId); - }) - ->then(function () use ($connection, $user, $channel, $payload) { - return parent::userJoinedPresenceChannel($connection, $user, $channel, $payload); - }); - } - - /** - * Handle the user when it left a presence channel. - * - * @param \Ratchet\ConnectionInterface $connection - * @param stdClass $user - * @param string $channel - * @param stdClass $payload - * @return PromiseInterface[bool] - */ - public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel): PromiseInterface - { - return $this->removeUserData($connection->app->id, $channel, $connection->socketId) - ->then(function () use ($connection, $channel, $user) { - return $this->removeUserSocket($connection->app->id, $channel, $user, $connection->socketId); - }) - ->then(function () use ($connection, $user, $channel) { - return parent::userLeftPresenceChannel($connection, $user, $channel); - }); - } - - /** - * Get the presence channel members. - * - * @param string|int $appId - * @param string $channel - * @return \React\Promise\PromiseInterface[array] - */ - public function getChannelMembers($appId, string $channel): PromiseInterface - { - return $this->publishClient - ->hgetall($this->getUsersRedisHash($appId, $channel)) - ->then(function ($list) { - return collect(Helpers::redisListToArray($list))->map(function ($user) { - return json_decode($user); - })->unique('user_id')->toArray(); - }); - } - - /** - * Get a member from a presence channel based on connection. - * - * @param \Ratchet\ConnectionInterface $connection - * @param string $channel - * @return \React\Promise\PromiseInterface[null|array] - */ - public function getChannelMember(ConnectionInterface $connection, string $channel): PromiseInterface - { - return $this->publishClient->hget( - $this->getUsersRedisHash($connection->app->id, $channel), $connection->socketId - ); - } - - /** - * Get the presence channels total members count. - * - * @param string|int $appId - * @param array $channelNames - * @return \React\Promise\PromiseInterface[array] - */ - public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface - { - $this->publishClient->multi(); - - foreach ($channelNames as $channel) { - $this->publishClient->hlen( - $this->getUsersRedisHash($appId, $channel) - ); - } - - return $this->publishClient->exec() - ->then(function ($data) use ($channelNames) { - return array_combine($channelNames, $data); - }); - } - - /** - * Get the socket IDs for a presence channel member. - * - * @param string|int $userId - * @param string|int $appId - * @param string $channelName - * @return \React\Promise\PromiseInterface[array] - */ - public function getMemberSockets($userId, $appId, $channelName): PromiseInterface - { - return $this->publishClient->smembers( - $this->getUserSocketsRedisHash($appId, $channelName, $userId) - ); - } - - /** - * Keep tracking the connections availability when they pong. - * - * @param \Ratchet\ConnectionInterface $connection - * @return PromiseInterface[bool] - */ - public function connectionPonged(ConnectionInterface $connection): PromiseInterface - { - // This will update the score with the current timestamp. - return $this->addConnectionToSet($connection, Carbon::now()) - ->then(function () use ($connection) { - $payload = [ - 'socketId' => $connection->socketId, - 'appId' => $connection->app->id, - 'serverId' => $this->getServerId(), - ]; - - return $this->publishClient - ->publish($this->getPongRedisHash($connection->app->id), json_encode($payload)); - }) - ->then(function () use ($connection) { - return parent::connectionPonged($connection); - }); - } - - /** - * Remove the obsolete connections that didn't ponged in a while. - * - * @return PromiseInterface[bool] - */ - public function removeObsoleteConnections(): PromiseInterface - { - return $this->lock()->get(function () { - return $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U')) - ->then(function ($connections) { - $promises = []; - foreach ($connections as $socketId => $appId) { - $connection = $this->fakeConnectionForApp($appId, $socketId); - - $promises[] = $this->unsubscribeFromAllChannels($connection); - } - - return all($promises); - }); - })->then(function () { - return parent::removeObsoleteConnections(); - }); - } - - /** - * Handle a message received from Redis on a specific channel. - * - * @param string $redisChannel - * @param string $payload - * @return void - */ - public function onMessage(string $redisChannel, string $payload) - { - $payload = json_decode($payload); - - if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) { - return; - } - - if ($redisChannel == $this->getPongRedisHash($payload->appId)) { - $connection = $this->fakeConnectionForApp($payload->appId, $payload->socketId); - - return parent::connectionPonged($connection); - } - - $payload->channel = Str::after($redisChannel, "{$payload->appId}:"); - - if (! $channel = $this->find($payload->appId, $payload->channel)) { - return; - } - - $appId = $payload->appId ?? null; - $socketId = $payload->socketId ?? null; - $serverId = $payload->serverId ?? null; - - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [ - 'fromServerId' => $serverId, - 'fromSocketId' => $socketId, - 'receiverServerId' => $this->getServerId(), - 'channel' => $channel, - 'payload' => $payload, - ]); - - unset($payload->socketId); - unset($payload->serverId); - unset($payload->appId); - - $channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId); - } - - public function find($appId, string $channel) - { - if (! $channelInstance = parent::find($appId, $channel)) { - $class = $this->getChannelClassName($channel); - $this->channels[$appId][$channel] = new $class($channel); - } - - return parent::find($appId, $channel); - } - - /** - * Build the Redis connection URL from Laravel database config. - * - * @return string - */ - protected function getConnectionUri() - { - $name = config('websockets.replication.modes.redis.connection', 'default'); - $config = config("database.redis.{$name}"); - - $host = $config['host']; - $port = $config['port'] ?: 6379; - - $query = []; - - if ($config['password']) { - $query['password'] = $config['password']; - } - - if ($config['database']) { - $query['db'] = $config['database']; - } - - $query = http_build_query($query); - - return "redis://{$host}:{$port}".($query ? "?{$query}" : ''); - } - - /** - * Get the Subscribe client instance. - * - * @return Client - */ - public function getSubscribeClient() - { - return $this->subscribeClient; - } - - /** - * Get the Publish client instance. - * - * @return Client - */ - public function getPublishClient() - { - return $this->publishClient; - } - - /** - * Get the Redis client used by other classes. - * - * @return Client - */ - public function getRedisClient() - { - return $this->getPublishClient(); - } - - /** - * Increment the subscribed count number. - * - * @param string|int $appId - * @param string|null $channel - * @param int $increment - * @return PromiseInterface[int] - */ - public function incrementSubscriptionsCount($appId, string $channel = null, int $increment = 1): PromiseInterface - { - return $this->publishClient->hincrby( - $this->getStatsRedisHash($appId, $channel), 'connections', $increment - ); - } - - /** - * Decrement the subscribed count number. - * - * @param string|int $appId - * @param string|null $channel - * @param int $decrement - * @return PromiseInterface[int] - */ - public function decrementSubscriptionsCount($appId, string $channel = null, int $increment = 1): PromiseInterface - { - return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1); - } - - /** - * Add the connection to the sorted list. - * - * @param \Ratchet\ConnectionInterface $connection - * @param \DateTime|string|null $moment - * @return PromiseInterface - */ - public function addConnectionToSet(ConnectionInterface $connection, $moment = null): PromiseInterface - { - $moment = $moment ? Carbon::parse($moment) : Carbon::now(); - - return $this->publishClient->zadd( - $this->getSocketsRedisHash(), - $moment->format('U'), "{$connection->app->id}:{$connection->socketId}" - ); - } - - /** - * Remove the connection from the sorted list. - * - * @param \Ratchet\ConnectionInterface $connection - * @return PromiseInterface - */ - public function removeConnectionFromSet(ConnectionInterface $connection): PromiseInterface - { - return $this->publishClient->zrem( - $this->getSocketsRedisHash(), - "{$connection->app->id}:{$connection->socketId}" - ); - } - - /** - * Get the connections from the sorted list, with last - * connection between certain timestamps. - * - * @param int $start - * @param int $stop - * @param bool $strict - * @return PromiseInterface[array] - */ - public function getConnectionsFromSet(int $start = 0, int $stop = 0, bool $strict = true): PromiseInterface - { - if ($strict) { - $start = "({$start}"; - $stop = "({$stop}"; - } - - return $this->publishClient - ->zrangebyscore($this->getSocketsRedisHash(), $start, $stop) - ->then(function ($list) { - return collect($list)->mapWithKeys(function ($appWithSocket) { - [$appId, $socketId] = explode(':', $appWithSocket); - - return [$socketId => $appId]; - })->toArray(); - }); - } - - /** - * Add a channel to the set list. - * - * @param string|int $appId - * @param string $channel - * @return PromiseInterface - */ - public function addChannelToSet($appId, string $channel): PromiseInterface - { - return $this->publishClient->sadd( - $this->getChannelsRedisHash($appId), $channel - ); - } - - /** - * Remove a channel from the set list. - * - * @param string|int $appId - * @param string $channel - * @return PromiseInterface - */ - public function removeChannelFromSet($appId, string $channel): PromiseInterface - { - return $this->publishClient->srem( - $this->getChannelsRedisHash($appId), $channel - ); - } - - /** - * Check if channel is on the list. - * - * @param string|int $appId - * @param string $channel - * @return PromiseInterface - */ - public function isChannelInSet($appId, string $channel): PromiseInterface - { - return $this->publishClient->sismember( - $this->getChannelsRedisHash($appId), $channel - ); - } - - /** - * Set data for a topic. Might be used for the presence channels. - * - * @param string|int $appId - * @param string|null $channel - * @param string $key - * @param string $data - * @return PromiseInterface - */ - public function storeUserData($appId, string $channel = null, string $key, $data): PromiseInterface - { - return $this->publishClient->hset( - $this->getUsersRedisHash($appId, $channel), $key, $data - ); - } - - /** - * Remove data for a topic. Might be used for the presence channels. - * - * @param string|int $appId - * @param string|null $channel - * @param string $key - * @return PromiseInterface - */ - public function removeUserData($appId, string $channel = null, string $key): PromiseInterface - { - return $this->publishClient->hdel( - $this->getUsersRedisHash($appId, $channel), $key - ); - } - - /** - * Subscribe to the topic for the app, or app and channel. - * - * @param string|int $appId - * @param string|null $channel - * @return PromiseInterface - */ - public function subscribeToTopic($appId, string $channel = null): PromiseInterface - { - $topic = $this->getRedisTopicName($appId, $channel); - - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [ - 'serverId' => $this->getServerId(), - 'pubsubTopic' => $topic, - ]); - - return $this->subscribeClient->subscribe($topic); - } - - /** - * Unsubscribe from the topic for the app, or app and channel. - * - * @param string|int $appId - * @param string|null $channel - * @return PromiseInterface - */ - public function unsubscribeFromTopic($appId, string $channel = null): PromiseInterface - { - $topic = $this->getRedisTopicName($appId, $channel); - - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [ - 'serverId' => $this->getServerId(), - 'pubsubTopic' => $topic, - ]); - - return $this->subscribeClient->unsubscribe($topic); - } - - /** - * Add the Presence Channel's User's Socket ID to a list. - * - * @param string|int $appId - * @param string $channel - * @param stdClass $user - * @param string $socketId - * @return PromiseInterface - */ - protected function addUserSocket($appId, string $channel, stdClass $user, string $socketId): PromiseInterface - { - return $this->publishClient->sadd( - $this->getUserSocketsRedisHash($appId, $channel, $user->user_id), $socketId - ); - } - - /** - * Remove the Presence Channel's User's Socket ID from the list. - * - * @param string|int $appId - * @param string $channel - * @param stdClass $user - * @param string $socketId - * @return PromiseInterface - */ - protected function removeUserSocket($appId, string $channel, stdClass $user, string $socketId): PromiseInterface - { - return $this->publishClient->srem( - $this->getUserSocketsRedisHash($appId, $channel, $user->user_id), $socketId - ); - } - - /** - * Get the Redis Keyspace name to handle subscriptions - * and other key-value sets. - * - * @param string|int|null $appId - * @param string|null $channel - * @return string - */ - public function getRedisKey($appId = null, string $channel = null, array $suffixes = []): string - { - $prefix = config('database.redis.options.prefix', null); - - $hash = "{$prefix}{$appId}"; - - if ($channel) { - $suffixes = array_merge([$channel], $suffixes); - } - - $suffixes = implode(':', $suffixes); - - if ($suffixes) { - $hash .= ":{$suffixes}"; - } - - return $hash; - } - - /** - * Get the pong Redis hash. - * - * @param string|int $appId - */ - public function getPongRedisHash($appId): string - { - return $this->getRedisKey($appId, null, ['pong']); - } - - /** - * Get the statistics Redis hash. - * - * @param string|int $appId - * @param string|null $channel - * @return string - */ - public function getStatsRedisHash($appId, string $channel = null): string - { - return $this->getRedisKey($appId, $channel, ['stats']); - } - - /** - * Get the sockets Redis hash used to store all sockets ids. - * - * @return string - */ - public function getSocketsRedisHash(): string - { - return $this->getRedisKey(null, null, ['sockets']); - } - - /** - * Get the channels Redis hash for a specific app id, used - * to store existing channels. - * - * @param string|int $appId - * @return string - */ - public function getChannelsRedisHash($appId): string - { - return $this->getRedisKey($appId, null, ['channels']); - } - - /** - * Get the Redis hash for storing presence channels users. - * - * @param string|int $appId - * @param string|null $channel - * @return string - */ - public function getUsersRedisHash($appId, string $channel = null): string - { - return $this->getRedisKey($appId, $channel, ['users']); - } - - /** - * Get the Redis hash for storing socket ids - * for a specific presence channels user. - * - * @param string|int $appId - * @param string|null $channel - * @param string|int|null $userId - * @return string - */ - public function getUserSocketsRedisHash($appId, string $channel = null, $userId = null): string - { - return $this->getRedisKey($appId, $channel, [$userId, 'userSockets']); - } - - /** - * Get the Redis topic name for PubSub - * used to transfer info between servers. - * - * @param string|int $appId - * @param string|null $channel - * @return string - */ - public function getRedisTopicName($appId, string $channel = null): string - { - return $this->getRedisKey($appId, $channel); - } - - /** - * Get a new RedisLock instance to avoid race conditions. - * - * @return RedisLock - */ - protected function lock() - { - return new RedisLock($this->publishClient, static::$lockName, 0); - } - - /** - * Create a fake connection for app that will mimick a connection - * by app ID and Socket ID to be able to be passed to the methods - * that accepts a connection class. - * - * @param string|int $appId - * @param string $socketId - * @return ConnectionInterface - */ - public function fakeConnectionForApp($appId, string $socketId) - { - return new MockableConnection($appId, $socketId); - } -} diff --git a/src/Channels/Channel.php b/src/Channels/Channel.php index 7084b93..226ce53 100644 --- a/src/Channels/Channel.php +++ b/src/Channels/Channel.php @@ -3,9 +3,6 @@ namespace BlaxSoftware\LaravelWebSockets\Channels; use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager; -use BlaxSoftware\LaravelWebSockets\DashboardLogger; -use BlaxSoftware\LaravelWebSockets\Events\SubscribedToChannel; -use BlaxSoftware\LaravelWebSockets\Events\UnsubscribedFromChannel; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature; use Illuminate\Support\Str; @@ -103,21 +100,10 @@ class Channel $this->saveConnection($connection); $connection->send(json_encode([ - 'event' => 'pusher_internal:subscription_succeeded', + 'event' => 'websocket_internal.subscription_succeeded', 'channel' => $this->getName(), ])); - DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [ - 'socketId' => $connection->socketId, - 'channel' => $this->getName(), - ]); - - SubscribedToChannel::dispatch( - $connection->app->id, - $connection->socketId, - $this->getName(), - ); - return true; } @@ -135,12 +121,6 @@ class Channel unset($this->connections[$connection->socketId]); - UnsubscribedFromChannel::dispatch( - $connection->app->id, - $connection->socketId, - $this->getName() - ); - return Helpers::createFulfilledPromise(true); } diff --git a/src/Channels/PresenceChannel.php b/src/Channels/PresenceChannel.php index 42b4d17..5968f1a 100644 --- a/src/Channels/PresenceChannel.php +++ b/src/Channels/PresenceChannel.php @@ -2,9 +2,6 @@ namespace BlaxSoftware\LaravelWebSockets\Channels; -use BlaxSoftware\LaravelWebSockets\DashboardLogger; -use BlaxSoftware\LaravelWebSockets\Events\SubscribedToChannel; -use BlaxSoftware\LaravelWebSockets\Events\UnsubscribedFromChannel; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature; use Ratchet\ConnectionInterface; @@ -45,7 +42,7 @@ class PresenceChannel extends PrivateChannel } $connection->send(json_encode([ - 'event' => 'pusher_internal:subscription_succeeded', + 'event' => 'websocket_internal.subscription_succeeded', 'channel' => $this->getName(), 'data' => json_encode([ 'presence' => [ @@ -60,7 +57,7 @@ class PresenceChannel extends PrivateChannel }); }) ->then(function () use ($connection, $user, $payload) { - // The `pusher_internal:member_added` event is triggered when a user joins a channel. + // The `websocket_internal.member_added` event is triggered when a user joins a channel. // It's quite possible that a user can have multiple connections to the same channel // (for example by having multiple browser tabs open) // and in this case the events will only be triggered when the first tab is opened. @@ -69,7 +66,7 @@ class PresenceChannel extends PrivateChannel ->then(function ($sockets) use ($payload, $connection, $user) { if (count($sockets) === 1) { $memberAddedPayload = [ - 'event' => 'pusher_internal:member_added', + 'event' => 'websocket_internal.member_added', 'channel' => $this->getName(), 'data' => $payload->channel_data, ]; @@ -78,20 +75,7 @@ class PresenceChannel extends PrivateChannel (object) $memberAddedPayload, $connection->socketId, $connection->app->id ); - - SubscribedToChannel::dispatch( - $connection->app->id, - $connection->socketId, - $this->getName(), - $user - ); } - - DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [ - 'socketId' => $connection->socketId, - 'channel' => $this->getName(), - 'duplicate-connection' => count($sockets) > 1, - ]); }); }); @@ -121,7 +105,7 @@ class PresenceChannel extends PrivateChannel return $this->channelManager ->userLeftPresenceChannel($connection, $user, $this->getName()) ->then(function () use ($connection, $user) { - // The `pusher_internal:member_removed` is triggered when a user leaves a channel. + // The `websocket_internal.member_removed` is triggered when a user leaves a channel. // It's quite possible that a user can have multiple connections to the same channel // (for example by having multiple browser tabs open) // and in this case the events will only be triggered when the last one is closed. @@ -130,7 +114,7 @@ class PresenceChannel extends PrivateChannel ->then(function ($sockets) use ($connection, $user) { if (count($sockets) === 0) { $memberRemovedPayload = [ - 'event' => 'pusher_internal:member_removed', + 'event' => 'websocket_internal.member_removed', 'channel' => $this->getName(), 'data' => json_encode([ 'user_id' => $user->user_id, @@ -141,13 +125,6 @@ class PresenceChannel extends PrivateChannel (object) $memberRemovedPayload, $connection->socketId, $connection->app->id ); - - UnsubscribedFromChannel::dispatch( - $connection->app->id, - $connection->socketId, - $this->getName(), - $user - ); } }); }); diff --git a/src/Concerns/PushesToPusher.php b/src/Concerns/PushesToPusher.php deleted file mode 100644 index a22b62d..0000000 --- a/src/Concerns/PushesToPusher.php +++ /dev/null @@ -1,27 +0,0 @@ -comment('Cleaning WebSocket Statistics...'); - - $days = $this->option('days') ?: config('statistics.delete_statistics_older_than_days'); - - $amountDeleted = StatisticsStore::delete( - now()->subDays($days), $this->argument('appId') - ); - - $this->info("Deleted {$amountDeleted} record(s) from the WebSocket statistics storage."); - } -} diff --git a/src/Console/Commands/FlushCollectedStatistics.php b/src/Console/Commands/FlushCollectedStatistics.php deleted file mode 100644 index 57e6d73..0000000 --- a/src/Console/Commands/FlushCollectedStatistics.php +++ /dev/null @@ -1,37 +0,0 @@ -comment('Flushing the collected WebSocket Statistics...'); - - StatisticsCollector::flush(); - - $this->line('Flush complete!'); - } -} diff --git a/src/Console/Commands/StartServer.php b/src/Console/Commands/StartServer.php index ccfc642..87ae550 100644 --- a/src/Console/Commands/StartServer.php +++ b/src/Console/Commands/StartServer.php @@ -5,7 +5,6 @@ namespace BlaxSoftware\LaravelWebSockets\Console\Commands; use BlaxSoftware\LaravelWebSockets\Broadcast\BroadcastSocketServer; use BlaxSoftware\LaravelWebSockets\Cache\IpcCache; use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager; -use BlaxSoftware\LaravelWebSockets\Facades\StatisticsCollector as StatisticsCollectorFacade; use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter; use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc; use BlaxSoftware\LaravelWebSockets\Server\Loggers\ConnectionLogger; @@ -243,22 +242,9 @@ class StartServer extends Command */ protected function configureStatistics() { - if (! $this->option('disable-statistics')) { - $intervalInSeconds = $this->option('statistics-interval') ?: config('websockets.statistics.interval_in_seconds', 3600); - - \Log::channel('websocket')->debug('Statistics enabled', [ - 'interval_seconds' => $intervalInSeconds, - ]); - - $this->loop->addPeriodicTimer($intervalInSeconds, function () { - \Log::channel('websocket')->debug('Statistics timer tick, saving...'); - $this->line('Saving statistics...', null, OutputInterface::VERBOSITY_VERBOSE); - - StatisticsCollectorFacade::save(); - }); - } else { - \Log::channel('websocket')->debug('Statistics disabled'); - } + // Statistics collection has been removed. + // A new CLI monitoring system will replace it. + \Log::channel('websocket')->debug('Statistics disabled (removed)'); } /** diff --git a/src/Contracts/StatisticsCollector.php b/src/Contracts/StatisticsCollector.php deleted file mode 100644 index 22ee731..0000000 --- a/src/Contracts/StatisticsCollector.php +++ /dev/null @@ -1,78 +0,0 @@ -header('X-App-Id')), app(LoopInterface::class)); - - $broadcaster = $this->getPusherBroadcaster([ - 'key' => $app->key, - 'secret' => $app->secret, - 'id' => $app->id, - ]); - - /* - * Since the dashboard itself is already secured by the - * Authorize middleware, we can trust all channel - * authentication requests in here. - */ - return $broadcaster->validAuthenticationResponse($request, []); - } -} diff --git a/src/Dashboard/Http/Controllers/SendMessage.php b/src/Dashboard/Http/Controllers/SendMessage.php deleted file mode 100644 index 83c31e8..0000000 --- a/src/Dashboard/Http/Controllers/SendMessage.php +++ /dev/null @@ -1,56 +0,0 @@ -validate([ - 'appId' => ['required', new AppId], - 'key' => 'required|string', - 'secret' => 'required|string', - 'event' => 'required|string', - 'channel' => 'required|string', - 'data' => 'required|json', - ]); - - $broadcaster = $this->getPusherBroadcaster([ - 'key' => $request->key, - 'secret' => $request->secret, - 'id' => $request->appId, - ]); - - try { - $decodedData = json_decode($request->data, true); - - $broadcaster->broadcast( - [$request->channel], - $request->event, - $decodedData ?: [] - ); - } catch (Throwable $e) { - return response()->json([ - 'ok' => false, - 'exception' => $e->getMessage(), - ]); - } - - return response()->json([ - 'ok' => true, - ]); - } -} diff --git a/src/Dashboard/Http/Controllers/ShowApps.php b/src/Dashboard/Http/Controllers/ShowApps.php deleted file mode 100644 index 56947a4..0000000 --- a/src/Dashboard/Http/Controllers/ShowApps.php +++ /dev/null @@ -1,27 +0,0 @@ - await($apps->all(), app(LoopInterface::class), 2.0), - 'port' => config('websockets.dashboard.port', 6001), - ]); - } -} diff --git a/src/Dashboard/Http/Controllers/ShowDashboard.php b/src/Dashboard/Http/Controllers/ShowDashboard.php deleted file mode 100644 index 6d1ee5e..0000000 --- a/src/Dashboard/Http/Controllers/ShowDashboard.php +++ /dev/null @@ -1,31 +0,0 @@ - await($apps->all(), app(LoopInterface::class), 2.0), - 'port' => config('websockets.dashboard.port', 6001), - 'channels' => DashboardLogger::$channels, - 'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX, - 'refreshInterval' => config('websockets.statistics.interval_in_seconds'), - ]); - } -} diff --git a/src/Dashboard/Http/Controllers/ShowStatistics.php b/src/Dashboard/Http/Controllers/ShowStatistics.php deleted file mode 100644 index a81fabe..0000000 --- a/src/Dashboard/Http/Controllers/ShowStatistics.php +++ /dev/null @@ -1,33 +0,0 @@ -whereAppId($appId) - ->latest() - ->limit(120); - }; - - $processCollection = function ($collection) { - return $collection->reverse(); - }; - - return StatisticsStore::getForGraph( - $processQuery, $processCollection - ); - } -} diff --git a/src/Dashboard/Http/Controllers/StoreApp.php b/src/Dashboard/Http/Controllers/StoreApp.php deleted file mode 100644 index 1d5d34f..0000000 --- a/src/Dashboard/Http/Controllers/StoreApp.php +++ /dev/null @@ -1,37 +0,0 @@ - (string) Str::uuid(), - 'key' => (string) Str::uuid(), - 'secret' => (string) Str::uuid(), - 'name' => $request->get('name'), - 'enable_client_messages' => $request->has('enable_client_messages'), - 'enable_statistics' => $request->has('enable_statistics'), - 'allowed_origins' => $request->get('allowed_origins'), - ]; - - await($apps->createApp($appData), app(LoopInterface::class)); - - return redirect()->route('laravel-websockets.apps'); - } -} diff --git a/src/Dashboard/Http/Middleware/Authorize.php b/src/Dashboard/Http/Middleware/Authorize.php deleted file mode 100644 index 9a0f2d3..0000000 --- a/src/Dashboard/Http/Middleware/Authorize.php +++ /dev/null @@ -1,22 +0,0 @@ -user()]) - ? $next($request) - : abort(403); - } -} diff --git a/src/Dashboard/Http/Requests/StoreAppRequest.php b/src/Dashboard/Http/Requests/StoreAppRequest.php deleted file mode 100644 index 6d0b2f9..0000000 --- a/src/Dashboard/Http/Requests/StoreAppRequest.php +++ /dev/null @@ -1,20 +0,0 @@ - 'required', - ]; - } -} diff --git a/src/DashboardLogger.php b/src/DashboardLogger.php deleted file mode 100644 index 26e0b7a..0000000 --- a/src/DashboardLogger.php +++ /dev/null @@ -1,107 +0,0 @@ - 'log-message', - 'channel' => $channelName, - 'data' => [ - 'type' => $type, - 'time' => date('H:i:s'), - 'details' => $details, - ], - ]; - - // Check if channel exists locally and broadcast - $channel = self::$channelManager->find($appId, $channelName); - if ($channel) { - $channel->broadcastLocally($appId, $payload); - } - - // Always broadcast across servers (preserving original behavior) - // The channel manager handles the replication logic - self::$channelManager->broadcastAcrossServers( - $appId, - null, - $channelName, - $payload - ); - } - - /** - * Reset cached state (useful for testing) - */ - public static function reset(): void - { - self::$enabled = null; - self::$channelManager = null; - } -} diff --git a/src/Events/ConnectionPonged.php b/src/Events/ConnectionPonged.php deleted file mode 100644 index 439c319..0000000 --- a/src/Events/ConnectionPonged.php +++ /dev/null @@ -1,38 +0,0 @@ -appId = $appId; - $this->socketId = $socketId; - } -} diff --git a/src/Events/SubscribedToChannel.php b/src/Events/SubscribedToChannel.php deleted file mode 100644 index 2ca1bc8..0000000 --- a/src/Events/SubscribedToChannel.php +++ /dev/null @@ -1,57 +0,0 @@ -appId = $appId; - $this->socketId = $socketId; - $this->channelName = $channelName; - $this->user = $user; - } -} diff --git a/src/Events/UnsubscribedFromChannel.php b/src/Events/UnsubscribedFromChannel.php deleted file mode 100644 index c9c1ed8..0000000 --- a/src/Events/UnsubscribedFromChannel.php +++ /dev/null @@ -1,57 +0,0 @@ -appId = $appId; - $this->socketId = $socketId; - $this->channelName = $channelName; - $this->user = $user; - } -} diff --git a/src/Events/WebSocketMessageReceived.php b/src/Events/WebSocketMessageReceived.php deleted file mode 100644 index a72c1f4..0000000 --- a/src/Events/WebSocketMessageReceived.php +++ /dev/null @@ -1,56 +0,0 @@ -appId = $appId; - $this->socketId = $socketId; - $this->message = $message; - $this->decodedMessage = json_decode($message->getPayload(), true); - } -} diff --git a/src/Facades/StatisticsCollector.php b/src/Facades/StatisticsCollector.php deleted file mode 100644 index 87ff062..0000000 --- a/src/Facades/StatisticsCollector.php +++ /dev/null @@ -1,19 +0,0 @@ -redis, $config['queue'], - $config['connection'] ?? $this->connection, - $config['retry_after'] ?? 60, - $config['block_for'] ?? null - ); - } -} diff --git a/src/Queue/AsyncRedisQueue.php b/src/Queue/AsyncRedisQueue.php deleted file mode 100644 index d16f866..0000000 --- a/src/Queue/AsyncRedisQueue.php +++ /dev/null @@ -1,25 +0,0 @@ -container->bound(ChannelManager::class) - ? $this->container->make(ChannelManager::class) - : null; - - return $channelManager && method_exists($channelManager, 'getRedisClient') - ? $channelManager->getRedisClient() - : parent::getConnection(); - } -} diff --git a/src/Rules/AppId.php b/src/Rules/AppId.php deleted file mode 100644 index f60d365..0000000 --- a/src/Rules/AppId.php +++ /dev/null @@ -1,36 +0,0 @@ -findById($value), Factory::create()) ? true : false; - } - - /** - * The validation message. - * - * @return string - */ - public function message() - { - return 'There is no app registered with the given id. Make sure the websockets config file contains an app for this id or that your custom AppManager returns an app for this id.'; - } -} diff --git a/src/Server/Exceptions/WebSocketException.php b/src/Server/Exceptions/WebSocketException.php index c2aa80d..9fe4608 100644 --- a/src/Server/Exceptions/WebSocketException.php +++ b/src/Server/Exceptions/WebSocketException.php @@ -7,14 +7,14 @@ use Exception; class WebSocketException extends Exception { /** - * Get the payload, Pusher-like formatted. + * Get the payload. * * @return array */ public function getPayload() { return [ - 'event' => 'pusher.error', + 'event' => 'websocket.error', 'data' => [ 'message' => $this->getMessage(), 'code' => $this->getCode(), diff --git a/src/Server/Messages/PusherChannelProtocolMessage.php b/src/Server/Messages/PusherChannelProtocolMessage.php deleted file mode 100644 index f126e43..0000000 --- a/src/Server/Messages/PusherChannelProtocolMessage.php +++ /dev/null @@ -1,90 +0,0 @@ -payload->event ?? ''; - - // Fast path for ping - most common pusher protocol message - if ($event === 'pusher:ping' || $event === 'pusher.ping') { - $this->pingFast($this->connection); - return; - } - - // Extract method name from event (e.g., 'pusher:subscribe' -> 'subscribe') - $colonPos = strpos($event, ':'); - if ($colonPos !== false) { - $eventName = substr($event, $colonPos + 1); - } else { - $dotPos = strpos($event, '.'); - $eventName = $dotPos !== false ? substr($event, $dotPos + 1) : ''; - } - - // Convert to camelCase if needed (e.g., 'channel-name' -> 'channelName') - if (strpos($eventName, '-') !== false) { - $eventName = lcfirst(str_replace('-', '', ucwords($eventName, '-'))); - } - - if ($eventName && $eventName !== 'respond' && method_exists($this, $eventName)) { - $this->$eventName($this->connection, $this->payload->data ?? new stdClass()); - } - } - - /** - * Fast ping handler - avoids promise chain and event dispatch - */ - protected function pingFast(ConnectionInterface $connection): void - { - // Update timestamp directly on connection (no promise chain) - $connection->lastPongedAt = time(); - - // Send pre-encoded response (no json_encode overhead) - $connection->send(self::PONG_RESPONSE); - - // Skip event dispatch for ping - it's high frequency and events are expensive - // If you need ping events, use: ConnectionPonged::dispatch($connection->app->id, $connection->socketId); - } - - /** - * Legacy ping handler - kept for compatibility - * @deprecated Use pingFast instead - */ - protected function ping(ConnectionInterface $connection): void - { - $this->pingFast($connection); - } - - /** - * Subscribe to channel. - * - * @see https://pusher.com/docs/pusher_protocol#pusher-subscribe - */ - protected function subscribe(ConnectionInterface $connection, stdClass $payload): void - { - $this->channelManager->subscribeToChannel($connection, $payload->channel, $payload); - } - - /** - * Unsubscribe from the channel. - */ - public function unsubscribe(ConnectionInterface $connection, stdClass $payload): void - { - $this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload); - } -} diff --git a/src/Server/Messages/PusherClientMessage.php b/src/Server/Messages/PusherClientMessage.php deleted file mode 100644 index 069f350..0000000 --- a/src/Server/Messages/PusherClientMessage.php +++ /dev/null @@ -1,79 +0,0 @@ -payload = $payload; - $this->connection = $connection; - $this->channelManager = $channelManager; - } - - /** - * Respond to the message construction. - * - * @return void - */ - public function respond() - { - if (! Str::startsWith($this->payload->event, 'client-')) { - return; - } - - if (! $this->connection->app->clientMessagesEnabled) { - return; - } - - $channel = $this->channelManager->find( - $this->connection->app->id, $this->payload->channel - ); - - optional($channel)->broadcastToEveryoneExcept( - $this->payload, $this->connection->socketId, $this->connection->app->id - ); - - DashboardLogger::log($this->connection->app->id, DashboardLogger::TYPE_WS_MESSAGE, [ - 'socketId' => $this->connection->socketId, - 'event' => $this->payload->event, - 'channel' => $this->payload->channel, - 'data' => $this->payload, - ]); - } -} diff --git a/src/Server/Messages/PusherMessageFactory.php b/src/Server/Messages/PusherMessageFactory.php deleted file mode 100644 index 6eb17fe..0000000 --- a/src/Server/Messages/PusherMessageFactory.php +++ /dev/null @@ -1,46 +0,0 @@ -getPayload()); - $event = $payload->event ?? ''; - - // Fast string prefix check (faster than Str::startsWith) - // Check first 7 chars for 'pusher.' or 'pusher:' - $isPusherEvent = ( - isset($event[6]) && - $event[0] === 'p' && - $event[1] === 'u' && - $event[2] === 's' && - $event[3] === 'h' && - $event[4] === 'e' && - $event[5] === 'r' && - ($event[6] === '.' || $event[6] === ':') - ); - - return $isPusherEvent - ? new PusherChannelProtocolMessage($payload, $connection, $channelManager) - : new PusherClientMessage($payload, $connection, $channelManager); - } -} diff --git a/src/Server/Router.php b/src/Server/Router.php index ae7ade3..0a9061a 100644 --- a/src/Server/Router.php +++ b/src/Server/Router.php @@ -71,10 +71,6 @@ class Router public function registerRoutes() { $this->get('/app/{appKey}', 'websockets.handler'); - $this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event')); - $this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels')); - $this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel')); - $this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users')); $this->get('/health', config('websockets.handlers.health')); $this->registerCustomRoutes(); diff --git a/src/Server/WebSocketHandler.php b/src/Server/WebSocketHandler.php index ec6e549..7bc1449 100644 --- a/src/Server/WebSocketHandler.php +++ b/src/Server/WebSocketHandler.php @@ -4,14 +4,12 @@ namespace BlaxSoftware\LaravelWebSockets\Server; use BlaxSoftware\LaravelWebSockets\Apps\App; use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager; -use BlaxSoftware\LaravelWebSockets\DashboardLogger; use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed; use BlaxSoftware\LaravelWebSockets\Events\NewConnection; -use BlaxSoftware\LaravelWebSockets\Events\WebSocketMessageReceived; -use BlaxSoftware\LaravelWebSockets\Facades\StatisticsCollector; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException; use Exception; +use Illuminate\Support\Str; use Ratchet\ConnectionInterface; use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\WebSocket\MessageComponentInterface; @@ -62,19 +60,10 @@ class WebSocketHandler implements MessageComponentInterface /** @var \GuzzleHttp\Psr7\Request $request */ $request = $connection->httpRequest; - if ($connection->app->statisticsEnabled) { - StatisticsCollector::connection($connection->app->id); - } - $this->channelManager->subscribeToApp($connection->app->id); $this->channelManager->connectionPonged($connection); - DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [ - 'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}", - 'socketId' => $connection->socketId, - ]); - NewConnection::dispatch($connection->app->id, $connection->socketId); } } catch (WebSocketException $exception) { @@ -98,19 +87,49 @@ class WebSocketHandler implements MessageComponentInterface return; } - Messages\PusherMessageFactory::createForMessage( - $message, $connection, $this->channelManager - )->respond(); + $payload = json_decode($message->getPayload()); - if ($connection->app->statisticsEnabled) { - StatisticsCollector::webSocketMessage($connection->app->id); + if (! isset($payload->event)) { + return; } - WebSocketMessageReceived::dispatch( - $connection->app->id, - $connection->socketId, - $message - ); + $event = $payload->event; + + if ($this->isProtocolAction($event, 'ping')) { + $connection->send(json_encode(['event' => 'websocket.pong'])); + $this->channelManager->connectionPonged($connection); + return; + } + + if ($this->isProtocolAction($event, 'subscribe')) { + $channel = $payload->data->channel ?? null; + if ($channel) { + $this->channelManager->subscribeToChannel($connection, $channel, $payload->data ?? new \stdClass); + } + return; + } + + if ($this->isProtocolAction($event, 'unsubscribe')) { + $channel = $payload->data->channel ?? null; + if ($channel) { + $this->channelManager->unsubscribeFromChannel($connection, $channel, $payload->data ?? new \stdClass); + } + return; + } + + // Client events (whisper) — must start with "client-" + if (Str::startsWith($event, 'client-')) { + $channel = $payload->channel ?? ($payload->data->channel ?? null); + if ($channel) { + $ch = $this->channelManager->find($connection->app->id, $channel); + if ($ch) { + $ch->broadcastToEveryoneExcept( + $payload, $connection->socketId, $connection->app->id, false + ); + } + } + return; + } } /** @@ -125,10 +144,6 @@ class WebSocketHandler implements MessageComponentInterface ->unsubscribeFromAllChannels($connection) ->then(function (bool $unsubscribed) use ($connection) { if (isset($connection->app)) { - if ($connection->app->statisticsEnabled) { - StatisticsCollector::disconnection($connection->app->id); - } - return $this->channelManager->unsubscribeFromApp($connection->app->id); } @@ -136,10 +151,6 @@ class WebSocketHandler implements MessageComponentInterface }) ->then(function () use ($connection) { if (isset($connection->app)) { - DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [ - 'socketId' => $connection->socketId, - ]); - ConnectionClosed::dispatch($connection->app->id, $connection->socketId); } }); @@ -273,7 +284,7 @@ class WebSocketHandler implements MessageComponentInterface protected function establishConnection(ConnectionInterface $connection) { $connection->send(json_encode([ - 'event' => 'pusher.connection_established', + 'event' => 'websocket.connection_established', 'data' => json_encode([ 'socket_id' => $connection->socketId, 'activity_timeout' => 30, @@ -282,4 +293,17 @@ class WebSocketHandler implements MessageComponentInterface return $this; } + + /** + * Check if an event matches a protocol action (e.g., subscribe, ping). + * Matches both dot and colon delimiters for backward compatibility. + * + * @param string $event + * @param string $action + * @return bool + */ + protected function isProtocolAction(string $event, string $action): bool + { + return str_ends_with($event, '.' . $action) || str_ends_with($event, ':' . $action); + } } diff --git a/src/Services/WebsocketService.php b/src/Services/WebsocketService.php index a3cf0d2..242d4f4 100644 --- a/src/Services/WebsocketService.php +++ b/src/Services/WebsocketService.php @@ -133,7 +133,7 @@ class WebsocketService // Subscribe (public channel) $client->send(json_encode([ - 'event' => 'pusher:subscribe', + 'event' => 'websocket.subscribe', 'data' => ['channel' => 'websocket'], ])); diff --git a/src/Statistics/Collectors/MemoryCollector.php b/src/Statistics/Collectors/MemoryCollector.php deleted file mode 100644 index 48e8b10..0000000 --- a/src/Statistics/Collectors/MemoryCollector.php +++ /dev/null @@ -1,191 +0,0 @@ -channelManager = app(ChannelManager::class); - } - - /** - * Handle the incoming websocket message. - * - * @param string|int $appId - * @return void - */ - public function webSocketMessage($appId) - { - $this->findOrMake($appId) - ->webSocketMessage(); - } - - /** - * Handle the incoming API message. - * - * @param string|int $appId - * @return void - */ - public function apiMessage($appId) - { - $this->findOrMake($appId) - ->apiMessage(); - } - - /** - * Handle the new conection. - * - * @param string|int $appId - * @return void - */ - public function connection($appId) - { - $this->findOrMake($appId) - ->connection(); - } - - /** - * Handle disconnections. - * - * @param string|int $appId - * @return void - */ - public function disconnection($appId) - { - $this->findOrMake($appId) - ->disconnection(); - } - - /** - * Save all the stored statistics. - * - * @return void - */ - public function save() - { - $this->getStatistics()->then(function ($statistics) { - foreach ($statistics as $appId => $statistic) { - $statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) { - if (! $isEnabled) { - return; - } - - if ($statistic->shouldHaveTracesRemoved()) { - $this->resetAppTraces($appId); - - return; - } - - $this->createRecord($statistic, $appId); - - $this->channelManager - ->getGlobalConnectionsCount($appId) - ->then(function ($connections) use ($statistic) { - $statistic->reset( - is_null($connections) ? 0 : $connections - ); - }); - }); - } - }); - } - - /** - * Flush the stored statistics. - * - * @return void - */ - public function flush() - { - $this->statistics = []; - } - - /** - * Get the saved statistics. - * - * @return PromiseInterface[array] - */ - public function getStatistics(): PromiseInterface - { - return Helpers::createFulfilledPromise($this->statistics); - } - - /** - * Get the saved statistics for an app. - * - * @param string|int $appId - * @return PromiseInterface[\BlaxSoftware\LaravelWebSockets\Statistics\Statistic|null] - */ - public function getAppStatistics($appId): PromiseInterface - { - return Helpers::createFulfilledPromise( - $this->statistics[$appId] ?? null - ); - } - - /** - * Remove all app traces from the database if no connections have been set - * in the meanwhile since last save. - * - * @param string|int $appId - * @return void - */ - public function resetAppTraces($appId) - { - unset($this->statistics[$appId]); - } - - /** - * Find or create a defined statistic for an app. - * - * @param string|int $appId - * @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic - */ - protected function findOrMake($appId): Statistic - { - if (! isset($this->statistics[$appId])) { - $this->statistics[$appId] = Statistic::new($appId); - } - - return $this->statistics[$appId]; - } - - /** - * Create a new record using the Statistic Store. - * - * @param \BlaxSoftware\LaravelWebSockets\Statistics\Statistic $statistic - * @param mixed $appId - * @return void - */ - public function createRecord(Statistic $statistic, $appId) - { - StatisticsStore::store($statistic->toArray()); - } -} diff --git a/src/Statistics/Collectors/RedisCollector.php b/src/Statistics/Collectors/RedisCollector.php deleted file mode 100644 index 9f0e555..0000000 --- a/src/Statistics/Collectors/RedisCollector.php +++ /dev/null @@ -1,374 +0,0 @@ -redis = Redis::connection( - config('websockets.replication.modes.redis.connection', 'default') - ); - } - - /** - * Handle the incoming websocket message. - * - * @param string|int $appId - * @return void - */ - public function webSocketMessage($appId) - { - $this->ensureAppIsInSet($appId) - ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'websocket_messages_count', 1); - } - - /** - * Handle the incoming API message. - * - * @param string|int $appId - * @return void - */ - public function apiMessage($appId) - { - $this->ensureAppIsInSet($appId) - ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'api_messages_count', 1); - } - - /** - * Handle the new conection. - * - * @param string|int $appId - * @return void - */ - public function connection($appId) - { - // Increment the current connections count by 1. - $this->ensureAppIsInSet($appId) - ->hincrby( - $this->channelManager->getStatsRedisHash($appId, null), - 'current_connections_count', 1 - ) - ->then(function ($currentConnectionsCount) use ($appId) { - // Get the peak connections count from Redis. - $this->channelManager - ->getPublishClient() - ->hget( - $this->channelManager->getStatsRedisHash($appId, null), - 'peak_connections_count' - ) - ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) { - // Extract the greatest number between the current peak connection count - // and the current connection number. - $peakConnectionsCount = is_null($currentPeakConnectionCount) - ? $currentConnectionsCount - : max($currentPeakConnectionCount, $currentConnectionsCount); - - // Then set it to the database. - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'peak_connections_count', $peakConnectionsCount - ); - }); - }); - } - - /** - * Handle disconnections. - * - * @param string|int $appId - * @return void - */ - public function disconnection($appId) - { - // Decrement the current connections count by 1. - $this->ensureAppIsInSet($appId) - ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'current_connections_count', -1) - ->then(function ($currentConnectionsCount) use ($appId) { - // Get the peak connections count from Redis. - $this->channelManager - ->getPublishClient() - ->hget($this->channelManager->getStatsRedisHash($appId, null), 'peak_connections_count') - ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) { - // Extract the greatest number between the current peak connection count - // and the current connection number. - $peakConnectionsCount = is_null($currentPeakConnectionCount) - ? $currentConnectionsCount - : max($currentPeakConnectionCount, $currentConnectionsCount); - - // Then set it to the database. - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'peak_connections_count', $peakConnectionsCount - ); - }); - }); - } - - /** - * Save all the stored statistics. - * - * @return void - */ - public function save() - { - $this->lock()->get(function () { - $this->channelManager - ->getPublishClient() - ->smembers(static::$redisSetName) - ->then(function ($members) { - foreach ($members as $appId) { - $this->channelManager - ->getPublishClient() - ->hgetall($this->channelManager->getStatsRedisHash($appId, null)) - ->then(function ($list) use ($appId) { - if (! $list) { - return; - } - - $statistic = $this->arrayToStatisticInstance( - $appId, Helpers::redisListToArray($list) - ); - - if ($statistic->shouldHaveTracesRemoved()) { - return $this->resetAppTraces($appId); - } - - $this->createRecord($statistic, $appId); - - $this->channelManager - ->getGlobalConnectionsCount($appId) - ->then(function ($currentConnectionsCount) use ($appId) { - $currentConnectionsCount === 0 || is_null($currentConnectionsCount) - ? $this->resetAppTraces($appId) - : $this->resetStatistics($appId, $currentConnectionsCount); - }); - }); - } - }); - }); - } - - /** - * Flush the stored statistics. - * - * @return void - */ - public function flush() - { - $this->getStatistics()->then(function ($statistics) { - foreach ($statistics as $appId => $statistic) { - $this->resetAppTraces($appId); - } - }); - } - - /** - * Get the saved statistics. - * - * @return PromiseInterface[array] - */ - public function getStatistics(): PromiseInterface - { - return $this->channelManager - ->getPublishClient() - ->smembers(static::$redisSetName) - ->then(function ($members) { - $appsWithStatistics = []; - - foreach ($members as $appId) { - $this->channelManager - ->getPublishClient() - ->hgetall($this->channelManager->getStatsRedisHash($appId, null)) - ->then(function ($list) use ($appId, &$appsWithStatistics) { - $appsWithStatistics[$appId] = $this->arrayToStatisticInstance( - $appId, Helpers::redisListToArray($list) - ); - }); - } - - return $appsWithStatistics; - }); - } - - /** - * Get the saved statistics for an app. - * - * @param string|int $appId - * @return PromiseInterface[\BlaxSoftware\LaravelWebSockets\Statistics\Statistic|null] - */ - public function getAppStatistics($appId): PromiseInterface - { - return $this->channelManager - ->getPublishClient() - ->hgetall($this->channelManager->getStatsRedisHash($appId, null)) - ->then(function ($list) use ($appId) { - return $this->arrayToStatisticInstance( - $appId, Helpers::redisListToArray($list) - ); - }); - } - - /** - * Reset the statistics to a specific connection count. - * - * @param string|int $appId - * @param int $currentConnectionCount - * @return void - */ - public function resetStatistics($appId, int $currentConnectionCount) - { - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'current_connections_count', $currentConnectionCount - ); - - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'peak_connections_count', max(0, $currentConnectionCount) - ); - - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'websocket_messages_count', 0 - ); - - $this->channelManager - ->getPublishClient() - ->hset( - $this->channelManager->getStatsRedisHash($appId, null), - 'api_messages_count', 0 - ); - } - - /** - * Remove all app traces from the database if no connections have been set - * in the meanwhile since last save. - * - * @param string|int $appId - * @return void - */ - public function resetAppTraces($appId) - { - parent::resetAppTraces($appId); - - $this->channelManager - ->getPublishClient() - ->hdel( - $this->channelManager->getStatsRedisHash($appId, null), - 'current_connections_count' - ); - - $this->channelManager - ->getPublishClient() - ->hdel( - $this->channelManager->getStatsRedisHash($appId, null), - 'peak_connections_count' - ); - - $this->channelManager - ->getPublishClient() - ->hdel( - $this->channelManager->getStatsRedisHash($appId, null), - 'websocket_messages_count' - ); - - $this->channelManager - ->getPublishClient() - ->hdel( - $this->channelManager->getStatsRedisHash($appId, null), - 'api_messages_count' - ); - - $this->channelManager - ->getPublishClient() - ->srem(static::$redisSetName, $appId); - } - - /** - * Ensure the app id is stored in the Redis database. - * - * @param string|int $appId - * @return \Clue\React\Redis\Client - */ - protected function ensureAppIsInSet($appId) - { - $this->channelManager - ->getPublishClient() - ->sadd(static::$redisSetName, $appId); - - return $this->channelManager->getPublishClient(); - } - - /** - * Get a new RedisLock instance to avoid race conditions. - * - * @return \Illuminate\Cache\CacheLock - */ - protected function lock() - { - return new RedisLock($this->redis, static::$redisLockName, 0); - } - - /** - * Transform a key-value pair to a Statistic instance. - * - * @param string|int $appId - * @param array $stats - * @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic - */ - protected function arrayToStatisticInstance($appId, array $stats) - { - return Statistic::new($appId) - ->setCurrentConnectionsCount($stats['current_connections_count'] ?? 0) - ->setPeakConnectionsCount($stats['peak_connections_count'] ?? 0) - ->setWebSocketMessagesCount($stats['websocket_messages_count'] ?? 0) - ->setApiMessagesCount($stats['api_messages_count'] ?? 0); - } -} diff --git a/src/Statistics/Statistic.php b/src/Statistics/Statistic.php deleted file mode 100644 index 07937b1..0000000 --- a/src/Statistics/Statistic.php +++ /dev/null @@ -1,220 +0,0 @@ -appId = $appId; - } - - /** - * Create a new statistic instance. - * - * @param string|int $appId - * @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic - */ - public static function new($appId) - { - return new static($appId); - } - - /** - * Set the current connections count. - * - * @param int $currentConnectionsCount - * @return $this - */ - public function setCurrentConnectionsCount(int $currentConnectionsCount) - { - $this->currentConnectionsCount = $currentConnectionsCount; - - return $this; - } - - /** - * Set the peak connections count. - * - * @param int $peakConnectionsCount - * @return $this - */ - public function setPeakConnectionsCount(int $peakConnectionsCount) - { - $this->peakConnectionsCount = $peakConnectionsCount; - - return $this; - } - - /** - * Set the peak connections count. - * - * @param int $webSocketMessagesCount - * @return $this - */ - public function setWebSocketMessagesCount(int $webSocketMessagesCount) - { - $this->webSocketMessagesCount = $webSocketMessagesCount; - - return $this; - } - - /** - * Set the peak connections count. - * - * @param int $apiMessagesCount - * @return $this - */ - public function setApiMessagesCount(int $apiMessagesCount) - { - $this->apiMessagesCount = $apiMessagesCount; - - return $this; - } - - /** - * Check if the app has statistics enabled. - * - * @return PromiseInterface - */ - public function isEnabled(): PromiseInterface - { - $deferred = new Deferred(); - - App::findById($this->appId)->then(function ($app) use ($deferred) { - $deferred->resolve($app->statisticsEnabled); - }); - - return $deferred->promise(); - } - - /** - * Handle a new connection increment. - * - * @return void - */ - public function connection() - { - $this->currentConnectionsCount++; - - $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount); - } - - /** - * Handle a disconnection decrement. - * - * @return void - */ - public function disconnection() - { - $this->currentConnectionsCount--; - - $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount); - } - - /** - * Handle a new websocket message. - * - * @return void - */ - public function webSocketMessage() - { - $this->webSocketMessagesCount++; - } - - /** - * Handle a new api message. - * - * @return void - */ - public function apiMessage() - { - $this->apiMessagesCount++; - } - - /** - * Reset all the connections to a specific count. - * - * @param int $currentConnectionsCount - * @return void - */ - public function reset(int $currentConnectionsCount) - { - $this->currentConnectionsCount = $currentConnectionsCount; - $this->peakConnectionsCount = max(0, $currentConnectionsCount); - $this->webSocketMessagesCount = 0; - $this->apiMessagesCount = 0; - } - - /** - * Check if the current statistic entry is empty. This means - * that the statistic entry can be easily deleted if no activity - * occured for a while. - * - * @return bool - */ - public function shouldHaveTracesRemoved(): bool - { - return $this->currentConnectionsCount === 0 && $this->peakConnectionsCount === 0; - } - - /** - * Transform the statistic to array. - * - * @return array - */ - public function toArray() - { - return [ - 'app_id' => $this->appId, - 'peak_connections_count' => $this->peakConnectionsCount, - 'websocket_messages_count' => $this->webSocketMessagesCount, - 'api_messages_count' => $this->apiMessagesCount, - ]; - } -} diff --git a/src/Statistics/Stores/DatabaseStore.php b/src/Statistics/Stores/DatabaseStore.php deleted file mode 100644 index a7d6b1e..0000000 --- a/src/Statistics/Stores/DatabaseStore.php +++ /dev/null @@ -1,140 +0,0 @@ -toDateTimeString()) - ->when(! is_null($appId), function ($query) use ($appId) { - return $query->whereAppId($appId); - }) - ->delete(); - } - - /** - * Get the query result as eloquent collection. - * - * @param callable $processQuery - * @return \Illuminate\Support\Collection - */ - public function getRawRecords(callable $processQuery = null) - { - return static::$model::query() - ->when(! is_null($processQuery), function ($query) use ($processQuery) { - return call_user_func($processQuery, $query); - }, function ($query) { - return $query->latest()->limit(120); - })->get(); - } - - /** - * Get the results for a specific query. - * - * @param callable $processQuery - * @param callable $processCollection - * @return array - */ - public function getRecords(callable $processQuery = null, callable $processCollection = null): array - { - return $this->getRawRecords($processQuery) - ->when(! is_null($processCollection), function ($collection) use ($processCollection) { - return call_user_func($processCollection, $collection); - }) - ->map(function (Model $statistic) { - return $this->statisticToArray($statistic); - }) - ->toArray(); - } - - /** - * Get the results for a specific query into a - * format that is easily to read for graphs. - * - * @param callable $processQuery - * @param callable $processCollection - * @return array - */ - public function getForGraph(callable $processQuery = null, callable $processCollection = null): array - { - $statistics = collect( - $this->getRecords($processQuery, $processCollection) - ); - - return $this->statisticsToGraph($statistics); - } - - /** - * Turn the statistic model to an array. - * - * @param \Illuminate\Database\Eloquent\Model $statistic - * @return array - */ - protected function statisticToArray(Model $statistic): array - { - return [ - 'timestamp' => (string) $statistic->created_at, - 'peak_connections_count' => $statistic->peak_connections_count, - 'websocket_messages_count' => $statistic->websocket_messages_count, - 'api_messages_count' => $statistic->api_messages_count, - ]; - } - - /** - * Turn the statistics collection to an array used for graph. - * - * @param \Illuminate\Support\Collection $statistics - * @return array - */ - protected function statisticsToGraph(Collection $statistics): array - { - return [ - 'peak_connections' => [ - 'x' => $statistics->pluck('timestamp')->toArray(), - 'y' => $statistics->pluck('peak_connections_count')->toArray(), - ], - 'websocket_messages_count' => [ - 'x' => $statistics->pluck('timestamp')->toArray(), - 'y' => $statistics->pluck('websocket_messages_count')->toArray(), - ], - 'api_messages_count' => [ - 'x' => $statistics->pluck('timestamp')->toArray(), - 'y' => $statistics->pluck('api_messages_count')->toArray(), - ], - ]; - } -} diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php index 04acbc0..9cdc736 100644 --- a/src/WebSocketsServiceProvider.php +++ b/src/WebSocketsServiceProvider.php @@ -2,37 +2,14 @@ namespace BlaxSoftware\LaravelWebSockets; -use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector; -use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowApps; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\StoreApp; -use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard; -use BlaxSoftware\LaravelWebSockets\Queue\AsyncRedisConnector; use BlaxSoftware\LaravelWebSockets\Server\Router; -use Clue\React\SQLite\DatabaseInterface; -use Clue\React\SQLite\Factory as SQLiteFactory; -use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use React\EventLoop\Factory; use React\EventLoop\LoopInterface; -use React\MySQL\ConnectionInterface; -use React\MySQL\Factory as MySQLFactory; -use SplFileInfo; -use Symfony\Component\Finder\Finder; class WebSocketsServiceProvider extends ServiceProvider { - /** - * Boot the service provider. - * - * @return void - */ public function boot() { $this->publishes([ @@ -49,46 +26,19 @@ class WebSocketsServiceProvider extends ServiceProvider ]); $this->registerDefaultWebsocketChannels(); - $this->registerEventLoop(); - - $this->registerSQLiteDatabase(); - - $this->registerMySqlDatabase(); - - $this->registerAsyncRedisQueueDriver(); - $this->registerWebSocketHandler(); - $this->registerRouter(); - $this->registerManagers(); - - $this->registerStatistics(); - - $this->registerDashboard(); - $this->registerBroadcastAuthRoute(); - $this->registerCommands(); } - /** - * Register the service provider. - * - * @return void - */ public function register() { // } - - /** - * Registers Broadcast::channel('websocket', fn () => true); in channels.php - * - * @return void - */ protected function registerDefaultWebsocketChannels() { \Illuminate\Support\Facades\Broadcast::channel('websocket', fn() => true); @@ -102,117 +52,15 @@ class WebSocketsServiceProvider extends ServiceProvider }); } - /** - * Register the async, non-blocking Redis queue driver. - * - * @return void - */ - protected function registerAsyncRedisQueueDriver() - { - Queue::extend('async-redis', function () { - return new AsyncRedisConnector($this->app['redis']); - }); - } - - protected function registerSQLiteDatabase() - { - $this->app->singleton(DatabaseInterface::class, function () { - $factory = new SQLiteFactory($this->app->make(LoopInterface::class)); - - $database = $factory->openLazy( - config('websockets.managers.sqlite.database', ':memory:') - ); - - $migrations = (new Finder()) - ->files() - ->ignoreDotFiles(true) - ->in(__DIR__ . '/../database/migrations/sqlite') - ->name('*.sql'); - - /** @var SplFileInfo $migration */ - foreach ($migrations as $migration) { - $database->exec($migration->getContents()); - } - - return $database; - }); - } - - protected function registerMySqlDatabase() - { - $this->app->singleton(ConnectionInterface::class, function () { - $factory = new MySQLFactory($this->app->make(LoopInterface::class)); - - $connectionKey = 'database.connections.' . config('websockets.managers.mysql.connection'); - - $auth = trim(config($connectionKey . '.username') . ':' . config($connectionKey . '.password'), ':'); - $connection = trim(config($connectionKey . '.host') . ':' . config($connectionKey . '.port'), ':'); - $database = config($connectionKey . '.database'); - - $database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@')); - - return $database; - }); - } - - /** - * Register the statistics-related contracts. - * - * @return void - */ - protected function registerStatistics() - { - $this->app->singleton(StatisticsStore::class, function ($app) { - $config = $app['config']['websockets']; - $class = $config['statistics']['store']; - - return new $class; - }); - - $this->app->singleton(StatisticsCollector::class, function ($app) { - $config = $app['config']['websockets']; - $replicationMode = $config['replication']['mode'] ?? 'local'; - - $class = $config['replication']['modes'][$replicationMode]['collector']; - - return new $class; - }); - } - - /** - * Register the dashboard components. - * - * @return void - */ - protected function registerDashboard() - { - $this->loadViewsFrom(__DIR__ . '/../resources/views/', 'websockets'); - - $this->registerDashboardRoutes(); - $this->registerDashboardGate(); - } - - /** - * Register the package commands. - * - * @return void - */ protected function registerCommands() { $this->commands([ Console\Commands\StartServer::class, Console\Commands\RestartServer::class, Console\Commands\SteerServer::class, - Console\Commands\CleanStatistics::class, - Console\Commands\FlushCollectedStatistics::class, ]); } - /** - * Register the routing. - * - * @return void - */ protected function registerRouter() { $this->app->singleton('websockets.router', function () { @@ -220,11 +68,6 @@ class WebSocketsServiceProvider extends ServiceProvider }); } - /** - * Register the managers for the app. - * - * @return void - */ protected function registerManagers() { $this->app->singleton(Contracts\AppManager::class, function ($app) { @@ -234,48 +77,8 @@ class WebSocketsServiceProvider extends ServiceProvider }); } - /** - * Register the dashboard routes. - * - * @return void - */ - protected function registerDashboardRoutes() - { - Route::group([ - 'domain' => config('websockets.dashboard.domain'), - 'prefix' => config('websockets.dashboard.path'), - 'as' => 'laravel-websockets.', - 'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]), - ], function () { - Route::get('/', ShowDashboard::class)->name('dashboard'); - Route::get('/apps', ShowApps::class)->name('apps'); - Route::post('/apps', StoreApp::class)->name('apps.store'); - Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics'); - Route::post('/auth', AuthenticateDashboard::class)->name('auth'); - Route::post('/event', SendMessage::class)->name('event'); - }); - } - - /** - * Register the dashboard gate. - * - * @return void - */ - protected function registerDashboardGate() - { - Gate::define('viewWebSocketsDashboard', function ($user = null) { - return $this->app->environment('local'); - }); - } - - /** - * Register the default broadcasting routes if not already defined. - * - * @return void - */ protected function registerBroadcastAuthRoute() { - // If broadcasting/auth route is not defined, load the default routes if (! Route::has('broadcasting/auth')) { $this->loadRoutesFrom(__DIR__ . '/../routes/api.php'); } diff --git a/src/Websocket/ChannelManager.php b/src/Websocket/ChannelManager.php index 02019e9..174abad 100644 --- a/src/Websocket/ChannelManager.php +++ b/src/Websocket/ChannelManager.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace BlaxSoftware\LaravelWebSockets\Websocket; use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager; -use BlaxSoftware\LaravelWebSockets\DashboardLogger; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\MockableConnection; use Carbon\Carbon; @@ -397,14 +396,6 @@ class ChannelManager extends LocalChannelManager $socketId = $payload->socketId ?? null; $serverId = $payload->serverId ?? null; - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [ - 'fromServerId' => $serverId, - 'fromSocketId' => $socketId, - 'receiverServerId' => $this->getServerId(), - 'channel' => $channel, - 'payload' => $payload, - ]); - unset($payload->socketId, $payload->serverId, $payload->appId); $channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId); @@ -604,11 +595,6 @@ class ChannelManager extends LocalChannelManager { $topic = $this->getRedisTopicName($appId, $channel); - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [ - 'serverId' => $this->getServerId(), - 'pubsubTopic' => $topic, - ]); - return $this->subscribeClient->subscribe($topic); } @@ -621,11 +607,6 @@ class ChannelManager extends LocalChannelManager { $topic = $this->getRedisTopicName($appId, $channel); - DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [ - 'serverId' => $this->getServerId(), - 'pubsubTopic' => $topic, - ]); - return $this->subscribeClient->unsubscribe($topic); } diff --git a/src/Websocket/Controller.php b/src/Websocket/Controller.php index 5b6078a..98f667a 100644 --- a/src/Websocket/Controller.php +++ b/src/Websocket/Controller.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace BlaxSoftware\LaravelWebSockets\Websocket; use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager; -use BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager; use BlaxSoftware\LaravelWebSockets\Channels\Channel; use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel; use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel; @@ -22,7 +21,7 @@ class Controller protected ConnectionInterface $connection, protected PrivateChannel|Channel|PresenceChannel|null $channel, protected string $event, - protected LocalChannelManager|RedisChannelManager $channelManager + protected LocalChannelManager $channelManager ) { $this->isMockConnection = $connection instanceof MockConnectionSocketPair; } @@ -57,7 +56,7 @@ class Controller ConnectionInterface $connection, PrivateChannel|Channel|PresenceChannel $channel, array $message, - LocalChannelManager|RedisChannelManager $channelManager + LocalChannelManager $channelManager ) { $event = self::get_event($message); if (count($event) !== 2) { @@ -378,16 +377,8 @@ class Controller private static function get_event($message) { - $event = explode('.', $message['event']); - - if (strpos($event[0], 'pusher.') > -1) { - $event = explode('.', $event[0]); - } - - if (strpos($event[0], 'pusher:') > -1) { - $event = explode(':', $event[0]); - } - - return $event; + // Split on '.' delimiter to get [controller, method, ...] + // e.g. "admin.dashboard[abc]" → ["admin", "dashboard[abc]"] + return explode('.', $message['event']); } } diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index aad9ae8..326dbf4 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -46,7 +46,7 @@ class Handler implements MessageComponentInterface * Pre-encoded static JSON responses for performance * Encoding once at startup is faster than encoding every time */ - private static string $PONG_RESPONSE = '{"event":"pusher.pong"}'; + private static string $PONG_RESPONSE = '{"event":"websocket.pong"}'; /** * GC collection counter - only collect every N pings @@ -141,8 +141,8 @@ class Handler implements MessageComponentInterface $event = $data['event'] ?? ''; - // Direct string comparison (faster than strtolower + comparison) - if ($event !== 'pusher:ping' && $event !== 'pusher.ping') { + // Match any prefix with . or : delimiter followed by 'ping' + if (!self::isProtocolAction($event, 'ping')) { return false; } @@ -210,8 +210,8 @@ class Handler implements MessageComponentInterface // Decode message (we already have payload string) $messageArray = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); - // Handle pusher protocol messages (subscribe, unsubscribe, etc.) - $this->handlePusherProtocolMessage($message, $connection, $messageArray); + // Handle protocol messages (client-* broadcasts) + $this->handleProtocolMessage($message, $connection, $messageArray); $channel = $this->handleChannelSubscriptions($messageArray, $connection); @@ -226,7 +226,7 @@ class Handler implements MessageComponentInterface Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload); } - if ($this->handlePusherEvent($messageArray, $connection)) { + if ($this->handleProtocolEvent($messageArray, $connection)) { return; } @@ -237,20 +237,14 @@ class Handler implements MessageComponentInterface * Handle pusher protocol messages (formerly in PusherMessageFactory) * Inlined for performance - avoids object creation */ - private function handlePusherProtocolMessage( + private function handleProtocolMessage( MessageInterface $message, ConnectionInterface $connection, array $messageArray ): void { $event = $messageArray['event'] ?? ''; - // Fast check - most messages don't start with 'pusher' or 'client-' - $firstChar = $event[0] ?? ''; - if ($firstChar !== 'p' && $firstChar !== 'c') { - return; - } - - // Check for client- messages + // Check for client- broadcast messages if (strpos($event, 'client-') === 0) { if (!$connection->app->clientMessagesEnabled) { return; @@ -269,11 +263,7 @@ class Handler implements MessageComponentInterface $connection->app->id ); } - return; } - - // Check for pusher: or pusher. messages (subscribe/unsubscribe handled elsewhere) - // This is handled by handleChannelSubscriptions for subscribe/unsubscribe } public function onOpen(ConnectionInterface $connection): void @@ -354,7 +344,7 @@ class Handler implements MessageComponentInterface protected function shouldRejectMessage(?Channel $channel, ConnectionInterface $connection, array $message): bool { $event = $message['event'] ?? ''; - $isUnsubscribe = $event === 'pusher:unsubscribe' || $event === 'pusher.unsubscribe'; + $isUnsubscribe = self::isProtocolAction($event, 'unsubscribe'); if (!$channel?->hasConnection($connection) && !$isUnsubscribe) { // The connection may have been removed from Channel::$connections by @@ -394,14 +384,21 @@ class Handler implements MessageComponentInterface return false; } - protected function handlePusherEvent(array $message, ConnectionInterface $connection): bool + /** + * Handle protocol-level events (subscribe, unsubscribe, ping, etc.). + * These are events with a known action after the delimiter (e.g. websocket.subscribe). + * Sends an immediate :response acknowledgement and short-circuits forkWithSocketPair. + */ + protected function handleProtocolEvent(array $message, ConnectionInterface $connection): bool { - if (!str_contains($message['event'], 'pusher')) { + $event = $message['event'] ?? ''; + + if (!self::isProtocolAction($event, 'subscribe') && !self::isProtocolAction($event, 'unsubscribe')) { return false; } $connection->send(json_encode([ - 'event' => $message['event'] . ':response', + 'event' => $event . ':response', 'data' => [ 'message' => 'Success', ], @@ -409,6 +406,22 @@ class Handler implements MessageComponentInterface return true; } + /** + * Check if an event name ends with a known protocol action. + * Matches any prefix with either . or : as delimiter. + * + * Examples that match isProtocolAction($event, 'subscribe'): + * websocket.subscribe, pusher:subscribe, my.prefix.subscribe + * + * Examples that do NOT match: + * admin.unsubscribeUserStatus (does not end with .subscribe or :subscribe) + */ + protected static function isProtocolAction(string $event, string $action): bool + { + return str_ends_with($event, '.' . $action) + || str_ends_with($event, ':' . $action); + } + /** * Check if hot reload mode is enabled */ @@ -1070,7 +1083,7 @@ class Handler implements MessageComponentInterface protected function establishConnection(ConnectionInterface $connection) { $connection->send(json_encode([ - 'event' => 'pusher.connection_established', + 'event' => 'websocket.connection_established', 'data' => json_encode([ 'socket_id' => $connection->socketId, 'activity_timeout' => 30, @@ -1108,14 +1121,14 @@ class Handler implements MessageComponentInterface return null; } - $eventLower = strtolower($message['event']); + $event = $message['event']; - if ($eventLower === 'pusher.subscribe' || $eventLower === 'pusher:subscribe') { - $this->handleSubscription($channel, $channel_name, $connection, $message); + if (self::isProtocolAction($event, 'unsubscribe')) { + $this->handleUnsubscription($channel, $channel_name, $connection); } - if (str_contains($message['event'], '.unsubscribe')) { - $this->handleUnsubscription($channel, $channel_name, $connection); + if (self::isProtocolAction($event, 'subscribe')) { + $this->handleSubscription($channel, $channel_name, $connection, $message); } return $channel; @@ -1151,9 +1164,9 @@ class Handler implements MessageComponentInterface } try { - $channel->subscribe($connection, (object) $message); + $channel->subscribe($connection, (object) ($message['data'] ?? [])); } catch (\Throwable $e) { - // Silently handle subscription errors + // Silently handle subscription errors (e.g. invalid signatures) } } diff --git a/tests/Apps/MysqlAppManagerTest.php b/tests/Apps/MysqlAppManagerTest.php deleted file mode 100644 index cc67f40..0000000 --- a/tests/Apps/MysqlAppManagerTest.php +++ /dev/null @@ -1,117 +0,0 @@ -set('websockets.managers.app', MysqlAppManager::class); - $app['config']->set('database.connections.mysql.database', 'websockets_test'); - $app['config']->set('database.connections.mysql.username', 'root'); - $app['config']->set('database.connections.mysql.password', 'password'); - - $app['config']->set('websockets.managers.mysql.table', 'websockets_apps'); - $app['config']->set('websockets.managers.mysql.connection', 'mysql'); - $app['config']->set('database.connections.default', 'mysql'); - } - - public function setUp(): void - { - parent::setUp(); - - // Skip if MySQL is not available - try { - \DB::connection('mysql')->getPdo(); - } catch (\Exception $e) { - $this->markTestSkipped('MySQL connection is not available: ' . $e->getMessage()); - } - - $this->artisan('migrate:fresh', [ - '--database' => 'mysql', - '--realpath' => true, - '--path' => __DIR__ . '/../../database/migrations/', - ]); - - $this->apps = app()->make(AppManager::class); - } - - public function test_can_return_all_apps() - { - $apps = $this->await($this->apps->all()); - $this->assertCount(0, $apps); - - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'test', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $apps = $this->await($this->apps->all()); - $this->assertCount(1, $apps); - } - - public function test_can_find_apps_by_id() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'test', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findById(1)); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('test', $app->key); - } - - public function test_can_find_apps_by_key() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'key', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findByKey('key')); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('key', $app->key); - } - - public function test_can_find_apps_by_secret() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'key', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findBySecret('secret')); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('key', $app->key); - } -} diff --git a/tests/Apps/SqliteAppManagerTest.php b/tests/Apps/SqliteAppManagerTest.php deleted file mode 100644 index d440d4f..0000000 --- a/tests/Apps/SqliteAppManagerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -set('websockets.managers.app', SQLiteAppManager::class); - } - - public function setUp(): void - { - parent::setUp(); - - $this->apps = app()->make(AppManager::class); - - // Test if SQLite async database is working - try { - $this->await($this->apps->all(), null, 2); - } catch (\Exception $e) { - $this->markTestSkipped('SQLite async database is not available: ' . $e->getMessage()); - } - } - - public function test_can_return_all_apps() - { - $apps = $this->await($this->apps->all()); - $this->assertCount(0, $apps); - - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'test', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $apps = $this->await($this->apps->all()); - $this->assertCount(1, $apps); - } - - public function test_can_find_apps_by_id() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'test', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findById(1)); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('test', $app->key); - } - - public function test_can_find_apps_by_key() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'key', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findByKey('key')); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('key', $app->key); - } - - public function test_can_find_apps_by_secret() - { - $this->await($this->apps->createApp([ - 'id' => 1, - 'key' => 'key', - 'secret' => 'secret', - 'name' => 'Test', - 'enable_client_messages' => true, - 'enable_statistics' => false, - ])); - - $app = $this->await($this->apps->findBySecret('secret')); - - $this->assertInstanceOf(App::class, $app); - $this->assertSame('key', $app->key); - } -} diff --git a/tests/Cache/IpcCacheTest.php b/tests/Cache/IpcCacheTest.php index 7ee3000..247064d 100644 --- a/tests/Cache/IpcCacheTest.php +++ b/tests/Cache/IpcCacheTest.php @@ -94,7 +94,7 @@ class IpcCacheTest extends TestCase public function test_it_can_store_complex_data() { $complexData = [ - 'event' => 'pusher:connection_established', + 'event' => 'websocket.connection_established', 'data' => [ 'socket_id' => '123.456', 'activity_timeout' => 120, diff --git a/tests/Commands/StatisticsCleanTest.php b/tests/Commands/StatisticsCleanTest.php deleted file mode 100644 index 68aeed7..0000000 --- a/tests/Commands/StatisticsCleanTest.php +++ /dev/null @@ -1,45 +0,0 @@ -newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel'], 'TestKey2'); - - $this->statisticsCollector->save(); - - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - - foreach ($this->statisticsStore->getRawRecords() as $record) { - $record->update(['created_at' => now()->subDays(10)]); - } - - $this->artisan('websockets:clean', [ - 'appId' => '12345', - '--days' => 1, - ]); - - $this->assertCount(1, $records = $this->statisticsStore->getRecords()); - } - - public function test_clean_statistics_older_than_given_days() - { - $rick = $this->newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel'], 'TestKey2'); - - $this->statisticsCollector->save(); - - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - - foreach ($this->statisticsStore->getRawRecords() as $record) { - $record->update(['created_at' => now()->subDays(10)]); - } - - $this->artisan('websockets:clean', ['--days' => 1]); - - $this->assertCount(0, $records = $this->statisticsStore->getRecords()); - } -} diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index f1b2d79..72adcfc 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -16,23 +16,7 @@ class ConnectionTest extends TestCase $this->startServer(); $response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey')); - $this->assertSame('{"event":"pusher.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response); - } - - /** - * @group integration - * @group requires-server - */ - public function test_unconnected_app_cannot_store_statistics() - { - $this->skipIfServerUnavailable(); - $this->startServer(); - - $response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey')); - $this->assertSame('{"event":"pusher.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response); - - $count = $this->await($this->statisticsCollector->getStatistics()); - $this->assertCount(0, $count); + $this->assertSame('{"event":"websocket.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response); } /** @@ -45,7 +29,7 @@ class ConnectionTest extends TestCase $this->startServer(); $response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin')); - $this->assertSame('{"event":"pusher.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response); + $this->assertSame('{"event":"websocket.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response); } /** @@ -58,16 +42,16 @@ class ConnectionTest extends TestCase $this->startServer(); $response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro'])); - $this->assertSame('{"event":"pusher.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response); + $this->assertSame('{"event":"websocket.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response); } public function test_origin_validation_should_pass_for_the_right_origin() { $connection = $this->newConnection('TestOrigin', ['Origin' => 'https://test.origin.com']); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); - $connection->assertSentEvent('pusher.connection_established'); + $connection->assertSentEvent('websocket.connection_established'); } public function test_close_connection() @@ -86,7 +70,7 @@ class ConnectionTest extends TestCase $this->assertEquals(1, $total); }); - $this->pusherServer->onClose($connection); + $this->wsHandler->onClose($connection); $this->channelManager ->getGlobalConnectionsCount('1234') @@ -105,9 +89,9 @@ class ConnectionTest extends TestCase { $connection = $this->newActiveConnection(['public-channel']); - $this->pusherServer->onError($connection, new UnknownAppKey('NonWorkingKey')); + $this->wsHandler->onError($connection, new UnknownAppKey('NonWorkingKey')); - $connection->assertSentEvent('pusher.error', [ + $connection->assertSentEvent('websocket.error', [ 'data' => [ 'message' => 'Could not find app key `NonWorkingKey`.', 'code' => 4001, @@ -125,15 +109,15 @@ class ConnectionTest extends TestCase $failedConnection = $this->newActiveConnection(['test-channel']); $failedConnection - ->assertSentEvent('pusher.error', ['data' => ['message' => 'Over capacity', 'code' => 4100]]) + ->assertSentEvent('websocket.error', ['data' => ['message' => 'Over capacity', 'code' => 4100]]) ->assertClosed(); } public function test_close_all_new_connections_after_stating_the_server_does_not_accept_new_connections() { $this->newActiveConnection(['test-channel']) - ->assertSentEvent('pusher.connection_established') - ->assertSentEvent('pusher_internal:subscription_succeeded'); + ->assertSentEvent('websocket.connection_established') + ->assertSentEvent('websocket_internal.subscription_succeeded'); $this->channelManager->declineNewConnections(); diff --git a/tests/Dashboard/AppsTest.php b/tests/Dashboard/AppsTest.php deleted file mode 100644 index f450a5d..0000000 --- a/tests/Dashboard/AppsTest.php +++ /dev/null @@ -1,38 +0,0 @@ -set('websockets.managers.app', SQLiteAppManager::class); - } - - public function test_can_list_all_apps() - { - $this->actingAs(factory(User::class)->create()) - ->get(route('laravel-websockets.apps')) - ->assertViewHas('apps', []); - } - - public function test_can_create_app() - { - $this->actingAs(factory(User::class)->create()) - ->post(route('laravel-websockets.apps.store', [ - 'name' => 'New App', - ])); - - $this->actingAs(factory(User::class)->create()) - ->get(route('laravel-websockets.apps')) - ->assertViewHas('apps', function ($apps) { - return count($apps) === 1 && $apps[0]['name'] === 'New App'; - }); - } -} diff --git a/tests/Dashboard/AuthTest.php b/tests/Dashboard/AuthTest.php deleted file mode 100644 index 4b8054e..0000000 --- a/tests/Dashboard/AuthTest.php +++ /dev/null @@ -1,89 +0,0 @@ -newActiveConnection(['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', - ]); - } - - public function test_can_authenticate_dashboard_over_private_channel() - { - $connection = $this->newConnection(); - - $this->pusherServer->onOpen($connection); - - $message = new SignedMessage([ - 'event' => 'pusher.subscribe', - 'data' => [ - 'channel' => 'private-channel', - ], - ], $connection, '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', - ]); - } - - public function test_can_authenticate_dashboard_over_presence_channel() - { - $connection = $this->newConnection(); - - $this->pusherServer->onOpen($connection); - - $user = json_encode([ - 'user_id' => 1, - 'user_info' => [ - 'name' => 'Rick', - ], - ]); - - $message = new SignedMessage([ - 'event' => 'pusher.subscribe', - 'data' => [ - 'channel' => 'presence-channel', - 'channel_data' => $user, - ], - ], $connection, 'presence-channel', $user); - - $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 deleted file mode 100644 index aa06859..0000000 --- a/tests/Dashboard/DashboardTest.php +++ /dev/null @@ -1,22 +0,0 @@ -get(route('laravel-websockets.dashboard')) - ->assertResponseStatus(403); - } - - public function test_can_see_dashboard() - { - $this->actingAs(factory(User::class)->create()) - ->get(route('laravel-websockets.dashboard')) - ->assertResponseOk(); - } -} diff --git a/tests/Dashboard/SendMessageTest.php b/tests/Dashboard/SendMessageTest.php deleted file mode 100644 index a3bf2b9..0000000 --- a/tests/Dashboard/SendMessageTest.php +++ /dev/null @@ -1,43 +0,0 @@ -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, - ]); - - $this->markTestIncomplete( - 'Broadcasting is not possible to be tested without receiving a Pusher error.' - ); - } - - public function test_cant_send_message_for_invalid_app() - { - $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 deleted file mode 100644 index ab63e28..0000000 --- a/tests/Dashboard/StatisticsTest.php +++ /dev/null @@ -1,42 +0,0 @@ -newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel']); - - $this->statisticsCollector->save(); - - $response = $this->actingAs(factory(User::class)->create()) - ->json('GET', route('laravel-websockets.statistics', ['appId' => '1234'])) - ->assertResponseOk() - ->seeJsonStructure([ - 'peak_connections' => ['x', 'y'], - 'websocket_messages_count' => ['x', 'y'], - 'api_messages_count' => ['x', 'y'], - ]); - } - - public function test_cant_get_statistics_for_invalid_app_id() - { - $rick = $this->newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel']); - - $this->statisticsCollector->save(); - - $this->actingAs(factory(User::class)->create()) - ->json('GET', route('laravel-websockets.statistics', ['appId' => 'not_found'])) - ->seeJson([ - 'peak_connections' => ['x' => [], 'y' => []], - 'websocket_messages_count' => ['x' => [], 'y' => []], - 'api_messages_count' => ['x' => [], 'y' => []], - ]); - } -} diff --git a/tests/FetchChannelTest.php b/tests/FetchChannelTest.php deleted file mode 100644 index 3dec800..0000000 --- a/tests/FetchChannelTest.php +++ /dev/null @@ -1,109 +0,0 @@ -startServer(); - - $requestPath = '/apps/1234/channels/my-channel'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'InvalidSecret', 'GET', $requestPath - )); - - $request = new Request('GET', "{$requestPath}?{$queryString}"); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(401, $response->getStatusCode()); - $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents()); - } - - public function test_it_returns_the_channel_information() - { - $this->newActiveConnection(['my-channel']); - $this->newActiveConnection(['my-channel']); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channel/my-channel'; - $routeParams = [ - 'appId' => '1234', - 'channelName' => 'my-channel', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannel::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([ - 'occupied' => true, - 'subscription_count' => 2, - ], json_decode($response->getContent(), true)); - } - - public function test_it_returns_presence_channel_information() - { - $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - $this->newPresenceConnection('presence-channel', ['user_id' => 2]); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channel/my-channel'; - - $routeParams = [ - 'appId' => '1234', - 'channelName' => 'presence-channel', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannel::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([ - 'occupied' => true, - 'subscription_count' => 2, - 'user_count' => 2, - ], json_decode($response->getContent(), true)); - } - - public function test_it_returns_404_for_invalid_channels() - { - $this->skipOnRedisReplication(); - - $this->startServer(); - - $this->newActiveConnection(['my-channel']); - - $requestPath = '/apps/1234/channels/invalid-channel'; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(404, $response->getStatusCode()); - $this->assertSame('{"error":"Unknown channel `invalid-channel`."}', $response->getBody()->getContents()); - } -} diff --git a/tests/FetchChannelsTest.php b/tests/FetchChannelsTest.php deleted file mode 100644 index 9573f63..0000000 --- a/tests/FetchChannelsTest.php +++ /dev/null @@ -1,178 +0,0 @@ -startServer(); - - $requestPath = '/apps/1234/channels'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'InvalidSecret', 'GET', $requestPath - )); - - $request = new Request('GET', "{$requestPath}?{$queryString}"); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(401, $response->getStatusCode()); - $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents()); - } - - public function test_it_returns_the_channel_information() - { - $this->newPresenceConnection('presence-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channels'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'GET', $requestPath - )); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannels::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([ - 'channels' => [ - 'presence-channel' => [], - ], - ], json_decode($response->getContent(), true)); - } - - public function test_it_returns_the_channel_information_for_prefix() - { - $this->newPresenceConnection('presence-global.1'); - $this->newPresenceConnection('presence-global.1'); - $this->newPresenceConnection('presence-global.2'); - $this->newPresenceConnection('presence-notglobal.2'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channels'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [ - 'filter_by_prefix' => 'presence-global', - ])); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannels::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([ - 'channels' => [ - 'presence-global.1' => [], - 'presence-global.2' => [], - ], - ], json_decode($response->getContent(), true)); - } - - public function test_it_returns_the_channel_information_for_prefix_with_user_count() - { - $this->newPresenceConnection('presence-global.1', ['user_id' => 1]); - $this->newPresenceConnection('presence-global.1', ['user_id' => 2]); - $this->newPresenceConnection('presence-global.2', ['user_id' => 3]); - $this->newPresenceConnection('presence-notglobal.2', ['user_id' => 4]); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channels'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [ - 'filter_by_prefix' => 'presence-global', - 'info' => 'user_count', - ])); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannels::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([ - 'channels' => [ - 'presence-global.1' => [ - 'user_count' => 2, - ], - 'presence-global.2' => [ - 'user_count' => 1, - ], - ], - ], json_decode($response->getContent(), true)); - } - - public function test_can_not_get_non_presence_channel_user_count() - { - $this->startServer(); - - $requestPath = '/apps/1234/channels'; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [ - 'info' => 'user_count', - ])); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('{"error":"Request must be limited to presence channels in order to fetch user_count"}', $response->getBody()->getContents()); - } - - public function test_it_returns_empty_object_for_no_channels_found() - { - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channels'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchChannels::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame('{"channels":{}}', $response->getContent()); - } -} diff --git a/tests/FetchUsersTest.php b/tests/FetchUsersTest.php deleted file mode 100644 index c9580cd..0000000 --- a/tests/FetchUsersTest.php +++ /dev/null @@ -1,117 +0,0 @@ -startServer(); - - $requestPath = '/apps/1234/channels/my-channel/users'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'InvalidSecret', 'GET', $requestPath - )); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(401, $response->getStatusCode()); - $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents()); - } - - public function test_it_only_returns_data_for_presence_channels() - { - $this->startServer(); - - $requestPath = '/apps/1234/channels/my-channel/users'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'GET', $requestPath - )); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('{"error":"Invalid presence channel `my-channel`"}', $response->getBody()->getContents()); - } - - public function test_it_returns_400_for_invalid_channels() - { - $this->startServer(); - - $requestPath = '/apps/1234/channels/invalid-channel/users'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'GET', $requestPath - )); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('{"error":"Invalid presence channel `invalid-channel`"}', $response->getBody()->getContents()); - } - - public function test_it_returns_connected_user_information() - { - $this->newPresenceConnection('presence-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channel/presence-channel/users'; - - $routeParams = [ - 'appId' => '1234', - 'channelName' => 'presence-channel', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchUsers::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)); - } - - public function test_multiple_clients_with_same_id_gets_counted_once() - { - $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/channel/presence-channel/users'; - - $routeParams = [ - 'appId' => '1234', - 'channelName' => 'presence-channel', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath)); - - $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(FetchUsers::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)); - } -} diff --git a/tests/HandlerLifecycleTest.php b/tests/HandlerLifecycleTest.php new file mode 100644 index 0000000..cc7691b --- /dev/null +++ b/tests/HandlerLifecycleTest.php @@ -0,0 +1,472 @@ +newConnection(); + $this->wsHandler->onOpen($connection); + + $connection->assertSentEvent('websocket.connection_established'); + } + + public function test_connection_established_contains_socket_id() + { + $connection = $this->newConnection(); + $this->wsHandler->onOpen($connection); + + $event = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established'); + $data = json_decode($event['data'], true); + + $this->assertNotNull($data['socket_id']); + $this->assertStringContainsString('.', $data['socket_id']); + } + + public function test_connection_established_contains_activity_timeout() + { + $connection = $this->newConnection(); + $this->wsHandler->onOpen($connection); + + $event = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established'); + $data = json_decode($event['data'], true); + + $this->assertEquals(30, $data['activity_timeout']); + } + + public function test_multiple_connections_get_unique_socket_ids() + { + $conn1 = $this->newConnection(); + $conn2 = $this->newConnection(); + $conn3 = $this->newConnection(); + + $this->wsHandler->onOpen($conn1); + $this->wsHandler->onOpen($conn2); + $this->wsHandler->onOpen($conn3); + + $socket1 = json_decode((collect($conn1->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null; + $socket2 = json_decode((collect($conn2->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null; + $socket3 = json_decode((collect($conn3->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null; + + $this->assertNotNull($socket1); + $this->assertNotNull($socket2); + $this->assertNotNull($socket3); + $this->assertNotEquals($socket1, $socket2); + $this->assertNotEquals($socket2, $socket3); + $this->assertNotEquals($socket1, $socket3); + } + + // ========================================================================= + // Public channel subscribe/unsubscribe + // ========================================================================= + + public function test_subscribe_to_public_channel() + { + $connection = $this->newActiveConnection(['test-channel']); + + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); + + $channel = $this->channelManager->find('1234', 'test-channel'); + $this->assertTrue($channel->hasConnection($connection)); + } + + public function test_subscribe_to_multiple_public_channels() + { + $connection = $this->newActiveConnection(['channel-a', 'channel-b', 'channel-c']); + + $channelA = $this->channelManager->find('1234', 'channel-a'); + $channelB = $this->channelManager->find('1234', 'channel-b'); + $channelC = $this->channelManager->find('1234', 'channel-c'); + + $this->assertTrue($channelA->hasConnection($connection)); + $this->assertTrue($channelB->hasConnection($connection)); + $this->assertTrue($channelC->hasConnection($connection)); + } + + public function test_unsubscribe_from_public_channel() + { + $connection = $this->newActiveConnection(['test-channel']); + $channel = $this->channelManager->find('1234', 'test-channel'); + + $this->assertTrue($channel->hasConnection($connection)); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'test-channel'], + ])); + + $this->assertFalse($channel->hasConnection($connection)); + } + + public function test_unsubscribe_does_not_affect_other_channels() + { + $connection = $this->newActiveConnection(['channel-a', 'channel-b']); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'channel-a'], + ])); + + $channelA = $this->channelManager->find('1234', 'channel-a'); + $channelB = $this->channelManager->find('1234', 'channel-b'); + + $this->assertFalse($channelA->hasConnection($connection)); + $this->assertTrue($channelB->hasConnection($connection)); + } + + // ========================================================================= + // Private channel subscribe/unsubscribe + // ========================================================================= + + public function test_subscribe_to_private_channel_with_valid_signature() + { + $connection = $this->newPrivateConnection('private-test'); + + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); + + $channel = $this->channelManager->find('1234', 'private-test'); + $this->assertTrue($channel->hasConnection($connection)); + } + + public function test_subscribe_to_private_channel_with_invalid_signature_is_rejected() + { + $connection = $this->newConnection(); + $this->wsHandler->onOpen($connection); + + $message = new Mocks\Message([ + 'event' => 'websocket.subscribe', + 'data' => [ + 'auth' => 'invalid-signature', + 'channel' => 'private-test', + ], + ]); + + $this->wsHandler->onMessage($connection, $message); + + // Invalid signature is silently caught — no subscription_succeeded + $connection->assertNotSentEvent('websocket_internal.subscription_succeeded'); + } + + // ========================================================================= + // Presence channel subscribe/unsubscribe + // ========================================================================= + + public function test_subscribe_to_presence_channel() + { + $connection = $this->newPresenceConnection('presence-chat', [ + 'user_id' => 1, + 'user_info' => ['name' => 'Rick'], + ]); + + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); + + $channel = $this->channelManager->find('1234', 'presence-chat'); + $this->assertTrue($channel->hasConnection($connection)); + } + + public function test_presence_channel_member_added_on_second_connection() + { + $rick = $this->newPresenceConnection('presence-chat', [ + 'user_id' => 1, + 'user_info' => ['name' => 'Rick'], + ]); + + $rick->resetEvents(); + + $morty = $this->newPresenceConnection('presence-chat', [ + 'user_id' => 2, + 'user_info' => ['name' => 'Morty'], + ]); + + // Rick should receive a member_added event + $rick->assertSentEvent('websocket_internal.member_added'); + } + + public function test_presence_channel_shows_member_count() + { + $rick = $this->newPresenceConnection('presence-chat', [ + 'user_id' => 1, + 'user_info' => ['name' => 'Rick'], + ]); + + $morty = $this->newPresenceConnection('presence-chat', [ + 'user_id' => 2, + 'user_info' => ['name' => 'Morty'], + ]); + + $channel = $this->channelManager->find('1234', 'presence-chat'); + $this->assertTrue($channel->hasConnection($rick)); + $this->assertTrue($channel->hasConnection($morty)); + } + + // ========================================================================= + // Ping/pong heartbeat + // ========================================================================= + + public function test_ping_receives_pong() + { + $connection = $this->newActiveConnection(['websocket']); + $connection->resetEvents(); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.ping', + 'data' => new \stdClass(), + ])); + + $connection->assertSentEvent('websocket.pong'); + } + + public function test_backward_compat_colon_ping_receives_pong() + { + $connection = $this->newActiveConnection(['websocket']); + $connection->resetEvents(); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'pusher:ping', + 'data' => new \stdClass(), + ])); + + $connection->assertSentEvent('websocket.pong'); + } + + public function test_ping_does_not_require_channel_subscription() + { + $connection = $this->newConnection(); + $this->wsHandler->onOpen($connection); + $connection->resetEvents(); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.ping', + 'data' => new \stdClass(), + ])); + + $connection->assertSentEvent('websocket.pong'); + } + + // ========================================================================= + // Protocol :response suffix + // ========================================================================= + + public function test_subscribe_gets_response_suffix() + { + $connection = $this->newActiveConnection(['websocket']); + $connection->resetEvents(); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', + 'data' => ['channel' => 'websocket'], + ])); + + $response = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response'); + $this->assertNotNull($response); + $this->assertEquals('Success', $response['data']['message']); + } + + public function test_unsubscribe_gets_response_suffix() + { + $connection = $this->newActiveConnection(['websocket']); + $connection->resetEvents(); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'websocket'], + ])); + + $response = collect($connection->sentData)->firstWhere('event', 'websocket.unsubscribe:response'); + $this->assertNotNull($response); + } + + // ========================================================================= + // Message rejection without subscription + // ========================================================================= + + public function test_message_to_unsubscribed_channel_gets_error() + { + $connection = $this->newActiveConnection(['websocket']); + + // Unsubscribe first + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'websocket'], + ])); + + $connection->resetEvents(); + + // Try to send a message to the channel + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'app.someAction', + 'data' => ['test' => true], + 'channel' => 'websocket', + ])); + + $error = collect($connection->sentData)->firstWhere('event', 'app.someAction:error'); + $this->assertNotNull($error); + $this->assertEquals('Subscription not established', $error['data']['message']); + } + + // ========================================================================= + // Connection close and error handling + // ========================================================================= + + public function test_on_close_removes_connection_from_channels() + { + $connection = $this->newActiveConnection(['websocket']); + $channel = $this->channelManager->find('1234', 'websocket'); + + $this->assertTrue($channel->hasConnection($connection)); + + $this->wsHandler->onClose($connection); + + $this->assertFalse($channel->hasConnection($connection)); + } + + public function test_on_error_does_not_close_other_connections() + { + $conn1 = $this->newActiveConnection(['websocket']); + $conn2 = $this->newActiveConnection(['websocket']); + + $this->wsHandler->onError($conn1, new \Exception('Test error')); + + // Generic exceptions are ignored; only ExceptionsWebSocketException is emitted. + $conn1->assertNotSentEvent('websocket.error'); + $conn2->assertNotSentEvent('websocket.error'); + + $channel = $this->channelManager->find('1234', 'websocket'); + $this->assertTrue($channel->hasConnection($conn2)); + } + + // ========================================================================= + // Broadcasting between connections (via Channel class) + // ========================================================================= + + public function test_broadcast_to_public_channel_reaches_all_connections() + { + $conn1 = $this->newActiveConnection(['news']); + $conn2 = $this->newActiveConnection(['news']); + $conn3 = $this->newActiveConnection(['news']); + + $conn1->resetEvents(); + $conn2->resetEvents(); + $conn3->resetEvents(); + + $channel = $this->channelManager->find('1234', 'news'); + $channel->broadcast('1234', (object) [ + 'event' => 'news.update', + 'data' => ['title' => 'Breaking News'], + 'channel' => 'news', + ]); + + $conn1->assertSentEvent('news.update'); + $conn2->assertSentEvent('news.update'); + $conn3->assertSentEvent('news.update'); + } + + public function test_broadcast_except_excludes_specified_socket() + { + $conn1 = $this->newActiveConnection(['chat']); + $conn2 = $this->newActiveConnection(['chat']); + + $established = collect($conn1->sentData)->firstWhere('event', 'websocket.connection_established'); + $excludeSocketId = json_decode($established['data'] ?? '{}', true)['socket_id'] ?? ''; + + $conn1->resetEvents(); + $conn2->resetEvents(); + + $channel = $this->channelManager->find('1234', 'chat'); + $channel->broadcastToEveryoneExcept( + (object) ['event' => 'chat.message', 'data' => ['text' => 'Hello']], + $excludeSocketId, + '1234' + ); + + // conn1 excluded, conn2 receives + $conn1->assertNotSentEvent('chat.message'); + $conn2->assertSentEvent('chat.message'); + } + + // ========================================================================= + // Connection count tracking + // ========================================================================= + + public function test_channel_connection_count_tracks_subscribe_and_unsubscribe() + { + $conn1 = $this->newActiveConnection(['counter-channel']); + $conn2 = $this->newActiveConnection(['counter-channel']); + + $channel = $this->channelManager->find('1234', 'counter-channel'); + $this->assertTrue($channel->hasConnection($conn1)); + $this->assertTrue($channel->hasConnection($conn2)); + + // Unsubscribe one + $this->wsHandler->onMessage($conn1, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'counter-channel'], + ])); + + $this->assertFalse($channel->hasConnection($conn1)); + $this->assertTrue($channel->hasConnection($conn2)); + } + + // ========================================================================= + // Message without app context + // ========================================================================= + + public function test_message_without_on_open_is_ignored() + { + $connection = new Mocks\Connection(); + $connection->httpRequest = new \GuzzleHttp\Psr7\Request('GET', '/?appKey=TestKey'); + + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.ping', + 'data' => new \stdClass(), + ])); + + $this->assertEmpty($connection->sentData); + } + + // ========================================================================= + // Re-subscription after unsubscribe + // ========================================================================= + + public function test_resubscribe_after_unsubscribe_works() + { + $connection = $this->newActiveConnection(['websocket']); + $channel = $this->channelManager->find('1234', 'websocket'); + + // Unsubscribe + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', + 'data' => ['channel' => 'websocket'], + ])); + $this->assertFalse($channel->hasConnection($connection)); + + // Re-subscribe + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', + 'data' => ['channel' => 'websocket'], + ])); + $this->assertTrue($channel->hasConnection($connection)); + } +} diff --git a/tests/HealthTest.php b/tests/HealthTest.php index dbc26fe..ed413d6 100644 --- a/tests/HealthTest.php +++ b/tests/HealthTest.php @@ -11,9 +11,9 @@ class HealthTest extends TestCase { $connection = $this->newConnection(); - $this->pusherServer = app(HealthHandler::class); + $this->wsHandler = app(HealthHandler::class); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); $this->assertTrue( Str::contains($connection->sentRawData[0], '{"ok":true}') diff --git a/tests/Ipc/SocketPairIpcWebsocketWorkflowTest.php b/tests/Ipc/SocketPairIpcWebsocketWorkflowTest.php index 14b776a..0f075e7 100644 --- a/tests/Ipc/SocketPairIpcWebsocketWorkflowTest.php +++ b/tests/Ipc/SocketPairIpcWebsocketWorkflowTest.php @@ -50,7 +50,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // Simulate what happens when a client connects $connectionEstablished = json_encode([ - 'event' => 'pusher:connection_established', + 'event' => 'websocket.connection_established', 'data' => json_encode([ 'socket_id' => '123.456', 'activity_timeout' => 30, @@ -81,7 +81,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase $this->assertNotNull($receivedData); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher:connection_established', $decoded['event']); + $this->assertEquals('websocket.connection_established', $decoded['event']); $data = json_decode($decoded['data'], true); $this->assertEquals('123.456', $data['socket_id']); @@ -117,7 +117,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // Simulate subscription success response $subscriptionSuccess = json_encode([ - 'event' => 'pusher_internal:subscription_succeeded', + 'event' => 'websocket_internal.subscription_succeeded', 'channel' => 'public-channel', 'data' => '{}', ]); @@ -144,7 +144,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher_internal:subscription_succeeded', $decoded['event']); + $this->assertEquals('websocket_internal.subscription_succeeded', $decoded['event']); $this->assertEquals('public-channel', $decoded['channel']); } @@ -189,7 +189,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase ]; $subscriptionSuccess = json_encode([ - 'event' => 'pusher_internal:subscription_succeeded', + 'event' => 'websocket_internal.subscription_succeeded', 'channel' => 'presence-room.1', 'data' => json_encode($presenceData), ]); @@ -216,7 +216,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher_internal:subscription_succeeded', $decoded['event']); + $this->assertEquals('websocket_internal.subscription_succeeded', $decoded['event']); $this->assertEquals('presence-room.1', $decoded['channel']); $data = json_decode($decoded['data'], true); @@ -387,7 +387,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // Simulate an error response $errorResponse = json_encode([ - 'event' => 'pusher:error', + 'event' => 'websocket.error', 'data' => [ 'message' => 'Could not find app key `InvalidKey`.', 'code' => 4001, @@ -416,7 +416,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher:error', $decoded['event']); + $this->assertEquals('websocket.error', $decoded['event']); $this->assertEquals(4001, $decoded['data']['code']); } @@ -448,7 +448,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase $ipc->setupChild(); $memberAdded = json_encode([ - 'event' => 'pusher_internal:member_added', + 'event' => 'websocket_internal.member_added', 'channel' => 'presence-room.1', 'data' => json_encode([ 'user_id' => 'user_4', @@ -478,7 +478,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher_internal:member_added', $decoded['event']); + $this->assertEquals('websocket_internal.member_added', $decoded['event']); $data = json_decode($decoded['data'], true); $this->assertEquals('user_4', $data['user_id']); @@ -512,7 +512,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase $ipc->setupChild(); $memberRemoved = json_encode([ - 'event' => 'pusher_internal:member_removed', + 'event' => 'websocket_internal.member_removed', 'channel' => 'presence-room.1', 'data' => json_encode([ 'user_id' => 'user_2', @@ -541,7 +541,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher_internal:member_removed', $decoded['event']); + $this->assertEquals('websocket_internal.member_removed', $decoded['event']); } /** @@ -574,7 +574,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // Simulate pong response $pongResponse = json_encode([ - 'event' => 'pusher:pong', + 'event' => 'websocket.pong', 'data' => '{}', ]); @@ -602,7 +602,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $decoded = json_decode($receivedData, true); - $this->assertEquals('pusher:pong', $decoded['event']); + $this->assertEquals('websocket.pong', $decoded['event']); // Ping/pong should be very fast $this->assertLessThan(50, $latency, "Ping/pong latency {$latency}ms exceeds 50ms"); @@ -637,13 +637,13 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // 1. Connection established $ipc->sendToParent(json_encode([ - 'event' => 'pusher:connection_established', + 'event' => 'websocket.connection_established', 'data' => json_encode(['socket_id' => '123.456', 'activity_timeout' => 30]), ])); // 2. Subscribe to channel $ipc->sendToParent(json_encode([ - 'event' => 'pusher_internal:subscription_succeeded', + 'event' => 'websocket_internal.subscription_succeeded', 'channel' => 'public-chat', 'data' => '{}', ])); @@ -657,13 +657,13 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase // 4. Ping response $ipc->sendToParent(json_encode([ - 'event' => 'pusher:pong', + 'event' => 'websocket.pong', 'data' => '{}', ])); // 5. Unsubscribe $ipc->sendToParent(json_encode([ - 'event' => 'pusher_internal:unsubscribed', + 'event' => 'websocket_internal.unsubscribed', 'channel' => 'public-chat', ])); @@ -693,11 +693,11 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase $this->assertCount(5, $receivedMessages); // Verify lifecycle order - $this->assertEquals('pusher:connection_established', $receivedMessages[0]['event']); - $this->assertEquals('pusher_internal:subscription_succeeded', $receivedMessages[1]['event']); + $this->assertEquals('websocket.connection_established', $receivedMessages[0]['event']); + $this->assertEquals('websocket_internal.subscription_succeeded', $receivedMessages[1]['event']); $this->assertEquals('new-message', $receivedMessages[2]['event']); - $this->assertEquals('pusher:pong', $receivedMessages[3]['event']); - $this->assertEquals('pusher_internal:unsubscribed', $receivedMessages[4]['event']); + $this->assertEquals('websocket.pong', $receivedMessages[3]['event']); + $this->assertEquals('websocket_internal.unsubscribed', $receivedMessages[4]['event']); } /** @@ -744,8 +744,8 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase $mock = new MockConnectionSocketPair($realConnection, $ipc); // Simulate sending multiple messages through mock - $mock->send(json_encode(['event' => 'pusher:connection_established', 'data' => '{}'])); - $mock->send(json_encode(['event' => 'pusher_internal:subscription_succeeded', 'channel' => 'test'])); + $mock->send(json_encode(['event' => 'websocket.connection_established', 'data' => '{}'])); + $mock->send(json_encode(['event' => 'websocket_internal.subscription_succeeded', 'channel' => 'test'])); $mock->send(json_encode(['event' => 'message', 'data' => 'Hello'])); $ipc->closeChild(); @@ -772,8 +772,8 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase pcntl_waitpid($pid, $status); $this->assertCount(3, $receivedMessages); - $this->assertEquals('pusher:connection_established', $receivedMessages[0]['event']); - $this->assertEquals('pusher_internal:subscription_succeeded', $receivedMessages[1]['event']); + $this->assertEquals('websocket.connection_established', $receivedMessages[0]['event']); + $this->assertEquals('websocket_internal.subscription_succeeded', $receivedMessages[1]['event']); $this->assertEquals('message', $receivedMessages[2]['event']); } } diff --git a/tests/PingTest.php b/tests/PingTest.php index 0964417..73cf350 100644 --- a/tests/PingTest.php +++ b/tests/PingTest.php @@ -8,10 +8,10 @@ class PingTest extends TestCase { $connection = $this->newActiveConnection(['public-channel']); - $message = new Mocks\Message(['event' => 'pusher.ping']); + $message = new Mocks\Message(['event' => 'websocket.ping']); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); - $connection->assertSentEvent('pusher.pong'); + $connection->assertSentEvent('websocket.pong'); } } diff --git a/tests/PresenceChannelTest.php b/tests/PresenceChannelTest.php index c2d1a86..6f44f09 100644 --- a/tests/PresenceChannelTest.php +++ b/tests/PresenceChannelTest.php @@ -2,38 +2,35 @@ namespace BlaxSoftware\LaravelWebSockets\Test; -use BlaxSoftware\LaravelWebSockets\API\TriggerEvent; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature; -use GuzzleHttp\Psr7\Request; -use Illuminate\Http\JsonResponse; -use Pusher\Pusher; use Ratchet\ConnectionInterface; class PresenceChannelTest extends TestCase { public function test_connect_to_presence_channel_with_invalid_signature() { - $this->expectException(InvalidSignature::class); - $connection = $this->newConnection(); $message = new Mocks\Message([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'auth' => 'invalid', 'channel' => 'presence-channel', ], ]); - $this->pusherServer->onOpen($connection); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onOpen($connection); + $this->wsHandler->onMessage($connection, $message); + + // Invalid signature should be silently rejected — no subscription_succeeded sent + $connection->assertNotSentEvent('websocket_internal.subscription_succeeded'); } public function test_connect_to_presence_channel_with_valid_signature() { $connection = $this->newConnection(); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); $user = [ 'user_id' => 1, @@ -45,16 +42,16 @@ class PresenceChannelTest extends TestCase $encodedUser = json_encode($user); $message = new Mocks\SignedMessage([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'channel' => 'presence-channel', 'channel_data' => $encodedUser, ], ], $connection, 'presence-channel', $encodedUser); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); - $connection->assertSentEvent('pusher_internal:subscription_succeeded', [ + $connection->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'presence-channel', ]); @@ -72,12 +69,12 @@ class PresenceChannelTest extends TestCase $pickleRick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); foreach ([$rick, $morty, $pickleRick] as $connection) { - $connection->assertSentEvent('pusher_internal:subscription_succeeded', [ + $connection->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'presence-channel', ]); } - $rick->assertSentEvent('pusher_internal:subscription_succeeded', [ + $rick->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'presence-channel', 'data' => json_encode([ 'presence' => [ @@ -88,7 +85,7 @@ class PresenceChannelTest extends TestCase ]), ]); - $morty->assertSentEvent('pusher_internal:subscription_succeeded', [ + $morty->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'presence-channel', 'data' => json_encode([ 'presence' => [ @@ -101,7 +98,7 @@ class PresenceChannelTest extends TestCase // The duplicated-user_id connection should get basically the list of ids // without dealing with duplicate user ids. - $pickleRick->assertSentEvent('pusher_internal:subscription_succeeded', [ + $pickleRick->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'presence-channel', 'data' => json_encode([ 'presence' => [ @@ -130,7 +127,7 @@ class PresenceChannelTest extends TestCase $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]); - $rick->assertSentEvent('pusher_internal:member_added', [ + $rick->assertSentEvent('websocket_internal.member_added', [ 'channel' => 'presence-channel', 'data' => json_encode(['user_id' => 2]), ]); @@ -141,9 +138,9 @@ class PresenceChannelTest extends TestCase $this->assertCount(2, $members); }); - $this->pusherServer->onClose($morty); + $this->wsHandler->onClose($morty); - $rick->assertSentEvent('pusher_internal:member_removed', [ + $rick->assertSentEvent('websocket_internal.member_removed', [ 'channel' => 'presence-channel', 'data' => json_encode(['user_id' => 2]), ]); @@ -173,13 +170,13 @@ class PresenceChannelTest extends TestCase }); $message = new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => [ 'channel' => 'presence-channel', ], ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); $this->channelManager ->getGlobalConnectionsCount('1234', 'presence-channel') @@ -201,7 +198,7 @@ class PresenceChannelTest extends TestCase 'channel' => 'presence-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'presence-channel']); @@ -218,35 +215,12 @@ class PresenceChannelTest extends TestCase 'channel' => 'presence-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertNotSentEvent('client-test-whisper'); } - public function test_statistics_get_collected_for_presenece_channels() - { - $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]); - - $this->statisticsCollector - ->getStatistics() - ->then(function ($statistics) { - $this->assertCount(1, $statistics); - }); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 2, - 'websocket_messages_count' => 2, - 'api_messages_count' => 0, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - public function test_local_connections_for_presence_channels() { $this->newPresenceConnection('presence-channel', ['user_id' => 1]); @@ -274,8 +248,8 @@ class PresenceChannelTest extends TestCase $firstConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']); // Make sure the observer sees a `member_added` event for `user:1` - $observerConnection->assertSentEvent('pusher_internal:member_added', [ - 'event' => 'pusher_internal:member_added', + $observerConnection->assertSentEvent('websocket_internal.member_added', [ + 'event' => 'websocket_internal.member_added', 'channel' => 'presence-channel', 'data' => json_encode(['user_id' => '1']), ])->resetEvents(); @@ -284,19 +258,19 @@ class PresenceChannelTest extends TestCase $secondConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']); // Make sure the observer was not notified of a `member_added` event (user was already connected) - $observerConnection->assertNotSentEvent('pusher_internal:member_added'); + $observerConnection->assertNotSentEvent('websocket_internal.member_added'); // Disconnect the first socket for user `1` on the server - $this->pusherServer->onClose($firstConnection); + $this->wsHandler->onClose($firstConnection); // Make sure the observer was not notified of a `member_removed` event (user still connected on another socket) - $observerConnection->assertNotSentEvent('pusher_internal:member_removed'); + $observerConnection->assertNotSentEvent('websocket_internal.member_removed'); // Disconnect the second (and last) socket for user `1` on the server - $this->pusherServer->onClose($secondConnection); + $this->wsHandler->onClose($secondConnection); // Make sure the observer was notified of a `member_removed` event (last socket for user was disconnected) - $observerConnection->assertSentEvent('pusher_internal:member_removed'); + $observerConnection->assertSentEvent('websocket_internal.member_removed'); $this->channelManager ->getMemberSockets('1', '1234', 'presence-channel') @@ -406,146 +380,4 @@ class PresenceChannelTest extends TestCase $message->getPayload(), ]); } - - public function test_it_fires_the_event_to_presence_channel() - { - $this->newPresenceConnection('presence-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['presence-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 1, - 'websocket_messages_count' => 1, - 'api_messages_count' => 1, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - - public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_presence_channel() - { - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['presence-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'presence-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'presence-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - } - - public function test_it_fires_event_across_servers_when_there_are_users_locally_for_presence_channel() - { - $wsConnection = $this->newPresenceConnection('presence-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['presence-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'presence-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'presence-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - - $wsConnection->assertSentEvent('some-event', [ - 'channel' => 'presence-channel', - 'data' => json_encode(['some-data' => 'yes']), - ]); - } } diff --git a/tests/PrivateChannelTest.php b/tests/PrivateChannelTest.php index 63490d7..1c0033a 100644 --- a/tests/PrivateChannelTest.php +++ b/tests/PrivateChannelTest.php @@ -2,49 +2,46 @@ namespace BlaxSoftware\LaravelWebSockets\Test; -use BlaxSoftware\LaravelWebSockets\API\TriggerEvent; use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature; -use GuzzleHttp\Psr7\Request; -use Illuminate\Http\JsonResponse; -use Pusher\Pusher; use Ratchet\ConnectionInterface; class PrivateChannelTest extends TestCase { public function test_connect_to_private_channel_with_invalid_signature() { - $this->expectException(InvalidSignature::class); - $connection = $this->newConnection(); $message = new Mocks\Message([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'auth' => 'invalid', 'channel' => 'private-channel', ], ]); - $this->pusherServer->onOpen($connection); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onOpen($connection); + $this->wsHandler->onMessage($connection, $message); + + // Invalid signature should be silently rejected — no subscription_succeeded sent + $connection->assertNotSentEvent('websocket_internal.subscription_succeeded'); } public function test_connect_to_private_channel_with_valid_signature() { $connection = $this->newConnection(); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); $message = new Mocks\SignedMessage([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'channel' => 'private-channel', ], ], $connection, 'private-channel'); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); - $connection->assertSentEvent('pusher_internal:subscription_succeeded', [ + $connection->assertSentEvent('websocket_internal.subscription_succeeded', [ 'channel' => 'private-channel', ]); @@ -66,13 +63,13 @@ class PrivateChannelTest extends TestCase }); $message = new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => [ 'channel' => 'private-channel', ], ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); $this->channelManager ->getGlobalConnectionsCount('1234', 'private-channel') @@ -94,7 +91,7 @@ class PrivateChannelTest extends TestCase 'channel' => 'private-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'private-channel']); @@ -111,35 +108,12 @@ class PrivateChannelTest extends TestCase 'channel' => 'private-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertNotSentEvent('client-test-whisper'); } - public function test_statistics_get_collected_for_private_channels() - { - $rick = $this->newPrivateConnection('private-channel'); - $morty = $this->newPrivateConnection('private-channel'); - - $this->statisticsCollector - ->getStatistics() - ->then(function ($statistics) { - $this->assertCount(1, $statistics); - }); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 2, - 'websocket_messages_count' => 2, - 'api_messages_count' => 0, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - public function test_local_connections_for_private_channels() { $this->newPrivateConnection('private-channel'); @@ -227,145 +201,4 @@ class PrivateChannelTest extends TestCase ]); } - public function test_it_fires_the_event_to_private_channel() - { - $this->newPrivateConnection('private-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['private-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 1, - 'websocket_messages_count' => 1, - 'api_messages_count' => 1, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - - public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_private_channel() - { - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['private-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'private-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'private-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - } - - public function test_it_fires_event_across_servers_when_there_are_users_locally_for_private_channel() - { - $wsConnection = $this->newPrivateConnection('private-channel'); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['private-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'private-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'private-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - - $wsConnection->assertSentEvent('some-event', [ - 'channel' => 'private-channel', - 'data' => json_encode(['some-data' => 'yes']), - ]); - } } diff --git a/tests/PublicChannelTest.php b/tests/PublicChannelTest.php index 53295cf..b4c531a 100644 --- a/tests/PublicChannelTest.php +++ b/tests/PublicChannelTest.php @@ -2,10 +2,6 @@ namespace BlaxSoftware\LaravelWebSockets\Test; -use BlaxSoftware\LaravelWebSockets\API\TriggerEvent; -use GuzzleHttp\Psr7\Request; -use Illuminate\Http\JsonResponse; -use Pusher\Pusher; use Ratchet\ConnectionInterface; class PublicChannelTest extends TestCase @@ -21,7 +17,7 @@ class PublicChannelTest extends TestCase }); $connection->assertSentEvent( - 'pusher.connection_established', + 'websocket.connection_established', [ 'data' => json_encode([ 'socket_id' => $connection->socketId, @@ -31,7 +27,7 @@ class PublicChannelTest extends TestCase ); $connection->assertSentEvent( - 'pusher_internal:subscription_succeeded', + 'websocket_internal.subscription_succeeded', ['channel' => 'public-channel'] ); } @@ -47,13 +43,13 @@ class PublicChannelTest extends TestCase }); $message = new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => [ 'channel' => 'public-channel', ], ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); $this->channelManager ->getGlobalConnectionsCount('1234', 'public-channel') @@ -75,7 +71,7 @@ class PublicChannelTest extends TestCase 'channel' => 'public-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'public-channel']); @@ -92,35 +88,12 @@ class PublicChannelTest extends TestCase 'channel' => 'public-channel', ]); - $this->pusherServer->onMessage($rick, $message); + $this->wsHandler->onMessage($rick, $message); $rick->assertNotSentEvent('client-test-whisper'); $morty->assertNotSentEvent('client-test-whisper'); } - public function test_statistics_get_collected_for_public_channels() - { - $rick = $this->newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel']); - - $this->statisticsCollector - ->getStatistics() - ->then(function ($statistics) { - $this->assertCount(1, $statistics); - }); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 2, - 'websocket_messages_count' => 2, - 'api_messages_count' => 0, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - public function test_local_connections_for_public_channels() { $this->newActiveConnection(['public-channel']); @@ -207,133 +180,4 @@ class PublicChannelTest extends TestCase $message->getPayload(), ]); } - - public function test_it_fires_the_event_to_public_channel() - { - $this->startServer(); - - $requestPath = '/apps/1234/events'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['public-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $response = $this->await($this->browser->post('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame([], json_decode((string) $response->getBody(), true)); - - $this->statisticsCollector - ->getAppStatistics('1234') - ->then(function ($statistic) { - $this->assertEquals([ - 'peak_connections_count' => 0, - 'websocket_messages_count' => 0, - 'api_messages_count' => 1, - 'app_id' => '1234', - ], $statistic->toArray()); - }); - } - - public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_public_channel() - { - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['public-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'public-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'public-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - } - - public function test_it_fires_event_across_servers_when_there_are_users_locally_for_public_channel() - { - $wsConnection = $this->newActiveConnection(['public-channel']); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $routeParams = [ - 'appId' => '1234', - ]; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'TestSecret', 'POST', $requestPath, [ - 'name' => 'some-event', - 'channels' => ['public-channel'], - 'data' => json_encode(['some-data' => 'yes']), - ], - )); - - $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams)); - - $controller = app(TriggerEvent::class); - - $controller->onOpen($connection, $request); - - /** @var JsonResponse $response */ - $response = array_pop($connection->sentRawData); - - $this->assertSame([], json_decode($response->getContent(), true)); - - if (method_exists($this->channelManager, 'getPublishClient')) { - $this->channelManager - ->getPublishClient() - ->assertCalledWithArgsCount(1, 'publish', [ - $this->channelManager->getRedisKey('1234', 'public-channel'), - json_encode([ - 'event' => 'some-event', - 'channel' => 'public-channel', - 'data' => json_encode(['some-data' => 'yes']), - 'appId' => '1234', - 'socketId' => null, - 'serverId' => $this->channelManager->getServerId(), - ]), - ]); - } - - $wsConnection->assertSentEvent('some-event', [ - 'channel' => 'public-channel', - 'data' => json_encode(['some-data' => 'yes']), - ]); - } } diff --git a/tests/ReplicationTest.php b/tests/ReplicationTest.php index 73f1d95..5559a91 100644 --- a/tests/ReplicationTest.php +++ b/tests/ReplicationTest.php @@ -27,7 +27,7 @@ class ReplicationTest extends TestCase { $connection = $this->newActiveConnection(['public-channel']); - $this->pusherServer->onClose($connection); + $this->wsHandler->onClose($connection); $this->getSubscribeClient() ->assertCalledWithArgs('unsubscribe', [$this->channelManager->getRedisKey('1234')]) diff --git a/tests/StatisticsStoreTest.php b/tests/StatisticsStoreTest.php deleted file mode 100644 index 69a426b..0000000 --- a/tests/StatisticsStoreTest.php +++ /dev/null @@ -1,95 +0,0 @@ -newActiveConnection(['public-channel']); - $morty = $this->newActiveConnection(['public-channel']); - - $this->statisticsCollector->save(); - - $this->assertCount(1, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('2', $records[0]['peak_connections_count']); - $this->assertEquals('2', $records[0]['websocket_messages_count']); - $this->assertEquals('0', $records[0]['api_messages_count']); - - $this->pusherServer->onClose($rick); - $this->pusherServer->onClose($morty); - - $this->statisticsCollector->save(); - - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('2', $records[1]['peak_connections_count']); - - $this->statisticsCollector->save(); - - // The last one should not generate any more records - // since the current state is empty. - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - } - - public function test_store_statistics_on_private_channel() - { - $rick = $this->newPrivateConnection('private-channel'); - $morty = $this->newPrivateConnection('private-channel'); - - $this->statisticsCollector->save(); - - $this->assertCount(1, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('2', $records[0]['peak_connections_count']); - $this->assertEquals('2', $records[0]['websocket_messages_count']); - $this->assertEquals('0', $records[0]['api_messages_count']); - - $this->pusherServer->onClose($rick); - $this->pusherServer->onClose($morty); - - $this->statisticsCollector->save(); - - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('2', $records[1]['peak_connections_count']); - - $this->statisticsCollector->save(); - - // The last one should not generate any more records - // since the current state is empty. - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - } - - public function test_store_statistics_on_presence_channel() - { - $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]); - $pickleRick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]); - - $this->statisticsCollector->save(); - - $this->assertCount(1, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('3', $records[0]['peak_connections_count']); - $this->assertEquals('3', $records[0]['websocket_messages_count']); - $this->assertEquals('0', $records[0]['api_messages_count']); - - $this->pusherServer->onClose($rick); - $this->pusherServer->onClose($morty); - $this->pusherServer->onClose($pickleRick); - - $this->statisticsCollector->save(); - - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - - $this->assertEquals('3', $records[1]['peak_connections_count']); - - $this->statisticsCollector->save(); - - // The last one should not generate any more records - // since the current state is empty. - $this->assertCount(2, $records = $this->statisticsStore->getRecords()); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index d311331..c98aac8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,8 +3,6 @@ namespace BlaxSoftware\LaravelWebSockets\Test; use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager; -use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector; -use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore; use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter; use BlaxSoftware\LaravelWebSockets\Helpers; use BlaxSoftware\LaravelWebSockets\Server\Loggers\HttpLogger; @@ -42,11 +40,11 @@ abstract class TestCase extends Orchestra protected $server; /** - * A test Pusher server. + * The WebSocket handler under test. * * @var \BlaxSoftware\LaravelWebSockets\Server\WebSocketHandler */ - protected $pusherServer; + protected $wsHandler; /** * The test Channel manager. @@ -55,19 +53,7 @@ abstract class TestCase extends Orchestra */ protected $channelManager; - /** - * The test Channel manager. - * - * @var \BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector - */ - protected $statisticsCollector; - /** - * The test Channel manager. - * - * @var \BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore - */ - protected $statisticsStore; /** * Get the loop instance. @@ -132,11 +118,7 @@ abstract class TestCase extends Orchestra $this->registerManagers(); - $this->registerStatisticsCollectors(); - - $this->registerStatisticsStores(); - - $this->pusherServer = $this->app->make(config('websockets.handlers.websocket')); + $this->wsHandler = $this->app->make(config('websockets.handlers.websocket')); if ($this->replicationMode === 'redis') { $this->registerRedis(); @@ -268,12 +250,6 @@ abstract class TestCase extends Orchestra $app['config']->set('websockets.replication.modes', [ 'local' => [ 'channel_manager' => \BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager::class, - 'collector' => \BlaxSoftware\LaravelWebSockets\Statistics\Collectors\MemoryCollector::class, - ], - 'redis' => [ - 'channel_manager' => \BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager::class, - 'connection' => 'default', - 'collector' => \BlaxSoftware\LaravelWebSockets\Statistics\Collectors\RedisCollector::class, ], ]); } @@ -341,34 +317,7 @@ abstract class TestCase extends Orchestra $this->app->offsetUnset(ChannelManager::class); } - /** - * Register the statistics collectors. - * - * @return void - */ - protected function registerStatisticsCollectors() - { - $this->statisticsCollector = $this->app->make(StatisticsCollector::class); - $this->artisan('websockets:flush'); - } - - /** - * Register the statistics stores that are - * not resolved by the package service provider. - * - * @return void - */ - protected function registerStatisticsStores() - { - $this->app->singleton(StatisticsStore::class, function () { - $class = config('websockets.statistics.store'); - - return new $class; - }); - - $this->statisticsStore = $this->app->make(StatisticsStore::class); - } /** * Register the Redis components for testing. @@ -424,17 +373,17 @@ abstract class TestCase extends Orchestra { $connection = $this->newConnection($appKey, $headers); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); foreach ($channelsToJoin as $channel) { $message = new Mocks\Message([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'channel' => $channel, ], ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); } return $connection; @@ -453,7 +402,7 @@ abstract class TestCase extends Orchestra { $connection = $this->newConnection($appKey, $headers); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); $user = $user ?: [ 'user_id' => 1, @@ -463,14 +412,14 @@ abstract class TestCase extends Orchestra $encodedUser = json_encode($user); $message = new Mocks\SignedMessage([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'channel' => $channel, 'channel_data' => $encodedUser, ], ], $connection, $channel, $encodedUser); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); return $connection; } @@ -487,16 +436,16 @@ abstract class TestCase extends Orchestra { $connection = $this->newConnection($appKey, $headers); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); $message = new Mocks\SignedMessage([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => [ 'channel' => $channel, ], ], $connection, $channel); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); return $connection; } diff --git a/tests/TriggerEventTest.php b/tests/TriggerEventTest.php deleted file mode 100644 index bf79844..0000000 --- a/tests/TriggerEventTest.php +++ /dev/null @@ -1,26 +0,0 @@ -startServer(); - - $connection = new Mocks\Connection; - - $requestPath = '/apps/1234/events'; - - $queryString = http_build_query(Pusher::build_auth_query_params( - 'TestKey', 'InvalidSecret', 'GET', $requestPath - )); - - $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}")); - - $this->assertSame(405, $response->getStatusCode()); - $this->assertSame('', $response->getBody()->getContents()); - } -} diff --git a/tests/Unit/ControllerResolverTest.php b/tests/Unit/ControllerResolverTest.php index 9ca5d3a..a9e9e8d 100644 --- a/tests/Unit/ControllerResolverTest.php +++ b/tests/Unit/ControllerResolverTest.php @@ -3,11 +3,11 @@ namespace BlaxSoftware\LaravelWebSockets\Tests\Unit; use BlaxSoftware\LaravelWebSockets\Websocket\ControllerResolver; -use PHPUnit\Framework\TestCase; +use BlaxSoftware\LaravelWebSockets\Test\TestCase; class ControllerResolverTest extends TestCase { - protected function setUp(): void + public function setUp(): void { parent::setUp(); ControllerResolver::clearCache(); @@ -26,18 +26,24 @@ class ControllerResolverTest extends TestCase /** @test */ public function it_caches_resolved_controllers() { + // Disable hot_reload so caching is used + config()->set('websockets.hot_reload', false); + ControllerResolver::clearCache(); + // First call resolves and caches ControllerResolver::resolve('pusher'); $stats = ControllerResolver::getStats(); - // scanned may or may not be true (we no longer auto-scan on resolve) - // but cached should be > 0 $this->assertGreaterThan(0, $stats['cached']); } /** @test */ public function it_caches_null_for_nonexistent_controllers() { + // Disable hot_reload so caching is used + config()->set('websockets.hot_reload', false); + ControllerResolver::clearCache(); + // First call - not found $result1 = ControllerResolver::resolve('nonexistent-controller-xyz'); $this->assertNull($result1); diff --git a/tests/Websocket/HandlerForkPathTest.php b/tests/Websocket/HandlerForkPathTest.php index e7bad4c..088652a 100644 --- a/tests/Websocket/HandlerForkPathTest.php +++ b/tests/Websocket/HandlerForkPathTest.php @@ -43,7 +43,7 @@ class HandlerForkPathTest extends TestCase // The handler should automatically use socket pair IPC // We can verify this by checking the handler was created successfully - $this->assertNotNull($this->pusherServer); + $this->assertNotNull($this->wsHandler); } /** @@ -54,18 +54,18 @@ class HandlerForkPathTest extends TestCase $connection = $this->newActiveConnection(['fork-test-channel']); // Verify connection was established (subscription event has pre-existing test issues) - $connection->assertSentEvent('pusher.connection_established'); + $connection->assertSentEvent('websocket.connection_established'); // Now unsubscribe $message = new Mocks\Message([ - 'event' => 'pusher:unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => ['channel' => 'fork-test-channel'], ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); // No error should be sent - $connection->assertNotSentEvent('pusher:unsubscribe:error'); + $connection->assertNotSentEvent('websocket.unsubscribe:error'); } /** @@ -82,7 +82,7 @@ class HandlerForkPathTest extends TestCase 'channel' => 'channel-two', // Not subscribed! ]); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); // Should receive an error event $connection->assertSentEvent('custom.action:error'); @@ -99,17 +99,17 @@ class HandlerForkPathTest extends TestCase for ($i = 0; $i < 5; $i++) { // Unsubscribe $unsubMsg = new Mocks\Message([ - 'event' => 'pusher:unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => ['channel' => 'cycle-channel'], ]); - $this->pusherServer->onMessage($connection, $unsubMsg); + $this->wsHandler->onMessage($connection, $unsubMsg); // Resubscribe $subMsg = new Mocks\Message([ - 'event' => 'pusher:subscribe', + 'event' => 'websocket.subscribe', 'data' => ['channel' => 'cycle-channel'], ]); - $this->pusherServer->onMessage($connection, $subMsg); + $this->wsHandler->onMessage($connection, $subMsg); } // No errors should have been sent @@ -149,7 +149,7 @@ class HandlerForkPathTest extends TestCase 'channel' => 'empty-data-channel', ]); - $this->pusherServer->onMessage($sender, $message); + $this->wsHandler->onMessage($sender, $message); $receiver->assertSentEvent('client-empty', [ 'data' => [], @@ -172,19 +172,19 @@ class HandlerForkPathTest extends TestCase } /** - * Test pusher: prefixed events receive response. + * Test that ping/pong works correctly. */ - public function test_pusher_prefixed_events_handled() + public function test_ping_pong_handled() { $connection = $this->newActiveConnection(['pusher-event-channel']); // Ping should work $pingMsg = new Mocks\Message([ - 'event' => 'pusher.ping', + 'event' => 'websocket.ping', ]); - $this->pusherServer->onMessage($connection, $pingMsg); - $connection->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($connection, $pingMsg); + $connection->assertSentEvent('websocket.pong'); } /** @@ -204,7 +204,7 @@ class HandlerForkPathTest extends TestCase 'channel' => 'no-whisper-channel', ]); - $this->pusherServer->onMessage($sender, $message); + $this->wsHandler->onMessage($sender, $message); // Neither should receive (whisper blocked) $sender->assertNotSentEvent('client-blocked'); diff --git a/tests/Websocket/HandlerSocketPairIntegrationTest.php b/tests/Websocket/HandlerSocketPairIntegrationTest.php index 3369022..85feb81 100644 --- a/tests/Websocket/HandlerSocketPairIntegrationTest.php +++ b/tests/Websocket/HandlerSocketPairIntegrationTest.php @@ -13,7 +13,7 @@ use BlaxSoftware\LaravelWebSockets\Test\TestCase; * * These tests verify that the complete message flow works correctly when using * the event-driven socket pair IPC mechanism. Unlike the isolated IPC tests, - * these tests use the actual Handler with pusherServer->onMessage(). + * these tests use the actual Handler with wsHandler->onMessage(). * * Note: These tests require pcntl_fork() and socket_create_pair() to be available. */ @@ -41,14 +41,14 @@ class HandlerSocketPairIntegrationTest extends TestCase $connection = $this->newActiveConnection(['public-channel']); $message = new Mocks\Message([ - 'event' => 'pusher.ping', + 'event' => 'websocket.ping', ]); $startTime = hrtime(true); - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); $elapsed = (hrtime(true) - $startTime) / 1_000_000; // ms - $connection->assertSentEvent('pusher.pong'); + $connection->assertSentEvent('websocket.pong'); // Fast path should be very fast (< 15ms typically) $this->assertLessThan(15, $elapsed, "Ping/pong took {$elapsed}ms - should be < 15ms for fast path"); @@ -71,7 +71,7 @@ class HandlerSocketPairIntegrationTest extends TestCase 'channel' => 'test-channel', ]); - $this->pusherServer->onMessage($sender, $message); + $this->wsHandler->onMessage($sender, $message); // Sender should NOT receive their own whisper $sender->assertNotSentEvent('client-test-event'); @@ -85,7 +85,7 @@ class HandlerSocketPairIntegrationTest extends TestCase /** * Test channel subscription sends connection established event. - * Note: The pusher_internal:subscription_succeeded event has pre-existing + * Note: The websocket_internal.subscription_succeeded event has pre-existing * issues in the test framework (channel->hasConnection check). */ public function test_channel_connection_established() @@ -93,7 +93,7 @@ class HandlerSocketPairIntegrationTest extends TestCase $connection = $this->newActiveConnection(['my-channel']); // Verify connection established was sent (this always works) - $connection->assertSentEvent('pusher.connection_established'); + $connection->assertSentEvent('websocket.connection_established'); } /** @@ -113,7 +113,7 @@ class HandlerSocketPairIntegrationTest extends TestCase 'channel' => 'broadcast-channel', ]); - $this->pusherServer->onMessage($alice, $message); + $this->wsHandler->onMessage($alice, $message); // Alice (sender) should NOT receive $alice->assertNotSentEvent('client-hello'); @@ -136,7 +136,7 @@ class HandlerSocketPairIntegrationTest extends TestCase { $connection = $this->newActiveConnection(['test-channel']); - $connection->assertSentEvent('pusher.connection_established', [ + $connection->assertSentEvent('websocket.connection_established', [ 'data' => json_encode([ 'socket_id' => $connection->socketId, 'activity_timeout' => 30, @@ -146,7 +146,7 @@ class HandlerSocketPairIntegrationTest extends TestCase /** * Test subscribing to multiple channels via separate connections. - * Note: pusher_internal:subscription_succeeded has pre-existing test issues. + * Note: websocket_internal.subscription_succeeded has pre-existing test issues. */ public function test_subscribe_to_multiple_channels_separately() { @@ -156,9 +156,9 @@ class HandlerSocketPairIntegrationTest extends TestCase $connC = $this->newActiveConnection(['channel-c']); // Each should have received connection established - $connA->assertSentEvent('pusher.connection_established'); - $connB->assertSentEvent('pusher.connection_established'); - $connC->assertSentEvent('pusher.connection_established'); + $connA->assertSentEvent('websocket.connection_established'); + $connB->assertSentEvent('websocket.connection_established'); + $connC->assertSentEvent('websocket.connection_established'); } /** @@ -178,7 +178,7 @@ class HandlerSocketPairIntegrationTest extends TestCase 'data' => ['count' => $i], 'channel' => 'rapid-channel', ]); - $this->pusherServer->onMessage($sender, $message); + $this->wsHandler->onMessage($sender, $message); } // At least one message should be received by receiver @@ -199,7 +199,7 @@ class HandlerSocketPairIntegrationTest extends TestCase // This should not throw an exception - should handle gracefully try { - $this->pusherServer->onMessage($connection, $message); + $this->wsHandler->onMessage($connection, $message); } catch (\JsonException $e) { // Expected - Handler may throw JsonException for invalid JSON $this->assertTrue(true); @@ -227,7 +227,7 @@ class HandlerSocketPairIntegrationTest extends TestCase 'channel' => 'channel-A', ]); - $this->pusherServer->onMessage($channelA_User1, $message); + $this->wsHandler->onMessage($channelA_User1, $message); // Only channel-A users should receive $channelA_User2->assertSentEvent('client-isolated'); diff --git a/tests/Websocket/HandlerStabilityTest.php b/tests/Websocket/HandlerStabilityTest.php index 358d487..50c7485 100644 --- a/tests/Websocket/HandlerStabilityTest.php +++ b/tests/Websocket/HandlerStabilityTest.php @@ -13,11 +13,12 @@ use Carbon\Carbon; * * Tests mirror the real-life frontend (Websocket.client.ts) behavior: * - Default channel: 'websocket' (public channel, the production default) - * - Heartbeat: raw socket.send({ event: 'pusher.ping', data: {} }) every 20s - * - Subscribe: pusher.subscribe with channel in data (dot format) - * - Unsubscribe: pusher.unsubscribe (dot format — server only recognizes dots) + * - Heartbeat: raw socket.send({ event: 'websocket.ping', data: {} }) every 20s + * - Subscribe: websocket.subscribe with channel in data (dot format) + * - Unsubscribe: websocket.unsubscribe (dot format) * - Error recovery: "Subscription not established" → re-subscribe → retry - * - Pusher events get :response suffix from handlePusherEvent() + * - Protocol events get :response suffix from handleProtocolEvent() + * - Server accepts any prefix (websocket.*, pusher.*, pusher:*, etc.) * * Groups (run subsets with --group / --exclude-group): * @group stability — Real-time tests using event loop timers (4+ minutes) @@ -50,15 +51,15 @@ class HandlerStabilityTest extends TestCase parent::setUp(); $this->pingMsg = new Mocks\Message([ - 'event' => 'pusher.ping', + 'event' => 'websocket.ping', 'data' => new \stdClass(), ]); $this->subMsg = new Mocks\Message([ - 'event' => 'pusher.subscribe', + 'event' => 'websocket.subscribe', 'data' => ['channel' => 'websocket'], ]); $this->unsubMsg = new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + 'event' => 'websocket.unsubscribe', 'data' => ['channel' => 'websocket'], ]); } @@ -87,8 +88,8 @@ class HandlerStabilityTest extends TestCase $this->runOnlyOnLocalReplication(); $connection = $this->newActiveConnection(['websocket']); - $connection->assertSentEvent('pusher.connection_established'); - $connection->assertSentEvent('pusher_internal:subscription_succeeded'); + $connection->assertSentEvent('websocket.connection_established'); + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); $connection->resetEvents(); $pingsSent = 0; @@ -126,10 +127,10 @@ class HandlerStabilityTest extends TestCase // Client heartbeat every 20s if ($now >= $nextPing) { $connection->resetEvents(); - $this->pusherServer->onMessage($connection, $this->pingMsg); + $this->wsHandler->onMessage($connection, $this->pingMsg); $pingsSent++; - $pong = collect($connection->sentData)->firstWhere('event', 'pusher.pong'); + $pong = collect($connection->sentData)->firstWhere('event', 'websocket.pong'); $this->assertNotNull($pong, "Ping #{$pingsSent} at ~" . ($pingsSent * 20) . "s should get pong"); $pongsSeen++; $nextPing = $now + 20; @@ -182,7 +183,7 @@ class HandlerStabilityTest extends TestCase for ($cycle = 0; $cycle < 8; $cycle++) { $activeConnection->lastPongedAt = Carbon::now(); $this->channelManager->updateConnectionInChannels($activeConnection); - $this->pusherServer->onMessage($activeConnection, $this->pingMsg); + $this->wsHandler->onMessage($activeConnection, $this->pingMsg); } // Stale: >120s without pong @@ -215,7 +216,7 @@ class HandlerStabilityTest extends TestCase public function test_connection_stable_under_message_bombardment() { $connection = $this->newActiveConnection(['websocket']); - $connection->assertSentEvent('pusher.connection_established'); + $connection->assertSentEvent('websocket.connection_established'); $connection->resetEvents(); // Phase 1: 10s of rapid pings (tryHandlePingFast hot path) @@ -225,7 +226,7 @@ class HandlerStabilityTest extends TestCase while (microtime(true) - $phaseStart < 10) { for ($batch = 0; $batch < 50; $batch++) { - $this->pusherServer->onMessage($connection, $this->pingMsg); + $this->wsHandler->onMessage($connection, $this->pingMsg); $totalPings++; } $totalPongs += count($connection->sentData); @@ -242,8 +243,8 @@ class HandlerStabilityTest extends TestCase $subUnsubCycles = 0; while (microtime(true) - $phaseStart < 10) { - $this->pusherServer->onMessage($connection, $this->unsubMsg); - $this->pusherServer->onMessage($connection, $this->subMsg); + $this->wsHandler->onMessage($connection, $this->unsubMsg); + $this->wsHandler->onMessage($connection, $this->subMsg); $subUnsubCycles++; if ($subUnsubCycles % 25 === 0) { $connection->resetEvents(); @@ -265,15 +266,15 @@ class HandlerStabilityTest extends TestCase $mixedPongs = 0; while (microtime(true) - $phaseStart < 10) { - $this->pusherServer->onMessage($connection, $this->pingMsg); + $this->wsHandler->onMessage($connection, $this->pingMsg); $mixedPings++; - $this->pusherServer->onMessage($connection, $this->subMsg); - $this->pusherServer->onMessage($connection, $this->unsubMsg); - $this->pusherServer->onMessage($connection, $this->subMsg); + $this->wsHandler->onMessage($connection, $this->subMsg); + $this->wsHandler->onMessage($connection, $this->unsubMsg); + $this->wsHandler->onMessage($connection, $this->subMsg); $mixedCount++; if ($mixedCount % 10 === 0) { - $mixedPongs += collect($connection->sentData)->where('event', 'pusher.pong')->count(); + $mixedPongs += collect($connection->sentData)->where('event', 'websocket.pong')->count(); $errors = collect($connection->sentData)->filter( fn($e) => @@ -285,14 +286,14 @@ class HandlerStabilityTest extends TestCase gc_collect_cycles(); } } - $mixedPongs += collect($connection->sentData)->where('event', 'pusher.pong')->count(); + $mixedPongs += collect($connection->sentData)->where('event', 'websocket.pong')->count(); $this->assertEquals($mixedPings, $mixedPongs, 'Phase 3: All pings should produce pongs'); $this->assertGreaterThan(500, $mixedCount, 'Phase 3: Should process substantial mixed volume'); // Final: connection still alive $connection->resetEvents(); - $this->pusherServer->onMessage($connection, $this->pingMsg); - $connection->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($connection, $this->pingMsg); + $connection->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertTrue($channel->hasConnection($connection), 'Connection must survive 30s bombardment'); @@ -314,7 +315,7 @@ class HandlerStabilityTest extends TestCase } foreach ($connections as $conn) { - $conn->assertSentEvent('pusher.connection_established'); + $conn->assertSentEvent('websocket.connection_established'); $conn->resetEvents(); } @@ -324,7 +325,7 @@ class HandlerStabilityTest extends TestCase while (microtime(true) - $start < 10) { foreach ($connections as $conn) { - $this->pusherServer->onMessage($conn, $this->pingMsg); + $this->wsHandler->onMessage($conn, $this->pingMsg); $totalPings++; } // Flush all connections to prevent OOM @@ -345,7 +346,7 @@ class HandlerStabilityTest extends TestCase // Close first 50 for ($i = 0; $i < 50; $i++) { - $this->pusherServer->onClose($connections[$i]); + $this->wsHandler->onClose($connections[$i]); } gc_collect_cycles(); @@ -356,7 +357,7 @@ class HandlerStabilityTest extends TestCase while (microtime(true) - $start2 < 5) { foreach ($remaining as $conn) { - $this->pusherServer->onMessage($conn, $this->pingMsg); + $this->wsHandler->onMessage($conn, $this->pingMsg); $phase2Pings++; } foreach ($remaining as $conn) { @@ -403,7 +404,7 @@ class HandlerStabilityTest extends TestCase while (microtime(true) - $start < 10) { foreach ($allConnections as $conn) { - $this->pusherServer->onMessage($conn, $this->pingMsg); + $this->wsHandler->onMessage($conn, $this->pingMsg); $totalPings++; } foreach ($allConnections as $conn) { @@ -416,7 +417,7 @@ class HandlerStabilityTest extends TestCase // Close all on 'blog' channel foreach ($connections['blog'] as $conn) { - $this->pusherServer->onClose($conn); + $this->wsHandler->onClose($conn); } // Other 4 channels fully operational — verify with ping @@ -425,8 +426,8 @@ class HandlerStabilityTest extends TestCase $this->assertNotNull($channel, "{$channelName} should still exist"); foreach ($connections[$channelName] as $idx => $conn) { $conn->resetEvents(); - $this->pusherServer->onMessage($conn, $this->pingMsg); - $conn->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($conn, $this->pingMsg); + $conn->assertSentEvent('websocket.pong'); $this->assertTrue( $channel->hasConnection($conn), "{$channelName} conn #{$idx} should be subscribed" @@ -445,7 +446,7 @@ class HandlerStabilityTest extends TestCase public function test_rapid_connect_disconnect_cycles() { $permanentConnection = $this->newActiveConnection(['websocket']); - $permanentConnection->assertSentEvent('pusher.connection_established'); + $permanentConnection->assertSentEvent('websocket.connection_established'); $permanentConnection->resetEvents(); $start = microtime(true); @@ -453,13 +454,13 @@ class HandlerStabilityTest extends TestCase while (microtime(true) - $start < 15) { $temp = $this->newActiveConnection(['websocket']); - $this->pusherServer->onClose($temp); + $this->wsHandler->onClose($temp); $cycles++; // Every 100 cycles, verify permanent connection is alive if ($cycles % 100 === 0) { - $this->pusherServer->onMessage($permanentConnection, $this->pingMsg); - $permanentConnection->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($permanentConnection, $this->pingMsg); + $permanentConnection->assertSentEvent('websocket.pong'); $permanentConnection->resetEvents(); gc_collect_cycles(); } @@ -469,8 +470,8 @@ class HandlerStabilityTest extends TestCase // Final verification $permanentConnection->resetEvents(); - $this->pusherServer->onMessage($permanentConnection, $this->pingMsg); - $permanentConnection->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($permanentConnection, $this->pingMsg); + $permanentConnection->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertNotNull($channel); @@ -494,7 +495,7 @@ class HandlerStabilityTest extends TestCase $bad = $this->newActiveConnection(['websocket']); $bad->resetEvents(); - $this->pusherServer->onMessage($bad, new Mocks\Message([ + $this->wsHandler->onMessage($bad, new Mocks\Message([ 'event' => 'blog.show[abc123]', 'data' => ['id' => '123'], 'channel' => 'nonexistent-channel', @@ -503,16 +504,16 @@ class HandlerStabilityTest extends TestCase $good1->resetEvents(); $good2->resetEvents(); - $this->pusherServer->onMessage($good1, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($good1, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $this->pusherServer->onMessage($good2, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($good2, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $good1->assertSentEvent('pusher.pong'); - $good2->assertSentEvent('pusher.pong'); + $good1->assertSentEvent('websocket.pong'); + $good2->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertTrue($channel->hasConnection($good1)); @@ -528,16 +529,17 @@ class HandlerStabilityTest extends TestCase public function test_subscription_not_established_error_is_recoverable() { $connection = $this->newActiveConnection(['websocket']); - $connection->assertSentEvent('pusher.connection_established'); - $connection->assertSentEvent('pusher_internal:subscription_succeeded'); + $connection->assertSentEvent('websocket.connection_established'); + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', 'data' => ['channel' => 'websocket'], ])); + // After unsubscribe, sending a message should get :error $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ + $this->wsHandler->onMessage($connection, new Mocks\Message([ 'event' => 'pusher.custom[xyz789]', 'data' => ['test' => 'recovery'], 'channel' => 'websocket', @@ -547,24 +549,25 @@ class HandlerStabilityTest extends TestCase $this->assertNotNull($errorEvent, 'Should get :error'); $this->assertEquals('Subscription not established', $errorEvent['data']['message']); + // Re-subscribe and verify recovery $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.subscribe', + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', 'data' => ['channel' => 'websocket'], ])); $channel = $this->channelManager->find('1234', 'websocket'); - $this->assertTrue($channel->hasConnection($connection), 'Re-subscribed'); + $this->assertTrue($channel->hasConnection($connection), 'Re-subscribed after error'); + // Protocol events should get :response again after recovery $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.custom[def456]', - 'data' => ['test' => 'post-recovery'], - 'channel' => 'websocket', + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', + 'data' => ['channel' => 'websocket'], ])); - $responseEvent = collect($connection->sentData)->firstWhere('event', 'pusher.custom[def456]:response'); - $this->assertNotNull($responseEvent, 'Post-recovery should get :response'); + $responseEvent = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response'); + $this->assertNotNull($responseEvent, 'Post-recovery subscribe should get :response'); $this->assertEquals('Success', $responseEvent['data']['message']); } @@ -580,24 +583,24 @@ class HandlerStabilityTest extends TestCase $conn3 = $this->newActiveConnection(['websocket']); $exception = new \BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey('BadKey'); - $this->pusherServer->onError($conn1, $exception); + $this->wsHandler->onError($conn1, $exception); - $conn1->assertSentEvent('pusher.error'); - $conn2->assertNotSentEvent('pusher.error'); - $conn3->assertNotSentEvent('pusher.error'); + $conn1->assertSentEvent('websocket.error'); + $conn2->assertNotSentEvent('websocket.error'); + $conn3->assertNotSentEvent('websocket.error'); $conn2->resetEvents(); $conn3->resetEvents(); - $this->pusherServer->onMessage($conn2, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($conn2, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $this->pusherServer->onMessage($conn3, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($conn3, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $conn2->assertSentEvent('pusher.pong'); - $conn3->assertSentEvent('pusher.pong'); + $conn2->assertSentEvent('websocket.pong'); + $conn3->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertTrue($channel->hasConnection($conn2)); @@ -615,20 +618,20 @@ class HandlerStabilityTest extends TestCase $survivor2 = $this->newActiveConnection(['websocket']); $doomed = $this->newActiveConnection(['websocket']); - $this->pusherServer->onClose($doomed); + $this->wsHandler->onClose($doomed); $survivor1->resetEvents(); $survivor2->resetEvents(); - $this->pusherServer->onMessage($survivor1, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($survivor1, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $this->pusherServer->onMessage($survivor2, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($survivor2, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $survivor1->assertSentEvent('pusher.pong'); - $survivor2->assertSentEvent('pusher.pong'); + $survivor1->assertSentEvent('websocket.pong'); + $survivor2->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertNotNull($channel); @@ -650,17 +653,17 @@ class HandlerStabilityTest extends TestCase $rawMessage = $this->createRawMessage('{invalid json!!!}'); try { - $this->pusherServer->onMessage($badConn, $rawMessage); + $this->wsHandler->onMessage($badConn, $rawMessage); } catch (\Throwable $e) { // Handler should catch, but even if it propagates, others unaffected } $goodConn->resetEvents(); - $this->pusherServer->onMessage($goodConn, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($goodConn, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $goodConn->assertSentEvent('pusher.pong'); + $goodConn->assertSentEvent('websocket.pong'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertTrue($channel->hasConnection($goodConn)); @@ -678,9 +681,9 @@ class HandlerStabilityTest extends TestCase public function test_full_client_lifecycle_mirrors_frontend() { $connection = $this->newConnection('TestKey'); - $this->pusherServer->onOpen($connection); + $this->wsHandler->onOpen($connection); - $established = collect($connection->sentData)->firstWhere('event', 'pusher.connection_established'); + $established = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established'); $this->assertNotNull($established); $data = json_decode($established['data'], true); @@ -688,49 +691,49 @@ class HandlerStabilityTest extends TestCase $this->assertNotEmpty($data['socket_id']); $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.subscribe', + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', 'data' => ['channel' => 'websocket', 'auth' => 'TestKey:fake-signature'], ])); - $connection->assertSentEvent('pusher_internal:subscription_succeeded'); - $connection->assertSentEvent('pusher.subscribe:response'); + $connection->assertSentEvent('websocket_internal.subscription_succeeded'); + $connection->assertSentEvent('websocket.subscribe:response'); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertNotNull($channel); $this->assertTrue($channel->hasConnection($connection)); $connection->resetEvents(); - $this->pusherServer->onMessage($connection, $this->pingMsg); - $connection->assertSentEvent('pusher.pong'); + $this->wsHandler->onMessage($connection, $this->pingMsg); + $connection->assertSentEvent('websocket.pong'); - $this->pusherServer->onClose($connection); + $this->wsHandler->onClose($connection); $this->assertFalse($channel->hasConnection($connection)); } /** - * Pusher-prefixed events get :response suffix from handlePusherEvent(). + * Protocol events get :response suffix from handleProtocolEvent(). * * @group protocol */ - public function test_pusher_events_get_response_suffix() + public function test_protocol_events_get_response_suffix() { $connection = $this->newActiveConnection(['websocket']); $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.custom-event', - 'data' => ['payload' => 'test'], - 'channel' => 'websocket', + // Subscribe/unsubscribe are the protocol events that get :response + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.subscribe', + 'data' => ['channel' => 'websocket'], ])); - $response = collect($connection->sentData)->firstWhere('event', 'pusher.custom-event:response'); - $this->assertNotNull($response, 'Pusher events should get :response'); + $response = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response'); + $this->assertNotNull($response, 'Protocol subscribe events should get :response'); $this->assertEquals('Success', $response['data']['message']); } /** - * Both ping formats produce pongs: pusher.ping (frontend) and pusher:ping (Pusher spec). + * Both ping formats produce pongs: websocket.ping (dot) and pusher:ping (colon, backward compat). * * @group protocol */ @@ -739,47 +742,49 @@ class HandlerStabilityTest extends TestCase $connection = $this->newActiveConnection(['websocket']); $connection->resetEvents(); - $this->pusherServer->onMessage($connection, new Mocks\Message([ - 'event' => 'pusher.ping', + $this->wsHandler->onMessage($connection, new Mocks\Message([ + 'event' => 'websocket.ping', 'data' => new \stdClass(), ])); - $this->pusherServer->onMessage($connection, new Mocks\Message([ + $this->wsHandler->onMessage($connection, new Mocks\Message([ 'event' => 'pusher:ping', 'data' => new \stdClass(), ])); $this->assertEquals( 2, - collect($connection->sentData)->where('event', 'pusher.pong')->count(), + collect($connection->sentData)->where('event', 'websocket.pong')->count(), 'Both ping formats should produce pongs' ); } /** - * Unsubscribe only works with dot format (pusher.unsubscribe). - * Colon format (pusher:unsubscribe) is NOT recognized. + * Unsubscribe works with dot format (websocket.unsubscribe). + * Colon format (pusher:unsubscribe) is also recognized via isProtocolAction(). * * @group protocol */ - public function test_unsubscribe_only_works_with_dot_format() + public function test_unsubscribe_works_with_both_dot_and_colon_format() { + // Dot format: websocket.unsubscribe $conn1 = $this->newActiveConnection(['websocket']); - $this->pusherServer->onMessage($conn1, new Mocks\Message([ - 'event' => 'pusher.unsubscribe', + $this->wsHandler->onMessage($conn1, new Mocks\Message([ + 'event' => 'websocket.unsubscribe', 'data' => ['channel' => 'websocket'], ])); $channel = $this->channelManager->find('1234', 'websocket'); $this->assertFalse($channel->hasConnection($conn1), 'Dot-format unsubscribes'); + // Colon format: pusher:unsubscribe — also recognized by isProtocolAction() $conn2 = $this->newActiveConnection(['websocket']); - $this->pusherServer->onMessage($conn2, new Mocks\Message([ + $this->wsHandler->onMessage($conn2, new Mocks\Message([ 'event' => 'pusher:unsubscribe', 'data' => ['channel' => 'websocket'], ])); $channel = $this->channelManager->find('1234', 'websocket'); - $this->assertTrue($channel->hasConnection($conn2), 'Colon-format does NOT unsubscribe'); + $this->assertFalse($channel->hasConnection($conn2), 'Colon-format also unsubscribes'); } /** @@ -792,7 +797,7 @@ class HandlerStabilityTest extends TestCase $connection = new Mocks\Connection(); $connection->httpRequest = new \GuzzleHttp\Psr7\Request('GET', '/?appKey=TestKey'); - $this->pusherServer->onMessage($connection, $this->pingMsg); + $this->wsHandler->onMessage($connection, $this->pingMsg); $this->assertEmpty($connection->sentData, 'No data sent to connection without app'); } diff --git a/tests/WebsocketServiceTest.php b/tests/WebsocketServiceTest.php new file mode 100644 index 0000000..f0fe2ec --- /dev/null +++ b/tests/WebsocketServiceTest.php @@ -0,0 +1,210 @@ +forever('ws_active_channels', ['websocket', 'private-user']); + cache()->forever('ws_socket_authed_users', ['1.1' => 1]); + cache()->forever('ws_connection_1-1', ['id' => '1.1']); + + $this->assertTrue(WebsocketService::resetAllTracking()); + + $this->assertEmpty(WebsocketService::getActiveChannels()); + $this->assertEmpty(WebsocketService::getAuthedUsers()); + } + + // ========================================================================= + // User auth tracking + // ========================================================================= + + public function test_set_user_authed_stores_user_in_cache() + { + $user = (object) ['id' => 42]; + + WebsocketService::setUserAuthed('1.123456', $user); + + $this->assertEquals([ + '1.123456' => 42, + ], WebsocketService::getAuthedUsers()); + } + + public function test_get_auth_returns_stored_user_for_socket() + { + $user = (object) ['id' => 42, 'name' => 'Rick']; + + WebsocketService::setUserAuthed('1.123456', $user); + + $auth = WebsocketService::getAuth('1.123456'); + $this->assertNotNull($auth); + $this->assertEquals(42, $auth->id); + $this->assertEquals('Rick', $auth->name); + } + + public function test_get_auth_returns_null_for_unknown_socket() + { + $this->assertNull(WebsocketService::getAuth('99.999')); + } + + public function test_clear_user_authed_removes_user() + { + $user = (object) ['id' => 42]; + WebsocketService::setUserAuthed('1.123', $user); + + $this->assertCount(1, WebsocketService::getAuthedUsers()); + + WebsocketService::clearUserAuthed('1.123'); + + $this->assertEmpty(WebsocketService::getAuthedUsers()); + $this->assertNull(WebsocketService::getAuth('1.123')); + } + + public function test_multiple_users_can_be_authed_simultaneously() + { + WebsocketService::setUserAuthed('1.100', (object) ['id' => 1]); + WebsocketService::setUserAuthed('1.200', (object) ['id' => 2]); + WebsocketService::setUserAuthed('1.300', (object) ['id' => 3]); + + $authed = WebsocketService::getAuthedUsers(); + + $this->assertCount(3, $authed); + $this->assertEquals(1, $authed['1.100']); + $this->assertEquals(2, $authed['1.200']); + $this->assertEquals(3, $authed['1.300']); + } + + public function test_clear_one_user_does_not_affect_others() + { + WebsocketService::setUserAuthed('1.100', (object) ['id' => 1]); + WebsocketService::setUserAuthed('1.200', (object) ['id' => 2]); + + WebsocketService::clearUserAuthed('1.100'); + + $authed = WebsocketService::getAuthedUsers(); + $this->assertCount(1, $authed); + $this->assertEquals(2, $authed['1.200']); + } + + // ========================================================================= + // isUserConnected / getUserSocketIds + // ========================================================================= + + public function test_is_user_connected_returns_true_for_authed_user() + { + WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]); + + $this->assertTrue(WebsocketService::isUserConnected(42)); + } + + public function test_is_user_connected_returns_false_for_unknown_user() + { + $this->assertFalse(WebsocketService::isUserConnected(999)); + } + + public function test_is_user_connected_returns_false_after_clear() + { + WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]); + WebsocketService::clearUserAuthed('1.100'); + + $this->assertFalse(WebsocketService::isUserConnected(42)); + } + + public function test_get_user_socket_ids_returns_all_sockets_for_user() + { + // Same user connected from two devices + WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]); + WebsocketService::setUserAuthed('1.200', (object) ['id' => 42]); + WebsocketService::setUserAuthed('1.300', (object) ['id' => 99]); + + $sockets = WebsocketService::getUserSocketIds(42); + + $this->assertCount(2, $sockets); + $this->assertContains('1.100', $sockets); + $this->assertContains('1.200', $sockets); + } + + public function test_get_user_socket_ids_returns_empty_for_unknown_user() + { + $this->assertEmpty(WebsocketService::getUserSocketIds(999)); + } + + // ========================================================================= + // Channel and connection tracking + // ========================================================================= + + public function test_get_active_channels_returns_empty_by_default() + { + $this->assertEmpty(WebsocketService::getActiveChannels()); + } + + public function test_get_active_channels_returns_cached_channels() + { + cache()->forever('ws_active_channels', ['websocket', 'private-user.1']); + + $channels = WebsocketService::getActiveChannels(); + + $this->assertEquals(['websocket', 'private-user.1'], $channels); + } + + public function test_get_channel_connections_returns_empty_for_unknown_channel() + { + $this->assertEmpty(WebsocketService::getChannelConnections('nonexistent')); + } + + public function test_get_channel_connections_returns_cached_sockets() + { + cache()->forever('ws_channel_connections_websocket', ['1.100', '1.200', '1.300']); + + $connections = WebsocketService::getChannelConnections('websocket'); + + $this->assertCount(3, $connections); + $this->assertContains('1.100', $connections); + } + + // ========================================================================= + // BroadcastClient availability + // ========================================================================= + + public function test_ws_available_returns_false_when_socket_file_missing() + { + // No Unix socket file exists in test — ws_available should be false + $this->assertFalse(ws_available()); + } + + public function test_websocket_service_whisper_returns_false_when_unavailable() + { + $this->assertFalse( + WebsocketService::whisper('event', ['data' => 'test'], ['1.100']) + ); + } + + public function test_websocket_service_broadcast_except_returns_false_when_unavailable() + { + $this->assertFalse( + WebsocketService::broadcastExcept('event', ['data' => 'test'], ['1.100']) + ); + } +}