#![cfg(feature = "ws")]
use std::time::Duration;
use futures::StreamExt;
use tears::subscription::SubscriptionSource;
use tears::subscription::websocket::{WebSocket, WebSocketMessage};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
use tokio::time::timeout;
use tokio_tungstenite::accept_async;
#[tokio::test]
async fn websocket_closes_connection_when_stream_dropped() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind local listener");
let addr = listener.local_addr().expect("local addr");
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.expect("accept tcp");
let mut ws = accept_async(tcp).await.expect("accept websocket");
loop {
match ws.next().await {
Some(Ok(msg)) if msg.is_close() => return true,
Some(Ok(_)) => {} Some(Err(_)) | None => return true,
}
}
});
let url = format!("ws://{addr}");
let ws = WebSocket::new(url);
let mut stream = ws.stream();
let sender = timeout(Duration::from_secs(2), async {
loop {
match stream.next().await {
Some(WebSocketMessage::Connected { sender }) => break Some(sender),
Some(_) => {}
None => break None,
}
}
})
.await
.expect("should connect within timeout")
.expect("stream should yield Connected before ending");
drop(stream);
let observed_close = timeout(Duration::from_secs(2), server).await;
drop(sender);
assert!(
matches!(observed_close, Ok(Ok(true))),
"server should observe the websocket connection closing after the stream is dropped \
(connection leaked)"
);
}
#[tokio::test]
async fn websocket_closes_connection_when_outer_task_is_aborted() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind local listener");
let addr = listener.local_addr().expect("local addr");
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.expect("accept tcp");
let mut ws = accept_async(tcp).await.expect("accept websocket");
loop {
match ws.next().await {
Some(Ok(msg)) if msg.is_close() => return true,
Some(Ok(_)) => {}
Some(Err(_)) | None => return true,
}
}
});
let url = format!("ws://{addr}");
let ws = WebSocket::new(url);
let (connected_tx, connected_rx) = oneshot::channel::<()>();
let mut connected_tx = Some(connected_tx);
let mut stream = ws.stream();
let task = tokio::spawn(async move {
while let Some(msg) = stream.next().await {
if matches!(msg, WebSocketMessage::Connected { .. }) {
if let Some(tx) = connected_tx.take() {
let _ = tx.send(());
}
}
}
});
timeout(Duration::from_secs(2), connected_rx)
.await
.expect("connected within timeout")
.expect("connected signal received");
task.abort();
let _ = task.await;
let observed_close = timeout(Duration::from_secs(2), server).await;
assert!(
matches!(observed_close, Ok(Ok(true))),
"server should observe the websocket connection closing after the outer task is aborted"
);
}
#[tokio::test]
async fn websocket_sends_close_frame_after_read_error() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind local listener");
let addr = listener.local_addr().expect("local addr");
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.expect("accept tcp");
let ws = accept_async(tcp).await.expect("accept websocket");
let mut raw = ws.into_inner();
raw.write_all(&[0x81, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00])
.await
.expect("send oversized frame header");
let mut buf = [0u8; 16];
let n = timeout(Duration::from_secs(2), raw.read(&mut buf))
.await
.expect("timed out waiting for client close frame")
.unwrap_or(0);
n > 0 && buf[0] == 0x88
});
let url = format!("ws://{addr}");
let ws = WebSocket::new(url);
let mut stream = ws.stream();
let _sender = timeout(Duration::from_secs(2), async {
loop {
match stream.next().await {
Some(WebSocketMessage::Connected { sender }) => return Some(sender),
Some(_) => {}
None => return None,
}
}
})
.await
.expect("connect timeout")
.expect("stream should yield Connected before the invalid frame arrives");
let item = timeout(Duration::from_secs(2), stream.next())
.await
.expect("error item timeout");
assert!(
matches!(item, Some(WebSocketMessage::Error { .. })),
"expected Error on capacity violation, got: {item:?}"
);
let saw_close_frame = timeout(Duration::from_secs(2), server)
.await
.expect("server task timeout")
.expect("server task panicked");
assert!(
saw_close_frame,
"client must send a WebSocket close frame (0x88) after a read error; \
write.close() was not called on the Some(Err(e)) path"
);
}