use std::fs;
use tempfile::TempDir;
use vership::config::VersionFileEntry;
use vership::version_files;
#[test]
fn text_mode_replaces_prev_with_version() {
let dir = TempDir::new().unwrap();
let readme = dir.path().join("README.md");
fs::write(&readme, "Install: rev: v1.2.3\nAlso: mise use tool@1.2.3\n").unwrap();
let entries = vec![
VersionFileEntry {
glob: "README.md".to_string(),
search: Some("rev: v{prev}".to_string()),
replace: Some("rev: v{version}".to_string()),
field: None,
},
VersionFileEntry {
glob: "README.md".to_string(),
search: Some("tool@{prev}".to_string()),
replace: Some("tool@{version}".to_string()),
field: None,
},
];
let touched = version_files::apply(dir.path(), &entries, "1.2.3", "1.3.0").unwrap();
let content = fs::read_to_string(&readme).unwrap();
assert_eq!(content, "Install: rev: v1.3.0\nAlso: mise use tool@1.3.0\n");
assert!(touched.contains(&"README.md".into()));
}
#[test]
fn text_mode_skips_file_without_match() {
let dir = TempDir::new().unwrap();
let readme = dir.path().join("README.md");
fs::write(&readme, "No version here\n").unwrap();
let entries = vec![VersionFileEntry {
glob: "README.md".to_string(),
search: Some("rev: v{prev}".to_string()),
replace: Some("rev: v{version}".to_string()),
field: None,
}];
let touched = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0").unwrap();
assert!(touched.is_empty());
let content = fs::read_to_string(&readme).unwrap();
assert_eq!(content, "No version here\n");
}
#[test]
fn text_mode_glob_matches_multiple_files() {
let dir = TempDir::new().unwrap();
let docs = dir.path().join("docs");
fs::create_dir(&docs).unwrap();
fs::write(docs.join("a.md"), "rev: v2.0.0\n").unwrap();
fs::write(docs.join("b.md"), "rev: v2.0.0\n").unwrap();
let entries = vec![VersionFileEntry {
glob: "docs/*.md".to_string(),
search: Some("rev: v{prev}".to_string()),
replace: Some("rev: v{version}".to_string()),
field: None,
}];
let touched = version_files::apply(dir.path(), &entries, "2.0.0", "2.1.0").unwrap();
assert_eq!(touched.len(), 2);
assert_eq!(
fs::read_to_string(docs.join("a.md")).unwrap(),
"rev: v2.1.0\n"
);
assert_eq!(
fs::read_to_string(docs.join("b.md")).unwrap(),
"rev: v2.1.0\n"
);
}
#[test]
fn field_mode_updates_json_version() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("package.json");
fs::write(
&pkg,
"{\n \"name\": \"test\",\n \"version\": \"1.0.0\"\n}\n",
)
.unwrap();
let entries = vec![VersionFileEntry {
glob: "package.json".to_string(),
search: None,
replace: None,
field: Some("version".to_string()),
}];
let touched = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0").unwrap();
assert_eq!(touched.len(), 1);
let content = fs::read_to_string(&pkg).unwrap();
assert!(content.contains("\"version\": \"1.1.0\""));
assert!(content.contains("\"name\": \"test\""));
}
#[test]
fn field_mode_wildcard_updates_all_values() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("package.json");
fs::write(
&pkg,
"{\n \"optionalDependencies\": {\n \"pkg-a\": \"1.0.0\",\n \"pkg-b\": \"1.0.0\"\n }\n}\n",
)
.unwrap();
let entries = vec![VersionFileEntry {
glob: "package.json".to_string(),
search: None,
replace: None,
field: Some("optionalDependencies.*".to_string()),
}];
let touched = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0").unwrap();
assert_eq!(touched.len(), 1);
let content = fs::read_to_string(&pkg).unwrap();
assert!(content.contains("\"pkg-a\": \"1.1.0\""));
assert!(content.contains("\"pkg-b\": \"1.1.0\""));
}
#[test]
fn field_mode_glob_matches_multiple_json_files() {
let dir = TempDir::new().unwrap();
let npm = dir.path().join("npm");
let cli_a = npm.join("cli-a");
let cli_b = npm.join("cli-b");
fs::create_dir_all(&cli_a).unwrap();
fs::create_dir_all(&cli_b).unwrap();
fs::write(cli_a.join("package.json"), "{\"version\": \"1.0.0\"}\n").unwrap();
fs::write(cli_b.join("package.json"), "{\"version\": \"1.0.0\"}\n").unwrap();
let entries = vec![VersionFileEntry {
glob: "npm/*/package.json".to_string(),
search: None,
replace: None,
field: Some("version".to_string()),
}];
let touched = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0").unwrap();
assert_eq!(touched.len(), 2);
}
#[test]
fn field_mode_missing_field_returns_error() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("package.json");
fs::write(&pkg, "{\"name\": \"test\"}\n").unwrap();
let entries = vec![VersionFileEntry {
glob: "package.json".to_string(),
search: None,
replace: None,
field: Some("version".to_string()),
}];
let result = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0");
assert!(result.is_err());
}
#[test]
fn field_mode_preserves_2_space_indent_with_trailing_newline() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("package.json");
let original = "{\n \"name\": \"test\",\n \"version\": \"1.0.0\"\n}\n";
fs::write(&pkg, original).unwrap();
let entries = vec![VersionFileEntry {
glob: "package.json".to_string(),
search: None,
replace: None,
field: Some("version".to_string()),
}];
version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0").unwrap();
let content = fs::read_to_string(&pkg).unwrap();
assert!(content.starts_with("{\n "));
assert!(content.ends_with("}\n"));
assert!(!content.contains(" ")); }
#[test]
fn config_validation_rejects_entry_with_both_field_and_search() {
let dir = TempDir::new().unwrap();
let entries = vec![VersionFileEntry {
glob: "README.md".to_string(),
search: Some("v{prev}".to_string()),
replace: Some("v{version}".to_string()),
field: Some("version".to_string()),
}];
let result = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0");
assert!(result.is_err());
}
#[test]
fn config_validation_rejects_entry_with_search_but_no_replace() {
let dir = TempDir::new().unwrap();
let entries = vec![VersionFileEntry {
glob: "README.md".to_string(),
search: Some("v{prev}".to_string()),
replace: None,
field: None,
}];
let result = version_files::apply(dir.path(), &entries, "1.0.0", "1.1.0");
assert!(result.is_err());
}
#[test]
fn field_mode_preserves_key_order() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("package.json");
fs::write(
&pkg,
"{\n \"name\": \"my-pkg\",\n \"version\": \"1.0.0\",\n \"description\": \"A package\"\n}\n",
)
.unwrap();
let entries = vec![VersionFileEntry {
glob: "package.json".to_string(),
search: None,
replace: None,
field: Some("version".to_string()),
}];
version_files::apply(dir.path(), &entries, "1.0.0", "2.0.0").unwrap();
let content = fs::read_to_string(&pkg).unwrap();
let name_pos = content.find("\"name\"").unwrap();
let version_pos = content.find("\"version\"").unwrap();
let desc_pos = content.find("\"description\"").unwrap();
assert!(name_pos < version_pos, "name should come before version");
assert!(
version_pos < desc_pos,
"version should come before description"
);
}
#[test]
fn field_mode_nested_path() {
let dir = TempDir::new().unwrap();
let pkg = dir.path().join("data.json");
fs::write(
&pkg,
"{\n \"metadata\": {\n \"build\": {\n \"version\": \"1.0.0\"\n }\n }\n}\n",
)
.unwrap();
let entries = vec![VersionFileEntry {
glob: "data.json".to_string(),
search: None,
replace: None,
field: Some("metadata.build.version".to_string()),
}];
let touched = version_files::apply(dir.path(), &entries, "1.0.0", "2.0.0").unwrap();
assert_eq!(touched.len(), 1);
let content = fs::read_to_string(&pkg).unwrap();
assert!(content.contains("\"version\": \"2.0.0\""));
}
#[test]
fn text_mode_replaces_multiple_occurrences_in_one_file() {
let dir = TempDir::new().unwrap();
let readme = dir.path().join("README.md");
fs::write(
&readme,
"First: rev: v1.0.0\nSecond: rev: v1.0.0\nThird: rev: v1.0.0\n",
)
.unwrap();
let entries = vec![VersionFileEntry {
glob: "README.md".to_string(),
search: Some("rev: v{prev}".to_string()),
replace: Some("rev: v{version}".to_string()),
field: None,
}];
version_files::apply(dir.path(), &entries, "1.0.0", "2.0.0").unwrap();
let content = fs::read_to_string(&readme).unwrap();
assert_eq!(content.matches("rev: v2.0.0").count(), 3);
assert_eq!(content.matches("rev: v1.0.0").count(), 0);
}