test(infra): cover review-002 stream/PEM/cap error arms + drop dead WsTimeouts Default
- forward_stream build-error arm (forward.rs): wire-level test asserts one INVALID_INPUT envelope then stream end with zero upstream contact (a panicking responder counts as the contact guard), plus the from_jsonschema integration mirror (undeclared key + non-scalar placeholder, each naming its rejection source) - PEM read-failure arms (http_client.rs): nonexistent CA path → CaBundleRead (sync new), nonexistent client-cert path → ClientCertRead (async reload, prior generation retained) - Over-cap poll_write rejection leg (byte_adapter.rs): cap+1 write → InvalidData naming the cap; stream stays usable for an at-cap write afterwards - SSE parser edges: CRLF split across feed chunks frames one line; invalid-UTF8 data lines drop without killing the frame stream - from_value structural rejects: non-object doc, missing `info`, missing `paths`, non-object `paths` each name the member - Connection-failure arms: accept-path ConnectionClosed → HandlerError::ConnectionClosed via stream_error_to_handler; read-pump demux-gone break ends the pump when the byte-stream side is dropped - Delete the caller-less `impl Default for WsTimeouts` (the extension is constructed explicitly) cargo llvm-cov --all-features: all named arms covered; TOTAL regions 94.18% (was 93.86%), lines 96.04% (was 95.77%); http_client.rs 86.56% lines (was 81.72%). docs(tasks): mark review-002-fu-stream-error-coverage done
This commit is contained in:
@@ -1188,6 +1188,54 @@ mod tests {
|
||||
.expect("pump emitted the cap-sized chunk");
|
||||
}
|
||||
|
||||
/// WS-14's rejection leg: a single `AsyncWrite` call one byte *over*
|
||||
/// `PENDING_BUFFER_CAP` fails synchronously with `InvalidData`,
|
||||
/// naming the cap, before any byte enters the write queue — the mux
|
||||
/// sees the stream error directly instead of the fault reaching the
|
||||
/// wire (the = cap acceptance is the test above; this is the
|
||||
/// over-cap rejection the WS-14 move introduced).
|
||||
#[tokio::test]
|
||||
async fn tungstenite_write_one_byte_over_the_cap_is_rejected_with_invalid_data() {
|
||||
let (client_io, mut server_io) = tokio::io::duplex(64);
|
||||
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
||||
client_io,
|
||||
tokio_tungstenite::tungstenite::protocol::Role::Client,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let (mut stream, _pumps) = split_tungstenite_to_bytes(ws);
|
||||
|
||||
let err = stream
|
||||
.write_all(&vec![0u8; PENDING_BUFFER_CAP + 1])
|
||||
.await
|
||||
.expect_err("the over-cap write must be rejected");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
assert!(
|
||||
err.to_string().contains("pending buffer cap"),
|
||||
"error names the cap: {err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains(&PENDING_BUFFER_CAP.to_string()),
|
||||
"error carries the cap value: {err}"
|
||||
);
|
||||
|
||||
// The rejection must not poison the stream: an exactly-at-cap
|
||||
// write still flows through the pump afterwards.
|
||||
let mut chunk = vec![0u8; PENDING_BUFFER_CAP];
|
||||
chunk[4..8].copy_from_slice(&(PENDING_BUFFER_CAP as u32 - 8).to_be_bytes());
|
||||
stream
|
||||
.write_all(&chunk)
|
||||
.await
|
||||
.expect("at-cap write after the rejection is accepted");
|
||||
stream.flush().await.expect("flush");
|
||||
tokio::time::timeout(std::time::Duration::from_secs(30), async {
|
||||
let mut buf = [0u8; 2];
|
||||
server_io.read_exact(&mut buf).await.expect("chunk read")
|
||||
})
|
||||
.await
|
||||
.expect("pump still emits after a rejected over-cap write");
|
||||
}
|
||||
|
||||
/// A valid chunk (length field ≤ `MAX_CHUNK_LEN`) still parses and
|
||||
/// flushes through the pump after the caps landed — the validation
|
||||
/// must not false-positive on well-framed traffic. (The pump-side
|
||||
@@ -1268,6 +1316,54 @@ mod tests {
|
||||
assert_eq!(&buf, b"from-peer");
|
||||
}
|
||||
|
||||
/// The read-pump's demux-gone break: with the byte-stream side
|
||||
/// (the `read_rx` holder) dropped while the WS is still open, the
|
||||
/// next inbound binary message fails its `read_tx.send` and ends
|
||||
/// the pump — it must not park on the now-unreceivable channel.
|
||||
/// The peer holds the socket open (no EOF, idle window far beyond
|
||||
/// the test budget), so the only exit for the pump is the failed
|
||||
/// send itself; a regression that parks here hangs until the test
|
||||
/// deadline rather than ending the task.
|
||||
#[tokio::test]
|
||||
async fn read_pump_breaks_when_the_byte_stream_side_is_dropped() {
|
||||
let (client_io, server_io) = tokio::io::duplex(1 << 16);
|
||||
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
||||
client_io,
|
||||
tokio_tungstenite::tungstenite::protocol::Role::Client,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let (_stream, pumps) = split_tungstenite_to_bytes(ws);
|
||||
drop(_stream);
|
||||
|
||||
let peer = tokio::spawn(async move {
|
||||
let peer = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
||||
server_io,
|
||||
tokio_tungstenite::tungstenite::protocol::Role::Server,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let (mut sink, mut reader) = peer.split();
|
||||
use futures::{SinkExt, StreamExt};
|
||||
sink.send(tokio_tungstenite::tungstenite::Message::Binary(
|
||||
b"after-drop".to_vec().into(),
|
||||
))
|
||||
.await
|
||||
.expect("peer send");
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(10), reader.next()).await;
|
||||
});
|
||||
|
||||
let ended = tokio::time::timeout(std::time::Duration::from_secs(5), pumps.read_task)
|
||||
.await
|
||||
.expect("the pump must end on the demux-gone break, not park");
|
||||
assert!(
|
||||
ended.is_ok(),
|
||||
"the pump ends by itself (the break), not by abort: {ended:?}"
|
||||
);
|
||||
|
||||
peer.abort();
|
||||
}
|
||||
|
||||
/// The lossless read-EOF signal on the tungstenite path (COV-03,
|
||||
/// the WS-11 constraint): the peer disappearing surfaces on
|
||||
/// `pumps.read_eof()` as `watch` = true — observable both before
|
||||
|
||||
@@ -341,15 +341,6 @@ pub struct WsTimeouts {
|
||||
pub write: Option<std::time::Duration>,
|
||||
}
|
||||
|
||||
impl Default for WsTimeouts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
idle: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT),
|
||||
write: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The upgrade handler. Requires the resolved identity in request
|
||||
/// extensions (stashed by [`ws_bearer_auth`]) — a WS session without
|
||||
/// an identity cannot run `AccessControl::check`.
|
||||
|
||||
Reference in New Issue
Block a user