use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use super::*;
type Local = BoundedSessions<LocalSessionManager>;
fn bounded(max: usize) -> Arc<Local> {
Arc::new(BoundedSessions::new(LocalSessionManager::default(), max))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn req_sec_024_a_session_closed_twice_is_counted_once() {
let sessions = bounded(3);
let mut ids = Vec::new();
for _ in 0..3 {
ids.push(sessions.create_session().await.unwrap().0);
}
assert!(matches!(
sessions.create_session().await,
Err(BoundedError::LimitReached)
));
for round in 0..200 {
let victim = ids.remove(round % ids.len());
let (a, b) = (Arc::clone(&sessions), Arc::clone(&sessions));
let (id_a, id_b) = (victim.clone(), victim.clone());
let first = tokio::spawn(async move { a.close_session(&id_a).await });
let second = tokio::spawn(async move { b.close_session(&id_b).await });
first.await.unwrap().unwrap();
second.await.unwrap().unwrap();
assert_eq!(
sessions.live(),
2,
"round {round}: closed twice, counted once"
);
ids.push(
sessions
.create_session()
.await
.unwrap_or_else(|_| panic!("round {round}: one slot is free"))
.0,
);
assert_eq!(sessions.live(), 3, "round {round}");
assert!(
matches!(
sessions.create_session().await,
Err(BoundedError::LimitReached)
),
"round {round}: and only one"
);
}
sessions
.close_session(&SessionId::from("no-such-session"))
.await
.unwrap();
assert_eq!(sessions.live(), 3);
}
#[tokio::test]
async fn req_sec_024_the_fallback_is_a_fixed_500() {
let sessions = bounded(0);
let error = sessions.create_session().await.map(|_| ()).unwrap_err();
assert_eq!(error.to_string(), "session limit reached");
assert!(std::error::Error::source(&error).is_none());
assert!(matches!(error, BoundedError::LimitReached));
let (result, refused) = scope_refusals(sessions.create_session()).await;
assert!(result.is_err() && refused);
let roomy = bounded(1);
let (result, refused) = scope_refusals(roomy.create_session()).await;
assert!(result.is_ok() && !refused);
let sessions = bounded(0);
let (inner, refused) = scope_refusals(async move {
tokio::spawn(async move { sessions.create_session().await.is_err() })
.await
.unwrap()
})
.await;
assert!(inner && !refused, "a spawned task has no cell");
}
struct Stalled;
struct NoStream;
impl Stream for NoStream {
type Item = ServerSseMessage;
fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
impl SessionManager for Stalled {
type Error = std::io::Error;
type Transport = <LocalSessionManager as SessionManager>::Transport;
async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
std::future::pending().await
}
async fn initialize_session(
&self,
_: &SessionId,
_: ClientJsonRpcMessage,
) -> Result<ServerJsonRpcMessage, Self::Error> {
Err(std::io::Error::other("unused"))
}
async fn has_session(&self, _: &SessionId) -> Result<bool, Self::Error> {
Ok(false)
}
async fn close_session(&self, _: &SessionId) -> Result<(), Self::Error> {
Ok(())
}
async fn create_stream(
&self,
_: &SessionId,
_: ClientJsonRpcMessage,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
Ok(NoStream)
}
async fn accept_message(
&self,
_: &SessionId,
_: ClientJsonRpcMessage,
) -> Result<(), Self::Error> {
Ok(())
}
async fn create_standalone_stream(
&self,
_: &SessionId,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
Ok(NoStream)
}
async fn resume(
&self,
_: &SessionId,
_: String,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
Ok(NoStream)
}
}
#[tokio::test]
async fn req_sec_024_a_cancelled_creation_gives_the_slot_back() {
let sessions = BoundedSessions::new(Stalled, 2);
for round in 0..10 {
let waited =
tokio::time::timeout(Duration::from_millis(5), sessions.create_session()).await;
assert!(waited.is_err(), "round {round}: the creation stalls");
assert_eq!(sessions.live(), 0, "round {round}: the slot is back");
}
let mut first = Box::pin(sessions.create_session());
let mut second = Box::pin(sessions.create_session());
assert!(poll_once(first.as_mut()).is_pending());
assert!(poll_once(second.as_mut()).is_pending());
assert_eq!(sessions.live(), 2);
assert!(matches!(
sessions.create_session().await,
Err(BoundedError::LimitReached)
));
drop(first);
assert_eq!(sessions.live(), 1, "dropping one frees one");
drop(second);
assert_eq!(sessions.live(), 0);
}
fn poll_once<F: std::future::Future>(future: Pin<&mut F>) -> Poll<F::Output> {
let waker = std::task::Waker::noop();
future.poll(&mut Context::from_waker(waker))
}
#[tokio::test]
async fn req_sec_024_restore_never_creates_a_session() {
let sessions = bounded(1);
let outcome = sessions
.restore_session(SessionId::from("restored"))
.await
.unwrap();
assert!(matches!(outcome, RestoreOutcome::NotSupported));
assert_eq!(sessions.live(), 0);
assert!(
!sessions
.has_session(&SessionId::from("restored"))
.await
.unwrap()
);
}