From e664bf4620571b14fb04414b5e34f104dda65002 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 14 Feb 2026 09:08:49 +0100 Subject: [PATCH] added tests, use specific CI --- .gitea/workflows/build-and-test.yml | 2 +- .gitea/workflows/build-release.yml | 8 +- src-tauri/src/commands/auth.rs | 194 +++++++++++++ src-tauri/src/commands/connectivity.rs | 15 ++ src-tauri/src/commands/conversions.rs | 50 ++++ src-tauri/src/commands/playback_mode.rs | 69 +++++ src-tauri/src/commands/playback_reporting.rs | 213 +++++++++++++++ src-tauri/src/commands/repository.rs | 119 ++++++++ src-tauri/src/commands/sessions.rs | 66 +++++ src-tauri/src/commands/storage.rs | 269 +++++++++++++++++++ src-tauri/src/commands/sync.rs | 135 ++++++++++ src-tauri/src/player/state.rs | 253 +++++++++++++++++ 12 files changed, 1388 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index dd415a7..cc3b738 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -16,7 +16,7 @@ on: jobs: build: name: Build APK and Run Tests - runs-on: ubuntu-latest + runs-on: [linux, amd64] container: image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest diff --git a/.gitea/workflows/build-release.yml b/.gitea/workflows/build-release.yml index e5ad41d..de88a81 100644 --- a/.gitea/workflows/build-release.yml +++ b/.gitea/workflows/build-release.yml @@ -17,7 +17,7 @@ env: jobs: test: name: Run Tests - runs-on: ubuntu-latest + runs-on: [linux, amd64] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -61,7 +61,7 @@ jobs: build-linux: name: Build Linux - runs-on: ubuntu-latest + runs-on: [linux, amd64] needs: test steps: - name: Checkout repository @@ -133,7 +133,7 @@ jobs: build-android: name: Build Android - runs-on: ubuntu-latest + runs-on: [linux, amd64] needs: test steps: - name: Checkout repository @@ -214,7 +214,7 @@ jobs: create-release: name: Create Release - runs-on: ubuntu-latest + runs-on: [linux, amd64] needs: [build-linux, build-android] if: startsWith(github.ref, 'refs/tags/v') steps: diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 37dc732..ba254f9 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -237,3 +237,197 @@ pub async fn auth_reauthenticate( Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_session_serialization() { + let session = Session { + user_id: "user-123".to_string(), + username: "john_doe".to_string(), + server_id: "server-456".to_string(), + server_url: "https://jellyfin.example.com".to_string(), + server_name: "My Jellyfin".to_string(), + access_token: "token-789-xyz".to_string(), + verified: true, + needs_reauth: false, + }; + + // Should serialize successfully + let json = serde_json::to_string(&session); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("user-123")); + assert!(serialized.contains("john_doe")); + assert!(serialized.contains("server-456")); + } + + #[test] + fn test_session_deserialization() { + let json = r#"{ + "userId": "user-123", + "username": "john_doe", + "serverId": "server-456", + "serverUrl": "https://jellyfin.example.com", + "serverName": "My Jellyfin", + "accessToken": "token-789", + "verified": true, + "needsReauth": false + }"#; + + let result: Result = serde_json::from_str(json); + assert!(result.is_ok()); + + let session = result.unwrap(); + assert_eq!(session.user_id, "user-123"); + assert_eq!(session.username, "john_doe"); + assert_eq!(session.server_id, "server-456"); + assert!(session.verified); + assert!(!session.needs_reauth); + } + + #[test] + fn test_session_roundtrip() { + let original = Session { + user_id: "user-999".to_string(), + username: "alice".to_string(), + server_id: "server-111".to_string(), + server_url: "https://server.local".to_string(), + server_name: "Home Server".to_string(), + access_token: "very-long-token-string".to_string(), + verified: true, + needs_reauth: false, + }; + + let json = serde_json::to_string(&original).unwrap(); + let deserialized: Session = serde_json::from_str(&json).unwrap(); + + assert_eq!(original.user_id, deserialized.user_id); + assert_eq!(original.username, deserialized.username); + assert_eq!(original.server_id, deserialized.server_id); + assert_eq!(original.server_url, deserialized.server_url); + assert_eq!(original.access_token, deserialized.access_token); + assert_eq!(original.verified, deserialized.verified); + } + + #[test] + fn test_session_clone() { + let session = Session { + user_id: "user-clone".to_string(), + username: "test_user".to_string(), + server_id: "server-clone".to_string(), + server_url: "https://clone.example.com".to_string(), + server_name: "Clone Server".to_string(), + access_token: "clone-token".to_string(), + verified: false, + needs_reauth: true, + }; + + let cloned = session.clone(); + assert_eq!(session.user_id, cloned.user_id); + assert_eq!(session.username, cloned.username); + assert_eq!(session.verified, cloned.verified); + assert_eq!(session.needs_reauth, cloned.needs_reauth); + } + + #[test] + fn test_session_unverified() { + let session = Session { + user_id: "user-unverified".to_string(), + username: "newuser".to_string(), + server_id: "server-new".to_string(), + server_url: "https://new.example.com".to_string(), + server_name: "New Server".to_string(), + access_token: "new-token".to_string(), + verified: false, + needs_reauth: true, + }; + + let json = serde_json::to_string(&session).unwrap(); + assert!(json.contains("false")); // verified: false + assert!(json.contains("true")); // needs_reauth: true + + let deserialized: Session = serde_json::from_str(&json).unwrap(); + assert!(!deserialized.verified); + assert!(deserialized.needs_reauth); + } + + #[test] + fn test_session_debug() { + let session = Session { + user_id: "user-debug".to_string(), + username: "debug_user".to_string(), + server_id: "server-debug".to_string(), + server_url: "https://debug.example.com".to_string(), + server_name: "Debug Server".to_string(), + access_token: "debug-token".to_string(), + verified: true, + needs_reauth: false, + }; + + let debug_str = format!("{:?}", session); + assert!(debug_str.contains("user-debug")); + assert!(debug_str.contains("Session")); + } + + #[test] + fn test_auth_manager_wrapper_structure() { + // Verify wrapper type exists and has correct structure + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_session_verifier_wrapper_structure() { + // Verify wrapper type exists and has correct structure + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_session_with_special_characters() { + let session = Session { + user_id: "user-special-éñ".to_string(), + username: "user@example.com".to_string(), + server_id: "server/123".to_string(), + server_url: "https://jellyfin.example.com:8096".to_string(), + server_name: "My Jellyfin (v10.8.0)".to_string(), + access_token: "token+with/special=chars".to_string(), + verified: true, + needs_reauth: false, + }; + + let json = serde_json::to_string(&session).unwrap(); + let deserialized: Session = serde_json::from_str(&json).unwrap(); + + assert_eq!(session.username, deserialized.username); + assert_eq!(session.server_name, deserialized.server_name); + assert_eq!(session.access_token, deserialized.access_token); + } + + #[test] + fn test_session_field_presence() { + let session = Session { + user_id: "u1".to_string(), + username: "user1".to_string(), + server_id: "s1".to_string(), + server_url: "url1".to_string(), + server_name: "name1".to_string(), + access_token: "token1".to_string(), + verified: true, + needs_reauth: false, + }; + + let json = serde_json::to_string(&session).unwrap(); + + // Verify camelCase serialization (serde rename_all = "camelCase") + assert!(json.contains("userId")); + assert!(json.contains("username")); + assert!(json.contains("serverId")); + assert!(json.contains("serverUrl")); + assert!(json.contains("serverName")); + assert!(json.contains("accessToken")); + assert!(json.contains("verified")); + assert!(json.contains("needsReauth")); + } +} diff --git a/src-tauri/src/commands/connectivity.rs b/src-tauri/src/commands/connectivity.rs index 5e02d94..e0e959e 100644 --- a/src-tauri/src/commands/connectivity.rs +++ b/src-tauri/src/commands/connectivity.rs @@ -74,3 +74,18 @@ pub async fn connectivity_mark_unreachable( monitor.mark_unreachable(error).await; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connectivity_monitor_wrapper_structure() { + // Test that wrapper can be created and holds Arc + // We can't instantiate ConnectivityMonitor directly in tests + // due to its dependencies, so we just test the wrapper type structure + + // This verifies the wrapper type exists and can hold Arc + assert_eq!(std::mem::size_of::() > 0, true); + } +} diff --git a/src-tauri/src/commands/conversions.rs b/src-tauri/src/commands/conversions.rs index 4c46d24..478c428 100644 --- a/src-tauri/src/commands/conversions.rs +++ b/src-tauri/src/commands/conversions.rs @@ -72,3 +72,53 @@ pub fn calc_progress(position: f64, duration: f64) -> f64 { pub fn convert_percent_to_volume(percent: f64) -> f64 { percent_to_volume(percent) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_time_seconds() { + assert_eq!(format_time_seconds(0.0), "0:00"); + assert_eq!(format_time_seconds(59.0), "0:59"); + assert_eq!(format_time_seconds(60.0), "1:00"); + assert_eq!(format_time_seconds(125.0), "2:05"); + assert_eq!(format_time_seconds(3661.0), "61:01"); + } + + #[test] + fn test_format_time_seconds_long() { + assert_eq!(format_time_seconds_long(0.0), "0:00"); + assert_eq!(format_time_seconds_long(59.0), "0:59"); + assert_eq!(format_time_seconds_long(3599.0), "59:59"); + assert_eq!(format_time_seconds_long(3600.0), "1:00:00"); + assert_eq!(format_time_seconds_long(3661.0), "1:01:01"); + assert_eq!(format_time_seconds_long(7384.0), "2:03:04"); + } + + #[test] + fn test_convert_ticks_to_seconds() { + assert_eq!(convert_ticks_to_seconds(0), 0.0); + assert_eq!(convert_ticks_to_seconds(10_000_000), 1.0); + assert_eq!(convert_ticks_to_seconds(5_000_000), 0.5); + assert_eq!(convert_ticks_to_seconds(60_000_000), 6.0); + } + + #[test] + fn test_calc_progress() { + assert_eq!(calc_progress(0.0, 100.0), 0.0); + assert_eq!(calc_progress(50.0, 100.0), 50.0); + assert_eq!(calc_progress(100.0, 100.0), 100.0); + assert_eq!(calc_progress(25.0, 0.0), 0.0); // Invalid duration + assert_eq!(calc_progress(150.0, 100.0), 100.0); // Clamped + } + + #[test] + fn test_convert_percent_to_volume() { + assert_eq!(convert_percent_to_volume(0.0), 0.0); + assert_eq!(convert_percent_to_volume(50.0), 0.5); + assert_eq!(convert_percent_to_volume(100.0), 1.0); + assert_eq!(convert_percent_to_volume(150.0), 1.0); // Clamped + assert_eq!(convert_percent_to_volume(-10.0), 0.0); // Clamped + } +} diff --git a/src-tauri/src/commands/playback_mode.rs b/src-tauri/src/commands/playback_mode.rs index 51c1f7b..3347c25 100644 --- a/src-tauri/src/commands/playback_mode.rs +++ b/src-tauri/src/commands/playback_mode.rs @@ -127,3 +127,72 @@ pub struct RemoteSessionStatus { pub is_playing: bool, pub now_playing_item: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_playback_mode_serialization() { + // Test Local playback mode + let local_mode = PlaybackMode::Local; + let json = serde_json::to_string(&local_mode); + assert!(json.is_ok()); + + // Test Remote playback mode + let remote_mode = PlaybackMode::Remote { + session_id: "session-123".to_string(), + }; + let json = serde_json::to_string(&remote_mode); + assert!(json.is_ok()); + } + + #[test] + fn test_remote_session_status_serialization() { + let status = RemoteSessionStatus { + position: 123.45, + duration: Some(600.0), + is_playing: true, + now_playing_item: None, + }; + + // Should serialize successfully + let json = serde_json::to_string(&status); + assert!(json.is_ok()); + + let serialized = json.unwrap(); + assert!(serialized.contains("123.45")); + assert!(serialized.contains("600")); + assert!(serialized.contains("true")); + } + + #[test] + fn test_remote_session_status_with_no_duration() { + let status = RemoteSessionStatus { + position: 0.0, + duration: None, + is_playing: false, + now_playing_item: None, + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains("null") || json.contains("\"duration\":null")); + } + + #[test] + fn test_remote_session_status_various_positions() { + let positions = vec![0.0, 30.5, 100.0, 3600.0]; + + for pos in positions { + let status = RemoteSessionStatus { + position: pos, + duration: Some(7200.0), + is_playing: true, + now_playing_item: None, + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains(&pos.to_string())); + } + } +} diff --git a/src-tauri/src/commands/playback_reporting.rs b/src-tauri/src/commands/playback_reporting.rs index 4fead89..ca2be51 100644 --- a/src-tauri/src/commands/playback_reporting.rs +++ b/src-tauri/src/commands/playback_reporting.rs @@ -182,3 +182,216 @@ pub async fn playback_mark_played( reporter_instance.report(operation, is_online).await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_playback_operation_start_creation() { + let operation = PlaybackOperation::Start { + item_id: "item-123".to_string(), + position_ticks: 15_000_000, + context: Some(PlaybackContext { + context_type: "series".to_string(), + context_id: Some("series-456".to_string()), + }), + }; + + // Verify enum variant can be created and pattern matched + if let PlaybackOperation::Start { item_id, position_ticks, context } = operation { + assert_eq!(item_id, "item-123"); + assert_eq!(position_ticks, 15_000_000); + assert!(context.is_some()); + let ctx = context.unwrap(); + assert_eq!(ctx.context_type, "series"); + assert_eq!(ctx.context_id, Some("series-456".to_string())); + } else { + panic!("Expected Start variant"); + } + } + + #[test] + fn test_playback_operation_start_without_context() { + let operation = PlaybackOperation::Start { + item_id: "item-789".to_string(), + position_ticks: 5_000_000, + context: None, + }; + + if let PlaybackOperation::Start { item_id, context, .. } = operation { + assert_eq!(item_id, "item-789"); + assert!(context.is_none()); + } else { + panic!("Expected Start variant"); + } + } + + #[test] + fn test_playback_operation_progress_creation() { + let operation = PlaybackOperation::Progress { + item_id: "item-999".to_string(), + position_ticks: 30_000_000, + is_paused: true, + }; + + if let PlaybackOperation::Progress { item_id, position_ticks, is_paused } = operation { + assert_eq!(item_id, "item-999"); + assert_eq!(position_ticks, 30_000_000); + assert!(is_paused); + } else { + panic!("Expected Progress variant"); + } + } + + #[test] + fn test_playback_operation_progress_playing() { + let operation = PlaybackOperation::Progress { + item_id: "item-555".to_string(), + position_ticks: 45_000_000, + is_paused: false, + }; + + if let PlaybackOperation::Progress { is_paused, .. } = operation { + assert!(!is_paused); + } else { + panic!("Expected Progress variant"); + } + } + + #[test] + fn test_playback_operation_stopped_creation() { + let operation = PlaybackOperation::Stopped { + item_id: "item-111".to_string(), + position_ticks: 120_000_000, + }; + + if let PlaybackOperation::Stopped { item_id, position_ticks } = operation { + assert_eq!(item_id, "item-111"); + assert_eq!(position_ticks, 120_000_000); + } else { + panic!("Expected Stopped variant"); + } + } + + #[test] + fn test_playback_operation_mark_played_creation() { + let operation = PlaybackOperation::MarkPlayed { + item_id: "item-222".to_string(), + }; + + if let PlaybackOperation::MarkPlayed { item_id } = operation { + assert_eq!(item_id, "item-222"); + } else { + panic!("Expected MarkPlayed variant"); + } + } + + #[test] + fn test_playback_context_with_series() { + let context = PlaybackContext { + context_type: "series".to_string(), + context_id: Some("series-789".to_string()), + }; + + assert_eq!(context.context_type, "series"); + assert_eq!(context.context_id, Some("series-789".to_string())); + } + + #[test] + fn test_playback_context_without_id() { + let context = PlaybackContext { + context_type: "folder".to_string(), + context_id: None, + }; + + assert_eq!(context.context_type, "folder"); + assert!(context.context_id.is_none()); + } + + #[test] + fn test_playback_context_clone() { + let context = PlaybackContext { + context_type: "container".to_string(), + context_id: Some("container-123".to_string()), + }; + + let cloned = context.clone(); + assert_eq!(cloned.context_type, "container"); + assert_eq!(cloned.context_id, Some("container-123".to_string())); + } + + #[test] + fn test_seconds_to_ticks_conversion() { + assert_eq!(seconds_to_ticks(0.0), 0); + assert_eq!(seconds_to_ticks(1.0), 10_000_000); + assert_eq!(seconds_to_ticks(1.5), 15_000_000); + assert_eq!(seconds_to_ticks(120.0), 1_200_000_000); + } + + #[test] + fn test_playback_reporter_wrapper_structure() { + // Verify wrapper type can hold Arc>> + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_playback_operation_debug_trait() { + // Verify Debug trait is implemented for operations + let operation = PlaybackOperation::Start { + item_id: "item-1".to_string(), + position_ticks: 0, + context: None, + }; + + let debug_str = format!("{:?}", operation); + assert!(debug_str.contains("Start")); + assert!(debug_str.contains("item-1")); + } + + #[test] + fn test_playback_operation_clone() { + let operation = PlaybackOperation::Progress { + item_id: "item-clone".to_string(), + position_ticks: 50_000_000, + is_paused: true, + }; + + let cloned = operation.clone(); + if let PlaybackOperation::Progress { item_id, is_paused, .. } = cloned { + assert_eq!(item_id, "item-clone"); + assert!(is_paused); + } else { + panic!("Clone failed to preserve variant"); + } + } + + #[test] + fn test_playback_operation_all_variants() { + // Test that all operation variants can be created and matched + let start_op = PlaybackOperation::Start { + item_id: "i1".to_string(), + position_ticks: 0, + context: None, + }; + assert!(matches!(start_op, PlaybackOperation::Start { .. })); + + let progress_op = PlaybackOperation::Progress { + item_id: "i2".to_string(), + position_ticks: 100, + is_paused: false, + }; + assert!(matches!(progress_op, PlaybackOperation::Progress { .. })); + + let stopped_op = PlaybackOperation::Stopped { + item_id: "i3".to_string(), + position_ticks: 200, + }; + assert!(matches!(stopped_op, PlaybackOperation::Stopped { .. })); + + let played_op = PlaybackOperation::MarkPlayed { + item_id: "i4".to_string(), + }; + assert!(matches!(played_op, PlaybackOperation::MarkPlayed { .. })); + } +} diff --git a/src-tauri/src/commands/repository.rs b/src-tauri/src/commands/repository.rs index 803f53c..8491807 100644 --- a/src-tauri/src/commands/repository.rs +++ b/src-tauri/src/commands/repository.rs @@ -460,3 +460,122 @@ pub async fn repository_get_similar_items( .await .map_err(|e| format!("{:?}", e)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_repository_manager_creation() { + let manager = RepositoryManager::new(); + // Should be able to create RepositoryManager without panicking + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_repository_manager_wrapper_structure() { + let manager = RepositoryManager::new(); + let wrapper = RepositoryManagerWrapper(manager); + // Verify wrapper holds the manager + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_repository_manager_get_nonexistent() { + let manager = RepositoryManager::new(); + // Getting a non-existent repository should return None + let result = manager.get("nonexistent-handle"); + assert!(result.is_none()); + } + + #[test] + fn test_uuid_handle_generation() { + let uuid = Uuid::new_v4(); + let handle = format!("{}", uuid); + // UUID should convert to a non-empty string + assert!(!handle.is_empty()); + assert!(handle.len() > 0); + } + + #[test] + fn test_uuid_handles_are_unique() { + let handle1 = format!("{}", Uuid::new_v4()); + let handle2 = format!("{}", Uuid::new_v4()); + // Two generated UUIDs should be different + assert_ne!(handle1, handle2); + } + + #[test] + fn test_uuid_handle_format() { + let uuid = Uuid::new_v4(); + let handle = format!("{}", uuid); + // UUID should have standard format with hyphens + let parts: Vec<&str> = handle.split('-').collect(); + assert_eq!(parts.len(), 5); + } + + #[test] + fn test_repository_manager_destroy_nonexistent() { + let manager = RepositoryManager::new(); + // Destroying a non-existent repository should not panic + manager.destroy("nonexistent-handle"); + } + + #[test] + fn test_repository_manager_is_send_sync() { + // Verify RepositoryManager can be used in async contexts + fn is_send_sync() {} + is_send_sync::(); + } + + #[test] + fn test_repository_manager_wrapper_is_send_sync() { + // Verify RepositoryManagerWrapper is Send + Sync + fn is_send_sync() {} + is_send_sync::(); + } + + #[test] + fn test_multiple_manager_instances() { + let manager1 = RepositoryManager::new(); + let manager2 = RepositoryManager::new(); + + // Multiple manager instances should be independent + let handle1_nonexistent = manager1.get("test"); + let handle2_nonexistent = manager2.get("test"); + + assert!(handle1_nonexistent.is_none()); + assert!(handle2_nonexistent.is_none()); + } + + #[test] + fn test_handle_string_properties() { + let uuid = Uuid::new_v4(); + let handle = format!("{}", uuid); + + // Handle should be alphanumeric with hyphens + for c in handle.chars() { + assert!(c.is_alphanumeric() || c == '-'); + } + } + + #[test] + fn test_repository_manager_concurrent_access() { + let manager = Arc::new(RepositoryManager::new()); + let mut handles = vec![]; + + // Verify manager can be wrapped in Arc for concurrent access + for _ in 0..3 { + let mgr = Arc::clone(&manager); + let handle = std::thread::spawn(move || { + let result = mgr.get("test"); + assert!(result.is_none()); + }); + handles.push(handle); + } + + for h in handles { + h.join().unwrap(); + } + } +} diff --git a/src-tauri/src/commands/sessions.rs b/src-tauri/src/commands/sessions.rs index b1a51c2..5c43bdb 100644 --- a/src-tauri/src/commands/sessions.rs +++ b/src-tauri/src/commands/sessions.rs @@ -30,3 +30,69 @@ pub async fn sessions_poll_now( ) -> Result, String> { poller.0.poll_now().await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_polling_hint_parsing() { + // Test valid hints + assert_eq!( + match "cast_active" { + "cast_active" => Some(PollingHint::CastActive), + "cast_discovery" => Some(PollingHint::CastDiscovery), + "normal" => Some(PollingHint::Normal), + _ => None, + }, + Some(PollingHint::CastActive) + ); + + assert_eq!( + match "cast_discovery" { + "cast_active" => Some(PollingHint::CastActive), + "cast_discovery" => Some(PollingHint::CastDiscovery), + "normal" => Some(PollingHint::Normal), + _ => None, + }, + Some(PollingHint::CastDiscovery) + ); + + assert_eq!( + match "normal" { + "cast_active" => Some(PollingHint::CastActive), + "cast_discovery" => Some(PollingHint::CastDiscovery), + "normal" => Some(PollingHint::Normal), + _ => None, + }, + Some(PollingHint::Normal) + ); + } + + #[test] + fn test_invalid_polling_hint() { + // Test invalid hint + let result = match "invalid" { + "cast_active" => Ok(PollingHint::CastActive), + "cast_discovery" => Ok(PollingHint::CastDiscovery), + "normal" => Ok(PollingHint::Normal), + _ => Err("Invalid polling hint"), + }; + + assert!(result.is_err()); + } + + #[test] + fn test_session_poller_wrapper_structure() { + // Test that wrapper type structure is correct + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_polling_hints_exist() { + // Verify polling hint variants exist + let _ = PollingHint::CastActive; + let _ = PollingHint::CastDiscovery; + let _ = PollingHint::Normal; + } +} diff --git a/src-tauri/src/commands/storage.rs b/src-tauri/src/commands/storage.rs index 2fae92e..182b366 100644 --- a/src-tauri/src/commands/storage.rs +++ b/src-tauri/src/commands/storage.rs @@ -1717,3 +1717,272 @@ pub async fn storage_get_series_audio_preference( Ok(preference) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_server_info_serialization() { + let server = ServerInfo { + id: "server-123".to_string(), + name: "My Server".to_string(), + url: "https://jellyfin.example.com".to_string(), + version: Some("10.8.0".to_string()), + }; + + let json = serde_json::to_string(&server); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("server-123")); + assert!(serialized.contains("My Server")); + } + + #[test] + fn test_server_info_without_version() { + let server = ServerInfo { + id: "server-456".to_string(), + name: "Test Server".to_string(), + url: "https://test.local".to_string(), + version: None, + }; + + let json = serde_json::to_string(&server).unwrap(); + assert!(json.contains("null") || json.contains("\"version\":null")); + } + + #[test] + fn test_server_info_roundtrip() { + let original = ServerInfo { + id: "srv-999".to_string(), + name: "Production".to_string(), + url: "https://prod.jellyfin.example.com:8096".to_string(), + version: Some("10.9.0".to_string()), + }; + + let json = serde_json::to_string(&original).unwrap(); + let deserialized: ServerInfo = serde_json::from_str(&json).unwrap(); + + assert_eq!(original.id, deserialized.id); + assert_eq!(original.name, deserialized.name); + assert_eq!(original.url, deserialized.url); + assert_eq!(original.version, deserialized.version); + } + + #[test] + fn test_user_info_serialization() { + let user = UserInfo { + id: "user-123".to_string(), + server_id: "server-456".to_string(), + username: "john_doe".to_string(), + is_active: true, + }; + + let json = serde_json::to_string(&user); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("user-123")); + assert!(serialized.contains("john_doe")); + } + + #[test] + fn test_user_info_inactive() { + let user = UserInfo { + id: "user-inactive".to_string(), + server_id: "server-789".to_string(), + username: "jane_doe".to_string(), + is_active: false, + }; + + let json = serde_json::to_string(&user).unwrap(); + assert!(json.contains("false")); + + let deserialized: UserInfo = serde_json::from_str(&json).unwrap(); + assert!(!deserialized.is_active); + } + + #[test] + fn test_active_session_serialization() { + let session = ActiveSession { + user_id: "user-001".to_string(), + username: "alice".to_string(), + server_id: "server-001".to_string(), + server_url: "https://jellyfin.example.com".to_string(), + server_name: "Home Jellyfin".to_string(), + access_token: "very-long-token-string-abc123".to_string(), + }; + + let json = serde_json::to_string(&session); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("alice")); + assert!(serialized.contains("Home Jellyfin")); + } + + #[test] + fn test_active_session_roundtrip() { + let original = ActiveSession { + user_id: "u999".to_string(), + username: "testuser".to_string(), + server_id: "s999".to_string(), + server_url: "https://test.example.com:8096".to_string(), + server_name: "Test Server".to_string(), + access_token: "token-xyz".to_string(), + }; + + let json = serde_json::to_string(&original).unwrap(); + let deserialized: ActiveSession = serde_json::from_str(&json).unwrap(); + + assert_eq!(original.user_id, deserialized.user_id); + assert_eq!(original.username, deserialized.username); + assert_eq!(original.access_token, deserialized.access_token); + } + + #[test] + fn test_security_status_with_keyring() { + let status = SecurityStatus { + using_keyring: true, + storage_type: "system_keyring".to_string(), + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains("true")); + assert!(json.contains("system_keyring")); + } + + #[test] + fn test_security_status_with_encrypted_file() { + let status = SecurityStatus { + using_keyring: false, + storage_type: "encrypted_file".to_string(), + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains("false")); + assert!(json.contains("encrypted_file")); + } + + #[test] + fn test_playback_progress_serialization() { + let progress = PlaybackProgress { + item_id: "item-123".to_string(), + position_ticks: 150_000_000, + is_played: true, + is_favorite: false, + play_count: 3, + }; + + let json = serde_json::to_string(&progress); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("item-123")); + assert!(serialized.contains("150000000")); + } + + #[test] + fn test_playback_progress_played_status() { + let progress = PlaybackProgress { + item_id: "item-456".to_string(), + position_ticks: 0, + is_played: true, + is_favorite: true, + play_count: 1, + }; + + let json = serde_json::to_string(&progress).unwrap(); + let deserialized: PlaybackProgress = serde_json::from_str(&json).unwrap(); + + assert!(deserialized.is_played); + assert!(deserialized.is_favorite); + assert_eq!(deserialized.play_count, 1); + } + + #[test] + fn test_playback_progress_not_played() { + let progress = PlaybackProgress { + item_id: "item-789".to_string(), + position_ticks: 30_000_000, + is_played: false, + is_favorite: false, + play_count: 0, + }; + + let json = serde_json::to_string(&progress).unwrap(); + let deserialized: PlaybackProgress = serde_json::from_str(&json).unwrap(); + + assert!(!deserialized.is_played); + assert_eq!(deserialized.play_count, 0); + } + + #[test] + fn test_database_wrapper_structure() { + // Verify DatabaseWrapper can be created and holds Mutex + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_credential_store_wrapper_structure() { + // Verify CredentialStoreWrapper can be created + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_thumbnail_cache_wrapper_structure() { + // Verify ThumbnailCacheWrapper holds Arc + assert_eq!(std::mem::size_of::() > 0, true); + } + + #[test] + fn test_user_info_camel_case() { + let user = UserInfo { + id: "u1".to_string(), + server_id: "s1".to_string(), + username: "user1".to_string(), + is_active: true, + }; + + let json = serde_json::to_string(&user).unwrap(); + // Verify camelCase serialization + assert!(json.contains("serverId")); + assert!(json.contains("isActive")); + } + + #[test] + fn test_active_session_camel_case() { + let session = ActiveSession { + user_id: "u1".to_string(), + username: "user1".to_string(), + server_id: "s1".to_string(), + server_url: "url1".to_string(), + server_name: "name1".to_string(), + access_token: "token1".to_string(), + }; + + let json = serde_json::to_string(&session).unwrap(); + // Verify camelCase serialization + assert!(json.contains("userId")); + assert!(json.contains("serverId")); + assert!(json.contains("serverUrl")); + assert!(json.contains("serverName")); + assert!(json.contains("accessToken")); + } + + #[test] + fn test_playback_progress_camel_case() { + let progress = PlaybackProgress { + item_id: "i1".to_string(), + position_ticks: 100, + is_played: true, + is_favorite: false, + play_count: 1, + }; + + let json = serde_json::to_string(&progress).unwrap(); + // Verify camelCase serialization + assert!(json.contains("itemId")); + assert!(json.contains("positionTicks")); + assert!(json.contains("isPlayed")); + assert!(json.contains("isFavorite")); + assert!(json.contains("playCount")); + } +} diff --git a/src-tauri/src/commands/sync.rs b/src-tauri/src/commands/sync.rs index 284cf7f..a5a486f 100644 --- a/src-tauri/src/commands/sync.rs +++ b/src-tauri/src/commands/sync.rs @@ -234,3 +234,138 @@ pub async fn sync_clear_user( db_service.execute(query).await.map_err(|e| e.to_string())?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sync_queue_item_serialization() { + let item = SyncQueueItem { + id: 1, + user_id: "user-123".to_string(), + operation: "favorite".to_string(), + item_id: Some("item-456".to_string()), + payload: Some(r#"{"isFavorite": true}"#.to_string()), + status: "pending".to_string(), + retry_count: 0, + created_at: Some("2024-02-14T08:00:00Z".to_string()), + error_message: None, + }; + + // Should serialize successfully + let json = serde_json::to_string(&item); + assert!(json.is_ok()); + + let serialized = json.unwrap(); + assert!(serialized.contains("user-123")); + assert!(serialized.contains("favorite")); + assert!(serialized.contains("pending")); + } + + #[test] + fn test_sync_queue_item_with_error() { + let item = SyncQueueItem { + id: 2, + user_id: "user-789".to_string(), + operation: "update_progress".to_string(), + item_id: Some("item-999".to_string()), + payload: None, + status: "failed".to_string(), + retry_count: 3, + created_at: Some("2024-02-14T07:00:00Z".to_string()), + error_message: Some("Connection timeout".to_string()), + }; + + let json = serde_json::to_string(&item).unwrap(); + assert!(json.contains("failed")); + assert!(json.contains("Connection timeout")); + assert!(json.contains("3")); // retry_count + } + + #[test] + fn test_sync_queue_item_without_optional_fields() { + let item = SyncQueueItem { + id: 3, + user_id: "user-000".to_string(), + operation: "clear_progress".to_string(), + item_id: None, + payload: None, + status: "completed".to_string(), + retry_count: 0, + created_at: None, + error_message: None, + }; + + let json = serde_json::to_string(&item).unwrap(); + assert!(json.contains("completed")); + assert!(json.contains("null") || json.contains("\"itemId\":null")); + } + + #[test] + fn test_sync_status_values() { + // Verify all expected status values + let valid_statuses = vec!["pending", "processing", "completed", "failed"]; + + for status in valid_statuses { + let item = SyncQueueItem { + id: 1, + user_id: "test".to_string(), + operation: "test".to_string(), + item_id: None, + payload: None, + status: status.to_string(), + retry_count: 0, + created_at: None, + error_message: None, + }; + + let json = serde_json::to_string(&item).unwrap(); + assert!(json.contains(status)); + } + } + + #[test] + fn test_query_param_generation() { + // Test QueryParam generation for sync operations + let user_id = "user-123".to_string(); + let operation = "favorite".to_string(); + + let params: Vec = vec![ + QueryParam::String(user_id.clone()), + QueryParam::String(operation.clone()), + QueryParam::Null, + QueryParam::Null, + ]; + + assert_eq!(params.len(), 4); + assert!(matches!(params[0], QueryParam::String(_))); + assert!(matches!(params[1], QueryParam::String(_))); + assert!(matches!(params[2], QueryParam::Null)); + assert!(matches!(params[3], QueryParam::Null)); + } + + #[test] + fn test_retry_count_increment() { + // Verify retry count management + let mut item = SyncQueueItem { + id: 1, + user_id: "user-123".to_string(), + operation: "favorite".to_string(), + item_id: None, + payload: None, + status: "pending".to_string(), + retry_count: 0, + created_at: None, + error_message: None, + }; + + // Simulate retries + for i in 1..=5 { + item.retry_count = i; + item.status = if i < 3 { "pending" } else { "failed" }.to_string(); + + assert!(item.retry_count == i); + } + } +} diff --git a/src-tauri/src/player/state.rs b/src-tauri/src/player/state.rs index 78740bd..b1267ba 100644 --- a/src-tauri/src/player/state.rs +++ b/src-tauri/src/player/state.rs @@ -81,3 +81,256 @@ impl PlayerState { matches!(self, PlayerState::Paused { .. }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_end_reason_finished() { + let reason = EndReason::Finished; + let json = serde_json::to_string(&reason); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("finished")); + } + + #[test] + fn test_end_reason_user_skip() { + let reason = EndReason::UserSkip; + let json = serde_json::to_string(&reason); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("userskip")); + } + + #[test] + fn test_end_reason_user_stop() { + let reason = EndReason::UserStop; + let json = serde_json::to_string(&reason).unwrap(); + assert!(json.contains("userstop")); + } + + #[test] + fn test_end_reason_error() { + let reason = EndReason::Error; + let json = serde_json::to_string(&reason).unwrap(); + assert!(json.contains("error")); + } + + #[test] + fn test_end_reason_new_track_loaded() { + let reason = EndReason::NewTrackLoaded; + let json = serde_json::to_string(&reason).unwrap(); + assert!(json.contains("newtrackloa")); + } + + #[test] + fn test_end_reason_all_variants() { + let reasons = vec![ + EndReason::Finished, + EndReason::UserSkip, + EndReason::UserStop, + EndReason::Error, + EndReason::NewTrackLoaded, + ]; + + for reason in reasons { + let json = serde_json::to_string(&reason); + assert!(json.is_ok()); + } + } + + #[test] + fn test_end_reason_equality() { + let reason1 = EndReason::Finished; + let reason2 = EndReason::Finished; + assert_eq!(reason1, reason2); + + let reason3 = EndReason::UserSkip; + assert_ne!(reason1, reason3); + } + + #[test] + fn test_end_reason_clone() { + let reason = EndReason::Finished; + let cloned = reason.clone(); + assert_eq!(reason, cloned); + } + + #[test] + fn test_player_state_idle_default() { + let state = PlayerState::default(); + assert!(matches!(state, PlayerState::Idle)); + } + + #[test] + fn test_player_state_idle_serialization() { + let state = PlayerState::Idle; + let json = serde_json::to_string(&state); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("idle")); + } + + #[test] + fn test_player_state_position_when_idle() { + let state = PlayerState::Idle; + assert_eq!(state.position(), None); + } + + #[test] + fn test_player_state_is_playing_when_idle() { + let state = PlayerState::Idle; + assert!(!state.is_playing()); + } + + #[test] + fn test_player_state_is_paused_when_idle() { + let state = PlayerState::Idle; + assert!(!state.is_paused()); + } + + #[test] + fn test_player_state_loading() { + let media = create_test_media_item("item-1", "Test Item"); + let state = PlayerState::Loading { media: media.clone() }; + assert!(matches!(state, PlayerState::Loading { .. })); + assert_eq!(state.position(), None); + assert!(!state.is_playing()); + } + + #[test] + fn test_player_state_playing_position() { + let media = create_test_media_item("item-2", "Playing Item"); + let state = PlayerState::Playing { + media, + position: 45.5, + duration: 180.0, + }; + + assert!(state.is_playing()); + assert!(!state.is_paused()); + assert_eq!(state.position(), Some(45.5)); + } + + #[test] + fn test_player_state_paused_position() { + let media = create_test_media_item("item-3", "Paused Item"); + let state = PlayerState::Paused { + media, + position: 123.75, + duration: 300.0, + }; + + assert!(state.is_paused()); + assert!(!state.is_playing()); + assert_eq!(state.position(), Some(123.75)); + } + + #[test] + fn test_player_state_seeking() { + let media = create_test_media_item("item-4", "Seeking Item"); + let state = PlayerState::Seeking { + media, + target: 60.0, + }; + + assert!(!state.is_playing()); + assert!(!state.is_paused()); + assert_eq!(state.position(), None); + } + + #[test] + fn test_player_state_error_with_media() { + let media = create_test_media_item("item-5", "Error Item"); + let state = PlayerState::Error { + media: Some(media), + error: "Playback failed".to_string(), + }; + + assert!(!state.is_playing()); + assert_eq!(state.position(), None); + } + + #[test] + fn test_player_state_error_without_media() { + let state = PlayerState::Error { + media: None, + error: "Connection lost".to_string(), + }; + + assert!(!state.is_playing()); + assert_eq!(state.position(), None); + } + + #[test] + fn test_player_state_clone() { + let state = PlayerState::Idle; + let cloned = state.clone(); + assert!(matches!(cloned, PlayerState::Idle)); + } + + #[test] + fn test_player_state_playing_serialization() { + let media = create_test_media_item("item-6", "Serial Item"); + let state = PlayerState::Playing { + media, + position: 30.0, + duration: 120.0, + }; + + let json = serde_json::to_string(&state); + assert!(json.is_ok()); + let serialized = json.unwrap(); + assert!(serialized.contains("playing")); + assert!(serialized.contains("30")); + } + + #[test] + fn test_player_state_multiple_positions() { + let positions = vec![0.0, 45.5, 100.0, 999.99]; + + for pos in positions { + let media = create_test_media_item("item-test", "Test"); + let state = PlayerState::Playing { + media, + position: pos, + duration: 1000.0, + }; + + assert_eq!(state.position(), Some(pos)); + } + } + + // Helper function to create test MediaItem instances + fn create_test_media_item(id: &str, title: &str) -> MediaItem { + MediaItem { + id: id.to_string(), + title: title.to_string(), + name: None, + artist: None, + album: None, + album_name: None, + album_id: None, + artist_items: None, + artists: None, + primary_image_tag: None, + item_type: Some("Video".to_string()), + playlist_id: None, + duration: Some(100.0), + artwork_url: None, + media_type: super::super::media::MediaType::Video, + source: super::super::media::MediaSource::DirectUrl { + url: "http://example.com/media".to_string(), + }, + video_codec: None, + needs_transcoding: false, + video_width: None, + video_height: None, + subtitles: vec![], + series_id: None, + server_id: None, + } + } +}