use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
use futures::StreamExt;
use tracing_test::traced_test;
use buffa::encoding::{decode_varint, encode_varint, skip_field, Tag, WireType};
use buffa::{DecodeContext, DecodeError, DefaultInstance, Message, SizeCache};
use bytes::{Buf, BufMut, Bytes};
use foundation_core::synca::OnSignal;
use foundation_core::valtron::initialize_pool;
use foundation_core::valtron::valtron_test;
use foundation_http::native::server::{HttpServer, ServerConfig};
use foundation_http::shared::app::HttpApp;
use foundation_http::shared::serve::Serve;
use foundation_connectrpc::shared::codec::CodecFor;
use foundation_connectrpc::{
ConnectResult, ConnectRpcServe, HandlerOptions, IdempotencyLevel, JsonCodec, ProcedureCodecs,
Request, Response, Router,
};
#[derive(Clone, Default, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
struct TestMsg {
id: i32,
name: String,
}
impl DefaultInstance for TestMsg {
fn default_instance() -> &'static Self {
static INST: std::sync::OnceLock<TestMsg> = std::sync::OnceLock::new();
INST.get_or_init(TestMsg::default)
}
}
impl Message for TestMsg {
fn compute_size(&self, _c: &mut SizeCache) -> u32 {
0
}
fn write_to(&self, _c: &mut SizeCache, buf: &mut impl BufMut) {
Tag::new(1, WireType::Varint).encode(buf);
encode_varint(self.id as u64, buf);
Tag::new(2, WireType::LengthDelimited).encode(buf);
encode_varint(self.name.len() as u64, buf);
buf.put_slice(self.name.as_bytes());
}
fn merge_field(
&mut self,
tag: Tag,
buf: &mut impl Buf,
_c: DecodeContext<'_>,
) -> Result<(), DecodeError> {
match tag.field_number() {
1 => {
self.id = decode_varint(buf)? as i32;
Ok(())
}
2 => {
let len = decode_varint(buf)? as usize;
let mut b = vec![0u8; len];
buf.copy_to_slice(&mut b);
self.name = String::from_utf8(b).map_err(|_| DecodeError::InvalidUtf8)?;
Ok(())
}
_ => skip_field(tag, buf),
}
}
fn clear(&mut self) {
*self = Self::default();
}
}
const PROCEDURE: &str = "/test.EchoService/Echo";
fn echo_serve() -> Arc<dyn Serve> {
let mut router = Router::new();
router.unary(
PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, req: Request<TestMsg>| async move { Ok(Response::new(req.msg)) },
HandlerOptions::new().with_idempotency(IdempotencyLevel::NoSideEffects),
);
Arc::new(ConnectRpcServe::new(router.into_handler()))
}
fn read_response(stream: &mut TcpStream) -> (u16, Vec<u8>) {
let mut buf = Vec::new();
let mut tmp = [0u8; 1024];
let header_end = loop {
if let Some(pos) = find_subsequence(&buf, b"\r\n\r\n") {
break pos + 4;
}
let n = stream.read(&mut tmp).expect("read headers");
assert!(n > 0, "connection closed before headers completed");
buf.extend_from_slice(&tmp[..n]);
};
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
let status = head
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|c| c.parse::<u16>().ok())
.expect("status line");
let content_length = head
.lines()
.find_map(|l| {
let (k, v) = l.split_once(':')?;
(k.trim().eq_ignore_ascii_case("content-length"))
.then(|| v.trim().parse::<usize>().ok())
.flatten()
})
.unwrap_or_else(|| {
panic!(
"no content-length; raw head:\n{head}\n---body-so-far---\n{}",
String::from_utf8_lossy(&buf[header_end..])
)
});
let mut body = buf[header_end..].to_vec();
while body.len() < content_length {
let n = stream.read(&mut tmp).expect("read body");
assert!(n > 0, "connection closed before body completed");
body.extend_from_slice(&tmp[..n]);
}
body.truncate(content_length);
(status, body)
}
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
#[test]
fn unary_round_trip_over_real_socket() {
let _guard = initialize_pool(41, Some(4));
let mut app = HttpApp::new_serve();
let serve = echo_serve();
app.router.add_route_any(PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let msg = TestMsg {
id: 99,
name: "socket".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let request = format!(
"POST {PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
json.len()
);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
stream.write_all(request.as_bytes()).expect("write head");
stream.write_all(&json).expect("write body");
stream.flush().unwrap();
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "unary over socket returns 200");
let echoed: TestMsg = JsonCodec.unmarshal(Bytes::from(body)).expect("decode echo");
assert_eq!(echoed, msg, "server echoed the request over the wire");
shutdown.turn_on();
}
const LIST_PROCEDURE: &str = "/test.ListService/Items";
fn list_serve() -> Arc<dyn Serve> {
let mut router = Router::new();
router.server_stream(
LIST_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, req: Request<TestMsg>| async move {
let base = req.msg.id;
let items: Vec<ConnectResult<TestMsg>> = (0..3)
.map(|i| {
Ok(TestMsg {
id: base + i,
name: format!("item-{i}"),
})
})
.collect();
Ok(futures::stream::iter(items))
},
HandlerOptions::new(),
);
Arc::new(ConnectRpcServe::new(router.into_handler()))
}
#[test]
fn server_stream_over_real_socket() {
let _guard = initialize_pool(42, Some(4));
let mut app = HttpApp::new_serve();
let serve = list_serve();
app.router.add_route_any(LIST_PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let msg = TestMsg {
id: 10,
name: "seed".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let envelope = {
let mut buf = Vec::new();
buf.push(0u8);
buf.extend_from_slice(&(json.len() as u32).to_be_bytes());
buf.extend_from_slice(&json);
buf
};
let request = format!(
"POST {LIST_PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/connect+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
envelope.len()
);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
stream.write_all(request.as_bytes()).expect("write head");
stream.write_all(&envelope).expect("write body");
stream.flush().unwrap();
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "server-stream over socket returns 200");
let (messages, saw_end_stream) = parse_connect_envelope_stream(&body);
assert_eq!(messages.len(), 3, "three server-stream items");
assert!(saw_end_stream, "EndStream frame present");
for (i, msg_bytes) in messages.iter().enumerate() {
let item: TestMsg = JsonCodec
.unmarshal(Bytes::from(msg_bytes.clone()))
.expect("decode item");
assert_eq!(item.id, 10 + i as i32, "item {i} id");
assert_eq!(item.name, format!("item-{i}"), "item {i} name");
}
shutdown.turn_on();
}
const FLAG_END_STREAM: u8 = 0x02;
fn parse_connect_envelope_stream(body: &[u8]) -> (Vec<Vec<u8>>, bool) {
let mut messages = Vec::new();
let mut end = false;
let mut pos = 0;
while pos + 5 <= body.len() {
let flags = body[pos];
let len = u32::from_be_bytes([body[pos + 1], body[pos + 2], body[pos + 3], body[pos + 4]])
as usize;
let start = pos + 5;
let stop = (start + len).min(body.len());
let payload = body[start..stop].to_vec();
if flags & FLAG_END_STREAM != 0 {
end = true;
} else {
messages.push(payload);
}
pos = stop;
}
(messages, end)
}
fn connect_with_retry(addr: std::net::SocketAddr) -> TcpStream {
for _ in 0..50 {
if let Ok(s) = TcpStream::connect(addr) {
return s;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("could not connect to test server at {addr}");
}
#[test]
fn h1_transport_capabilities_are_correct() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::Proto;
let client = NativeHttpClient::from_system();
let transport = H1Transport::new(Arc::new(client));
let caps = transport.capabilities();
assert!(caps.request_streaming, "h1 supports chunked upload");
assert!(!caps.full_duplex, "h1 is half-duplex");
assert!(!caps.h2_trailers);
assert_eq!(caps.http_versions, &[Proto::HTTP11]);
assert!(!caps.multiplexed);
}
#[valtron_test(seed = 44, threads = 8)]
#[traced_test]
async fn h1_client_transport_over_real_socket() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::{
Proto, RequestDescriptor, SimpleHeaders, SimpleMethod, SimpleUrl,
};
use foundation_core::url::Uri;
let mut app = HttpApp::new_serve();
let serve = echo_serve();
app.router.add_route_any(PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let _ = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("server ready");
let transport = H1Transport::new(Arc::new(NativeHttpClient::from_system()));
let url = format!("http://127.0.0.1:{}{PROCEDURE}", addr.port());
let uri = Uri::parse(&url).expect("parse URL");
let descriptor = RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(url.clone()),
request_uri: uri,
headers: {
let mut h = SimpleHeaders::new();
h.insert(
foundation_netio::shared::http::SimpleHeader::CONTENT_TYPE,
vec!["application/json".to_string()],
);
h
},
method: SimpleMethod::POST,
};
let stream = transport.open(descriptor).expect("open");
let mut head = stream.head;
let mut recv_body = stream.recv_body;
let msg = TestMsg {
id: 77,
name: "transport".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal");
stream
.send_body
.try_send(Bytes::from(json.to_vec()))
.expect("send request body");
stream.send_body.close();
let (status, _headers) = head
.next()
.await
.expect("response head chunk")
.expect("response head ok");
assert_eq!(status, foundation_netio::shared::http::Status::OK);
let body_bytes = recv_body
.next()
.await
.expect("response body chunk")
.expect("response body ok");
let echoed: TestMsg = JsonCodec.unmarshal(body_bytes).expect("decode echo");
assert_eq!(
echoed, msg,
"H1Transport: echoed message matches request over real socket"
);
shutdown.turn_on();
}
#[valtron_test(seed = 45, threads = 8)]
#[traced_test]
async fn h1_client_server_stream_over_real_socket() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_core::url::Uri;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::{
Proto, RequestDescriptor, SimpleHeaders, SimpleMethod, SimpleUrl,
};
let mut app = HttpApp::new_serve();
let serve = list_serve();
app.router.add_route_any(LIST_PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let _ = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("server ready");
let transport = H1Transport::new(Arc::new(NativeHttpClient::from_system()));
let url = format!("http://127.0.0.1:{}{LIST_PROCEDURE}", addr.port());
let uri = Uri::parse(&url).expect("parse URL");
let descriptor = RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(url.clone()),
request_uri: uri,
headers: {
let mut h = SimpleHeaders::new();
h.insert(
foundation_netio::shared::http::SimpleHeader::CONTENT_TYPE,
vec!["application/connect+json".to_string()],
);
h
},
method: SimpleMethod::POST,
};
let stream = transport.open(descriptor).expect("open");
let mut head = stream.head;
let mut recv_body = stream.recv_body;
let seed = TestMsg {
id: 10,
name: "seed".to_string(),
};
let json = JsonCodec.marshal(&seed).expect("marshal").to_vec();
let mut envelope = Vec::with_capacity(5 + json.len());
envelope.push(0u8);
envelope.extend_from_slice(&(json.len() as u32).to_be_bytes());
envelope.extend_from_slice(&json);
stream
.send_body
.try_send(Bytes::from(envelope))
.expect("send request envelope");
stream.send_body.close();
let (status, _headers) = head
.next()
.await
.expect("response head chunk")
.expect("response head ok");
assert_eq!(status, foundation_netio::shared::http::Status::OK);
let mut body = Vec::new();
while let Some(chunk) = recv_body.next().await {
let bytes = chunk.expect("body chunk ok");
body.extend_from_slice(&bytes);
}
let (messages, saw_end_stream) = parse_connect_envelope_stream(&body);
assert_eq!(
messages.len(),
3,
"H1 client received all 3 server-stream items"
);
assert!(saw_end_stream, "H1 client saw the EndStream frame");
for (i, msg_bytes) in messages.iter().enumerate() {
let item: TestMsg = JsonCodec
.unmarshal(Bytes::from(msg_bytes.clone()))
.expect("decode item");
assert_eq!(item.id, 10 + i as i32, "item {i} id over H1 client");
assert_eq!(item.name, format!("item-{i}"), "item {i} name over H1 client");
}
shutdown.turn_on();
}