#![allow(clippy::expect_used)]
use deps_cli::exit::{EXIT_CLEAN, exit_code};
use deps_cli::report::{CheckContext, CheckReport, FailOnPolicy, check_manifest};
use deps_cli::{format, walk};
use deps_core::osv::OsvClient;
use deps_core::policy_config::PolicyConfig;
use deps_core::{EcosystemRegistry, HttpCache};
use deps_engine::setup::{EcosystemRuntime, register_ecosystems};
use std::sync::Arc;
use std::time::Duration;
fn offline_context() -> (EcosystemRegistry, CheckContext) {
let policy = PolicyConfig::default();
let runtime = EcosystemRuntime::from_policy(&policy);
let cache = Arc::new(HttpCache::with_policy(Arc::clone(&runtime.policy)));
cache.set_offline(true);
assert!(
cache.is_offline(),
"test setup bug: HttpCache must actually be offline before this helper is trusted"
);
let registry = EcosystemRegistry::new();
let _ = register_ecosystems(®istry, Arc::clone(&cache), &runtime);
let mut policy = policy;
policy.network.offline = true;
let ctx = CheckContext {
cache: Arc::clone(&cache),
osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy,
};
(registry, ctx)
}
async fn run_pipeline(dir: &std::path::Path) -> (CheckReport, bool) {
let (registry, ctx) = offline_context();
let outcome = walk::walk(&[dir.to_path_buf()], ®istry, false, false);
assert!(!outcome.truncated);
let mut findings = Vec::new();
let mut had_execution_error = false;
for manifest in outcome.manifests {
let content = deps_core::fs_probe::read_to_string_capped(&manifest.path, 10_000_000)
.expect("read fixture manifest")
.expect("fixture manifest under size cap");
let result = check_manifest(
&manifest.ecosystem,
&manifest.uri_path,
&manifest.display_path,
&content,
&ctx,
)
.await
.expect("check_manifest must not fail for a well-formed fixture");
had_execution_error |= result.registry_unreachable;
findings.extend(result.findings);
}
(CheckReport { findings }, had_execution_error)
}
#[tokio::test]
async fn test_offline_run_completes_without_network_access() {
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 (report, _had_error) =
tokio::time::timeout(Duration::from_secs(10), run_pipeline(dir.path()))
.await
.expect("offline run must not block on network I/O");
assert!(
!report.findings.is_empty(),
"offline mode must still report on what it can determine"
);
}
#[tokio::test]
async fn test_empty_directory_produces_no_findings_and_clean_exit() {
let dir = tempfile::tempdir().expect("create temp dir");
let (report, had_error) = run_pipeline(dir.path()).await;
assert!(report.findings.is_empty());
assert!(!had_error);
assert_eq!(
exit_code(&report, &FailOnPolicy::default_categories(), had_error),
EXIT_CLEAN
);
}
#[tokio::test]
async fn test_multi_ecosystem_fixture_tree_discovers_both_manifests() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write cargo manifest");
std::fs::write(
dir.path().join("package.json"),
r#"{"name":"fixture","dependencies":{"left-pad":"1.0.0"}}"#,
)
.expect("write npm manifest");
let (report, _had_error) = run_pipeline(dir.path()).await;
let ecosystems: std::collections::HashSet<_> =
report.findings.iter().map(|f| f.ecosystem).collect();
assert!(ecosystems.contains(&deps_core::EcosystemId::Cargo));
assert!(ecosystems.contains(&deps_core::EcosystemId::Npm));
}
#[tokio::test]
async fn test_table_and_json_formatters_render_the_same_pipeline_output() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let (report, _had_error) = run_pipeline(dir.path()).await;
let table = format::table::render(&report);
assert!(table.contains("Cargo.toml"));
let json = format::json::render(&report).expect("json render must succeed");
let document: format::json::ReportDocument =
serde_json::from_str(&json).expect("json must round-trip");
assert_eq!(document.schema_version, format::json::SCHEMA_VERSION);
assert_eq!(document.findings.len(), report.findings.len());
}
#[tokio::test]
async fn test_sarif_formatter_renders_the_same_pipeline_output() {
let dir = tempfile::tempdir().expect("create temp dir");
let manifest_dir = dir.path().join("weird dir#name");
std::fs::create_dir(&manifest_dir).expect("create nested fixture dir");
std::fs::write(
manifest_dir.join("Cargo.toml"),
"[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let (report, _had_error) = run_pipeline(dir.path()).await;
assert!(
!report.findings.is_empty(),
"serde 1.0 must produce at least one finding"
);
let sarif = format::sarif::to_sarif(&report);
let results = sarif.runs[0]
.results
.as_ref()
.expect("results must be present");
assert_eq!(results.len(), report.findings.len());
for result in results {
let uri = result
.locations
.as_ref()
.expect("locations must be present")[0]
.physical_location
.as_ref()
.expect("physicalLocation must be present")
.artifact_location
.as_ref()
.expect("artifactLocation must be present")
.uri
.as_ref()
.expect("uri must be present");
assert!(
!uri.contains('#'),
"a literal '#' in {uri:?} would be read as a URI fragment separator"
);
assert!(
!uri.contains(' '),
"a literal space in {uri:?} is not a valid URI-reference"
);
assert!(
!uri.contains('\\'),
"{uri:?} must use '/' separators, not the platform's own (possibly '\\\\') display form"
);
assert!(
!std::path::Path::new(uri).is_absolute(),
"{uri:?} must stay relative to the walked root, matching table/json's own display_path"
);
}
let json = format::sarif::render(&report).expect("sarif render must succeed");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("sarif must round-trip");
assert_eq!(parsed["version"], "2.1.0");
}
#[tokio::test]
async fn test_walk_paths_default_to_current_directory_semantics_via_single_file() {
let dir = tempfile::tempdir().expect("create temp dir");
let manifest = dir.path().join("Cargo.toml");
std::fs::write(&manifest, "[package]\nname = \"fixture\"\n").expect("write fixture manifest");
let (registry, ctx) = offline_context();
let outcome = walk::walk(std::slice::from_ref(&manifest), ®istry, false, false);
assert_eq!(outcome.manifests.len(), 1);
let content = deps_core::fs_probe::read_to_string_capped(&manifest, 10_000_000)
.expect("read manifest")
.expect("under size cap");
let result = check_manifest(
&outcome.manifests[0].ecosystem,
&manifest,
&manifest,
&content,
&ctx,
)
.await
.expect("check_manifest must succeed");
assert!(result.findings.is_empty());
}
#[tokio::test]
async fn test_sarif_formatter_relativizes_an_absolute_single_file_path() {
let dir = tempfile::tempdir().expect("create temp dir");
let manifest = dir.path().join("Cargo.toml");
assert!(
manifest.is_absolute(),
"test setup bug: fixture path must be absolute"
);
std::fs::write(
&manifest,
"[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
)
.expect("write fixture manifest");
let (registry, ctx) = offline_context();
let outcome = walk::walk(std::slice::from_ref(&manifest), ®istry, false, false);
assert_eq!(outcome.manifests.len(), 1);
assert!(
outcome.manifests[0].display_path.is_absolute(),
"test setup bug: this must exercise the absolute-display_path branch"
);
let content = deps_core::fs_probe::read_to_string_capped(&manifest, 10_000_000)
.expect("read manifest")
.expect("under size cap");
let result = check_manifest(
&outcome.manifests[0].ecosystem,
&manifest,
&manifest,
&content,
&ctx,
)
.await
.expect("check_manifest must succeed");
assert!(
!result.findings.is_empty(),
"serde 1.0 must produce at least one finding"
);
let report = CheckReport {
findings: result.findings,
};
let sarif = format::sarif::to_sarif(&report);
let results = sarif.runs[0]
.results
.as_ref()
.expect("results must be present");
for result in results {
let uri = result
.locations
.as_ref()
.expect("locations must be present")[0]
.physical_location
.as_ref()
.expect("physicalLocation must be present")
.artifact_location
.as_ref()
.expect("artifactLocation must be present")
.uri
.as_ref()
.expect("uri must be present");
assert!(
!std::path::Path::new(uri).is_absolute(),
"{uri:?} must not stay absolute — it leaks local machine path structure into a \
document meant to be uploaded to GitHub code scanning"
);
}
}
#[test]
fn test_respect_gitignore_flag_reaches_the_real_exit_code_wiring() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(dir.path().join(".git")).expect("create .git marker");
std::fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.expect("write fixture manifest");
let exe = env!("CARGO_BIN_EXE_deps-cli");
let output = std::process::Command::new(exe)
.args(["check", "--offline", "--respect-gitignore"])
.arg(dir.path())
.output()
.expect("run the real deps-cli binary");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(2),
"a manifest excluded by --respect-gitignore must exit 2 (execution error), not 0 — \
stderr: {stderr}"
);
assert!(
stderr.contains("excluded from the scan"),
"must warn about the excluded manifest, stderr: {stderr}"
);
}
#[test]
fn test_default_mode_ignores_gitignore_through_the_real_binary_and_exits_clean() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(dir.path().join(".git")).expect("create .git marker");
std::fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.expect("write fixture manifest");
let exe = env!("CARGO_BIN_EXE_deps-cli");
let output = std::process::Command::new(exe)
.args(["check", "--offline"])
.arg(dir.path())
.output()
.expect("run the real deps-cli binary");
assert_eq!(
output.status.code(),
Some(0),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(unix)]
#[tokio::test]
async fn test_follow_symlinks_lockfile_lookup_uses_symlinks_directory_not_targets() {
let root = tempfile::tempdir().expect("create walked root");
let dir_a = root.path().join("a");
let dir_b = root.path().join("b");
std::fs::create_dir(&dir_a).expect("mkdir a");
std::fs::create_dir(&dir_b).expect("mkdir b");
std::fs::write(
dir_b.join("manifest-data"),
"[package]\nname = \"x\"\nversion = \"0.1.0\"\n\n[dependencies]\nonce_cell = \"1\"\n",
)
.expect("write target manifest in B");
std::fs::write(
dir_a.join("Cargo.lock"),
"version = 4\n\n[[package]]\nname = \"once_cell\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
)
.expect("write A's Cargo.lock");
std::os::unix::fs::symlink(dir_b.join("manifest-data"), dir_a.join("Cargo.toml"))
.expect("symlink A/Cargo.toml -> B/Cargo.toml");
let (registry, ctx) = offline_context();
let outcome = walk::walk(&[root.path().to_path_buf()], ®istry, false, true);
assert_eq!(
outcome.manifests.len(),
1,
"ignored_manifests: {:?}, walk_errors: {:?}",
outcome.ignored_manifests,
outcome.walk_errors
);
let manifest = &outcome.manifests[0];
assert_eq!(
manifest.path.canonicalize().expect("canonicalize path"),
dir_b
.join("manifest-data")
.canonicalize()
.expect("canonicalize B/Cargo.toml")
);
assert_eq!(manifest.uri_path, dir_a.join("Cargo.toml"));
let lockfile_provider = manifest
.ecosystem
.lockfile_provider()
.expect("cargo ecosystem must have a lock file provider");
let uri_path_uri = url::Url::from_file_path(&manifest.uri_path).expect("uri_path to file uri");
let path_uri = url::Url::from_file_path(&manifest.path).expect("path to file uri");
assert_eq!(
lockfile_provider.locate_lockfile(&uri_path_uri),
Some(dir_a.join("Cargo.lock")),
"lockfile lookup driven by uri_path must find A's Cargo.lock"
);
assert_eq!(
lockfile_provider.locate_lockfile(&path_uri),
None,
"lockfile lookup driven by the resolved target path (B) must find nothing — B has no \
Cargo.lock; if this were Some, check_manifest would silently anchor lockfile lookup \
at the wrong directory"
);
let content = std::fs::read_to_string(&manifest.path).expect("read manifest content");
check_manifest(
&manifest.ecosystem,
&manifest.uri_path,
&manifest.display_path,
&content,
&ctx,
)
.await
.expect("check_manifest must succeed with the correct (uri_path) wiring");
assert_eq!(
ctx.lockfile_cache.len(),
1,
"check_manifest called with uri_path must have found and cached A's Cargo.lock"
);
let buggy_ctx = CheckContext {
cache: Arc::clone(&ctx.cache),
osv: Arc::clone(&ctx.osv),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy: ctx.policy.clone(),
};
check_manifest(
&manifest.ecosystem,
&manifest.path,
&manifest.display_path,
&content,
&buggy_ctx,
)
.await
.expect("check_manifest must still succeed (absence of a lock file is not an error)");
assert_eq!(
buggy_ctx.lockfile_cache.len(),
0,
"check_manifest called with the resolved target path (the bug M2 fixes) must find no \
lockfile at all, proving the two wirings genuinely diverge"
);
}