laravel-websockets/tests/Websocket/ChildMessageOutcomeTest.php

116 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace BlaxSoftware\LaravelWebSockets\Test\Websocket;
use BlaxSoftware\LaravelWebSockets\Websocket\ChildMessageOutcome;
use PHPUnit\Framework\TestCase;
/**
* The parent answers for a message child that died without replying (#1462),
* and only then: never after a real reply, never for a clean exit, and never
* before the child's last frames have been read.
*/
class ChildMessageOutcomeTest extends TestCase
{
private const EXIT_OK = 0; // exit(0)
private const EXIT_FATAL = 255 << 8; // exit(255), what PHP does after a fatal error
private const KILLED = 9; // terminated by SIGKILL
private array $crashes = [];
private function outcome(string $event = 'atc-traffic.subscribe[x1]'): ChildMessageOutcome
{
$this->crashes = [];
return new ChildMessageOutcome($event, function (int $status) {
$this->crashes[] = $status;
});
}
public function test_a_killed_child_without_reply_is_answered_once_both_signals_are_in()
{
foreach ([[true, false], [false, true]] as [$streamFirst]) {
$outcome = $this->outcome();
$outcome->frameReceived('{"event":"atc-traffic.subscribe[x1]:progress","data":[]}');
if ($streamFirst) {
$outcome->streamClosed();
$this->assertSame([], $this->crashes, 'No verdict before the exit status is known.');
$outcome->exited(self::KILLED);
} else {
$outcome->exited(self::KILLED);
$this->assertSame([], $this->crashes, 'No verdict while frames may still be unread.');
$outcome->streamClosed();
}
$this->assertSame([self::KILLED], $this->crashes);
}
}
public function test_a_fatal_exit_without_reply_is_answered()
{
$outcome = $this->outcome();
$outcome->streamClosed();
$outcome->exited(self::EXIT_FATAL);
$this->assertSame([self::EXIT_FATAL], $this->crashes);
}
public function test_a_child_that_replied_is_left_alone_even_if_it_crashed_afterwards()
{
foreach (['response', 'error'] as $suffix) {
$outcome = $this->outcome();
$outcome->frameReceived('{"event":"atc-traffic.subscribe[x1]:' . $suffix . '","data":{"ok":true},"channel":"websocket"}');
$outcome->streamClosed();
$outcome->exited(self::EXIT_FATAL);
$this->assertTrue($outcome->replied());
$this->assertSame([], $this->crashes, "A {$suffix} frame counts as the reply.");
}
}
public function test_frames_for_other_events_or_broadcasts_are_not_a_reply()
{
$outcome = $this->outcome('dashboard.index');
$outcome->frameReceived('B:{"event":"dashboard.index:response"}');
$outcome->frameReceived('{"event":"dashboard.indexer:response"}');
$outcome->frameReceived('{"data":1,"event":"dashboard.index:response"}');
$outcome->frameReceived(null);
$this->assertFalse($outcome->replied());
}
public function test_event_names_are_matched_in_their_json_encoded_form()
{
$event = 'admin/user.show[a"b]';
$outcome = $this->outcome($event);
$outcome->frameReceived(json_encode(['event' => $event . ':response', 'data' => null]));
$this->assertTrue($outcome->replied());
}
public function test_a_clean_exit_or_an_unknown_status_sends_nothing()
{
foreach ([self::EXIT_OK, null] as $status) {
$outcome = $this->outcome();
$outcome->streamClosed();
$outcome->exited($status);
$this->assertSame([], $this->crashes);
}
}
public function test_the_verdict_is_given_only_once()
{
$outcome = $this->outcome();
$outcome->streamClosed();
$outcome->exited(self::KILLED);
$outcome->streamClosed();
$outcome->exited(self::KILLED);
$this->assertCount(1, $this->crashes);
}
}