EV-Nats

 view release on metacpan or  search on metacpan

t/17_mock_protocol.t  view on Meta::CPAN

    is_deeply \@delivered, [
        ['s.a', 'abc',   undef],                    # hmsg hdr_len > total: raw payload
        ['s.a', '',      undef, 'abcd'],            # hmsg hdr_len == total: empty body
        ['s.a', 'abc',   undef],                    # hmsg zero hdr_len
        ['s.a', 'body',  'rep', "HDR\n"],           # hmsg reply + headers
        ['s.a', '',      undef],                    # msg empty payload
        ['s.a', 'abc',   'my.reply'],               # msg with reply
        ['s.a', 'abc',   undef],                    # msg trailing spaces
        ['s.a', 'hello', undef],                    # split msg header/body
    ], 'well-formed frames delivered exactly, malformed ones dropped'
        or diag explain \@delivered;
    is $pong, 'pong=1', 'client still answered PING after the fuzz';
    is_deeply \@errs, [], 'no errors raised by malformed frames'
        or diag explain \@errs;
};

subtest 'inbound control line is capped' => sub {
    plan tests => 2;
    my $mock = MockNats->new(on_accept => sub {
        my ($c, $report) = @_;
        MockNats->handshake($c) or return;
        # Never send a newline again: rbuf must not grow without bound.
        my $sent = 0;
        for (1 .. 400) {
            my $n = syswrite($c, 'A' x 65536);
            last unless $n;
            $sent += $n;
            select undef, undef, undef, 0.01;
        }
        print $report "sent=$sent\n";
    })->start;

    my @errs;
    my $nats = EV::Nats->new(
        host => '127.0.0.1', port => $mock->port,
        reconnect => 0,
        on_error  => sub { push @errs, $_[0] },
    );
    run_guarded(8);
    $mock->stop;

    ok((grep { /maximum control line exceeded/ } @errs),
       'peer that never sends a newline is cut off')
        or diag "errors: @errs";
    ok !$nats->is_connected, 'connection torn down';
};

subtest 'tls_required server never sees a plaintext CONNECT' => sub {
    plan tests => 2;
    my $mock = MockNats->new(on_accept => sub {
        my ($c, $report) = @_;
        MockNats->send_info($c,
            '{"server_id":"fake","max_payload":1048576,"tls_required":true}');
        my $buf = MockNats->read_until($c, qr/CONNECT/, 3);
        print $report ($buf ? 'LEAKED' : 'NO-CONNECT') . "\n";
    })->start;

    my @errs;
    my $nats = EV::Nats->new(
        host => '127.0.0.1', port => $mock->port,
        user => 'u', pass => 'secret',   # credentials that must not leak
        reconnect => 0,
        on_error  => sub { push @errs, $_[0] },
    );
    run_guarded(6);
    my $verdict = $mock->report(5);
    $mock->stop;

    is $verdict, 'NO-CONNECT', 'no CONNECT on the wire for a tls_required server';
    ok((grep { /requires TLS/ } @errs), 'error explains the refusal')
        or diag "errors: @errs";
};

subtest 'flush() is not satisfied by a keepalive PONG' => sub {
    plan tests => 4;
    # The keepalive ping and flush() share one pong FIFO. The mock answers
    # the handshake PING, then holds back: PONG #1 only after TWO further
    # PINGs (one keepalive + one from flush), PONG #2 only after SIX. If a
    # keepalive PING pushes no FIFO placeholder, PONG #1 pops flush's
    # callback and it reports success on someone else's PONG.
    my $mock = MockNats->new(on_accept => sub {
        my ($c, $report) = @_;
        MockNats->handshake($c) or return;
        my ($pings, $pongs, $buf) = (0, 0, '');
        my $deadline = time + 12;
        while (time < $deadline && $pongs < 2) {
            my $rin = ''; vec($rin, fileno($c), 1) = 1;
            if (select(my $r = $rin, undef, undef, 0.1)) {
                my $n = sysread($c, my $ch, 4096); last unless $n;
                $buf .= $ch;
                my $new = () = $buf =~ /PING\r\n/g;
                if ($new) { $pings += $new; $buf = substr($buf, -5) }
            }
            if    ($pings >= 2 && $pongs == 0) { syswrite($c, "PONG\r\n"); $pongs = 1 }
            elsif ($pings >= 6 && $pongs == 1) { syswrite($c, "PONG\r\n"); $pongs = 2 }
        }
        print $report "pongs=$pongs\n";
    })->start;

    my ($flush_fired, $flush_err) = (0, 'unset');
    my $nats;
    $nats = EV::Nats->new(
        host => '127.0.0.1', port => $mock->port,
        ping_interval => 1000, max_pings_outstanding => 100,
        on_error   => sub { diag "error: $_[0]" },
        on_connect => sub {
            my $f; $f = EV::timer 1.4, 0, sub {
                undef $f;
                $nats->flush(sub {
                    ($flush_fired, $flush_err) = (1, $_[0]);
                    EV::break;
                });
            };
        },
    );
    # Phase 1: PONG #1 lands at ~+1.4s; the 6th PING (which unlocks
    # PONG #2) is not sent before +5s. 2.5s after arming, flush must
    # still be waiting for its own PONG.
    my $phase1 = EV::timer 3.9, 0, sub { EV::break };
    run_guarded(8);
    ok !$flush_fired, 'one PONG for two PINGs did not complete flush()';



( run in 2.098 seconds using v1.01-cache-2.11-cpan-f0ff5d10edf )