|
|
|
|
//! WebRTC Integration Tests
|
|
|
|
|
//!
|
|
|
|
|
//! Comprehensive tests for WebRTC functionality including:
|
|
|
|
|
//! - ICE servers configuration (STUN)
|
|
|
|
|
//! - WebRTC signaling (Offer/Answer/ICE candidate exchange)
|
|
|
|
|
//! - Permission checks (`USE_VOICE_CHAT` permission required)
|
|
|
|
|
//! - Multi-user peer-to-peer scenarios
|
|
|
|
|
//!
|
|
|
|
|
//! These tests validate the complete WebRTC flow from ICE server discovery
|
|
|
|
|
//! to peer connection establishment through signaling messages.
|
|
|
|
|
|
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
|
|
|
|
|
|
mod support;
|
|
|
|
|
use synctv_core::models::{
|
|
|
|
|
RoomAdminPermissionBits, RoomGuestPermissionBits, RoomMemberPermissionBits,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Test Infrastructure Setup
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
enum WebRTCMode {
|
|
|
|
|
SignalingOnly,
|
|
|
|
|
PeerToPeer,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode,
|
|
|
|
|
enable_builtin_stun: bool,
|
|
|
|
|
stun_port: u16,
|
|
|
|
|
stun_host: String,
|
|
|
|
|
stun_external_addr: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
struct ServerConfig {
|
|
|
|
|
advertise_host: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct Config {
|
|
|
|
|
server: ServerConfig,
|
|
|
|
|
webrtc: WebRTCConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Config {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
server: ServerConfig::default(),
|
|
|
|
|
webrtc: WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode::PeerToPeer,
|
|
|
|
|
enable_builtin_stun: false,
|
|
|
|
|
stun_port: 3478,
|
|
|
|
|
stun_host: "0.0.0.0".to_string(),
|
|
|
|
|
stun_external_addr: String::new(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a test configuration with WebRTC enabled
|
|
|
|
|
fn test_webrtc_config() -> Config {
|
|
|
|
|
let mut config = Config {
|
|
|
|
|
webrtc: WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode::PeerToPeer,
|
|
|
|
|
enable_builtin_stun: true,
|
|
|
|
|
stun_port: 3478,
|
|
|
|
|
stun_host: "0.0.0.0".to_string(),
|
|
|
|
|
stun_external_addr: "203.0.113.1:3478".to_string(),
|
|
|
|
|
},
|
|
|
|
|
..Config::default()
|
|
|
|
|
};
|
|
|
|
|
config.server.advertise_host = "test.example.com".to_string();
|
|
|
|
|
config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod ice_servers {
|
|
|
|
|
use super::*;
|
|
|
|
|
use synctv_core::service::{ConfiguredIceServer, IceServerList};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_stun_url_format() {
|
|
|
|
|
let config = test_webrtc_config();
|
|
|
|
|
let stun_url = format!(
|
|
|
|
|
"stun:{}:{}",
|
|
|
|
|
config.server.advertise_host, config.webrtc.stun_port
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(stun_url, "stun:test.example.com:3478");
|
|
|
|
|
assert!(stun_url.starts_with("stun:"));
|
|
|
|
|
assert!(stun_url.contains(":3478"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_external_ice_server_settings_roundtrip_supports_turn_credentials() {
|
|
|
|
|
let servers = IceServerList(vec![
|
|
|
|
|
ConfiguredIceServer::new(vec!["stun:stun.example.com:3478".to_string()]),
|
|
|
|
|
ConfiguredIceServer::new(vec!["turn:turn.example.com:3478?transport=udp".to_string()])
|
|
|
|
|
.with_auth("turn-user", "turn-password"),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
let json = servers.to_string();
|
|
|
|
|
let parsed: IceServerList = json.parse().expect("Should deserialize");
|
|
|
|
|
|
|
|
|
|
assert_eq!(parsed, servers);
|
|
|
|
|
assert_eq!(parsed.0[1].username.as_deref(), Some("turn-user"));
|
|
|
|
|
assert_eq!(parsed.0[1].credential.as_deref(), Some("turn-password"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod webrtc_modes {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_signaling_only_mode() {
|
|
|
|
|
let config = WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode::SignalingOnly,
|
|
|
|
|
enable_builtin_stun: false,
|
|
|
|
|
stun_port: 3478,
|
|
|
|
|
stun_host: "0.0.0.0".to_string(),
|
|
|
|
|
stun_external_addr: String::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.mode, WebRTCMode::SignalingOnly);
|
|
|
|
|
assert!(!config.enable_builtin_stun);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_peer_to_peer_mode() {
|
|
|
|
|
let config = WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode::PeerToPeer,
|
|
|
|
|
enable_builtin_stun: true,
|
|
|
|
|
stun_port: 3478,
|
|
|
|
|
stun_host: "0.0.0.0".to_string(),
|
|
|
|
|
stun_external_addr: "203.0.113.1:3478".to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.mode, WebRTCMode::PeerToPeer);
|
|
|
|
|
assert!(config.enable_builtin_stun);
|
|
|
|
|
assert!(!config.stun_external_addr.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_builtin_stun_configuration() {
|
|
|
|
|
let config = WebRTCConfig {
|
|
|
|
|
mode: WebRTCMode::PeerToPeer,
|
|
|
|
|
enable_builtin_stun: true,
|
|
|
|
|
stun_port: 3478,
|
|
|
|
|
stun_host: "0.0.0.0".to_string(),
|
|
|
|
|
stun_external_addr: "203.0.113.1:3478".to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(config.enable_builtin_stun);
|
|
|
|
|
assert_eq!(config.stun_port, 3478);
|
|
|
|
|
assert_eq!(config.stun_host, "0.0.0.0");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod permissions {
|
|
|
|
|
use super::*;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
use synctv_api::{
|
|
|
|
|
ApiError, ClientApiImpl, EndpointRateLimitCategory, RequestMetadata, TransportProtocol,
|
|
|
|
|
};
|
|
|
|
|
use synctv_core::cache::{l2_backend::RedisCacheL2, KeyBuilder, UsernameCache};
|
|
|
|
|
use synctv_core::models::room_settings::{AllowGuestJoin, GuestAddedPermissions};
|
|
|
|
|
use synctv_core::repository::SettingsRepository;
|
|
|
|
|
use synctv_core::service::JwtService;
|
|
|
|
|
use synctv_core::service::UserServiceRuntimeOptions;
|
|
|
|
|
use synctv_core::service::{
|
|
|
|
|
BruteForceProtection, InMemoryTokenBlacklistStore, RoomService, RuntimeSettingsStore,
|
|
|
|
|
SettingsService, UserService,
|
|
|
|
|
};
|
|
|
|
|
use synctv_core::service::{ConfiguredIceServer, IceServerList};
|
|
|
|
|
use synctv_core_testing::{
|
|
|
|
|
create_test_pool_with_db_and_label, opaque_register_user, redis_connection_manager,
|
|
|
|
|
start_redis_url_with_label, test_redis_key_prefix, RedisContainer, TestContainer,
|
|
|
|
|
};
|
|
|
|
|
use synctv_realtime::sync::{ConnectionLimits, ConnectionManager};
|
|
|
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
|
|
|
|
|
|
struct ClientApiFixture {
|
|
|
|
|
_postgres: TestContainer,
|
|
|
|
|
_redis: RedisContainer,
|
|
|
|
|
pool: sqlx::PgPool,
|
|
|
|
|
user_service: Arc<UserService>,
|
|
|
|
|
room_service: Arc<RoomService>,
|
|
|
|
|
runtime_settings_store: Arc<RuntimeSettingsStore>,
|
|
|
|
|
client_api: ClientApiImpl,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn build_client_api_fixture(label: &str) -> ClientApiFixture {
|
|
|
|
|
let (postgres, pool) = create_test_pool_with_db_and_label("synctv_test", label).await;
|
|
|
|
|
let (redis, redis_url) = start_redis_url_with_label(label).await;
|
|
|
|
|
let redis_key_prefix = test_redis_key_prefix(label);
|
|
|
|
|
|
|
|
|
|
let redis_client = redis::Client::open(redis_url.as_str()).expect("Redis client");
|
|
|
|
|
let redis_conn = Arc::new(tokio::sync::RwLock::new(
|
|
|
|
|
redis_connection_manager(&redis_client).await,
|
|
|
|
|
));
|
|
|
|
|
let username_cache = UsernameCache::new(
|
|
|
|
|
Arc::new(RedisCacheL2::from_runtime(synctv_core::shared_runtime(
|
|
|
|
|
redis_conn.clone(),
|
|
|
|
|
))),
|
|
|
|
|
format!("{redis_key_prefix}un:"),
|
|
|
|
|
100,
|
|
|
|
|
300,
|
|
|
|
|
);
|
|
|
|
|
let brute_force = BruteForceProtection::new_with_config(
|
|
|
|
|
redis_key_prefix.clone(),
|
|
|
|
|
Arc::new(synctv_core::service::RedisAttemptTracker::new(
|
|
|
|
|
redis_conn.clone(),
|
|
|
|
|
50_000,
|
|
|
|
|
synctv_core::service::BruteForceConfig::default().attempts_ttl_secs,
|
|
|
|
|
)),
|
|
|
|
|
Arc::new(synctv_core::service::RedisAttemptTracker::new(
|
|
|
|
|
redis_conn,
|
|
|
|
|
100_000,
|
|
|
|
|
synctv_core::service::BruteForceConfig::default().ip_attempts_ttl_secs,
|
|
|
|
|
)),
|
|
|
|
|
synctv_core::service::BruteForceConfig::default(),
|
|
|
|
|
);
|
|
|
|
|
let jwt_service =
|
|
|
|
|
JwtService::new("this-is-a-test-secret-with-enough-entropy-for-jwt-signing-32chars")
|
|
|
|
|
.expect("JwtService");
|
|
|
|
|
let token_blacklist: Arc<dyn synctv_core::service::TokenBlacklistStore> =
|
|
|
|
|
Arc::new(InMemoryTokenBlacklistStore::new(10_000, 3600, 86400));
|
|
|
|
|
|
|
|
|
|
let user_service = UserService::new_with_runtime(
|
|
|
|
|
&pool,
|
|
|
|
|
jwt_service.clone(),
|
|
|
|
|
username_cache,
|
|
|
|
|
token_blacklist,
|
|
|
|
|
KeyBuilder::new(redis_key_prefix),
|
|
|
|
|
brute_force,
|
|
|
|
|
UserServiceRuntimeOptions {
|
|
|
|
|
password_registration_policy_override: Some(
|
|
|
|
|
synctv_core::service::RegistrationPolicy::Immediate,
|
|
|
|
|
),
|
|
|
|
|
..synctv_core::service::UserServiceRuntimeOptions::test_defaults()
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let user_service = Arc::new(user_service);
|
|
|
|
|
let room_service = Arc::new(
|
|
|
|
|
RoomService::new_for_tests(pool.clone(), (*user_service).clone())
|
|
|
|
|
.expect("room service should build"),
|
|
|
|
|
);
|
|
|
|
|
let settings_repo = SettingsRepository::new(pool.clone());
|
|
|
|
|
let settings_service = Arc::new(SettingsService::new(settings_repo, pool.clone()));
|
|
|
|
|
let runtime_settings_store = Arc::new(RuntimeSettingsStore::new(settings_service));
|
|
|
|
|
runtime_settings_store
|
|
|
|
|
.init(CancellationToken::new())
|
|
|
|
|
.expect("initialize runtime settings store");
|
|
|
|
|
let builtin_stun_url = format!(
|
|
|
|
|
"stun:{}:{}",
|
|
|
|
|
test_webrtc_config().server.advertise_host,
|
|
|
|
|
test_webrtc_config().webrtc.stun_port
|
|
|
|
|
);
|
|
|
|
|
let client_api = ClientApiImpl::new_with_runtime(
|
|
|
|
|
synctv_api::ClientApiOptions {
|
|
|
|
|
read_pool: None,
|
|
|
|
|
user_service: user_service.clone(),
|
|
|
|
|
room_service: room_service.clone(),
|
|
|
|
|
connection_service: Arc::new(ConnectionManager::new(ConnectionLimits::default())),
|
|
|
|
|
runtime_settings: Arc::new(synctv_api::ApiRuntimeSettings::default()),
|
|
|
|
|
publish_key_service: None,
|
|
|
|
|
jwt_service,
|
|
|
|
|
live_streaming_infrastructure: None,
|
|
|
|
|
runtime_settings_store: Some(runtime_settings_store.clone()),
|
|
|
|
|
public_id_codec: Arc::new(synctv_api::PublicIdCodec::plain()),
|
|
|
|
|
chat_service: None,
|
|
|
|
|
provider_stores: Arc::new(
|
|
|
|
|
synctv_core::provider::ProviderStoreRegistry::local_only("test:provider:"),
|
|
|
|
|
),
|
|
|
|
|
email_api: None,
|
|
|
|
|
passkey_service: None,
|
|
|
|
|
},
|
|
|
|
|
synctv_api::ClientApiRuntime {
|
|
|
|
|
builtin_stun_url: Some(builtin_stun_url),
|
|
|
|
|
..support::client_api_runtime()
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ClientApiFixture {
|
|
|
|
|
_postgres: postgres,
|
|
|
|
|
_redis: redis,
|
|
|
|
|
pool,
|
|
|
|
|
user_service,
|
|
|
|
|
room_service,
|
|
|
|
|
runtime_settings_store,
|
|
|
|
|
client_api,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn register_fixture_user(
|
|
|
|
|
fixture: &ClientApiFixture,
|
|
|
|
|
username: &str,
|
|
|
|
|
) -> synctv_core::models::User {
|
|
|
|
|
opaque_register_user(
|
|
|
|
|
fixture.user_service.as_ref(),
|
|
|
|
|
username,
|
|
|
|
|
Some(format!("{username}@test.com")),
|
|
|
|
|
"TestPassword123!",
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("register user")
|
|
|
|
|
.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn persist_external_ice_servers(fixture: &ClientApiFixture, servers: IceServerList) {
|
|
|
|
|
let mut settings = fixture
|
|
|
|
|
.runtime_settings_store
|
|
|
|
|
.runtime_settings()
|
|
|
|
|
.expect("runtime settings should load");
|
|
|
|
|
settings.webrtc.external_ice_servers = servers;
|
|
|
|
|
fixture
|
|
|
|
|
.runtime_settings_store
|
|
|
|
|
.persist_runtime_settings(&settings)
|
|
|
|
|
.await
|
|
|
|
|
.expect("external ice servers should persist");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
#[ignore = "Requires Docker"]
|
|
|
|
|
async fn test_get_ice_servers_includes_configured_turn_servers_with_credentials() {
|
|
|
|
|
let fixture = build_client_api_fixture("api-webrtc-custom-ice").await;
|
|
|
|
|
|
|
|
|
|
persist_external_ice_servers(
|
|
|
|
|
&fixture,
|
|
|
|
|
IceServerList(vec![ConfiguredIceServer::new(vec![
|
|
|
|
|
"turn:turn.example.com:3478?transport=udp".to_string(),
|
|
|
|
|
"turns:turn.example.com:5349".to_string(),
|
|
|
|
|
])
|
|
|
|
|
.with_auth("turn-user", "turn-password")]),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let creator = register_fixture_user(&fixture, "webrtc_turn_creator").await;
|
|
|
|
|
let member = register_fixture_user(&fixture, "webrtc_turn_member").await;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let room = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.create_room(
|
|
|
|
|
"WebRTC Custom ICE Room".to_string(),
|
|
|
|
|
String::new(),
|
|
|
|
|
creator.id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("create room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.join_room(room.id, member.id, None)
|
|
|
|
|
.await
|
|
|
|
|
.expect("join room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.grant_permission(
|
|
|
|
|
room.id,
|
|
|
|
|
creator.id,
|
|
|
|
|
member.id,
|
|
|
|
|
RoomMemberPermissionBits::USE_VOICE_CHAT,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("grant USE_VOICE_CHAT");
|
|
|
|
|
|
|
|
|
|
let response = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.get_ice_servers(&room.id, &member.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect("get ice servers");
|
|
|
|
|
|
|
|
|
|
assert_eq!(response.servers.len(), 2);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[0].urls,
|
|
|
|
|
vec!["stun:test.example.com:3478".to_string()]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[1].urls,
|
|
|
|
|
vec![
|
|
|
|
|
"turn:turn.example.com:3478?transport=udp".to_string(),
|
|
|
|
|
"turns:turn.example.com:5349".to_string(),
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(response.servers[1].username.as_deref(), Some("turn-user"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[1].credential.as_deref(),
|
|
|
|
|
Some("turn-password")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
#[ignore = "Requires Docker"]
|
|
|
|
|
async fn test_guest_with_use_voice_chat_permission_can_bootstrap_ice_servers() {
|
|
|
|
|
let fixture = build_client_api_fixture("api-webrtc-guest-ice").await;
|
|
|
|
|
|
|
|
|
|
persist_external_ice_servers(
|
|
|
|
|
&fixture,
|
|
|
|
|
IceServerList(vec![ConfiguredIceServer::new(vec![
|
|
|
|
|
"turn:guest-turn.example.com:3478?transport=udp".to_string(),
|
|
|
|
|
])
|
|
|
|
|
.with_auth("guest-turn-user", "guest-turn-password")]),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let creator = register_fixture_user(&fixture, "webrtc_guest_creator").await;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let room = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.create_room(
|
|
|
|
|
"WebRTC Guest ICE Room".to_string(),
|
|
|
|
|
String::new(),
|
|
|
|
|
creator.id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("create room");
|
|
|
|
|
|
|
|
|
|
let mut settings = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.get_room_settings(&room.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect("load room settings");
|
|
|
|
|
settings.allow_guest_join = AllowGuestJoin(true);
|
|
|
|
|
settings.guest_added_permissions =
|
|
|
|
|
GuestAddedPermissions(RoomGuestPermissionBits::USE_VOICE_CHAT);
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.set_settings(room.id, creator.id, settings)
|
|
|
|
|
.await
|
|
|
|
|
.expect("enable guest WebRTC");
|
|
|
|
|
|
|
|
|
|
let guest_version = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.get_room_guest_version(&room.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect("guest version");
|
|
|
|
|
let token = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.jwt_service
|
|
|
|
|
.sign_guest_token_with_version(&room.id, guest_version)
|
|
|
|
|
.expect("guest token");
|
|
|
|
|
let public_room_id = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.public_id_codec
|
|
|
|
|
.encode_room_id(room.id)
|
|
|
|
|
.expect("public room id");
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let metadata = RequestMetadata::new(TransportProtocol::Http)
|
|
|
|
|
.with_authorization(Some(format!("Bearer {token}")));
|
|
|
|
|
let response = ClientApiImpl::execute_room_actor_endpoint(
|
|
|
|
|
Arc::new(fixture.client_api.clone()),
|
|
|
|
|
&metadata,
|
|
|
|
|
public_room_id,
|
|
|
|
|
EndpointRateLimitCategory::Read,
|
|
|
|
|
|client_api, actor| async move { client_api.get_ice_servers_for_actor(&actor).await },
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("guest ICE bootstrap");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[0].urls,
|
|
|
|
|
vec!["stun:test.example.com:3478".to_string()]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[1].urls,
|
|
|
|
|
vec!["turn:guest-turn.example.com:3478?transport=udp".to_string()]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[1].username.as_deref(),
|
|
|
|
|
Some("guest-turn-user")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.servers[1].credential.as_deref(),
|
|
|
|
|
Some("guest-turn-password")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
#[ignore = "Requires Docker"]
|
|
|
|
|
async fn test_get_ice_servers_accepts_either_business_permission_and_denies_without_both() {
|
|
|
|
|
let fixture = build_client_api_fixture("api-webrtc-permissions").await;
|
|
|
|
|
|
|
|
|
|
let creator = register_fixture_user(&fixture, "webrtc_creator").await;
|
|
|
|
|
let member = register_fixture_user(&fixture, "webrtc_member").await;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let room = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.create_room(
|
|
|
|
|
"WebRTC Permission Room".to_string(),
|
|
|
|
|
String::new(),
|
|
|
|
|
creator.id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("create room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.join_room(room.id, member.id, None)
|
|
|
|
|
.await
|
|
|
|
|
.expect("join room");
|
|
|
|
|
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.member_service()
|
|
|
|
|
.revoke_permission(
|
|
|
|
|
room.id,
|
|
|
|
|
creator.id,
|
|
|
|
|
member.id,
|
|
|
|
|
RoomMemberPermissionBits::USE_VOICE_CHAT,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("revoke USE_VOICE_CHAT");
|
|
|
|
|
|
|
|
|
|
fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.get_ice_servers(&room.id, &member.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect("USE_P2P_MEDIA alone should authorize ICE bootstrap");
|
|
|
|
|
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.member_service()
|
|
|
|
|
.revoke_permission(
|
|
|
|
|
room.id,
|
|
|
|
|
creator.id,
|
|
|
|
|
member.id,
|
|
|
|
|
RoomAdminPermissionBits::USE_P2P_MEDIA,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("revoke USE_P2P_MEDIA");
|
|
|
|
|
|
|
|
|
|
let err = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.get_ice_servers(&room.id, &member.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect_err("members without an RTC business permission must be denied");
|
|
|
|
|
|
|
|
|
|
match err {
|
|
|
|
|
ApiError::Authorization(message) => {
|
|
|
|
|
assert!(
|
|
|
|
|
message.contains("Forbidden"),
|
|
|
|
|
"expected permission failure to map to authorization error: {message}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
other => panic!("expected authorization error, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
#[ignore = "Requires Docker"]
|
|
|
|
|
async fn test_get_ice_servers_rejects_banned_room() {
|
|
|
|
|
let fixture = build_client_api_fixture("api-webrtc-banned-room").await;
|
|
|
|
|
|
|
|
|
|
let creator = register_fixture_user(&fixture, "webrtc_banned_creator").await;
|
|
|
|
|
let member = register_fixture_user(&fixture, "webrtc_banned_member").await;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let room = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.create_room(
|
|
|
|
|
"WebRTC Banned Room".to_string(),
|
|
|
|
|
String::new(),
|
|
|
|
|
creator.id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("create room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.join_room(room.id, member.id, None)
|
|
|
|
|
.await
|
|
|
|
|
.expect("join room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.ban_room(&room.id, &creator.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect("ban room");
|
|
|
|
|
|
|
|
|
|
let err = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.get_ice_servers(&room.id, &member.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect_err("banned room must reject webrtc bootstrap");
|
|
|
|
|
|
|
|
|
|
match err {
|
|
|
|
|
ApiError::Authorization(message) => assert!(
|
|
|
|
|
message.contains("Forbidden"),
|
|
|
|
|
"banned room should be rejected as authorization failure: {message}"
|
|
|
|
|
),
|
|
|
|
|
other => panic!("expected authorization error, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
#[ignore = "Requires Docker"]
|
|
|
|
|
async fn test_get_ice_servers_rejects_closed_room() {
|
|
|
|
|
let fixture = build_client_api_fixture("api-webrtc-closed-room").await;
|
|
|
|
|
|
|
|
|
|
let creator = register_fixture_user(&fixture, "webrtc_closed_creator").await;
|
|
|
|
|
let member = register_fixture_user(&fixture, "webrtc_closed_member").await;
|
|
|
|
|
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
let mut room = fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.create_room(
|
|
|
|
|
"WebRTC Closed Room".to_string(),
|
|
|
|
|
String::new(),
|
|
|
|
|
creator.id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("create room");
|
|
|
|
|
fixture
|
|
|
|
|
.room_service
|
|
|
|
|
.join_room(room.id, member.id, None)
|
|
|
|
|
.await
|
|
|
|
|
.expect("join room");
|
|
|
|
|
|
|
|
|
|
let original_version = room.version;
|
|
|
|
|
room.close();
|
|
|
|
|
synctv_core::repository::RoomRepository::new(fixture.pool.clone())
|
|
|
|
|
.update(&room, original_version)
|
|
|
|
|
.await
|
|
|
|
|
.expect("close room");
|
|
|
|
|
|
|
|
|
|
let err = fixture
|
|
|
|
|
.client_api
|
|
|
|
|
.get_ice_servers(&room.id, &member.id)
|
|
|
|
|
.await
|
|
|
|
|
.expect_err("closed room must reject webrtc bootstrap");
|
|
|
|
|
|
|
|
|
|
match err {
|
|
|
|
|
ApiError::Authorization(message) => assert!(
|
|
|
|
|
message.contains("Forbidden"),
|
|
|
|
|
"closed room should be rejected as authorization failure: {message}"
|
|
|
|
|
),
|
|
|
|
|
other => panic!("expected authorization error, got {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod ice_filtering {
|
|
|
|
|
use std::net::IpAddr;
|
|
|
|
|
|
|
|
|
|
/// Check if an IP address is private, loopback, or link-local
|
|
|
|
|
const fn is_private_or_internal(ip: &IpAddr) -> bool {
|
|
|
|
|
match ip {
|
|
|
|
|
IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_loopback() || ipv4.is_link_local(),
|
|
|
|
|
IpAddr::V6(ipv6) => ipv6.is_loopback() || ipv6.is_unicast_link_local(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_private_ip_detection() {
|
|
|
|
|
let private_ips = vec![
|
|
|
|
|
"10.0.0.1",
|
|
|
|
|
"172.16.0.1",
|
|
|
|
|
"192.168.1.1",
|
|
|
|
|
"127.0.0.1",
|
|
|
|
|
"169.254.1.1",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for ip in private_ips {
|
|
|
|
|
let parsed: IpAddr = ip.parse().unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
is_private_or_internal(&parsed),
|
|
|
|
|
"{ip} should be considered private/internal"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_public_ip_detection() {
|
|
|
|
|
let public_ips = vec!["8.8.8.8", "1.1.1.1", "203.0.113.1"];
|
|
|
|
|
|
|
|
|
|
for ip in public_ips {
|
|
|
|
|
let parsed: IpAddr = ip.parse().unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
!is_private_or_internal(&parsed),
|
|
|
|
|
"{ip} should be considered public"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_candidate_type_identification() {
|
|
|
|
|
let host_candidate = "candidate:1 1 udp 2130706431 192.168.1.100 54321 typ host";
|
|
|
|
|
let srflx_candidate = "candidate:2 1 udp 1694498815 203.0.113.1 54321 typ srflx";
|
|
|
|
|
let relay_candidate = "candidate:3 1 udp 16777215 203.0.113.2 54321 typ relay";
|
|
|
|
|
|
|
|
|
|
assert!(host_candidate.contains("typ host"));
|
|
|
|
|
assert!(srflx_candidate.contains("typ srflx"));
|
|
|
|
|
assert!(relay_candidate.contains("typ relay"));
|
|
|
|
|
}
|
|
|
|
|
}
|