PAGI-Server

 view release on metacpan or  search on metacpan

t/37-connection-state.t  view on Meta::CPAN

    my $c2 = PAGI::Server::ConnectionState->new();
    my $comp = 0;
    $c2->on_complete(sub { $comp = 1 });
    $c2->_mark_disconnected('write_error');
    $c2->_mark_complete;                          # must be ignored
    ok(!$comp, 'on_complete not fired after abnormal disconnect');
    is($c2->disconnect_reason, 'write_error', 'disconnect reason preserved');
};

subtest 'on_disconnect after completion does not fire' => sub {
    my $conn = PAGI::Server::ConnectionState->new();
    $conn->_mark_complete;

    my $called = 0;
    $conn->on_disconnect(sub { $called = 1 });

    ok(!$called, 'on_disconnect registered after clean completion never fires');
};

subtest 'on_complete callback errors do not break others' => sub {
    my $conn = PAGI::Server::ConnectionState->new();
    my $cb2_called = 0;

    $conn->on_complete(sub { die "error in cb1" });
    $conn->on_complete(sub { $cb2_called = 1 });

    my @warnings;
    local $SIG{__WARN__} = sub { push @warnings, @_ };

    $conn->_mark_complete;

    ok($cb2_called, 'cb2 still called despite cb1 error');
    like($warnings[0], qr/callback error/, 'warning emitted');
};

# =============================================================================
# Test: _handle_disconnect marks connection state BEFORE resuming parked
# drain waiters. A producer parked on a blocking backpressure await
# (_wait_for_drain) can be resumed synchronously the moment its Future is
# resolved (Future::AsyncAwait resumes inline off ->done, same as ->on_ready
# below); if that resumption races ahead of the connection-state marking, the
# app observes is_connected() == 1 immediately after its own disconnect was
# detected. Exercises the real Connection._handle_disconnect ordering
# directly (unit-level, no live socket needed).
# =============================================================================

subtest 'disconnect marks connection state before resuming parked drain waiters' => sub {
    my $conn = PAGI::Server::Connection->new(app => sub { });
    my $conn_state = PAGI::Server::ConnectionState->new(connection => $conn);
    $conn->{current_connection_state} = $conn_state;

    # A producer parked on a blocking backpressure await -- pushed directly
    # onto _drain_waiters (the same queue _wait_for_drain uses), without
    # needing a real stream/buffer to get there.
    my $parked = Future->new;
    push @{$conn->{_drain_waiters}}, $parked;

    my ($observed_connected, $observed_reason);
    $parked->on_ready(sub {
        # Fires synchronously from within _handle_disconnect below, exactly
        # as an awaiting coroutine resumes -- this is the resumed app's very
        # first chance to look at its own connection state.
        $observed_connected = $conn_state->is_connected;
        $observed_reason    = $conn_state->disconnect_reason;
    });

    $conn->_handle_disconnect('client_closed');

    is($observed_connected, 0,
        'resumed waiter observes is_connected already false (no stale-true window)');
    is($observed_reason, 'client_closed',
        'resumed waiter observes disconnect_reason already set');
};

# =============================================================================
# Test: response_complete accessor (SHOULD-level; unsupported -> undef)
# =============================================================================

subtest 'response_complete is undef (unsupported)' => sub {
    my $conn = PAGI::Server::ConnectionState->new();

    ok($conn->can('response_complete'), 'response_complete method exists');

    my $result = eval { $conn->response_complete };
    ok(!$@, 'response_complete does not throw') or diag("error: $@");
    is($result, undef, 'response_complete returns undef (unsupported)');
};

# =============================================================================
# Test: Server implements disconnect reason code paths (source inspection)
# =============================================================================

subtest 'server implements disconnect reason code paths' => sub {
    # Read the Connection.pm source
    my $source = do {
        open my $fh, '<', 'lib/PAGI/Server/Connection.pm' or die "Cannot read: $!";
        local $/;
        <$fh>;
    };

    # Verify protocol_error is set on parse failures
    like(
        $source,
        qr/_handle_disconnect\('protocol_error'\)/,
        'protocol_error reason used for parse failures'
    );

    # Verify server_shutdown auto-detection exists
    like(
        $source,
        qr/server_shutdown/,
        'server_shutdown reason is referenced'
    );

    like(
        $source,
        qr/\$self->\{server\}\{shutting_down\}/,
        'server shutdown state is checked'
    );
};



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