chore(repository): delete online_integration_test.rs, which never compiled
The file was not declared in repository/mod.rs and imported crate::api::jellyfin,
a module that does not exist. It had never been built, let alone run.
Dead would be reason enough, but it was worse than dead. Its mock reimplemented
the URL builders and then asserted against itself, and online.rs still carries
the comment recording where that leads: the mock used the correct stream.mp4
endpoint while the real implementation shipped /Videos/{id}/download, which 404s
on real servers and silently broke every movie and TV download. The "test" stayed
green throughout. Its own test_image_url_basic asserted api_key= appears in image
URLs while the mock two lines above it documented the opposite.
Deleted rather than revived: it asserts on constructed URL strings, which is the
pattern the mock-server harness replaces. The lesson it left is preserved in the
online.rs comment that referenced it — assert against a response from a mock
server, never against a mock that re-derives the thing under test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,429 +0,0 @@
|
||||
#[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::<Vec<_>>()
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user