EV-Kafka

 view release on metacpan or  search on metacpan

t/20_mock_conn.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use IO::Socket::INET;
use EV;
use EV::Kafka;

# In-process mock broker: a localhost listener that replies to ApiVersions
# (the connect handshake) with a hand-crafted response. Verifies that the
# Conn handshake works against bytes we control, without docker.

plan tests => 4;

# Spawn a listener on a free port.
my $server = IO::Socket::INET->new(
    LocalAddr => '127.0.0.1',
    LocalPort => 0,
    Listen    => 1,
    Proto     => 'tcp',
    ReuseAddr => 1,
) or BAIL_OUT "cannot bind localhost listener: $!";
$server->blocking(0);

my $port = $server->sockport;
note "mock broker listening on 127.0.0.1:$port";

# Build a small ApiVersions response. v0 layout:
#   error_code(i16) + api_versions_array(i32 count, then [api_key, min_ver, max_ver]*)
sub i16 { pack 'n', $_[0] }
sub i32 { pack 'N', $_[0] }
sub i64 { pack 'q>', $_[0] }

my $apis_body =
      i16(0)         # no error
    . i32(2)         # 2 entries
    . i16(0) . i16(0) . i16(7)     # API_PRODUCE, v0..v7
    . i16(18) . i16(0) . i16(0);   # API_API_VERSIONS, v0..v0

# Wrap with response framing: size + correlation_id + body.
sub frame_response {
    my ($corr_id, $body) = @_;
    my $payload = i32($corr_id) . $body;
    return i32(length $payload) . $payload;
}

# Accept the connection and parse the request to recover the correlation id.
my $client_fh;
my @incoming;        # bytes read from client
my $request_corr_id;
my $client_read_w;   # kept alive by closure on the outer lexical

my $accept_w = EV::io fileno($server), EV::READ, sub {
    $client_fh = $server->accept or return;
    $client_fh->blocking(0);
    note "mock broker accepted connection";

    $client_read_w = EV::io fileno($client_fh), EV::READ, sub {
        my $buf;
        my $n = sysread $client_fh, $buf, 4096;
        if (!defined $n || $n == 0) {
            undef $client_read_w;
            return;
        }
        push @incoming, $buf;
        my $all = join '', @incoming;
        # Request: size(i32) + api(i16) + version(i16) + corr(i32) + client_id(string) + ...
        return if length($all) < 4;
        my $size = unpack 'N', substr($all, 0, 4);
        return if length($all) < 4 + $size;
        # Skip api+version
        $request_corr_id = unpack 'N', substr($all, 4 + 2 + 2, 4);
        # Reply with the canned ApiVersions response.
        syswrite $client_fh, frame_response($request_corr_id, $apis_body);
        undef $client_read_w;
    };
};

# Drive the client.
my $conn = EV::Kafka::Conn::_new('EV::Kafka::Conn', undef);
my $connected = 0;
my $errored;
$conn->on_connect(sub { $connected = 1; EV::break });
$conn->on_error(sub { $errored = $_[0]; EV::break });
$conn->connect('127.0.0.1', $port, 5.0);

# Run the loop with a safety timeout.
my $timeout = EV::timer 5, 0, sub {
    diag "mock broker test timed out";
    EV::break;
};
EV::run;

ok !$errored, 'no error during mock handshake' or diag $errored;
ok $connected, 'on_connect fires after canned ApiVersions reply';
ok defined $request_corr_id, 'mock broker observed an ApiVersions request';
ok $conn->connected, 'conn reports ready state';

eval { $conn->disconnect; };
close $server;



( run in 3.197 seconds using v1.01-cache-2.11-cpan-364913b4093 )