fix(websocket): configurable WS session cap (WS-09)
This commit is contained in:
@@ -688,3 +688,73 @@ async fn ws_sessions_registry_tracks_and_aborts_live_sessions() {
|
||||
assert!(drained);
|
||||
ws.close().await;
|
||||
}
|
||||
|
||||
/// WS-09 acceptance: the session cap is enforced on the upgrade — a
|
||||
/// caller over the configured cap is rejected with 503, and an ended
|
||||
/// session frees its slot for the next caller.
|
||||
#[tokio::test]
|
||||
async fn session_cap_rejects_over_limit_with_503_and_frees_slots_on_end() {
|
||||
use alkhttp::server::HttpAdapter;
|
||||
|
||||
let mut registry_val = OperationRegistry::new();
|
||||
registry_val
|
||||
.register(HandlerRegistration::new(
|
||||
OperationSpec::new(
|
||||
"echo/run",
|
||||
OperationType::Query,
|
||||
Visibility::External,
|
||||
serde_json::json!({}),
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(echo_handler()),
|
||||
OperationProvenance::Local,
|
||||
None,
|
||||
None,
|
||||
alkcall::core::types::Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let registry = Arc::new(registry_val);
|
||||
struct StaticTok;
|
||||
impl IdentityProvider for StaticTok {
|
||||
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_from_token(&self, token: &alkcall::core::auth::AuthToken) -> Option<Identity> {
|
||||
let s = String::from_utf8_lossy(&token.raw).to_string();
|
||||
(s == "tok-1").then(|| identity("alice", &[]))
|
||||
}
|
||||
}
|
||||
|
||||
// The adapter's built-in surface: WS route + bearer middleware, the
|
||||
// whole deployment shape the cap knob configures.
|
||||
let adapter =
|
||||
HttpAdapter::new(Arc::new(StaticTok), Arc::clone(®istry)).with_ws_max_sessions(1);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = format!("ws://{}", listener.local_addr().unwrap());
|
||||
let app: axum::Router = adapter.router().clone();
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
let url = format!("{addr}/alk/channels");
|
||||
|
||||
// First session: admitted.
|
||||
let mut first = WsClient::connect_authorized(&url, "tok-1").await.unwrap();
|
||||
let env = call_and_await(&mut first, "req-1", "echo/run", serde_json::json!({})).await;
|
||||
assert_eq!(env.r#type, EVENT_RESPONDED, "first session admitted");
|
||||
|
||||
// Second session over the cap: rejected with 503 at the upgrade.
|
||||
let status = WsClient::connect_status(&url, Some("tok-1")).await;
|
||||
assert_eq!(status, Some(503), "second session rejected over the cap");
|
||||
|
||||
// Ending the first session frees the slot: the next caller is admitted.
|
||||
first.close().await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
let mut third = WsClient::connect_authorized(&url, "tok-1").await.unwrap();
|
||||
let env = call_and_await(&mut third, "req-2", "echo/run", serde_json::json!({})).await;
|
||||
assert_eq!(env.r#type, EVENT_RESPONDED, "slot freed after session end");
|
||||
third.close().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user