added tests, use specific CI

This commit is contained in:
Duncan Tourolle 2026-02-14 09:08:49 +01:00
parent 57f8a54dac
commit e664bf4620
12 changed files with 1388 additions and 5 deletions

View File

@ -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

View File

@ -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:

View File

@ -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<Session, _> = 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::<AuthManagerWrapper>() > 0, true);
}
#[test]
fn test_session_verifier_wrapper_structure() {
// Verify wrapper type exists and has correct structure
assert_eq!(std::mem::size_of::<SessionVerifierWrapper>() > 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"));
}
}

View File

@ -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<Mutex>
assert_eq!(std::mem::size_of::<ConnectivityMonitorWrapper>() > 0, true);
}
}

View File

@ -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
}
}

View File

@ -127,3 +127,72 @@ pub struct RemoteSessionStatus {
pub is_playing: bool,
pub now_playing_item: Option<crate::jellyfin::NowPlayingItem>,
}
#[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()));
}
}
}

View File

@ -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<TokioMutex<Option<T>>>
assert_eq!(std::mem::size_of::<PlaybackReporterWrapper>() > 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 { .. }));
}
}

View File

@ -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::<RepositoryManager>() > 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::<RepositoryManagerWrapper>() > 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<T: Send + Sync>() {}
is_send_sync::<RepositoryManager>();
}
#[test]
fn test_repository_manager_wrapper_is_send_sync() {
// Verify RepositoryManagerWrapper is Send + Sync
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<RepositoryManagerWrapper>();
}
#[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();
}
}
}

View File

@ -30,3 +30,69 @@ pub async fn sessions_poll_now(
) -> Result<Vec<SessionInfo>, 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::<SessionPollerWrapper>() > 0, true);
}
#[test]
fn test_polling_hints_exist() {
// Verify polling hint variants exist
let _ = PollingHint::CastActive;
let _ = PollingHint::CastDiscovery;
let _ = PollingHint::Normal;
}
}

View File

@ -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<Database>
assert_eq!(std::mem::size_of::<DatabaseWrapper>() > 0, true);
}
#[test]
fn test_credential_store_wrapper_structure() {
// Verify CredentialStoreWrapper can be created
assert_eq!(std::mem::size_of::<CredentialStoreWrapper>() > 0, true);
}
#[test]
fn test_thumbnail_cache_wrapper_structure() {
// Verify ThumbnailCacheWrapper holds Arc<ThumbnailCache>
assert_eq!(std::mem::size_of::<ThumbnailCacheWrapper>() > 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"));
}
}

View File

@ -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<QueryParam> = 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);
}
}
}

View File

@ -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,
}
}
}