use std::collections::HashMap;
use std::sync::Arc;
use arcly_http_core::auth::jwt::{JwtConfig, JwtService};
use arcly_http_core::core::engine::DiContainerBuilder;
use arcly_http_realtime::realtime::connection::ConnectionRegistry;
use arcly_http_realtime::realtime::gateway::GatewayRuntime;
use arcly_http_realtime::realtime::ws::{ws_route, WsTuning};
use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
const SECRET: &str = "ws-query-token-test-secret";
fn jwt() -> JwtService {
JwtService::new(JwtConfig {
secret: SECRET.to_owned(),
..Default::default()
})
}
async fn serve() -> String {
let registry: &'static ConnectionRegistry = Box::leak(Box::new(ConnectionRegistry::new()));
let mut dispatch: HashMap<
&'static str,
arcly_http_realtime::realtime::gateway::MessageHandler,
> = HashMap::new();
dispatch.insert(
"whoami",
Arc::new(
|client: arcly_http_realtime::realtime::connection::WsClient, _data| {
Box::pin(async move {
let sub = client
.claims()
.and_then(|c| c.get("sub").and_then(|v| v.as_str()))
.unwrap_or("anonymous")
.to_owned();
client.emit("whoami", sub).await;
Ok(())
})
},
),
);
let runtime: &'static GatewayRuntime = Box::leak(Box::new(GatewayRuntime {
path: "/ws/dashboard",
on_connect: Box::new(|_| Box::pin(async {})),
on_disconnect: Box::new(|_| Box::pin(async {})),
dispatch,
}));
let mut b = DiContainerBuilder::new();
b.register(jwt());
let container = b.freeze();
let app = axum::Router::new().route(
"/ws/dashboard",
ws_route(
runtime,
registry,
container,
WsTuning::new(16, 0, std::time::Duration::ZERO),
),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("ws://{addr}/ws/dashboard")
}
async fn whoami(url: &str) -> String {
let (mut socket, _) = tokio_tungstenite::connect_async(url)
.await
.expect("handshake must succeed");
socket
.send(Message::Text(r#"{"event":"whoami"}"#.into()))
.await
.unwrap();
loop {
match socket.next().await.expect("a reply").unwrap() {
Message::Text(t) => {
let v: serde_json::Value = serde_json::from_str(&t).unwrap();
return v["data"].as_str().unwrap_or_default().to_owned();
}
_ => continue,
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn browser_style_client_authenticates_via_query_token() {
let url = serve().await;
let token = jwt()
.issue_access("dashboard-user", "admin", "d@example.com")
.unwrap();
assert_eq!(
whoami(&format!("{url}?access_token={token}")).await,
"dashboard-user",
"the query token must populate claims for gateway handlers"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_forged_query_token_confers_no_identity() {
let url = serve().await;
assert_eq!(
whoami(&format!("{url}?access_token=forged.not.a.jwt")).await,
"anonymous",
);
assert_eq!(whoami(&url).await, "anonymous", "no token at all");
}