Files
cli/sunbeam-sdk/tests/test_livekit.rs

351 lines
11 KiB
Rust
Raw Normal View History

#![cfg(feature = "integration")]
mod helpers;
use helpers::*;
use sunbeam_sdk::client::{AuthMethod, ServiceClient};
use sunbeam_sdk::media::LiveKitClient;
use sunbeam_sdk::media::types::*;
const LIVEKIT_URL: &str = "http://localhost:7880";
const API_KEY: &str = "devkey";
const API_SECRET: &str = "devsecret";
fn livekit() -> LiveKitClient {
LiveKitClient::from_parts(
LIVEKIT_URL.into(),
AuthMethod::Bearer(livekit_test_token()),
)
}
// ---------------------------------------------------------------------------
// 1. Token generation
// ---------------------------------------------------------------------------
#[test]
fn token_generation_basic() {
let grants = VideoGrants {
room_join: Some(true),
room: Some("my-room".into()),
can_publish: Some(true),
can_subscribe: Some(true),
..Default::default()
};
let token = LiveKitClient::generate_access_token(API_KEY, API_SECRET, "user-1", &grants, 3600)
.expect("generate_access_token");
// JWT has three dot-separated segments
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3, "JWT must have 3 segments");
assert!(!parts[0].is_empty(), "header must not be empty");
assert!(!parts[1].is_empty(), "claims must not be empty");
assert!(!parts[2].is_empty(), "signature must not be empty");
}
#[test]
fn token_generation_empty_grants() {
let grants = VideoGrants::default();
let token = LiveKitClient::generate_access_token(API_KEY, API_SECRET, "empty-grants", &grants, 600)
.expect("empty grants should still generate a token");
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);
}
#[test]
fn token_generation_different_grants_produce_different_tokens() {
let grants_a = VideoGrants {
room_create: Some(true),
..Default::default()
};
let grants_b = VideoGrants {
room_join: Some(true),
room: Some("specific-room".into()),
can_publish: Some(true),
..Default::default()
};
let token_a = LiveKitClient::generate_access_token(API_KEY, API_SECRET, "user-a", &grants_a, 600)
.expect("token_a");
let token_b = LiveKitClient::generate_access_token(API_KEY, API_SECRET, "user-b", &grants_b, 600)
.expect("token_b");
assert_ne!(token_a, token_b, "different grants/identities must produce different tokens");
}
// ---------------------------------------------------------------------------
// 2. Room CRUD
// ---------------------------------------------------------------------------
#[tokio::test]
async fn room_crud() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
// Create
let room = lk
.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room");
assert_eq!(room.name, room_name);
assert!(!room.sid.is_empty(), "room must have a sid");
// List — our room should appear
let list = lk.list_rooms().await.expect("list_rooms");
assert!(
list.rooms.iter().any(|r| r.name == room_name),
"created room should appear in list_rooms"
);
// Update metadata (may require roomAdmin grant — handle gracefully)
match lk
.update_room_metadata(&serde_json::json!({
"room": &room_name,
"metadata": "hello-integration-test"
}))
.await
{
Ok(updated) => {
assert_eq!(updated.metadata.as_deref(), Some("hello-integration-test"));
}
Err(_) => {
// roomAdmin grant not available in test token — acceptable
}
}
// Delete
lk.delete_room(&serde_json::json!({ "room": &room_name }))
.await
.expect("delete_room");
// Verify deletion
let list_after = lk.list_rooms().await.expect("list_rooms after delete");
assert!(
!list_after.rooms.iter().any(|r| r.name == room_name),
"deleted room should no longer appear"
);
}
// ---------------------------------------------------------------------------
// 3. Send data
// ---------------------------------------------------------------------------
#[tokio::test]
async fn send_data_to_room() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
// Create room first
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for send_data");
// Send data — may succeed (no-op with no participants) or error; either is acceptable
let result = lk
.send_data(&serde_json::json!({
"room": &room_name,
"data": "aGVsbG8=",
"kind": 0
}))
.await;
// We don't require success — just ensure we don't panic.
// Some LiveKit versions silently accept, others return an error.
match &result {
Ok(()) => {} // fine
Err(e) => {
let msg = format!("{e}");
// Acceptable errors involve missing participants or similar
assert!(
!msg.is_empty(),
"error message should be non-empty"
);
}
}
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
// ---------------------------------------------------------------------------
// 4. Participants
// ---------------------------------------------------------------------------
#[tokio::test]
async fn list_participants_empty_room() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for list_participants");
// No participants have joined — list should be empty
// LiveKit dev mode may reject this with 401 if roomAdmin grant isn't sufficient
match lk.list_participants(&serde_json::json!({ "room": &room_name })).await {
Ok(resp) => assert!(resp.participants.is_empty(), "room should have no participants"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("401") || msg.contains("unauthenticated"),
"unexpected error (expected 401 auth issue): {e}"
);
}
}
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
#[tokio::test]
async fn get_participant_not_found() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for get_participant");
// Non-existent participant — should error
let result = lk
.get_participant(&serde_json::json!({
"room": &room_name,
"identity": "ghost-user"
}))
.await;
assert!(result.is_err(), "get_participant for non-existent identity should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(!err_msg.is_empty(), "error message should be non-empty");
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
// ---------------------------------------------------------------------------
// 5. Remove participant
// ---------------------------------------------------------------------------
#[tokio::test]
async fn remove_participant_not_found() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for remove_participant");
let result = lk
.remove_participant(&serde_json::json!({
"room": &room_name,
"identity": "ghost-user"
}))
.await;
assert!(result.is_err(), "removing non-existent participant should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(!err_msg.is_empty(), "error message should describe the issue");
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
// ---------------------------------------------------------------------------
// 6. Mute track
// ---------------------------------------------------------------------------
#[tokio::test]
async fn mute_track_no_tracks() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for mute_track");
// No participants/tracks exist — should error
let result = lk
.mute_track(&serde_json::json!({
"room": &room_name,
"identity": "ghost-user",
"track_sid": "TR_nonexistent",
"muted": true
}))
.await;
assert!(result.is_err(), "muting a non-existent track should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(!err_msg.is_empty(), "error message should describe the issue");
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
// ---------------------------------------------------------------------------
// 7. Egress
// ---------------------------------------------------------------------------
#[tokio::test]
async fn list_egress_empty() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
// Egress service may not be available in dev mode (returns 500 internal panic)
match lk.list_egress(&serde_json::json!({})).await {
Ok(resp) => assert!(resp.items.is_empty(), "no egress sessions should exist initially"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("500") || msg.contains("internal") || msg.contains("panic"),
"unexpected error (expected 500 from missing egress service): {e}"
);
}
}
}
#[tokio::test]
async fn start_room_composite_egress_error() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let room_name = unique_name("test-room");
lk.create_room(&serde_json::json!({ "name": &room_name }))
.await
.expect("create_room for egress");
// Attempt egress without a valid output config — should error
let result = lk
.start_room_composite_egress(&serde_json::json!({
"room_name": &room_name,
"layout": "speaker-dark"
}))
.await;
assert!(result.is_err(), "starting egress without output config should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(!err_msg.is_empty(), "error message should describe the missing output");
// Cleanup
let _ = lk.delete_room(&serde_json::json!({ "room": &room_name })).await;
}
#[tokio::test]
async fn stop_egress_not_found() {
wait_for_healthy(LIVEKIT_URL, TIMEOUT).await;
let lk = livekit();
let result = lk
.stop_egress(&serde_json::json!({
"egress_id": "EG_nonexistent_00000"
}))
.await;
assert!(result.is_err(), "stopping a non-existent egress should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(!err_msg.is_empty(), "error message should describe the issue");
}