#![cfg(feature = "sandbox-it")]
#[allow(dead_code)]
mod support;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use rightsize_modules::{KafkaContainer, RedpandaContainer};
macro_rules! require_backend {
() => {
if !support::requested_backend_available() {
support::skip_notice();
return;
}
};
}
#[derive(Debug, PartialEq, Eq)]
struct BrokerMetadata {
node_id: i32,
host: String,
port: i32,
}
fn metadata_v1_request(correlation_id: i32) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&3i16.to_be_bytes()); body.extend_from_slice(&1i16.to_be_bytes()); body.extend_from_slice(&correlation_id.to_be_bytes());
body.extend_from_slice(&(-1i16).to_be_bytes()); body.extend_from_slice(&(-1i32).to_be_bytes()); let mut framed = Vec::new();
framed.extend_from_slice(&(body.len() as i32).to_be_bytes());
framed.extend_from_slice(&body);
framed
}
fn fetch_first_broker(addr: &str) -> BrokerMetadata {
let mut stream = TcpStream::connect(addr).expect("connect to bootstrap_servers()");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set_read_timeout");
let correlation_id = 7;
stream
.write_all(&metadata_v1_request(correlation_id))
.expect("write the Metadata v1 request");
let mut len_buf = [0u8; 4];
stream
.read_exact(&mut len_buf)
.expect("read the response length prefix");
let len = i32::from_be_bytes(len_buf);
assert!(
(4..=1_000_000).contains(&len),
"implausible Metadata response length: {len}"
);
let mut body = vec![0u8; len as usize];
stream
.read_exact(&mut body)
.expect("read the full Metadata response body");
let echoed = i32::from_be_bytes([body[0], body[1], body[2], body[3]]);
assert_eq!(
echoed, correlation_id,
"correlation id mismatch — not a well-formed Metadata response"
);
let mut offset = 4;
let brokers_count = i32::from_be_bytes([
body[offset],
body[offset + 1],
body[offset + 2],
body[offset + 3],
]);
offset += 4;
assert!(
brokers_count >= 1,
"Metadata response advertised zero brokers"
);
let node_id = i32::from_be_bytes([
body[offset],
body[offset + 1],
body[offset + 2],
body[offset + 3],
]);
offset += 4;
let host_len = i16::from_be_bytes([body[offset], body[offset + 1]]) as usize;
offset += 2;
let host = String::from_utf8(body[offset..offset + host_len].to_vec())
.expect("broker host must be valid UTF-8");
offset += host_len;
let port = i32::from_be_bytes([
body[offset],
body[offset + 1],
body[offset + 2],
body[offset + 3],
]);
BrokerMetadata {
node_id,
host,
port,
}
}
#[tokio::test]
async fn redpanda_advertises_the_mapped_host_port_via_metadata() {
require_backend!();
let guard = RedpandaContainer::new()
.start()
.await
.expect("redpanda must start");
let mapped_port = guard
.get_mapped_port(9092)
.expect("the Kafka port must be exposed and mapped");
let addr = guard.bootstrap_servers().replacen("PLAINTEXT://", "", 1);
let broker = fetch_first_broker(&addr);
eprintln!(
"redpanda Metadata: node_id={} host={} port={}",
broker.node_id, broker.host, broker.port
);
assert_eq!(
(broker.host.as_str(), broker.port as u16),
("127.0.0.1", mapped_port),
"Redpanda's advertised listener did not equal 127.0.0.1:{mapped_port} — the \
discriminating check this test exists for"
);
let _ = guard.schema_registry_url();
guard.stop().await.unwrap();
}
#[tokio::test]
async fn kafka_advertises_the_mapped_host_port_via_metadata() {
require_backend!();
let guard = KafkaContainer::new()
.start()
.await
.expect("kafka must start");
let mapped_port = guard
.get_mapped_port(9092)
.expect("the Kafka port must be exposed and mapped");
let addr = guard.bootstrap_servers().replacen("PLAINTEXT://", "", 1);
let broker = fetch_first_broker(&addr);
eprintln!(
"kafka Metadata: node_id={} host={} port={}",
broker.node_id, broker.host, broker.port
);
assert_eq!(
(broker.host.as_str(), broker.port as u16),
("127.0.0.1", mapped_port),
"Kafka's advertised listener did not equal 127.0.0.1:{mapped_port} — the \
discriminating check this test exists for"
);
guard.stop().await.unwrap();
}