test(websocket): axum-flavor unit tests for text→1002 and cap-trip arms (WS-19)
- AxumFraming arms were exercised only indirectly via tungstenite (shared generic pumps); drive the axum message types directly with an in-process fake WebSocket (futures mpsc-backed Sink+Stream stand-in for the split halves) - text test: read pump maps a text message to the WriteMsg close carrying 1002 + the text reason - cap-trip test: an above-MAX_CHUNK_LEN header through the write pump closes with 1011 naming the violation Verification: scripts/verify.sh OK (345 passed), test-support suite ok, clippy -D warnings clean, fmt clean
This commit is contained in:
@@ -1110,7 +1110,7 @@ mod tests {
|
||||
/// An 8-byte bogus chunk header claiming `MAX_CHUNK_LEN + 1`
|
||||
/// payload bytes (WS-04/HY-09 acceptance: the parser must fail the
|
||||
/// stream loudly instead of silently waiting for ~4 GiB).
|
||||
fn oversize_header() -> Vec<u8> {
|
||||
pub(crate) fn oversize_header() -> Vec<u8> {
|
||||
let mut header = vec![0u8; 8];
|
||||
header[4..8].copy_from_slice(&(MAX_CHUNK_LEN + 1).to_be_bytes());
|
||||
header
|
||||
@@ -1745,3 +1745,170 @@ mod tests {
|
||||
.expect("peer close accepted");
|
||||
}
|
||||
}
|
||||
|
||||
/// WS-19: the axum-flavor `AxumFraming` arms have no direct unit test —
|
||||
/// the cap-trip and text→1002 closes are asserted on tungstenite only
|
||||
/// (via the shared generic pumps). These drive the same generic pumps
|
||||
/// with `AxumFraming` over a fake axum `WebSocket` sink/stream pair
|
||||
/// (no server needed): a `mpsc`-backed sink/stream pair standing in
|
||||
/// for the split halves of `axum::extract::ws::WebSocket`.
|
||||
#[cfg(test)]
|
||||
mod axum_framing_tests {
|
||||
use super::*;
|
||||
use futures::channel::mpsc as fut_mpsc;
|
||||
|
||||
/// In-process stand-in for the split halves of an axum
|
||||
/// `WebSocket`: messages flow stream→`rx` and `tx`→sink, so the
|
||||
/// generic pumps run against `AxumFraming` unchanged.
|
||||
struct AxumFakeSocket {
|
||||
sink_tx: futures_mpsc::Sender<AxumMessage>,
|
||||
stream_rx:
|
||||
std::pin::Pin<Box<dyn futures::Stream<Item = Result<AxumMessage, AxumMessage>> + Send>>,
|
||||
}
|
||||
|
||||
impl futures::Stream for AxumFakeSocket {
|
||||
type Item = Result<AxumMessage, AxumMessage>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
self.stream_rx.as_mut().poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::Sink<AxumMessage> for AxumFakeSocket {
|
||||
type Error = ();
|
||||
|
||||
fn poll_ready(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.sink_tx).poll_ready(cx).map_err(|_| ())
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: AxumMessage) -> Result<(), Self::Error> {
|
||||
self.sink_tx.start_send(item).map_err(|_| ())
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
futures::Sink::poll_flush(Pin::new(&mut self.sink_tx), cx).map_err(|_| ())
|
||||
}
|
||||
|
||||
fn poll_close(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
futures::Sink::poll_close(Pin::new(&mut self.sink_tx), cx).map_err(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
type AxumPairParts = (
|
||||
futures::stream::SplitSink<AxumFakeSocket, AxumMessage>,
|
||||
futures::stream::SplitStream<AxumFakeSocket>,
|
||||
futures_mpsc::Receiver<AxumMessage>,
|
||||
fut_mpsc::Sender<Result<AxumMessage, AxumMessage>>,
|
||||
);
|
||||
|
||||
fn axum_pair() -> AxumPairParts {
|
||||
let (outbound_tx, outbound_rx) = fut_mpsc::channel::<AxumMessage>(4);
|
||||
let (inbound_tx, inbound_rx) = fut_mpsc::channel::<Result<AxumMessage, AxumMessage>>(4);
|
||||
let socket = AxumFakeSocket {
|
||||
sink_tx: outbound_tx,
|
||||
stream_rx: Box::pin(inbound_rx),
|
||||
};
|
||||
let (sink, stream) = socket.split();
|
||||
(sink, stream, outbound_rx, inbound_tx)
|
||||
}
|
||||
|
||||
/// WS-19 mirror over `AxumFraming`: a text message on the read
|
||||
/// side triggers the 1002 protocol-error close carrying the text
|
||||
/// reason (the same generic read-pump arm the tungstenite tests
|
||||
/// exercise — asserted here on the axum message types).
|
||||
#[tokio::test]
|
||||
async fn axum_framing_text_message_requests_protocol_close_with_reason() {
|
||||
let (_sink, stream, _outbound_rx, mut inbound_tx) = axum_pair();
|
||||
|
||||
let (write_tx_for_read, mut write_rx_for_read) = fut_mpsc::channel::<WriteMsg>(WRITE_SLOTS);
|
||||
let read_task = tokio::spawn(run_read_pump::<AxumFraming, _, _>(
|
||||
stream,
|
||||
mpsc::channel::<Vec<u8>>(READ_SLOTS).0,
|
||||
write_tx_for_read,
|
||||
make_write_error_slot().0,
|
||||
None,
|
||||
|| {},
|
||||
));
|
||||
|
||||
inbound_tx
|
||||
.send(Ok(AxumMessage::Text("text frame".into())))
|
||||
.await
|
||||
.expect("inbound message accepted");
|
||||
|
||||
let close = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
while let Some(msg) = write_rx_for_read.next().await {
|
||||
if let WriteMsg::CloseWith(code, reason) = msg {
|
||||
return <AxumFraming as WsFraming>::close_message(code, reason);
|
||||
}
|
||||
}
|
||||
AxumMessage::Text("queue closed".into())
|
||||
})
|
||||
.await
|
||||
.expect("close requested");
|
||||
|
||||
let AxumMessage::Close(Some(frame)) = close else {
|
||||
panic!("expected a close frame, got {close:?}");
|
||||
};
|
||||
assert_eq!(frame.code, WS_PROTOCOL_ERROR, "text closes with 1002");
|
||||
assert_eq!(
|
||||
frame.reason, "text messages not supported",
|
||||
"close reason names the text cause"
|
||||
);
|
||||
read_task.abort();
|
||||
}
|
||||
|
||||
/// WS-19 mirror of the over-cap close on the axum flavor: a
|
||||
/// header claiming above-`MAX_CHUNK_LEN` payload fails the pump —
|
||||
/// the peer sees the 1011 close naming the violation and the
|
||||
/// pump's error slot carries the reason.
|
||||
#[tokio::test]
|
||||
async fn axum_framing_cap_trip_fails_the_pump_with_the_internal_close() {
|
||||
let (sink, _stream, mut outbound_rx, _inbound_tx) = axum_pair();
|
||||
let (slot, _rx) = make_write_error_slot();
|
||||
|
||||
let write_task = tokio::spawn(run_write_pump::<AxumFraming, _>(
|
||||
sink,
|
||||
{
|
||||
let (mut tx, rx) = fut_mpsc::channel::<WriteMsg>(WRITE_SLOTS);
|
||||
tx.send(WriteMsg::Bytes(tests::oversize_header()))
|
||||
.await
|
||||
.expect("queue write accepted");
|
||||
rx
|
||||
},
|
||||
slot,
|
||||
None,
|
||||
));
|
||||
|
||||
let close = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
loop {
|
||||
match outbound_rx.next().await {
|
||||
Some(m @ AxumMessage::Close(_)) => return m,
|
||||
Some(_) => continue,
|
||||
None => return AxumMessage::Text("stream ended".into()),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("close observed");
|
||||
|
||||
let AxumMessage::Close(Some(frame)) = close else {
|
||||
panic!("expected a close frame, got {close:?}");
|
||||
};
|
||||
assert_eq!(frame.code, WS_INTERNAL_ERROR, "cap trip closes with 1011");
|
||||
assert!(
|
||||
frame.reason.contains("MAX_CHUNK_LEN"),
|
||||
"close reason names the violation: {}",
|
||||
frame.reason
|
||||
);
|
||||
write_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user