use futures::StreamExt as _;
use jmap_base_client::ClientError;
use jmap_base_client::{DEFAULT_WS_MAX_MESSAGE_BYTES, connect_ws_allow_plaintext};
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
async fn first_frame_after_server_close(
code: CloseCode,
reason: &'static str,
) -> Option<Result<jmap_base_client::WsFrame, ClientError>> {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("must bind 127.0.0.1:0");
let addr = listener.local_addr().expect("must report local addr");
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("must accept");
let mut ws = tokio_tungstenite::accept_async(stream)
.await
.expect("must complete WS handshake");
ws.close(Some(CloseFrame {
code,
reason: reason.into(),
}))
.await
.expect("server close must succeed");
let _ = ws.next().await;
});
let mut session =
connect_ws_allow_plaintext(&format!("ws://{addr}/"), None, DEFAULT_WS_MAX_MESSAGE_BYTES)
.await
.expect("client must complete WS handshake");
let first = session.next_frame().await;
drop(session);
server.await.expect("server task must not panic");
first
}
#[tokio::test]
async fn close_1008_surfaces_ws_closed_with_code() {
match first_frame_after_server_close(CloseCode::Policy, "token expired").await {
Some(Err(ClientError::WsClosed { code, reason })) => {
assert_eq!(code, 1008);
assert_eq!(reason, "token expired");
}
other => panic!("expected WsClosed {{ code: 1008, .. }}, got {other:?}"),
}
}
#[tokio::test]
async fn close_1000_still_ends_stream_with_none() {
let first = first_frame_after_server_close(CloseCode::Normal, "").await;
assert!(
first.is_none(),
"normal close must yield None, got {first:?}"
);
}