use core::future::Future;
use core::pin::Pin;
use std::sync::Mutex;
use super::{DISCOVERY_ANSWER, MODERN_SERVER, TestError, at, client_at, json};
use crate::application::client::handshake::{
DISCOVER, HandshakeError, INITIALIZE, INITIALIZED, handshake,
};
use crate::application::client::{ClientTransport, TransportError};
use crate::domain::ProtocolVersion;
use crate::domain::ir::JsonObject;
struct Recording {
answer: Option<JsonObject>,
failure: Option<String>,
methods: Mutex<Vec<String>>,
params: Mutex<Vec<JsonObject>>,
}
impl Recording {
fn answering(answer: &str) -> Result<Self, TestError> {
Ok(Self {
answer: Some(json(answer)?),
failure: None,
methods: Mutex::new(Vec::new()),
params: Mutex::new(Vec::new()),
})
}
fn failing(reason: &'static str) -> Self {
Self {
answer: None,
failure: Some(reason.to_owned()),
methods: Mutex::new(Vec::new()),
params: Mutex::new(Vec::new()),
}
}
fn methods(&self) -> Vec<String> {
self.methods
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn params(&self, index: usize) -> Result<serde_json::Value, TestError> {
let stored = self
.params
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(index)
.cloned()
.ok_or(TestError::NoCallAt(index))?;
Ok(serde_json::from_str(stored.as_str())?)
}
fn record(&self, method: &str, params: &JsonObject) {
self.methods
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(method.to_owned());
self.params
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(params.clone());
}
}
impl ClientTransport for Recording {
fn send(
&self,
method: &str,
params: &JsonObject,
) -> Pin<Box<dyn Future<Output = Result<JsonObject, TransportError>> + Send + '_>> {
self.record(method, params);
let answer = self.answer.clone();
let failure = self.failure.clone().unwrap_or_default();
Box::pin(std::future::ready(
answer.ok_or(TransportError::Unavailable(failure)),
))
}
fn notify(
&self,
method: &str,
params: &JsonObject,
) -> Pin<Box<dyn Future<Output = Result<(), TransportError>> + Send + '_>> {
self.record(method, params);
Box::pin(std::future::ready(Ok(())))
}
}
#[tokio::test]
async fn the_result_is_read_before_the_closing_notification() -> Result<(), TestError> {
let transport = Recording::answering(MODERN_SERVER)?;
let config = client_at(ProtocolVersion::V2025_06_18);
let session = handshake(&transport, &config).await?;
assert_eq!(transport.methods(), vec![INITIALIZE, INITIALIZED]);
assert_eq!(session.version, ProtocolVersion::V2025_06_18);
Ok(())
}
#[tokio::test]
async fn the_negotiated_revision_is_the_servers_not_the_clients() -> Result<(), TestError> {
let transport = Recording::answering(MODERN_SERVER)?;
let config = crate::application::client::ClientConfig::speaking(
[ProtocolVersion::V2025_11_25, ProtocolVersion::V2025_06_18],
crate::domain::TransportKind::StreamableHttp,
)?;
let session = handshake(&transport, &config).await?;
assert_eq!(session.version, ProtocolVersion::V2025_06_18);
Ok(())
}
#[tokio::test]
async fn the_offer_that_goes_out_names_the_clients_preferred_revision() -> Result<(), TestError> {
let transport = Recording::answering(MODERN_SERVER)?;
let config = crate::application::client::ClientConfig::speaking(
[ProtocolVersion::V2025_11_25, ProtocolVersion::V2025_06_18],
crate::domain::TransportKind::StreamableHttp,
)?;
handshake(&transport, &config).await?;
let params = transport.params(0)?;
assert_eq!(
at(¶ms, "protocolVersion"),
Some(&serde_json::json!("2025-11-25"))
);
Ok(())
}
#[tokio::test]
async fn discovery_is_sent_where_there_is_no_initialize() -> Result<(), TestError> {
let transport = Recording::answering(DISCOVERY_ANSWER)?;
let config = client_at(ProtocolVersion::V2026_07_28);
handshake(&transport, &config).await?;
assert_eq!(transport.methods(), vec![DISCOVER]);
Ok(())
}
#[tokio::test]
async fn a_negotiation_failure_sends_no_notification() -> Result<(), TestError> {
let transport = Recording::answering(
r#"{"protocolVersion":"2099-01-01","capabilities":{},
"serverInfo":{"name":"n","version":"1"}}"#,
)?;
let config = client_at(ProtocolVersion::V2025_06_18);
let error = expect_err(handshake(&transport, &config).await)?;
assert_eq!(transport.methods(), vec![INITIALIZE]);
assert!(matches!(error, HandshakeError::Negotiation(_)));
Ok(())
}
#[tokio::test]
async fn a_transport_failure_surfaces_as_a_transport_error() -> Result<(), TestError> {
let transport = Recording::failing("connection refused");
let config = client_at(ProtocolVersion::V2025_06_18);
let error = expect_err(handshake(&transport, &config).await)?;
let HandshakeError::Transport(TransportError::Unavailable(detail)) = &error else {
return Err(TestError::WrongShape { got: error });
};
assert!(
detail.contains("connection refused"),
"the detail should say why, and said {detail:?}"
);
Ok(())
}
#[tokio::test]
async fn a_handshake_that_fails_sends_no_notification() -> Result<(), TestError> {
let transport = Recording::failing("connection reset");
let config = client_at(ProtocolVersion::V2025_06_18);
let _error = expect_err(handshake(&transport, &config).await)?;
assert_eq!(transport.methods(), vec![INITIALIZE]);
Ok(())
}
fn expect_err<T>(result: Result<T, HandshakeError>) -> Result<HandshakeError, TestError> {
match result {
Ok(_) => Err(TestError::ExpectedAnError),
Err(error) => Ok(error),
}
}