PAGI-Server

 view release on metacpan or  search on metacpan

t/52-mandatory-validation.t  view on Meta::CPAN

        PeerAddr => '127.0.0.1', PeerPort => $sse_port, Proto => 'tcp', Timeout => 5,
    );
    ok($sock, 'raw socket connected') or last;
    $sock->blocking(0);

    my $read_until = sub {
        my ($stop) = @_;
        my $buf = '';
        my $deadline = time + 5;
        while (time < $deadline) {
            my $chunk;
            my $n = sysread($sock, $chunk, 4096);
            if (defined $n && $n > 0) { $buf .= $chunk }
            elsif (defined $n && $n == 0) { last }   # EOF
            last if $buf =~ $stop;
            $loop->loop_once(0.02);
        }
        return $buf;
    };

    print $sock "GET /sse-newline-event HTTP/1.1\r\nHost: 127.0.0.1:$sse_port\r\n"
              . "Accept: text/event-stream\r\n\r\n";
    my $stream = $read_until->(qr/\r\n0\r\n\r\n/);
    like($stream, qr/must not contain newline/, 'SSE exchange completed on the raw socket');
    like($stream, qr/\r\n0\r\n\r\n/, 'SSE stream ended with the chunked terminator');

    print $sock "GET /plain HTTP/1.1\r\nHost: 127.0.0.1:$sse_port\r\n\r\n";
    like($read_until->(qr/PLAIN-OK/), qr/^HTTP\/1\.1 200.*PLAIN-OK/s,
        'ordinary request served on the SAME socket after the SSE stream');
    close $sock;
}

$sse_server->shutdown->get;
$loop->remove($sse_client);

# --- WebSocket: shape and mis-sequencing errors on the send path ---
my $ws_app = async sub {
    my ($scope, $receive, $send) = @_;
    if ($scope->{type} eq 'websocket') {
        my $e = await $receive->();   # websocket.connect
        my $err1 = do { local $@; eval { await $send->({ type => 'websocket.send', bytes => 'a', text => 'b' }) }; $@ };
        ($WsErrs::SHAPE = $err1 // 'NO-ERROR') =~ s/\n/ /g;
        my $err2 = do { local $@; eval { await $send->({ type => 'websocket.keepalive', interval => 1 }) }; $@ };
        ($WsErrs::SEQ = $err2 // 'NO-ERROR') =~ s/\n/ /g;
        await $send->({ type => 'websocket.accept' });
        await $send->({ type => 'websocket.close' });
        return;
    }
    die "unsupported scope $scope->{type}";
};

my $ws_server = PAGI::Server->new(app => $ws_app, host => '127.0.0.1', port => 0, quiet => 1, validate_events => 0);
$loop->add($ws_server);
$ws_server->listen->get;
my $ws_port = $ws_server->port;

# Raw handshake boilerplate lifted from t/04-websocket.t: the app under test
# reads/writes application-level events, so a raw socket lets us drive the
# handshake without a WebSocket client library getting in the way. Pumps the
# loop until the server closes the connection, so that whatever the app
# coroutine recorded in its package vars is settled by the time we assert.
use IO::Socket::INET;

my $ws_handshake_and_drain = sub {
    my ($port) = @_;
    my $sock = IO::Socket::INET->new(
        PeerAddr => '127.0.0.1',
        PeerPort => $port,
        Proto    => 'tcp',
        Timeout  => 5,
    );
    return unless $sock;

    my $key = 'dGhlIHNhbXBsZSBub25jZQ==';
    print $sock "GET / HTTP/1.1\r\n";
    print $sock "Host: 127.0.0.1:$port\r\n";
    print $sock "Upgrade: websocket\r\n";
    print $sock "Connection: Upgrade\r\n";
    print $sock "Sec-WebSocket-Key: $key\r\n";
    print $sock "Sec-WebSocket-Version: 13\r\n";
    print $sock "\r\n";

    $sock->blocking(0);
    my $deadline = time + 5;
    while (time < $deadline) {
        my $buf;
        my $n = sysread($sock, $buf, 4096);
        last if defined($n) && $n == 0;   # EOF: server closed
        $loop->loop_once(0.1);
    }
    close $sock;
    return 1;
};

SKIP: {
    skip "Cannot connect", 2 unless $ws_handshake_and_drain->($ws_port);

    like( $WsErrs::SHAPE // '', qr/exactly one of bytes\/text/,
        'websocket.send with both bytes and text fails shape validation' );
    like( $WsErrs::SEQ // '', qr/cannot send 'websocket\.keepalive'/,
        'websocket.keepalive before accept fails sequencing' );
}

$ws_server->shutdown->get;

# --- WebSocket: sends after a terminal state raise, not swallowed ---
# Same class of bug as the SSE carve-out above: websocket.close and a
# completed denial response both flip the connection's {closed} flag as a
# side effect (see _handle_disconnect_and_close), independent of a genuine
# client disconnect. A post-close/post-denial-complete send must still raise
# through advance_websocket, not silently no-op on the transport-closed check.

my $ws_after_close_app = async sub {
    my ($scope, $receive, $send) = @_;
    if ($scope->{type} eq 'websocket') {
        my $e = await $receive->();   # websocket.connect
        await $send->({ type => 'websocket.accept' });
        # App-initiated close with no client close received: the transport
        # isn't gone for real, only the app's own logical close.
        await $send->({ type => 'websocket.close' });
        my $err = do { local $@; eval { await $send->({ type => 'websocket.send', text => 'late' }) }; $@ };



( run in 1.959 second using v1.01-cache-2.11-cpan-364913b4093 )