use std::time::Duration;
use fastwebsockets::{Frame, OpCode, Payload, Role, WebSocket};
use h2ts_server::{bridge_with, control_channel, BridgeConfig, CloseFrame, KeepAlive};
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn bridge_surfaces_client_close_reason() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CloseFrame>();
let config = BridgeConfig {
on_close: Some(Box::new(move |cf: &CloseFrame| {
let _ = tx.send(cf.clone());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws
.write_frame(Frame::close(4000, b"bye"))
.await
.unwrap();
let got = rx.recv().await.unwrap();
assert_eq!(
got,
CloseFrame {
code: 4000,
reason: "bye".to_string()
}
);
}
#[tokio::test]
async fn bridge_surfaces_ping_and_still_auto_pongs() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
let config = BridgeConfig {
on_ping: Some(Box::new(move |p: &[u8]| {
let _ = tx.send(p.to_vec());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws
.write_frame(Frame::new(
true,
OpCode::Ping,
None,
Payload::Owned(b"hi".to_vec()),
))
.await
.unwrap();
assert_eq!(rx.recv().await.unwrap(), b"hi".to_vec(), "on_ping saw it");
let frame = client_ws.read_frame().await.unwrap();
assert_eq!(frame.opcode, OpCode::Pong, "auto-pong still happens");
assert_eq!(frame.payload.to_vec(), b"hi".to_vec());
}
#[tokio::test]
async fn bridge_sends_configured_close_on_peer_eof() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, peer_test) = tokio::io::duplex(16 * 1024);
let config = BridgeConfig {
close: CloseFrame {
code: 4001,
reason: "upstream gone".to_string(),
},
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws.set_auto_close(false);
drop(peer_test);
let frame = client_ws.read_frame().await.unwrap();
assert_eq!(frame.opcode, OpCode::Close);
let payload = frame.payload.to_vec();
assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 4001);
assert_eq!(&payload[2..], b"upstream gone");
}
#[tokio::test]
async fn control_handle_sends_ping_and_close() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (control, control_rx) = control_channel();
let config = BridgeConfig {
control: Some(control_rx),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws.set_auto_pong(false);
client_ws.set_auto_close(false);
control.ping(b"ping!".to_vec()).unwrap();
let frame = client_ws.read_frame().await.unwrap();
assert_eq!(frame.opcode, OpCode::Ping);
assert_eq!(frame.payload.to_vec(), b"ping!".to_vec());
control.close(4003, "done").unwrap();
let frame = client_ws.read_frame().await.unwrap();
assert_eq!(frame.opcode, OpCode::Close);
let payload = frame.payload.to_vec();
assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 4003);
assert_eq!(&payload[2..], b"done");
}
#[tokio::test]
async fn control_sends_pong_and_on_pong_surfaces_received_pong() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (control, control_rx) = control_channel();
let (pong_tx, mut pong_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
let config = BridgeConfig {
control: Some(control_rx),
on_pong: Some(Box::new(move |p: &[u8]| {
let _ = pong_tx.send(p.to_vec());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let (client_saw_tx, mut client_saw_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(async move {
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
while let Ok(f) = client_ws.read_frame().await {
match f.opcode {
OpCode::Pong => {
let _ = client_saw_tx.send(f.payload.to_vec());
}
OpCode::Close => break,
_ => {} }
}
});
control.pong(b"unsolicited".to_vec()).unwrap();
assert_eq!(client_saw_rx.recv().await.unwrap(), b"unsolicited".to_vec());
control.ping(b"rtt-probe".to_vec()).unwrap();
assert_eq!(pong_rx.recv().await.unwrap(), b"rtt-probe".to_vec());
}
#[tokio::test]
async fn keepalive_closes_peer_on_no_pong() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CloseFrame>();
let config = BridgeConfig {
keepalive: Some(KeepAlive::new(
Duration::from_millis(50),
Duration::from_millis(50),
)),
on_close: Some(Box::new(move |cf: &CloseFrame| {
let _ = tx.send(cf.clone());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws.set_auto_pong(false);
client_ws.set_auto_close(false);
let ping = client_ws.read_frame().await.unwrap();
assert_eq!(ping.opcode, OpCode::Ping);
let close = client_ws.read_frame().await.unwrap();
assert_eq!(close.opcode, OpCode::Close);
let payload = close.payload.to_vec();
assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 1001); assert_eq!(&payload[2..], b"keepalive timeout");
let surfaced = rx.recv().await.unwrap();
assert_eq!(surfaced.code, 1001);
assert_eq!(surfaced.reason, "keepalive timeout");
}
#[tokio::test]
async fn keepalive_uses_a_custom_close_frame() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CloseFrame>();
let config = BridgeConfig {
keepalive: Some(KeepAlive {
interval: Duration::from_millis(50),
timeout: Duration::from_millis(50),
close: CloseFrame {
code: 4020,
reason: "custom-bye".to_string(),
},
}),
on_close: Some(Box::new(move |cf: &CloseFrame| {
let _ = tx.send(cf.clone());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
client_ws.set_auto_pong(false);
client_ws.set_auto_close(false);
let ping = client_ws.read_frame().await.unwrap();
assert_eq!(ping.opcode, OpCode::Ping);
let close = client_ws.read_frame().await.unwrap();
assert_eq!(close.opcode, OpCode::Close);
let payload = close.payload.to_vec();
assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 4020);
assert_eq!(&payload[2..], b"custom-bye");
let surfaced = rx.recv().await.unwrap();
assert_eq!(surfaced.code, 4020);
assert_eq!(surfaced.reason, "custom-bye");
}
#[tokio::test]
async fn wscontrol_send_fails_after_the_bridge_ends() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, _peer_test) = tokio::io::duplex(16 * 1024);
let (control, control_rx) = control_channel();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CloseFrame>();
let config = BridgeConfig {
control: Some(control_rx),
on_close: Some(Box::new(move |cf: &CloseFrame| {
let _ = tx.send(cf.clone());
})),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
control.ping(b"alive".to_vec()).unwrap();
drop(client_io);
let _ = rx
.recv()
.await
.expect("on_close should fire when the bridge ends");
let err = control.close(1000, "too late").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
assert_eq!(
control.ping(b"x".to_vec()).unwrap_err().kind(),
std::io::ErrorKind::BrokenPipe
);
}
#[tokio::test]
async fn keepalive_stays_up_while_peer_responds() {
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let (peer_for_bridge, mut peer_test) = tokio::io::duplex(16 * 1024);
let config = BridgeConfig {
keepalive: Some(KeepAlive::new(
Duration::from_millis(30),
Duration::from_millis(30),
)),
..Default::default()
};
tokio::spawn(async move {
let _ = bridge_with(server_io, peer_for_bridge, config).await;
});
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(150)).await; let _ = peer_test.write_all(b"still-alive").await;
tokio::time::sleep(Duration::from_millis(200)).await; });
let mut client_ws = WebSocket::after_handshake(client_io, Role::Client);
let frame = tokio::time::timeout(Duration::from_secs(2), client_ws.read_frame())
.await
.expect("timed out — keepalive may have closed a healthy connection")
.unwrap();
assert_eq!(
frame.opcode,
OpCode::Binary,
"expected peer data, got {:?}",
frame.opcode
);
assert_eq!(frame.payload.to_vec(), b"still-alive".to_vec());
}