From d1bfbeb5398f086e0a9fa0c950688d6e794511a6 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 27 Nov 2023 14:47:52 +0000 Subject: [PATCH] Implements Autobahn tests against the WebSocket spec (#22) * implement spec tests * update action * formatting * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * check for frame * Fix code styling --- .github/workflows/spec-tests.yml | 57 ++++++++++++++++++++++++++++ src/WebSockets/WsConnection.php | 18 ++++++--- tests/Feature/Reverb/ServerTest.php | 8 ---- tests/ReverbTestCase.php | 18 --------- tests/Specification/client-spec.json | 11 ++++++ tests/Specification/spec-analyze.php | 33 ++++++++++++++++ tests/Specification/spec-server.php | 40 +++++++++++++++++++ 7 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/spec-tests.yml create mode 100644 tests/Specification/client-spec.json create mode 100644 tests/Specification/spec-analyze.php create mode 100644 tests/Specification/spec-server.php diff --git a/.github/workflows/spec-tests.yml b/.github/workflows/spec-tests.yml new file mode 100644 index 00000000..57ec80f0 --- /dev/null +++ b/.github/workflows/spec-tests.yml @@ -0,0 +1,57 @@ +name: spec tests + +on: + push: + branches: + - main + - '*.x' + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: [8.2, 8.3] + laravel: [10] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: | + composer require "illuminate/contracts=^${{ matrix.laravel }}" --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Pull Autobahn Docker image + run: docker pull crossbario/autobahn-testsuite + + - name: Start WebSocket server + working-directory: tests/Specification + run: php spec-server.php & + + - name: Run specification tests + working-directory: tests/Specification + run: | + docker run --rm \ + -v $PWD:/mnt/autobahn \ + -v $PWD/reports:/mnt/autobahn/reports \ + --add-host host.docker.internal:host-gateway \ + crossbario/autobahn-testsuite \ + wstest -m fuzzingclient -s /mnt/autobahn/client-spec.json + + - name: Analyze test results + working-directory: tests/Specification + run: php spec-analyze.php diff --git a/src/WebSockets/WsConnection.php b/src/WebSockets/WsConnection.php index 28c5da39..9a2cbbca 100644 --- a/src/WebSockets/WsConnection.php +++ b/src/WebSockets/WsConnection.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use Laravel\Reverb\Http\Connection; use Ratchet\RFC6455\Messaging\CloseFrameChecker; +use Ratchet\RFC6455\Messaging\DataInterface; use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\FrameInterface; use Ratchet\RFC6455\Messaging\MessageBuffer; @@ -56,10 +57,12 @@ public function openBuffer(): void /** * Send a message to the connection. */ - public function send(string $message): void + public function send(mixed $message): void { $this->connection->send( - (new Frame($message))->getContents() + $message instanceof DataInterface ? + $message->getContents() : + (new Frame($message))->getContents() ); } @@ -69,8 +72,9 @@ public function send(string $message): void public function control(FrameInterface $message): void { match ($message->getOpcode()) { - Frame::OP_PING => $this->send(new Frame('pong', opcode: Frame::OP_PONG)), - Frame::OP_CLOSE => $this->close(), + Frame::OP_PING => $this->send(new Frame($message->getPayload(), opcode: Frame::OP_PONG)), + Frame::OP_PONG => fn () => null, + Frame::OP_CLOSE => $this->close($message), }; } @@ -93,8 +97,12 @@ public function onClose(callable $callback): void /** * Close the connection. */ - public function close(): void + public function close(FrameInterface $frame = null): void { + if ($frame) { + $this->send($frame); + } + $this->connection->close(); } diff --git a/tests/Feature/Reverb/ServerTest.php b/tests/Feature/Reverb/ServerTest.php index 93364eba..d2180fed 100644 --- a/tests/Feature/Reverb/ServerTest.php +++ b/tests/Feature/Reverb/ServerTest.php @@ -6,7 +6,6 @@ use Laravel\Reverb\Jobs\PingInactiveConnections; use Laravel\Reverb\Jobs\PruneStaleConnections; use Laravel\Reverb\Tests\ReverbTestCase; -use Ratchet\RFC6455\Messaging\Frame; use React\Promise\Deferred; use function Ratchet\Client\connect; @@ -315,13 +314,6 @@ $this->connect(); }); -it('can receive a pong control frame', function () { - $frame = new Frame('ping', true, Frame::OP_PING); - $response = $this->sendRaw($frame); - - expect($response)->toBe('pong'); -}); - it('clears application state between requests', function () { $this->subscribe('test-channel'); diff --git a/tests/ReverbTestCase.php b/tests/ReverbTestCase.php index e9ae48bc..e92e3c55 100644 --- a/tests/ReverbTestCase.php +++ b/tests/ReverbTestCase.php @@ -182,24 +182,6 @@ public function send(array $message, WebSocket $connection = null): string return await($promise->promise()); } - /** - * Send a message to the connected client. - */ - public function sendRaw(mixed $message, WebSocket $connection = null): string - { - $promise = new Deferred; - - $connection = $connection ?: $this->connect(); - - $connection->on('message', function ($message) use ($promise) { - $promise->resolve((string) $message); - }); - - $connection->send($message); - - return await($promise->promise()); - } - /** * Disconnect the connected client. */ diff --git a/tests/Specification/client-spec.json b/tests/Specification/client-spec.json new file mode 100644 index 00000000..7060a60b --- /dev/null +++ b/tests/Specification/client-spec.json @@ -0,0 +1,11 @@ +{ + "options": { "failByDrop": false }, + "outdir": "/mnt/autobahn/reports", + "servers": [{ "agent": "Reverb", "url": "ws://host.docker.internal:8080", "options": { "version": 18 } }], + "cases": ["*"], + "exclude-cases": [ + "12.*", + "13.*" + ], + "exclude-agent-cases": {} + } \ No newline at end of file diff --git a/tests/Specification/spec-analyze.php b/tests/Specification/spec-analyze.php new file mode 100644 index 00000000..fee9ab9a --- /dev/null +++ b/tests/Specification/spec-analyze.php @@ -0,0 +1,33 @@ + $result) { + if ($result['behavior'] === 'INFORMATIONAL') { + continue; + } + + if (in_array($result['behavior'], ['OK', 'NON-STRICT'])) { + echo '✅ Test case '.$name.' passed.'.PHP_EOL; + } else { + $hasFailures = true; + echo '❌ Test case '.$name.' failed.'.PHP_EOL; + } +} + +exit($hasFailures ? 1 : 0); diff --git a/tests/Specification/spec-server.php b/tests/Specification/spec-server.php new file mode 100644 index 00000000..4be0dbaa --- /dev/null +++ b/tests/Specification/spec-server.php @@ -0,0 +1,40 @@ +start(); + +function routes() +{ + $routes = new RouteCollection; + $routes->add( + 'sockets', + Route::get('/', function (RequestInterface $request, WsConnection $connection) { + $connection->onMessage(function ($message) use ($connection) { + $connection->send($message); + }); + $connection->openBuffer(); + }) + ); + + return $routes; +}