#![allow(clippy::expect_used)]
use deps_cli::exit::{EXIT_CLEAN, ExecutionOutcome, 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::{LicensePolicyConfig, PolicyConfig};
use deps_core::{EcosystemRegistry, HttpCache, NetworkMode};
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(NetworkMode::Offline);
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))),
deps_dev: Arc::new(deps_core::DepsDevClient::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,
walk::GitignorePolicy::Ignore,
walk::SymlinkPolicy::Skip,
);
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 || result.license_fetch_incomplete;
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(),
ExecutionOutcome::from_had_execution_error(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,
walk::GitignorePolicy::Ignore,
walk::SymlinkPolicy::Skip,
);
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,
walk::GitignorePolicy::Ignore,
walk::SymlinkPolicy::Skip,
);
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)]
#[test]
fn test_broken_symlink_manifest_reaches_the_real_exit_code_wiring() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("package.json"),
"{\"name\": \"fixture\", \"version\": \"0.1.0\"}\n",
)
.expect("write a second, recognized manifest");
std::os::unix::fs::symlink(
dir.path().join("does-not-exist.toml"),
dir.path().join("Cargo.toml"),
)
.expect("create broken symlink replacing Cargo.toml");
let exe = env!("CARGO_BIN_EXE_deps-cli");
let output = std::process::Command::new(exe)
.args(["check", "--offline", "--fail-on", "vulnerable"])
.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 replaced by a broken symlink must exit 2 (execution error), not 0, even \
though a second manifest was found and scanned cleanly — stderr: {stderr}"
);
assert!(
stderr.contains("could not be resolved"),
"must warn with the broken-symlink-specific message, not the generic excluded-manifest \
one, stderr: {stderr}"
);
assert!(
!stderr.contains("no manifests were discovered"),
"the second manifest must have been found — this exit code must come from the broken \
symlink alone, stderr: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn test_broken_symlink_manifest_still_reported_under_follow_symlinks() {
let dir = tempfile::tempdir().expect("create temp dir");
std::os::unix::fs::symlink(
dir.path().join("does-not-exist.toml"),
dir.path().join("Cargo.toml"),
)
.expect("create broken symlink replacing Cargo.toml");
let exe = env!("CARGO_BIN_EXE_deps-cli");
let output = std::process::Command::new(exe)
.args(["check", "--offline", "--follow-symlinks"])
.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),
"--follow-symlinks must not turn a broken symlink into a clean exit — stderr: {stderr}"
);
assert!(stderr.contains("could not be resolved"), "stderr: {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,
walk::GitignorePolicy::Ignore,
walk::SymlinkPolicy::Follow,
);
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),
deps_dev: Arc::clone(&ctx.deps_dev),
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"
);
}
fn live_context(license_policy: LicensePolicyConfig) -> (EcosystemRegistry, CheckContext) {
let policy = PolicyConfig {
license_policy,
..PolicyConfig::default()
};
let runtime = EcosystemRuntime::from_policy(&policy);
let cache = Arc::new(HttpCache::with_policy(Arc::clone(&runtime.policy)));
let registry = EcosystemRegistry::new();
let _ = register_ecosystems(®istry, Arc::clone(&cache), &runtime);
let ctx = CheckContext {
cache: Arc::clone(&cache),
osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
deps_dev: Arc::new(deps_core::DepsDevClient::new(Arc::clone(&cache))),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy,
};
(registry, ctx)
}
mod tier3_license_prefetch_parity {
use super::*;
use deps_cli::report::Category;
#[cfg(feature = "dart")]
#[tokio::test]
#[ignore = "hits the real pub.dev API"]
async fn test_live_dart_tier3_license_feeds_check_license_policy() {
let (registry, ctx) =
live_context(LicensePolicyConfig::new().with_allow(vec!["0BSD".to_string()]));
let url = deps_core::test_util::test_uri("/test/pubspec.yaml");
let manifest_path = url.to_file_path().expect("file-scheme uri");
let ecosystem = registry.for_uri(&url).expect("Dart ecosystem not found");
let content = "dependencies:\n http: 1.2.0\n";
let result = check_manifest(&ecosystem, &manifest_path, &manifest_path, content, &ctx)
.await
.expect("check_manifest must not fail for a well-formed fixture");
assert!(
result
.findings
.iter()
.any(|f| f.category == Category::License
&& f.dependency_name.as_deref() == Some("http")),
"expected a license-policy finding for 'http', got: {:?}",
result.findings
);
}
#[cfg(feature = "swift")]
#[tokio::test]
#[ignore = "hits the real GitHub API"]
async fn test_live_swift_tier3_license_feeds_check_license_policy() {
let (registry, ctx) =
live_context(LicensePolicyConfig::new().with_allow(vec!["0BSD".to_string()]));
let url = deps_core::test_util::test_uri("/test/Package.swift");
let manifest_path = url.to_file_path().expect("file-scheme uri");
let ecosystem = registry.for_uri(&url).expect("Swift ecosystem not found");
let content =
r#".package(url: "https://github.com/apple/swift-nio.git", .exact("2.65.0"))"#;
let result = check_manifest(&ecosystem, &manifest_path, &manifest_path, content, &ctx)
.await
.expect("check_manifest must not fail for a well-formed fixture");
let has_license_finding = result.findings.iter().any(|f| {
f.category == Category::License
&& f.dependency_name.as_deref() == Some("apple/swift-nio")
});
if deps_core::test_util::should_skip_on_empty_result(
!has_license_finding,
"test_live_swift_tier3_license_feeds_check_license_policy",
ecosystem
.registry()
.get_versions(&deps_core::PackageName::new("apple/swift-nio")),
)
.await
{
return;
}
assert!(
has_license_finding,
"expected a license-policy finding for 'apple/swift-nio', got: {:?}",
result.findings
);
}
#[cfg(feature = "gradle")]
#[tokio::test]
#[ignore = "hits the real Maven Central API"]
async fn test_live_gradle_tier3_license_feeds_check_license_policy() {
let _guard = deps_core::fs_probe::snapshot_guard_async().await;
let (registry, ctx) =
live_context(LicensePolicyConfig::new().with_allow(vec!["0BSD".to_string()]));
let url = deps_core::test_util::test_uri("/test/build.gradle.kts");
let manifest_path = url.to_file_path().expect("file-scheme uri");
let ecosystem = registry.for_uri(&url).expect("Gradle ecosystem not found");
let content =
"dependencies {\n implementation(\"com.squareup.okhttp3:okhttp:4.12.0\")\n}\n";
let result = check_manifest(&ecosystem, &manifest_path, &manifest_path, content, &ctx)
.await
.expect("check_manifest must not fail for a well-formed fixture");
assert!(
result
.findings
.iter()
.any(|f| f.category == Category::License
&& f.dependency_name.as_deref() == Some("com.squareup.okhttp3:okhttp")),
"expected a license-policy finding for 'com.squareup.okhttp3:okhttp', got: {:?}",
result.findings
);
}
#[cfg(feature = "deno")]
#[tokio::test]
#[ignore = "hits the real JSR API"]
async fn test_live_deno_tier3_license_feeds_check_license_policy() {
let (registry, ctx) =
live_context(LicensePolicyConfig::new().with_allow(vec!["0BSD".to_string()]));
let url = deps_core::test_util::test_uri("/test/deno.json");
let manifest_path = url.to_file_path().expect("file-scheme uri");
let ecosystem = registry.for_uri(&url).expect("Deno ecosystem not found");
let content = r#"{"imports": {"@std/fs": "jsr:@std/fs@1.0.24"}}"#;
let result = check_manifest(&ecosystem, &manifest_path, &manifest_path, content, &ctx)
.await
.expect("check_manifest must not fail for a well-formed fixture");
assert!(
result
.findings
.iter()
.any(|f| f.category == Category::License
&& f.dependency_name.as_deref() == Some("jsr:@std/fs")),
"expected a license-policy finding for 'jsr:@std/fs', got: {:?}",
result.findings
);
}
}
mod tier3_wiring_regression {
use super::*;
use deps_cli::report::Category;
use deps_core::Ecosystem;
use deps_core::policy_config::DiagnosticsConfig;
use deps_engine::test_util::TestTier3Ecosystem;
fn wiring_test_context(policy: PolicyConfig) -> CheckContext {
let cache = Arc::new(HttpCache::new());
CheckContext {
cache: Arc::clone(&cache),
osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
deps_dev: Arc::new(deps_core::DepsDevClient::new(cache)),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy,
}
}
#[tokio::test]
async fn check_manifest_reaches_tier3_prefetch_wiring_without_network() {
let policy = PolicyConfig {
diagnostics: DiagnosticsConfig::new().with_vulnerabilities_enabled(false),
license_policy: LicensePolicyConfig::new().with_allow(vec!["0BSD".to_string()]),
..PolicyConfig::default()
};
let ctx = wiring_test_context(policy);
let ecosystem: Arc<dyn Ecosystem> =
Arc::new(TestTier3Ecosystem::returning(vec!["MIT".to_string()]));
let url = deps_core::test_util::test_uri("/test/manifest.toml");
let manifest_path = url.to_file_path().expect("file-scheme uri");
let result = check_manifest(&ecosystem, &manifest_path, &manifest_path, "unused", &ctx)
.await
.expect("check_manifest must not fail for this fixture");
assert!(
result
.findings
.iter()
.any(|f| f.category == Category::License
&& f.dependency_name.as_deref() == Some("dep-0")),
"check_manifest did not reach the tier-3 license prefetch wiring: {:?}",
result.findings
);
}
#[tokio::test]
async fn check_manifest_skips_tier3_prefetch_when_license_policy_is_empty() {
let policy = PolicyConfig {
diagnostics: DiagnosticsConfig::new().with_vulnerabilities_enabled(false),
..PolicyConfig::default()
};
assert!(
policy.license_policy.to_policy().is_empty(),
"test setup bug: this policy must have no license_policy configured"
);
let ctx = wiring_test_context(policy);
let ecosystem: Arc<dyn Ecosystem> = Arc::new(TestTier3Ecosystem::pending());
let url = deps_core::test_util::test_uri("/test/manifest.toml");
let manifest_path = url.to_file_path().expect("file-scheme uri");
tokio::time::timeout(
Duration::from_secs(5),
check_manifest(&ecosystem, &manifest_path, &manifest_path, "unused", &ctx),
)
.await
.expect("must not hang: an empty license_policy must skip the tier-3 fetch entirely")
.expect("check_manifest must not fail for this fixture");
}
#[tokio::test]
async fn analyze_manifest_scope_none_skips_tier3_prefetch_even_with_a_configured_policy() {
let policy = PolicyConfig {
diagnostics: DiagnosticsConfig::new().with_vulnerabilities_enabled(false),
license_policy: deps_core::policy_config::LicensePolicyConfig::new()
.with_allow(vec!["0BSD".to_string()]),
..PolicyConfig::default()
};
assert!(
!policy.license_policy.to_policy().is_empty(),
"test setup bug: this policy must have a configured license_policy"
);
let ctx = wiring_test_context(policy);
let ecosystem: Arc<dyn Ecosystem> = Arc::new(TestTier3Ecosystem::pending());
let url = deps_core::test_util::test_uri("/test/manifest.toml");
let manifest_path = url.to_file_path().expect("file-scheme uri");
tokio::time::timeout(
Duration::from_secs(5),
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::update_default(),
),
)
.await
.expect(
"must not hang: AnalysisScope::update_default() must skip the tier-3 fetch regardless of \
license_policy",
)
.expect("analyze_manifest must not fail for this fixture");
}
}
mod gossip_prefetch_wiring_regression {
use super::*;
use deps_core::Metadata;
use deps_core::ecosystem::BoxFuture;
use deps_core::ecosystem::private::Sealed;
use deps_core::policy_config::{DiagnosticsConfig, GossipConfig};
use deps_core::{
Dependency, Ecosystem, EcosystemId, PackageName, ParseResult, Registry, VersionReq,
};
use deps_engine::test_util::{StubRegistry, TestTier3Ecosystem};
use std::any::Any;
struct StubDep {
name: PackageName,
}
impl Dependency for StubDep {
fn name(&self) -> &PackageName {
&self.name
}
fn name_range(&self) -> deps_core::position::Range {
deps_core::position::Range::default()
}
fn version_requirement(&self) -> Option<&VersionReq> {
None
}
fn version_range(&self) -> Option<deps_core::position::Range> {
None
}
fn source(&self) -> deps_core::parser::DependencySource {
deps_core::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct StubParseResult {
dep: StubDep,
uri: url::Url,
}
impl ParseResult for StubParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
vec![&self.dep]
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
&self.uri
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct CoveredEcosystem;
impl Sealed for CoveredEcosystem {}
impl Ecosystem for CoveredEcosystem {
fn ecosystem_id(&self) -> EcosystemId {
EcosystemId::Cargo
}
fn display_name(&self) -> &'static str {
"test-gossip-covered"
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
uri: &'a url::Url,
) -> BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>> {
let uri = uri.clone();
Box::pin(async move {
Ok(Box::new(StubParseResult {
dep: StubDep {
name: PackageName::new("dep-0"),
},
uri,
}) as Box<dyn ParseResult>)
})
}
fn registry(&self) -> Arc<dyn Registry> {
Arc::new(StubRegistry)
}
fn formatter(&self) -> &dyn deps_core::lsp_helpers::EcosystemFormatter {
&deps_core::test_util::StubFormatter::DEFAULT
}
fn completion_insert_text(&self, _metadata: &dyn Metadata) -> Option<String> {
None
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn mock_gossip_context(policy: PolicyConfig, deps_dev_base_url: String) -> CheckContext {
let cache = Arc::new(HttpCache::new());
CheckContext {
cache: Arc::clone(&cache),
osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
deps_dev: Arc::new(deps_core::DepsDevClient::for_test(cache, deps_dev_base_url)),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy,
}
}
fn covered_ecosystem_manifest_path() -> std::path::PathBuf {
let url = deps_core::test_util::test_uri("/test/manifest.toml");
url.to_file_path().expect("file-scheme uri")
}
fn no_osv_policy() -> PolicyConfig {
PolicyConfig {
diagnostics: DiagnosticsConfig::new().with_vulnerabilities_enabled(false),
..PolicyConfig::default()
}
}
#[tokio::test]
async fn analyze_manifest_reaches_gossip_prefetch_for_a_covered_ecosystem_when_enabled() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/v3alpha/findingsbatch")
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
"requests": [{"packageKey": {"system": "CARGO"}}]
})))
.with_status(200)
.with_body(r#"{"responses":[],"nextPageToken":""}"#)
.create_async()
.await;
let policy = PolicyConfig {
gossip: GossipConfig::new().with_enabled(true),
..no_osv_policy()
};
let ctx = mock_gossip_context(policy, server.url());
let ecosystem: Arc<dyn Ecosystem> = Arc::new(CoveredEcosystem);
let manifest_path = covered_ecosystem_manifest_path();
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::update_default(),
)
.await
.expect("analyze_manifest must not fail for this fixture");
mock.assert_async().await;
}
#[tokio::test]
async fn analyze_manifest_skips_gossip_prefetch_when_offline() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/v3alpha/findingsbatch")
.with_status(200)
.with_body(r#"{"responses":[],"nextPageToken":""}"#)
.expect(0)
.create_async()
.await;
let policy = PolicyConfig {
gossip: GossipConfig::new().with_enabled(true),
network: deps_core::policy_config::NetworkConfig::new().with_offline(true),
..no_osv_policy()
};
let ctx = mock_gossip_context(policy, server.url());
let ecosystem: Arc<dyn Ecosystem> = Arc::new(CoveredEcosystem);
let manifest_path = covered_ecosystem_manifest_path();
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::update_default(),
)
.await
.expect("analyze_manifest must not fail for this fixture");
mock.assert_async().await;
}
#[tokio::test]
async fn analyze_manifest_skips_gossip_prefetch_for_an_uncovered_ecosystem() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/v3alpha/findingsbatch")
.with_status(200)
.with_body(r#"{"responses":[],"nextPageToken":""}"#)
.expect(0)
.create_async()
.await;
let policy = PolicyConfig {
gossip: GossipConfig::new().with_enabled(true),
..no_osv_policy()
};
let ctx = mock_gossip_context(policy, server.url());
let ecosystem: Arc<dyn Ecosystem> = Arc::new(TestTier3Ecosystem::returning(Vec::new()));
let manifest_path = covered_ecosystem_manifest_path();
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::update_default(),
)
.await
.expect("analyze_manifest must not fail for this fixture");
mock.assert_async().await;
}
#[tokio::test]
async fn analyze_manifest_skips_gossip_prefetch_when_gossip_disabled() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/v3alpha/findingsbatch")
.with_status(200)
.with_body(r#"{"responses":[],"nextPageToken":""}"#)
.expect(0)
.create_async()
.await;
let ctx = mock_gossip_context(no_osv_policy(), server.url());
let ecosystem: Arc<dyn Ecosystem> = Arc::new(CoveredEcosystem);
let manifest_path = covered_ecosystem_manifest_path();
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::update_default(),
)
.await
.expect("analyze_manifest must not fail for this fixture");
mock.assert_async().await;
}
#[tokio::test]
async fn analyze_manifest_skips_gossip_prefetch_under_security_only_scope() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/v3alpha/findingsbatch")
.with_status(200)
.with_body(r#"{"responses":[],"nextPageToken":""}"#)
.expect(0)
.create_async()
.await;
let policy = PolicyConfig {
gossip: GossipConfig::new().with_enabled(true),
..no_osv_policy()
};
let ctx = mock_gossip_context(policy, server.url());
let ecosystem: Arc<dyn Ecosystem> = Arc::new(CoveredEcosystem);
let manifest_path = covered_ecosystem_manifest_path();
deps_cli::analyze::analyze_manifest(
&ecosystem,
&manifest_path,
"unused",
&ctx,
deps_cli::analyze::AnalysisScope::vulnerabilities_only(),
)
.await
.expect("analyze_manifest must not fail for this fixture");
mock.assert_async().await;
}
}