use std::fs; use std::path::Path; use assert_cmd::Command; use httpmock::Method::{GET, POST}; use httpmock::MockServer; use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::TempDir; fn make_ffprobe_stub(dir: &Path) -> std::path::PathBuf { let bin_dir = dir.join("bin"); fs::create_dir_all(&bin_dir).unwrap(); let script_path = bin_dir.join("ffprobe"); let script = r#"#!/usr/bin/env sh echo '{"format":{"duration":"7200"},"streams":[{"codec_type":"video","codec_name":"h264","height":1080}]}' "#; fs::write(&script_path, script).unwrap(); let mut perms = fs::metadata(&script_path).unwrap().permissions(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; perms.set_mode(0o755); fs::set_permissions(&script_path, perms).unwrap(); } script_path } fn prepend_path(path: &Path) -> String { let current = std::env::var("PATH").unwrap_or_default(); format!("{}:{}", path.display(), current) } #[test] fn tmdb_flow_dry_run_with_mock_server() { let server = MockServer::start(); let search_mock = server.mock(|when, then| { when.method(GET) .path("/search/movie") .query_param("api_key", "test") .query_param("query", "Some Movie") .query_param("year", "2020"); then.status(200) .header("content-type", "application/json") .body(r#"{"results":[{"id":123,"title":"Some Movie","release_date":"2020-01-02"}]}"#); }); let details_mock = server.mock(|when, then| { when.method(GET) .path("/movie/123") .query_param("api_key", "test"); then.status(200) .header("content-type", "application/json") .body(r#"{"id":123,"title":"Some Movie","release_date":"2020-01-02","runtime":120,"imdb_id":"tt123"}"#); }); let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Some.Movie.2020.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .env("MOV_RENAMARR_PROVIDER", "tmdb") .env("MOV_RENAMARR_TMDB_API_KEY", "test") .env("MOV_RENAMARR_TMDB_BASE_URL", server.url("")) .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success().stdout(contains("renamed")); search_mock.assert_hits(1); details_mock.assert_hits(1); } #[test] fn omdb_flow_dry_run_with_mock_server() { let server = MockServer::start(); let search_mock = server.mock(|when, then| { when.method(GET) .path("/") .query_param("apikey", "test") .query_param("s", "Another Movie") .query_param("type", "movie") .query_param("y", "2019"); then.status(200) .header("content-type", "application/json") .body(r#"{"Search":[{"Title":"Another Movie","Year":"2019","imdbID":"tt999"}],"Response":"True"}"#); }); let details_mock = server.mock(|when, then| { when.method(GET) .path("/") .query_param("apikey", "test") .query_param("i", "tt999") .query_param("plot", "short"); then.status(200) .header("content-type", "application/json") .body(r#"{"Title":"Another Movie","Year":"2019","imdbID":"tt999","Runtime":"95 min","Response":"True"}"#); }); let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Another.Movie.2019.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .env("MOV_RENAMARR_PROVIDER", "omdb") .env("MOV_RENAMARR_OMDB_API_KEY", "test") .env("MOV_RENAMARR_OMDB_BASE_URL", server.url("")) .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success().stdout(contains("renamed")); search_mock.assert_hits(1); details_mock.assert_hits(1); } #[test] fn creates_default_config_on_no_args() { let temp = TempDir::new().unwrap(); let config_home = temp.path().join("config"); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.env("XDG_CONFIG_HOME", &config_home); cmd.assert().success().stderr(contains("Config file:")); let config_path = config_home.join("mov-renamarr").join("config.toml"); assert!(config_path.exists()); let contents = fs::read_to_string(config_path).unwrap(); assert!(contents.contains("provider = \"auto\"")); } #[test] fn no_lookup_uses_parsed_title_and_year() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Test.Movie.2021.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .arg("--no-lookup") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success().stdout(contains("parsed")); } #[test] fn no_lookup_with_llm_parse_renames_missing_year() { let server = MockServer::start(); let llm_mock = server.mock(|when, then| { when.method(POST) .path("/api/generate"); then.status(200) .header("content-type", "application/json") .body(r#"{"response":"{\"title\":\"Mystery Movie\",\"year\":\"2011\",\"alt_titles\":[]}"}"#); }); let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Mystery.Movie.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .arg("--no-lookup") .arg("--llm-mode").arg("parse") .arg("--llm-endpoint").arg(server.url("")) .arg("--llm-model").arg("qwen") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert() .success() .stdout(contains("Mystery Movie (2011)")) .stdout(contains("parsed")); llm_mock.assert_hits(1); } #[test] fn collision_policy_skips_existing_destination() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Some.Movie.2020.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); // Pre-create destination to trigger collision skip. let dest_dir = output.join("Some Movie (2020)"); fs::create_dir_all(&dest_dir).unwrap(); let dest_path = dest_dir.join("Some Movie (2020) [1080p].mkv"); fs::write(&dest_path, b"existing").unwrap(); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--no-lookup") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success().stdout(contains("destination exists")); assert!(dest_path.exists()); assert!(input.join("Some.Movie.2020.mkv").exists()); } #[test] fn sidecars_are_copied_when_enabled() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Film.2020.mkv"), b"stub").unwrap(); fs::write(input.join("Film.2020.srt"), b"sub").unwrap(); fs::write(input.join("Film.2020.nfo"), b"nfo").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--no-lookup") .arg("--sidecars") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success(); let out_dir = output.join("Film (2020)"); assert!(out_dir.join("Film (2020) [1080p].mkv").exists()); assert!(out_dir.join("Film (2020) [1080p].srt").exists()); assert!(out_dir.join("Film (2020) [1080p].nfo").exists()); } #[test] fn rename_in_place_uses_input_as_output() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); fs::create_dir_all(&input).unwrap(); fs::write(input.join("Alien.1979.1080p.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--rename-in-place") .arg("--no-lookup") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert().success().stdout(contains("renamed")); let renamed = input.join("Alien (1979)").join("Alien (1979) [1080p].mkv"); assert!(renamed.exists()); assert!(!input.join("Alien.1979.1080p.mkv").exists()); } #[test] fn completions_generate_output() { let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--completions").arg("bash"); cmd.assert().success().stdout(contains("mov-renamarr")); } #[test] fn print_config_outputs_toml() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--print-config") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")); cmd.assert() .success() .stdout(contains("provider = \"auto\"")) .stdout(contains("cache_ttl_days")); } #[test] fn dry_run_summary_suppresses_per_file_output() { let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Film.2020.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .arg("--dry-run-summary") .arg("--no-lookup") .arg("--color").arg("never") .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert() .success() .stdout(contains("Processed: 1")) .stdout(predicates::str::contains("[1/1]").not()); } #[test] fn explain_prints_candidates_on_skip() { let server = MockServer::start(); let search_mock = server.mock(|when, then| { when.method(GET) .path("/search/movie") .query_param("api_key", "test") .query_param("query", "Zootopia") .query_param("year", "2016"); then.status(200) .header("content-type", "application/json") .body(r#"{"results":[{"id":55,"title":"Totally Different","release_date":"1990-01-01"}]}"#); }); let details_mock = server.mock(|when, then| { when.method(GET) .path("/movie/55") .query_param("api_key", "test"); then.status(200) .header("content-type", "application/json") .body(r#"{"id":55,"title":"Totally Different","release_date":"1990-01-01","runtime":120,"imdb_id":"tt055"}"#); }); let temp = TempDir::new().unwrap(); let input = temp.path().join("input"); let output = temp.path().join("output"); fs::create_dir_all(&input).unwrap(); fs::create_dir_all(&output).unwrap(); fs::write(input.join("Zootopia.2016.mkv"), b"stub").unwrap(); let ffprobe = make_ffprobe_stub(temp.path()); let mut cmd = Command::new(assert_cmd::cargo_bin!("mov-renamarr")); cmd.arg("--input").arg(&input) .arg("--output").arg(&output) .arg("--dry-run") .arg("--provider").arg("tmdb") .arg("--min-score").arg("99") .arg("--explain") .arg("--color").arg("never") .env("MOV_RENAMARR_TMDB_API_KEY", "test") .env("MOV_RENAMARR_TMDB_BASE_URL", server.url("")) .env("XDG_CONFIG_HOME", temp.path().join("config")) .env("XDG_CACHE_HOME", temp.path().join("cache")) .env("PATH", prepend_path(ffprobe.parent().unwrap())); cmd.assert() .success() .stdout(contains("skipped")) .stdout(contains("Candidates:")) .stdout(contains("Totally Different")); search_mock.assert_hits(1); details_mock.assert_hits(1); }