#![allow(clippy::expect_used)]
use deps_cli::format::DryRun;
#[cfg(unix)]
use deps_cli::update::ApplyError;
use deps_cli::update::{Outcome, PlannedUpdateItem, UpdatePlan, apply_plan};
use deps_core::edit::ManifestEdit;
use deps_core::position::{Position, Range};
fn exe() -> &'static str {
env!("CARGO_BIN_EXE_deps-cli")
}
#[test]
fn test_update_offline_empty_plan_is_clean_and_reports_no_eligible_updates() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let output = std::process::Command::new(exe())
.args(["update", "--offline"])
.arg(dir.path().join("Cargo.toml"))
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(0),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("No eligible updates."), "stdout: {stdout}");
}
#[test]
fn test_update_offline_json_format_empty_plan_schema() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let output = std::process::Command::new(exe())
.args(["update", "--offline", "--dry-run", "--format", "json"])
.arg(dir.path().join("Cargo.toml"))
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(0),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let document: deps_cli::format::json::UpdateReportDocument =
serde_json::from_str(&stdout).expect("stdout must be the versioned JSON document");
assert!(document.dry_run);
assert!(document.items.is_empty());
}
#[test]
fn test_update_security_only_offline_is_a_hard_error() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let output = std::process::Command::new(exe())
.args(["update", "--offline", "--security-only"])
.arg(dir.path().join("Cargo.toml"))
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("security-only") && stderr.contains("offline"),
"stderr must explain the conflict, got: {stderr}"
);
}
#[test]
fn test_update_unrecognized_manifest_path_is_execution_error() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(dir.path().join("not-a-manifest.txt"), "hello\n")
.expect("write non-manifest file");
let output = std::process::Command::new(exe())
.args(["update", "--offline"])
.arg(dir.path().join("not-a-manifest.txt"))
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_update_malformed_config_is_execution_error() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let config_path = dir.path().join("deps.toml");
std::fs::write(&config_path, "this is not valid deps.toml content [[[\n")
.expect("write malformed config");
let output = std::process::Command::new(exe())
.args(["update", "--offline", "--config"])
.arg(&config_path)
.arg(dir.path().join("Cargo.toml"))
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(unix)]
#[test]
fn test_update_symlinked_manifest_argument_is_refused_before_any_read() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real-Cargo.toml");
std::fs::write(
&real,
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write real manifest");
let link = dir.path().join("Cargo.toml");
std::os::unix::fs::symlink(&real, &link).expect("create symlinked manifest argument");
let output = std::process::Command::new(exe())
.args(["update", "--offline"])
.arg(&link)
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("symlink"),
"must explain the symlink refusal, got: {stderr}"
);
}
fn applied_plan(range: Range, new_text: &str) -> UpdatePlan {
UpdatePlan {
items: vec![PlannedUpdateItem {
name: "serde".to_string(),
current: deps_cli::update::CurrentVersion::Resolved(deps_core::ConcreteVersion::from(
"1.0.0",
)),
outcome: Outcome::Applied {
edit: ManifestEdit {
range,
new_text: new_text.to_string(),
},
target: deps_core::ConcreteVersion::from(new_text),
},
advisory_ids: Vec::new(),
ignore_rule_overridden: false,
gossip_excluded_version: None,
cooldown_fallback: None,
}],
}
}
#[cfg(unix)]
#[test]
fn test_apply_plan_refuses_a_symlinked_manifest_path() {
let dir = tempfile::tempdir().expect("create temp dir");
let target = dir.path().join("real-target.toml");
let original = "serde = \"1.0.0\"\n";
std::fs::write(&target, original).expect("write symlink target");
let link = dir.path().join("Cargo.toml");
std::os::unix::fs::symlink(&target, &link).expect("create symlinked manifest path");
let plan = applied_plan(
Range::new(Position::new(0, 9), Position::new(0, 14)),
"1.2.0",
);
let result = apply_plan(&plan, &link, original, DryRun::No);
assert!(
matches!(result, Err(ApplyError::Write { .. })),
"expected a write-refusal error, got {result:?}"
);
assert!(
std::fs::symlink_metadata(&link)
.expect("symlink must still exist")
.file_type()
.is_symlink(),
"the manifest path symlink itself must be left untouched"
);
assert_eq!(
std::fs::read_to_string(&target).expect("read target"),
original,
"the symlink's target must never be written through"
);
}
#[test]
fn test_apply_plan_writes_through_a_real_manifest_path() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("Cargo.toml");
let original = "serde = \"1.0.0\"\n";
std::fs::write(&path, original).expect("write manifest");
let plan = applied_plan(
Range::new(Position::new(0, 9), Position::new(0, 14)),
"1.2.0",
);
apply_plan(&plan, &path, original, DryRun::No).expect("apply_plan must succeed");
assert_eq!(
std::fs::read_to_string(&path).expect("read manifest"),
"serde = \"1.2.0\"\n"
);
}
#[test]
fn test_apply_plan_dry_run_leaves_the_manifest_byte_for_byte_intact() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("Cargo.toml");
let original = "serde = \"1.0.0\"\n";
std::fs::write(&path, original).expect("write manifest");
let plan = applied_plan(
Range::new(Position::new(0, 9), Position::new(0, 14)),
"1.2.0",
);
apply_plan(&plan, &path, original, DryRun::Yes).expect("apply_plan must succeed under dry_run");
assert_eq!(
std::fs::read_to_string(&path).expect("read manifest"),
original,
"dry_run must never write, even though a real edit was planned"
);
}