#![cfg(all(
feature = "signal-client-async",
feature = "access-token",
not(feature = "signal-client-tokio")
))]
use std::time::{Duration, Instant};
use livekit_api::access_token::{AccessToken, VideoGrants};
use livekit_api::signal_client::{SignalClient, SignalError, SignalOptions};
const TEST_SECRET: &str = "secret";
const TEST_API_KEY: &str = "APItest";
const TEST_ROOM: &str = "test-room";
const TEST_IDENTITY: &str = "tester";
const SIGNAL_CONTROL_ATTRIBUTE: &str = "lk.mock";
fn online_lk_test_server() -> Option<String> {
let base =
std::env::var("LK_TEST_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1:9999".to_owned());
let authority = base.split("://").nth(1).unwrap_or(&base).trim_end_matches('/');
if std::net::TcpStream::connect(authority).is_ok() {
return Some(base);
}
eprintln!("skipping: mock test server not reachable at {base}");
None
}
fn token(mode: &str) -> String {
let mut at = AccessToken::with_api_key(TEST_API_KEY, TEST_SECRET)
.with_ttl(Duration::from_secs(60 * 60))
.with_identity(TEST_IDENTITY)
.with_grants(VideoGrants {
room_join: true,
room: TEST_ROOM.to_owned(),
..Default::default()
});
if !mode.is_empty() {
let control = format!(r#"{{"signal":"{mode}"}}"#);
at = at.with_attributes([(SIGNAL_CONTROL_ATTRIBUTE, control.as_str())]);
}
at.to_jwt().expect("mint token")
}
#[test]
fn signal_async_happy_join() {
let Some(base) = online_lk_test_server() else { return };
futures::executor::block_on(async {
let (client, join, _events) =
SignalClient::connect(&base, &token(""), SignalOptions::default(), None)
.await
.expect("connect must succeed against the mock");
assert_eq!(join.room.expect("room").name, TEST_ROOM);
assert!(join.ping_interval > 0, "the mock supplies keepalive config");
assert!(join.ping_timeout > 0, "the mock supplies keepalive config");
client.close().await;
});
}
#[test]
fn signal_async_close_before_join_is_a_close() {
let Some(base) = online_lk_test_server() else { return };
futures::executor::block_on(async {
let err = SignalClient::connect(
&base,
&token("close_before_join"),
SignalOptions::default(),
None,
)
.await
.err()
.expect("connect must fail when the server closes before the join");
assert!(
matches!(err, SignalError::Closed),
"a close before the answer is a close, not a timeout, got {err:?}"
);
});
}
#[test]
fn signal_async_no_first_message_times_out() {
let Some(base) = online_lk_test_server() else { return };
futures::executor::block_on(async {
let started = Instant::now();
let err = SignalClient::connect(
&base,
&token("no_first_message"),
SignalOptions::default(),
None,
)
.await
.err()
.expect("connect must fail when the server never answers");
assert!(
matches!(err, SignalError::Timeout(_)),
"a silent server is a timeout, got {err:?}"
);
assert!(
started.elapsed() >= Duration::from_secs(1),
"the timeout fired after {:?}, too fast to be the first-message deadline",
started.elapsed()
);
});
}