//! Global runtime settings store integration tests. use synctv_core::service::*; use synctv_core_testing::{err, ok}; #[test] fn test_room_password_policy_parse_and_display() { assert_eq!( ok( "optional".parse::(), "optional policy should parse", ), RoomPasswordPolicy::Optional ); assert_eq!( ok( "required".parse::(), "required policy should parse", ), RoomPasswordPolicy::Required ); assert_eq!( ok( "forbidden".parse::(), "forbidden policy should parse", ), RoomPasswordPolicy::Forbidden ); assert_eq!(RoomPasswordPolicy::Optional.to_string(), "optional"); assert_eq!(RoomPasswordPolicy::Required.to_string(), "required"); assert_eq!(RoomPasswordPolicy::Forbidden.to_string(), "forbidden"); assert!("true".parse::().is_err()); assert!("invalid_policy".parse::().is_err()); } #[test] fn test_external_ice_server_list_supports_public_defaults_and_custom_servers() { let list = IceServerList::new(); assert!(list.0.is_empty()); let defaults = IceServerList::public_stun_defaults(); assert_eq!(defaults.0.len(), 2); assert_eq!(defaults.0[0].urls, ["stun:stun.cloudflare.com:3478"]); assert_eq!(defaults.0[1].urls, ["stun:stun.l.google.com:19302"]); let custom = IceServerList(vec![ ConfiguredIceServer::new(vec!["stun:custom1.example.com:19302".to_string()]), ConfiguredIceServer::new(vec![ "turn:custom2.example.com:3478?transport=udp".to_string() ]) .with_auth("turn-user", "turn-secret"), ConfiguredIceServer::new(vec!["turns:custom3.example.com:5349".to_string()]) .with_auth("turn-user-2", "turn-secret-2"), ]); assert_eq!(custom.0.len(), 3); let empty = IceServerList(vec![]); assert!(empty.0.is_empty()); let json = custom.to_string(); let parsed: IceServerList = ok(json.parse(), "ICE server list should parse"); assert_eq!(parsed.0.len(), 3); assert_eq!(parsed.0[1].username.as_deref(), Some("turn-user")); assert_eq!(parsed.0[1].credential.as_deref(), Some("turn-secret")); } #[test] fn test_cors_allowed_origins_updates() { let list = CorsAllowedOrigins::new(); assert!(list.0.is_empty()); let with_origins = CorsAllowedOrigins(vec![ "https://example.com".to_string(), "https://app.example.com".to_string(), "https://*.example.com".to_string(), ]); assert_eq!(with_origins.0.len(), 3); let empty = CorsAllowedOrigins(vec![]); assert!(empty.0.is_empty()); let json = with_origins.to_string(); let parsed: CorsAllowedOrigins = ok(json.parse(), "CORS origins should parse"); assert_eq!(parsed.0.len(), 3); } #[test] fn test_public_settings_skips_empty_rtmp_advertise_address() { let defaults = PublicSettings::defaults(); let json = ok( serde_json::to_string(&defaults), "public settings should serialize", ); assert!(!json.contains("advertise_address")); assert_eq!(defaults.max_pinned_chat_messages_per_room, 20); assert!(json.contains("max_pinned_chat_messages_per_room")); } #[test] fn test_invalid_json_for_stun_server_list() { assert!("not json".parse::().is_err()); assert!("{invalid}".parse::().is_err()); assert!("[not-a-url]".parse::().is_err()); } #[test] fn test_invalid_json_for_cors_origins() { assert!("not json".parse::().is_err()); assert!("{invalid}".parse::().is_err()); assert!("[not-a-url]".parse::().is_err()); } #[test] fn test_custom_setting_value_roundtrip() { let stun_list = IceServerList(vec![ ConfiguredIceServer::new(vec!["stun:example.com:19302".to_string()]), ConfiguredIceServer::new(vec!["turn:turn.example.com:3478".to_string()]) .with_auth("alice", "secret"), ]); let stun_json = stun_list.to_string(); let stun_parsed: IceServerList = ok(stun_json.parse(), "STUN list should parse"); assert_eq!(stun_list, stun_parsed); let cors_origins = CorsAllowedOrigins(vec!["https://example.com".to_string()]); let cors_json = cors_origins.to_string(); let cors_parsed: CorsAllowedOrigins = ok(cors_json.parse(), "CORS origins should parse"); assert_eq!(cors_origins, cors_parsed); } #[tokio::test] #[ignore = "Requires Docker"] async fn test_email_enabled_update_requires_complete_email_config() { use std::sync::Arc; use synctv_core::repository::SettingsRepository; use synctv_core::service::SettingsService; use synctv_core_testing::create_test_pool; let (_container, pool) = create_test_pool().await; let service = Arc::new(SettingsService::new( SettingsRepository::new(pool.clone()), pool, )); ok( service.initialize().await, "settings service should initialize", ); let registry = RuntimeSettingsStore::new(service.clone()); ok( registry.init(tokio_util::sync::CancellationToken::new()), "runtime settings store should init", ); let mut settings = ok( registry.runtime_settings(), "runtime settings snapshot should load", ); settings.email.enabled = true; let error = err( registry.persist_runtime_settings(&settings).await, "email.enabled=true must reject incomplete SMTP settings", ); assert!( error .to_string() .contains(synctv_core::service::EmailSmtpHostSetting::KEY), "expected missing smtp_host validation error, got: {error:?}" ); assert!( service .get(synctv_core::service::EmailEnabledSetting::KEY) .await .is_err(), "failed update must not persist email.enabled" ); } #[tokio::test] #[ignore = "Requires Docker"] async fn test_email_enabled_batch_update_accepts_complete_email_config() { use std::sync::Arc; use synctv_core::repository::SettingsRepository; use synctv_core::service::SettingsService; use synctv_core_testing::create_test_pool; let (_container, pool) = create_test_pool().await; let service = Arc::new(SettingsService::new( SettingsRepository::new(pool.clone()), pool, )); ok( service.initialize().await, "settings service should initialize", ); let registry = RuntimeSettingsStore::new(service.clone()); ok( registry.init(tokio_util::sync::CancellationToken::new()), "runtime settings store should init", ); let mut settings = ok( registry.runtime_settings(), "runtime settings snapshot should load", ); settings.email.enabled = true; settings.email.smtp_host = Some("smtp.example.com".to_string()); settings.email.smtp_port = 587; settings.email.from_email = Some("noreply@example.com".to_string()); ok( registry.persist_runtime_settings(&settings).await, "complete enabled email settings should persist", ); assert_eq!( ok( service .get(synctv_core::service::EmailEnabledSetting::KEY) .await, "email.enabled should load" ) .value, "true", "email.enabled should persist after complete batch update" ); } #[test] fn test_room_password_policy_has_no_contradictory_boolean_state() { let policies = [ RoomPasswordPolicy::Optional, RoomPasswordPolicy::Required, RoomPasswordPolicy::Forbidden, ]; let serialized = policies .into_iter() .map(|policy| policy.to_string()) .collect::>(); assert_eq!( serialized, std::collections::HashSet::from([ "optional".to_string(), "required".to_string(), "forbidden".to_string(), ]) ); } #[test] fn test_room_password_policy_rejects_non_policy_values() { for invalid in [ "true", "false", "required,forbidden", "", "optional|required", ] { assert!( invalid.parse::().is_err(), "{invalid:?} must not parse as a room password policy" ); } }