Initial commit

This commit is contained in:
2025-12-30 10:51:50 -05:00
parent 12315c4925
commit ee7e765181
22 changed files with 6714 additions and 1 deletions

298
tests/integration.rs Normal file
View File

@@ -0,0 +1,298 @@
use std::fs;
use std::path::Path;
use assert_cmd::Command;
use httpmock::Method::{GET, POST};
use httpmock::MockServer;
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());
}