Anmelden
DE
apps/caramail-server / tests / TcpServerTest.php · commit 070afda
<?phpnamespace Tests;use App\Chat\ChatServer;use App\Chat\RoomStore;use App\Chat\TempId;use App\Net\TcpServer;use PHPUnit\Framework\TestCase;/** The real thing: a server on a TCP port of this machine, two clients talking through it. */final class TcpServerTest extends TestCase{    private TcpServer $server;    private string $address;    private string $file;    protected function setUp(): void    {        if (PHP_INT_SIZE === 4 || getenv('JUMPHP_WEB_TERMINAL')) {            self::markTestSkipped('no listening socket in a browser');        }        $this->file = sys_get_temp_dir() . '/caramail-tcp-' . bin2hex(random_bytes(4)) . '.json';        $this->server = new TcpServer(new ChatServer(new RoomStore($this->file)));        $this->address = $this->server->listen('127.0.0.1', 0);        $this->wsAddress = $this->server->listenWebSocket('127.0.0.1', 0);    }    private string $wsAddress;    /** A masked client frame. */    private static function wsFrame(int $opcode, string $payload): string    {        $mask = random_bytes(4);        $masked = '';        for ($i = 0; $i < strlen($payload); $i++) {            $masked .= $payload[$i] ^ $mask[$i % 4];        }        $len = strlen($payload);        return chr(0x80 | $opcode) . ($len < 126 ? chr(0x80 | $len) : chr(0x80 | 126) . pack('n', $len)) . $mask . $masked;    }    /** @return list<array{0: int, 1: string}> the server's frames read until one matches $until */    private function wsRead($client, string &$buffer, \Closure $until): array    {        $frames = [];        for ($i = 0; $i < 200; $i++) {            $this->server->pollOnce(0.01);            $buffer .= (string)fread($client, 65536);            while (strlen($buffer) >= 2) {                $len = ord($buffer[1]) & 0x7F;                $pos = 2;                if ($len === 126) {                    if (strlen($buffer) < 4) {                        break;                    }                    $len = unpack('n', substr($buffer, 2, 2))[1];                    $pos = 4;                }                if (strlen($buffer) < $pos + $len) {                    break;                }                $frames[] = $frame = [ord($buffer[0]) & 0x0F, substr($buffer, $pos, $len)];                $buffer = (string)substr($buffer, $pos + $len);                if ($until($frame)) {                    return $frames;                }            }        }        self::fail('no expected frame; got ' . json_encode($frames));    }    protected function tearDown(): void    {        @unlink($this->file);    }    /** @return resource */    private function client()    {        $c = stream_socket_client('tcp://' . $this->address, $errno, $error, 2);        self::assertNotFalse($c, $error);        stream_set_blocking($c, false);        return $c;    }    /** Runs the server until $client got a line starting with $code; returns that line. */    private function waitFor($client, string $code, string &$buffer): string    {        for ($i = 0; $i < 200; $i++) {            $this->server->pollOnce(0.01);            $buffer .= (string)fread($client, 65536);            foreach (explode("\n", $buffer) as $n => $line) {                if (str_starts_with($line, $code . "\t")) {                    $buffer = implode("\n", array_slice(explode("\n", $buffer), $n + 1));                    return $line;                }            }        }        self::fail("no $code frame; got: " . json_encode($buffer));    }    public function testAWebSocketClientChatsWithATcpClient(): void    {        $ws = stream_socket_client('tcp://' . $this->wsAddress, $errno, $error, 2);        stream_set_blocking($ws, false);        fwrite($ws, "GET /ws HTTP/1.1\r\nHost: chat\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n");        $buf = '';        for ($i = 0; $i < 100 && !str_contains($buf, "\r\n\r\n"); $i++) {            $this->server->pollOnce(0.01);            $buf .= (string)fread($ws, 65536);        }        self::assertStringContainsString('s3pPLMBiTxaQ9kYGzzhZRbK+xOo=', $buf, 'the RFC 6455 accept key');        $buf = (string)substr($buf, strpos($buf, "\r\n\r\n") + 4);        $this->wsRead($ws, $buf, fn ($f) => $f[0] === 1 && str_contains($f[1], 'hello'));        fwrite($ws, self::wsFrame(1, '{"t":"join","room":"CHAT"}'));        $this->wsRead($ws, $buf, fn ($f) => $f[0] === 1 && str_contains($f[1], '"room"'));        $frames = $this->wsRead($ws, $buf, fn ($f) => $f[0] === 2 && str_contains($f[1], 'NewCaraChat'));        self::assertSame("\x00100\t" . ChatServer::BANNER . "\n", end($frames)[1], 'peer 0 + the banner frame');        fwrite($ws, self::wsFrame(2, "\x00" . "0\t0\t" . TempId::make('Web', 30, 0, 'Nice') . "\n20\tAccueil\t0\n"));        $this->wsRead($ws, $buf, fn ($f) => $f[0] === 2 && str_starts_with($f[1], "\x0020\t"));        $tcp = $this->client();        $tb = '';        $this->waitFor($tcp, '100', $tb);        fwrite($tcp, "0\t0\t" . TempId::make('Tcp', 40, 1, 'Brest') . "\n20\tAccueil\t0\n70\t0100Coucou le web\n");        $frames = $this->wsRead($ws, $buf, fn ($f) => $f[0] === 2 && str_contains($f[1], 'Coucou le web'));        self::assertStringStartsWith("\x0070\t", end($frames)[1]);        fclose($ws);        fclose($tcp);    }    public function testTwoClientsChatInARoomOverTcp(): void    {        $a = $this->client();        $b = $this->client();        $ba = $bb = '';        self::assertSame("100\t" . ChatServer::BANNER, $this->waitFor($a, '100', $ba));        $this->waitFor($b, '100', $bb);        fwrite($a, "0\t0\t" . TempId::make('Alice', 24, 1, 'Lyon') . "\n");        $this->waitFor($a, '56', $ba);        // a frame cut in two writes is put back together        fwrite($b, "0\t0\t" . TempId::make('Bob', 30, 0, 'Paris'));        $this->server->pollOnce(0.01);        fwrite($b, "\n");        $this->waitFor($b, '56', $bb);        fwrite($a, "20\tAccueil\t0\n");        $this->waitFor($a, '20', $ba);        fwrite($b, "20\tAccueil\t0\n");        $this->waitFor($b, '20', $bb);        fwrite($b, "70\t0100Bonsoir\n");        self::assertStringEndsWith("\t0\tBob\t0100Bonsoir\t", $this->waitFor($a, '70', $ba));        self::assertSame(2, $this->server->clientCount());        fclose($b);        self::assertStringStartsWith("85\t1\t", $this->waitFor($a, '85', $ba));        self::assertSame(1, $this->server->clientCount());        fclose($a);    }}