#[cfg(test)] mod tests { use crate::api::jellyfin::{ GetItemsOptions, ImageType, ImageOptions, SortOrder, }; /// Mock for testing URL construction without a real server struct MockOnlineRepository { server_url: String, access_token: String, } impl MockOnlineRepository { fn new(server_url: &str, access_token: &str) -> Self { Self { server_url: server_url.to_string(), access_token: access_token.to_string(), } } /// Test helper: construct image URL similar to backend fn get_image_url( &self, item_id: &str, image_type: &str, options: Option<&ImageOptions>, ) -> String { let mut url = format!( "{}/Items/{}/Images/{}", self.server_url, item_id, image_type ); // No api_key — image downloads use X-Emby-Authorization header let mut params: Vec<(&str, String)> = Vec::new(); if let Some(opts) = options { if let Some(max_width) = opts.max_width { params.push(("maxWidth", max_width.to_string())); } if let Some(max_height) = opts.max_height { params.push(("maxHeight", max_height.to_string())); } if let Some(quality) = opts.quality { params.push(("quality", quality.to_string())); } if let Some(tag) = &opts.tag { params.push(("tag", tag.clone())); } } let query_string = params .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); if !query_string.is_empty() { url.push('?'); url.push_str(&query_string); } url } /// Test helper: construct subtitle URL fn get_subtitle_url( &self, item_id: &str, media_source_id: &str, stream_index: usize, format: &str, ) -> String { format!( "{}/Videos/{}/Subtitles/{}/{}/subtitles.{}?api_key={}", self.server_url, item_id, media_source_id, stream_index, format, self.access_token ) } /// Test helper: construct video download URL fn get_video_download_url( &self, item_id: &str, quality: &str, ) -> String { let (max_width, bitrate) = match quality { "1080p" => ("1920", "15000k"), "720p" => ("1280", "8000k"), "480p" => ("854", "3000k"), _ => ("0", ""), // original }; if quality == "original" { format!("{}/Videos/{}/stream.mp4?api_key={}", self.server_url, item_id, self.access_token) } else { format!( "{}/Videos/{}/stream.mp4?maxWidth={}&videoBitrate={}&api_key={}", self.server_url, item_id, max_width, bitrate, self.access_token ) } } } // ===== Image URL Tests ===== #[test] fn test_image_url_basic() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_image_url("item123", "Primary", None); assert!(url.contains("https://jellyfin.example.com")); assert!(url.contains("/Items/item123/Images/Primary")); assert!(url.contains("api_key=token123")); } #[test] fn test_image_url_with_max_width() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let options = ImageOptions { max_width: Some(300), max_height: None, quality: None, tag: None, }; let url = repo.get_image_url("item123", "Primary", Some(&options)); assert!(url.contains("maxWidth=300")); assert!(url.contains("api_key=token123")); } #[test] fn test_image_url_with_all_options() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let options = ImageOptions { max_width: Some(1920), max_height: Some(1080), quality: Some(90), tag: Some("abc123".to_string()), }; let url = repo.get_image_url("item456", "Backdrop", Some(&options)); assert!(url.contains("/Items/item456/Images/Backdrop")); assert!(url.contains("maxWidth=1920")); assert!(url.contains("maxHeight=1080")); assert!(url.contains("quality=90")); assert!(url.contains("tag=abc123")); assert!(url.contains("api_key=token123")); } #[test] fn test_image_url_different_image_types() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let image_types = vec!["Primary", "Backdrop", "Logo", "Thumb"]; for image_type in image_types { let url = repo.get_image_url("item123", image_type, None); assert!(url.contains(&format!("/Images/{}", image_type))); } } #[test] fn test_image_url_credentials_included_in_backend() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "secret_token"); let url = repo.get_image_url("item123", "Primary", None); // Credentials should be included in backend-generated URL assert!(url.contains("api_key=secret_token")); } #[test] fn test_image_url_proper_encoding() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let options = ImageOptions { max_width: Some(300), max_height: None, quality: None, tag: Some("tag-with-special-chars".to_string()), }; let url = repo.get_image_url("item123", "Primary", Some(&options)); // URL should be properly formatted assert!(url.contains("?")); assert!(url.contains("&") || !url.contains("&&")); // No double ampersands assert!(!url.ends_with("&")); // No trailing ampersand } // ===== Subtitle URL Tests ===== #[test] fn test_subtitle_url_vtt_format() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_subtitle_url("item123", "source456", 0, "vtt"); assert!(url.contains("Videos/item123")); assert!(url.contains("Subtitles/source456/0")); assert!(url.contains("subtitles.vtt")); assert!(url.contains("api_key=token123")); } #[test] fn test_subtitle_url_srt_format() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_subtitle_url("item123", "source456", 1, "srt"); assert!(url.contains("Subtitles/source456/1")); assert!(url.contains("subtitles.srt")); } #[test] fn test_subtitle_url_multiple_streams() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); for stream_index in 0..5 { let url = repo.get_subtitle_url("item123", "source456", stream_index, "vtt"); assert!(url.contains(&format!("/{}/subtitles", stream_index))); } } #[test] fn test_subtitle_url_different_media_sources() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let media_sources = vec!["src1", "src2", "src3"]; for media_source_id in media_sources { let url = repo.get_subtitle_url("item123", media_source_id, 0, "vtt"); assert!(url.contains(&format!("Subtitles/{}/", media_source_id))); } } // ===== Video Download URL Tests ===== #[test] fn test_video_download_url_original_quality() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_video_download_url("item123", "original"); assert!(url.contains("Videos/item123/stream.mp4")); assert!(url.contains("api_key=token123")); assert!(!url.contains("maxWidth")); // Original should have no transcoding params } #[test] fn test_video_download_url_1080p() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_video_download_url("item123", "1080p"); assert!(url.contains("maxWidth=1920")); assert!(url.contains("videoBitrate=15000k")); } #[test] fn test_video_download_url_720p() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_video_download_url("item123", "720p"); assert!(url.contains("maxWidth=1280")); assert!(url.contains("videoBitrate=8000k")); } #[test] fn test_video_download_url_480p() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let url = repo.get_video_download_url("item123", "480p"); assert!(url.contains("maxWidth=854")); assert!(url.contains("videoBitrate=3000k")); } #[test] fn test_video_download_url_quality_presets() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); let qualities = vec!["original", "1080p", "720p", "480p"]; for quality in qualities { let url = repo.get_video_download_url("item123", quality); assert!(url.contains("Videos/item123/stream.mp4")); } } // ===== Security Tests ===== #[test] fn test_credentials_never_exposed_in_frontend() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "super_secret_token"); let image_url = repo.get_image_url("item123", "Primary", None); let subtitle_url = repo.get_subtitle_url("item123", "src123", 0, "vtt"); let download_url = repo.get_video_download_url("item123", "720p"); // Image URLs no longer contain api_key — auth is via X-Emby-Authorization header assert!(!image_url.contains("api_key=")); // Subtitle and download URLs still use api_key (used directly, not via download_bytes) assert!(subtitle_url.contains("api_key=super_secret_token")); assert!(download_url.contains("api_key=super_secret_token")); } #[test] fn test_url_parameter_injection_prevention() { let repo = MockOnlineRepository::new("https://jellyfin.example.com", "token123"); // Try to inject parameters through item_id let malicious_id = "item123&extraParam=malicious"; let url = repo.get_image_url(malicious_id, "Primary", None); // URL should contain the full item_id, backend should handle escaping assert!(url.contains(malicious_id)); // Backend should be responsible for proper URL encoding } // ===== URL Format Tests ===== #[test] fn test_image_url_format_correctness() { let repo = MockOnlineRepository::new("https://server.com", "token"); let url = repo.get_image_url("id123", "Primary", None); // Should be valid format (no api_key — auth via header) assert!(url.starts_with("https://server.com")); assert!(url.contains("/Items/id123/Images/Primary")); assert!(!url.contains("api_key=")); } #[test] fn test_query_string_properly_separated() { let repo = MockOnlineRepository::new("https://server.com", "token"); let options = ImageOptions { max_width: Some(300), max_height: Some(200), quality: None, tag: None, }; let url = repo.get_image_url("id123", "Primary", Some(&options)); // Should have single ? separator with params let question_marks = url.matches('?').count(); assert_eq!(question_marks, 1); // Should have params for maxWidth and maxHeight assert!(url.contains("maxWidth=300")); assert!(url.contains("maxHeight=200")); } #[test] fn test_special_characters_in_urls() { let repo = MockOnlineRepository::new("https://server.com", "token_with_special-chars"); let url = repo.get_image_url("item-with-special_chars", "Primary", None); // Should handle special characters in id (no token in URL anymore) assert!(url.contains("item-with-special_chars")); } // ===== Backend vs Frontend Responsibility Tests ===== #[test] fn test_backend_owns_url_construction() { // This test documents that URL construction is ONLY in backend let repo = MockOnlineRepository::new("https://jellyfin.example.com", "secret_token"); // Backend generates full URL with credentials let url = repo.get_image_url("item123", "Primary", None); // URL is complete and ready to use (auth via header, not api_key) assert!(url.starts_with("https://")); assert!(url.contains("/Items/item123/Images/Primary")); // Frontend never constructs URLs directly // Frontend only receives pre-constructed URLs from backend } #[test] fn test_url_includes_all_necessary_parameters() { let repo = MockOnlineRepository::new("https://server.com", "token"); let options = ImageOptions { max_width: Some(300), max_height: Some(200), quality: Some(90), tag: Some("abc".to_string()), }; let url = repo.get_image_url("item123", "Primary", Some(&options)); // All provided options should be in URL assert!(url.contains("maxWidth=300")); assert!(url.contains("maxHeight=200")); assert!(url.contains("quality=90")); assert!(url.contains("tag=abc")); } #[test] fn test_optional_parameters_omitted_when_not_provided() { let repo = MockOnlineRepository::new("https://server.com", "token"); let options = ImageOptions { max_width: None, max_height: None, quality: None, tag: None, }; let url = repo.get_image_url("item123", "Primary", Some(&options)); // Should have no query params (no api_key, no options) assert!(!url.contains("?")); assert!(!url.contains("maxWidth")); assert!(!url.contains("maxHeight")); assert!(!url.contains("quality")); assert!(!url.contains("tag")); } }