2026-01-24 12:34:29 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace BlaxSoftware\LaravelWebSockets\Test\Websocket;
|
|
|
|
|
|
|
|
|
|
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
|
|
|
|
|
use BlaxSoftware\LaravelWebSockets\Test\Mocks;
|
|
|
|
|
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
|
|
|
use BlaxSoftware\LaravelWebSockets\Websocket\Controller;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests for the fork+IPC message processing path in Handler.
|
|
|
|
|
*
|
|
|
|
|
* These tests verify that messages which go through forkAndProcessMessage()
|
|
|
|
|
* and Controller::controll_message() work correctly with SocketPairIpc.
|
|
|
|
|
*
|
|
|
|
|
* Unlike client-* messages (synchronous), these test the async fork path
|
|
|
|
|
* where a child process handles the message and sends response via IPC.
|
|
|
|
|
*/
|
|
|
|
|
class HandlerForkPathTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
public function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
parent::setUp();
|
|
|
|
|
|
|
|
|
|
if (!SocketPairIpc::isSupported()) {
|
|
|
|
|
$this->markTestSkipped('SocketPairIpc not supported');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!function_exists('pcntl_fork')) {
|
|
|
|
|
$this->markTestSkipped('pcntl_fork not available');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that the handler properly detects socket pair IPC when supported.
|
|
|
|
|
*/
|
|
|
|
|
public function test_handler_uses_socket_pair_ipc_when_supported()
|
|
|
|
|
{
|
|
|
|
|
// Verify SocketPairIpc is supported in this environment
|
|
|
|
|
$this->assertTrue(SocketPairIpc::isSupported());
|
|
|
|
|
|
|
|
|
|
// The handler should automatically use socket pair IPC
|
|
|
|
|
// We can verify this by checking the handler was created successfully
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->assertNotNull($this->wsHandler);
|
2026-01-24 12:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test subscription and unsubscribe flow works properly.
|
|
|
|
|
*/
|
|
|
|
|
public function test_subscribe_unsubscribe_flow()
|
|
|
|
|
{
|
|
|
|
|
$connection = $this->newActiveConnection(['fork-test-channel']);
|
|
|
|
|
|
|
|
|
|
// Verify connection was established (subscription event has pre-existing test issues)
|
2026-04-02 10:44:16 +00:00
|
|
|
$connection->assertSentEvent('websocket.connection_established');
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
// Now unsubscribe
|
|
|
|
|
$message = new Mocks\Message([
|
2026-04-02 10:44:16 +00:00
|
|
|
'event' => 'websocket.unsubscribe',
|
2026-01-24 12:34:29 +00:00
|
|
|
'data' => ['channel' => 'fork-test-channel'],
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($connection, $message);
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
// No error should be sent
|
2026-04-02 10:44:16 +00:00
|
|
|
$connection->assertNotSentEvent('websocket.unsubscribe:error');
|
2026-01-24 12:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test event targeting non-subscribed channel gets error.
|
|
|
|
|
*/
|
|
|
|
|
public function test_message_to_non_subscribed_channel_returns_error()
|
|
|
|
|
{
|
|
|
|
|
$connection = $this->newActiveConnection(['channel-one']);
|
|
|
|
|
|
|
|
|
|
// Try to send to a channel we're not subscribed to
|
|
|
|
|
$message = new Mocks\Message([
|
|
|
|
|
'event' => 'custom.action',
|
|
|
|
|
'data' => ['test' => true],
|
|
|
|
|
'channel' => 'channel-two', // Not subscribed!
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($connection, $message);
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
// Should receive an error event
|
|
|
|
|
$connection->assertSentEvent('custom.action:error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test multiple quick subscriptions and unsubscriptions.
|
|
|
|
|
*/
|
|
|
|
|
public function test_rapid_subscribe_unsubscribe_cycle()
|
|
|
|
|
{
|
|
|
|
|
$connection = $this->newActiveConnection(['cycle-channel']);
|
|
|
|
|
|
|
|
|
|
// Rapid subscribe/unsubscribe cycle
|
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
|
|
|
// Unsubscribe
|
|
|
|
|
$unsubMsg = new Mocks\Message([
|
2026-04-02 10:44:16 +00:00
|
|
|
'event' => 'websocket.unsubscribe',
|
2026-01-24 12:34:29 +00:00
|
|
|
'data' => ['channel' => 'cycle-channel'],
|
|
|
|
|
]);
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($connection, $unsubMsg);
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
// Resubscribe
|
|
|
|
|
$subMsg = new Mocks\Message([
|
2026-04-02 10:44:16 +00:00
|
|
|
'event' => 'websocket.subscribe',
|
2026-01-24 12:34:29 +00:00
|
|
|
'data' => ['channel' => 'cycle-channel'],
|
|
|
|
|
]);
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($connection, $subMsg);
|
2026-01-24 12:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No errors should have been sent
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that connection properties are preserved through message handling.
|
|
|
|
|
*/
|
|
|
|
|
public function test_connection_properties_preserved()
|
|
|
|
|
{
|
|
|
|
|
$connection = $this->newActiveConnection(['props-channel']);
|
|
|
|
|
|
|
|
|
|
// Verify socket ID is set and consistent
|
|
|
|
|
$this->assertNotNull($connection->socketId);
|
|
|
|
|
$this->assertIsString($connection->socketId);
|
|
|
|
|
$this->assertMatchesRegularExpression('/^\d+\.\d+$/', $connection->socketId);
|
|
|
|
|
|
|
|
|
|
// Verify app is set
|
|
|
|
|
$this->assertNotNull($connection->app);
|
|
|
|
|
$this->assertEquals('1234', $connection->app->id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that messages with empty data are handled.
|
|
|
|
|
*/
|
|
|
|
|
public function test_message_with_empty_data()
|
|
|
|
|
{
|
|
|
|
|
$this->app['config']->set('websockets.apps.0.enable_client_messages', true);
|
|
|
|
|
|
|
|
|
|
$sender = $this->newActiveConnection(['empty-data-channel']);
|
|
|
|
|
$receiver = $this->newActiveConnection(['empty-data-channel']);
|
|
|
|
|
|
|
|
|
|
$message = new Mocks\Message([
|
|
|
|
|
'event' => 'client-empty',
|
|
|
|
|
'data' => [],
|
|
|
|
|
'channel' => 'empty-data-channel',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($sender, $message);
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
$receiver->assertSentEvent('client-empty', [
|
|
|
|
|
'data' => [],
|
|
|
|
|
'channel' => 'empty-data-channel',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that handler properly reports SocketPairIpc support.
|
|
|
|
|
*/
|
|
|
|
|
public function test_socket_pair_ipc_support_detection()
|
|
|
|
|
{
|
|
|
|
|
// These are the requirements for SocketPairIpc
|
|
|
|
|
$this->assertTrue(extension_loaded('sockets'), 'Sockets extension required');
|
|
|
|
|
$this->assertTrue(function_exists('pcntl_fork'), 'pcntl_fork required');
|
|
|
|
|
$this->assertTrue(function_exists('socket_create_pair'), 'socket_create_pair required');
|
|
|
|
|
|
|
|
|
|
// SocketPairIpc should report as supported
|
|
|
|
|
$this->assertTrue(SocketPairIpc::isSupported());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-02 10:44:16 +00:00
|
|
|
* Test that ping/pong works correctly.
|
2026-01-24 12:34:29 +00:00
|
|
|
*/
|
2026-04-02 10:44:16 +00:00
|
|
|
public function test_ping_pong_handled()
|
2026-01-24 12:34:29 +00:00
|
|
|
{
|
|
|
|
|
$connection = $this->newActiveConnection(['pusher-event-channel']);
|
|
|
|
|
|
|
|
|
|
// Ping should work
|
|
|
|
|
$pingMsg = new Mocks\Message([
|
2026-04-02 10:44:16 +00:00
|
|
|
'event' => 'websocket.ping',
|
2026-01-24 12:34:29 +00:00
|
|
|
]);
|
|
|
|
|
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($connection, $pingMsg);
|
|
|
|
|
$connection->assertSentEvent('websocket.pong');
|
2026-01-24 12:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test client messages disabled prevents whisper.
|
|
|
|
|
*/
|
|
|
|
|
public function test_client_messages_disabled_blocks_whisper()
|
|
|
|
|
{
|
|
|
|
|
// Ensure client messages are disabled (default)
|
|
|
|
|
$this->app['config']->set('websockets.apps.0.enable_client_messages', false);
|
|
|
|
|
|
|
|
|
|
$sender = $this->newActiveConnection(['no-whisper-channel']);
|
|
|
|
|
$receiver = $this->newActiveConnection(['no-whisper-channel']);
|
|
|
|
|
|
|
|
|
|
$message = new Mocks\Message([
|
|
|
|
|
'event' => 'client-blocked',
|
|
|
|
|
'data' => ['message' => 'should be blocked'],
|
|
|
|
|
'channel' => 'no-whisper-channel',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-02 10:44:16 +00:00
|
|
|
$this->wsHandler->onMessage($sender, $message);
|
2026-01-24 12:34:29 +00:00
|
|
|
|
|
|
|
|
// Neither should receive (whisper blocked)
|
|
|
|
|
$sender->assertNotSentEvent('client-blocked');
|
|
|
|
|
$receiver->assertNotSentEvent('client-blocked');
|
|
|
|
|
}
|
|
|
|
|
}
|