added tests, use specific CI
This commit is contained in:
parent
57f8a54dac
commit
e664bf4620
@ -16,7 +16,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build APK and Run Tests
|
name: Build APK and Run Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: [linux, amd64]
|
||||||
container:
|
container:
|
||||||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run Tests
|
name: Run Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: [linux, amd64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
name: Build Linux
|
name: Build Linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: [linux, amd64]
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -133,7 +133,7 @@ jobs:
|
|||||||
|
|
||||||
build-android:
|
build-android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
runs-on: ubuntu-latest
|
runs-on: [linux, amd64]
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -214,7 +214,7 @@ jobs:
|
|||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: [linux, amd64]
|
||||||
needs: [build-linux, build-android]
|
needs: [build-linux, build-android]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@ -237,3 +237,197 @@ pub async fn auth_reauthenticate(
|
|||||||
|
|
||||||
Ok(result)
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -74,3 +74,18 @@ pub async fn connectivity_mark_unreachable(
|
|||||||
monitor.mark_unreachable(error).await;
|
monitor.mark_unreachable(error).await;
|
||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -72,3 +72,53 @@ pub fn calc_progress(position: f64, duration: f64) -> f64 {
|
|||||||
pub fn convert_percent_to_volume(percent: f64) -> f64 {
|
pub fn convert_percent_to_volume(percent: f64) -> f64 {
|
||||||
percent_to_volume(percent)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -127,3 +127,72 @@ pub struct RemoteSessionStatus {
|
|||||||
pub is_playing: bool,
|
pub is_playing: bool,
|
||||||
pub now_playing_item: Option<crate::jellyfin::NowPlayingItem>,
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -182,3 +182,216 @@ pub async fn playback_mark_played(
|
|||||||
|
|
||||||
reporter_instance.report(operation, is_online).await
|
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 { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -460,3 +460,122 @@ pub async fn repository_get_similar_items(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{:?}", e))
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -30,3 +30,69 @@ pub async fn sessions_poll_now(
|
|||||||
) -> Result<Vec<SessionInfo>, String> {
|
) -> Result<Vec<SessionInfo>, String> {
|
||||||
poller.0.poll_now().await
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1717,3 +1717,272 @@ pub async fn storage_get_series_audio_preference(
|
|||||||
|
|
||||||
Ok(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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -234,3 +234,138 @@ pub async fn sync_clear_user(
|
|||||||
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
db_service.execute(query).await.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -81,3 +81,256 @@ impl PlayerState {
|
|||||||
matches!(self, PlayerState::Paused { .. })
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user