use std::path::PathBuf;
use super::*;
use crate::core::config::{GithubConfig, JiraConfig, RepositoryConfig};
fn config_with_repos(entries: &[(&str, Option<&str>)]) -> Config {
Config {
repositories: entries
.iter()
.map(|(path, name)| RepositoryConfig {
path: PathBuf::from(path),
name: name.map(str::to_string),
..Default::default()
})
.collect(),
..Default::default()
}
}
fn options(title: &str) -> DdManifestOptions {
DdManifestOptions {
title: title.to_string(),
..Default::default()
}
}
#[test]
fn maps_engagement_metadata() {
let cfg = config_with_repos(&[("/src/northwind-web", Some("Northwind Web"))]);
let opts = DdManifestOptions {
title: "Acme — Technical Due Diligence".to_string(),
analyst: Some("J. Reviewer".to_string()),
client: Some("Acme Holdings".to_string()),
gaps: vec!["Stage `dora` did not complete.".to_string()],
..Default::default()
};
let manifest = build_dd_manifest(&cfg, &opts).expect("builds");
assert_eq!(manifest.report.title, "Acme — Technical Due Diligence");
assert_eq!(manifest.report.analyst.as_deref(), Some("J. Reviewer"));
assert_eq!(manifest.report.client.as_deref(), Some("Acme Holdings"));
assert_eq!(manifest.report.gaps.len(), 1);
assert_eq!(
manifest.repositories,
vec![DdRepositoryEntry {
name: "Northwind Web".to_string(),
path: PathBuf::from("/src/northwind-web"),
authorship: None,
}]
);
let toml = manifest.to_toml().expect("serializes");
for absent in ["slug", "ref =", "metrics", "template"] {
assert!(!toml.contains(absent), "{absent} must not appear:\n{toml}");
}
}
#[test]
fn absent_metadata_is_omitted_not_blanked() {
let cfg = config_with_repos(&[("/src/a", Some("A"))]);
let toml = build_dd_manifest(&cfg, &options("T"))
.expect("builds")
.to_toml()
.expect("serializes");
assert!(!toml.contains("analyst"), "{toml}");
assert!(!toml.contains("client"), "{toml}");
assert!(!toml.contains("gaps"), "{toml}");
}
#[test]
fn names_fall_back_to_the_directory_basename() {
let cfg = config_with_repos(&[
("/src/northwind-web", None),
("/src/billing", Some(" ")),
("/src/ledger", Some("Ledger Service")),
]);
let manifest = build_dd_manifest(&cfg, &options("T")).expect("builds");
let names: Vec<&str> = manifest
.repositories
.iter()
.map(|r| r.name.as_str())
.collect();
assert_eq!(names, vec!["northwind-web", "billing", "Ledger Service"]);
}
#[test]
fn a_blank_configured_name_falls_back_the_same_way_for_every_caller() {
use std::path::Path;
let path = Path::new("/src/billing");
for configured in [None, Some(""), Some(" "), Some("\t")] {
assert_eq!(
super::repo_name(configured, path),
"billing",
"a blank configured name must fall back to the basename: {configured:?}"
);
}
assert_eq!(
super::repo_name(Some(" Ledger Service "), path),
"Ledger Service",
"a configured name is trimmed, not taken verbatim"
);
}
#[test]
fn repositories_keep_config_order() {
let cfg = config_with_repos(&[("/src/c", None), ("/src/a", None), ("/src/b", None)]);
let manifest = build_dd_manifest(&cfg, &options("T")).expect("builds");
let names: Vec<&str> = manifest
.repositories
.iter()
.map(|r| r.name.as_str())
.collect();
assert_eq!(names, vec!["c", "a", "b"], "config order, never sorted");
}
#[test]
fn two_builds_are_byte_identical() {
let cfg = config_with_repos(&[
("/src/northwind-web", Some("Northwind Web")),
("/src/billing", None),
]);
let opts = DdManifestOptions {
title: "Acme — Technical Due Diligence".to_string(),
analyst: Some("J. Reviewer".to_string()),
client: None,
gaps: vec!["a".to_string(), "b".to_string()],
..Default::default()
};
let first = build_dd_manifest(&cfg, &opts).expect("builds");
let second = build_dd_manifest(&cfg, &opts).expect("builds");
assert_eq!(first, second);
assert_eq!(
first.to_toml().expect("serializes"),
second.to_toml().expect("serializes"),
"same input, same bytes"
);
}
#[test]
fn configured_token_never_reaches_the_manifest() {
let token = "ghp_TESTONLYtoken0123456789abcdef"; let mut cfg = config_with_repos(&[("/src/a", Some("A"))]);
cfg.github = Some(GithubConfig {
token: Some(token.to_string()),
..Default::default()
});
let opts = DdManifestOptions {
title: format!("Acme ({token})"),
analyst: Some(format!("analyst {token}")),
client: Some(format!("client {token}")),
gaps: vec![format!(
"Stage `collect` did not complete: GET /orgs/acme/repos returned 401 for {token}"
)],
..Default::default()
};
let toml = build_dd_manifest(&cfg, &opts)
.expect("builds")
.to_toml()
.expect("serializes");
assert!(
!toml.contains(token),
"token leaked into the manifest:\n{toml}"
);
assert!(
toml.contains("[REDACTED]"),
"the value must be replaced, not merely dropped:\n{toml}"
);
assert!(toml.contains("Stage `collect` did not complete"), "{toml}");
}
const EXCERPT_BOUNDARY: usize = crate::audit::MAX_REASON_CHARS;
fn manifest_from_stage_failure(cfg: &Config, message: String) -> String {
let mut stats = crate::audit::AuditSweepStats::default();
stats.record(
crate::audit::SweepStage::Collect,
std::time::Instant::now(),
Err(anyhow::anyhow!(message)),
);
let secrets = configured_secrets(cfg);
let opts = DdManifestOptions {
title: "Acme — Technical Due Diligence".to_string(),
gaps: crate::audit::sweep_gap_lines(&stats, &secrets),
..Default::default()
};
build_dd_manifest(cfg, &opts)
.expect("builds")
.to_toml()
.expect("serializes")
}
fn config_with_tokens(github: &str, jira: &str) -> Config {
let mut cfg = config_with_repos(&[("/src/a", Some("A"))]);
cfg.github = Some(GithubConfig {
token: Some(github.to_string()),
..Default::default()
});
cfg.jira = Some(JiraConfig {
token: Some(jira.to_string()),
..Default::default()
});
cfg
}
const SCRUB_MIN_CHARS: usize = 8;
fn unscrubbable_fragment(token: &str) -> String {
token.chars().take(SCRUB_MIN_CHARS).collect()
}
#[test]
fn scrub_min_chars_is_what_the_fragment_assertions_assume() {
let needle: String = "abcdefghijklmnop".chars().take(SCRUB_MIN_CHARS).collect();
assert_eq!(
trusty_common::credentials::scrub_secrets(&format!("value {needle} here"), &[&needle]),
"value [REDACTED] here",
"a needle of exactly SCRUB_MIN_CHARS must be scrubbable"
);
let shorter: String = needle.chars().take(SCRUB_MIN_CHARS - 1).collect();
let text = format!("value {shorter} here");
assert_eq!(
trusty_common::credentials::scrub_secrets(&text, &[&shorter]),
text,
"one character shorter must be refused"
);
}
fn assert_no_fragment(toml: &str, token: &str, label: &str) {
assert!(!toml.contains(token), "{label}: full token leaked:\n{toml}");
assert!(
!toml.contains(&unscrubbable_fragment(token)),
"{label}: a prefix fragment survived:\n{toml}"
);
}
#[test]
fn a_token_straddling_the_excerpt_boundary_leaves_no_fragment() {
let token = "ghp_STRADDLE0123456789abcdefGHIJKLMNOPQR"; let lead = "GET /orgs/acme/repos returned 401 for ";
let pad = "x".repeat(EXCERPT_BOUNDARY - 10 - lead.chars().count());
let message = format!(
"{lead}{pad}{token}: bad credentials. The remainder of this cause chain exists to \
carry the message past 200 characters so the excerpt is guaranteed to truncate."
);
assert!(message.chars().count() > 200, "the excerpt must truncate");
let cfg = config_with_tokens(token, "unused-jira-token-000");
let toml = manifest_from_stage_failure(&cfg, message);
assert_no_fragment(&toml, token, "straddling token");
assert!(toml.contains("[REDACTED]"), "{toml}");
assert!(toml.contains("not assessed"), "{toml}");
}
#[test]
fn a_token_beyond_the_boundary_survives_the_shift_scrubbing_causes() {
let early = "ghp_EARLY0123456789abcdefGHIJKLMNOPQRSTUV"; let late = "jira_LATE0123456789abcdefGHIJKLMNOPQRSTUV"; let message = format!(
"GET /rest/api/3/search failed for {early}; retried with {} and got 401 for {late}, \
then gave up.",
"y".repeat(EXCERPT_BOUNDARY)
);
let cfg = config_with_tokens(early, late);
let toml = manifest_from_stage_failure(&cfg, message);
assert_no_fragment(&toml, early, "early token");
assert_no_fragment(&toml, late, "late token");
}
#[test]
fn redaction_before_truncation_keeps_the_excerpt_within_budget() {
let token = "ghp_BUDGET0123456789abcdefGHIJKLMNOPQRST"; let message = std::iter::repeat_n(format!("401 for {token}"), 12)
.collect::<Vec<_>>()
.join(" and ");
let cfg = config_with_tokens(token, "unused-jira-token-000");
let mut stats = crate::audit::AuditSweepStats::default();
stats.record(
crate::audit::SweepStage::Collect,
std::time::Instant::now(),
Err(anyhow::anyhow!(message)),
);
let line = crate::audit::sweep_gap_lines(&stats, &configured_secrets(&cfg)).remove(0);
assert!(line.contains('…'), "must still truncate: {line}");
assert!(
line.chars().count() < 400,
"one verbose error must not dominate the Gaps section ({} chars)",
line.chars().count()
);
assert!(!line.contains(&unscrubbable_fragment(token)), "{line}");
}
#[test]
fn empty_config_is_an_actionable_error() {
let err = build_dd_manifest(&Config::default(), &options("T")).expect_err("no repositories");
assert!(matches!(err, DdManifestError::NoRepositories));
assert!(err.to_string().contains("config.yaml"), "{err}");
}
#[test]
fn round_trips_through_the_review_schema() {
let cfg = config_with_repos(&[("/src/a", Some("A")), ("/src/b", None)]);
let opts = DdManifestOptions {
title: "T".to_string(),
analyst: Some("An".to_string()),
client: None,
gaps: vec!["one gap".to_string()],
ticketing: Some(PathBuf::from("ticketing.json")),
..Default::default()
};
let toml = build_dd_manifest(&cfg, &opts)
.expect("builds")
.to_toml()
.expect("serializes");
let parsed: toml::Value = toml::from_str(&toml).expect("valid TOML");
assert_eq!(parsed["report"]["title"].as_str(), Some("T"));
assert_eq!(parsed["report"]["gaps"].as_array().map(Vec::len), Some(1));
assert_eq!(
parsed["report"]["ticketing"].as_str(),
Some("ticketing.json")
);
let repos = parsed["repositories"].as_array().expect("array of tables");
assert_eq!(repos.len(), 2);
for repo in repos {
assert!(repo.get("path").is_some(), "local source key required");
assert!(repo.get("remote").is_none(), "exactly one source key");
assert!(repo.get("name").is_some());
}
}
#[test]
fn an_absent_ticketing_artifact_omits_the_key() {
let cfg = config_with_repos(&[("/src/a", Some("A"))]);
let toml = build_dd_manifest(&cfg, &options("T"))
.expect("builds")
.to_toml()
.expect("serializes");
let parsed: toml::Value = toml::from_str(&toml).expect("valid TOML");
assert!(
parsed["report"].get("ticketing").is_none(),
"an absent artifact must omit the key entirely: {toml}"
);
}
#[test]
fn relative_paths_are_anchored_to_base_dir() {
let cfg = config_with_repos(&[("./small-repo", Some("Small")), ("/abs/other", Some("Abs"))]);
let opts = DdManifestOptions {
title: "T".to_string(),
base_dir: PathBuf::from("/work/audit"),
..Default::default()
};
let manifest = build_dd_manifest(&cfg, &opts).expect("builds");
assert_eq!(
manifest.repositories[0].path,
PathBuf::from("/work/audit/./small-repo"),
"a relative path is anchored, never passed through"
);
assert_eq!(
manifest.repositories[1].path,
PathBuf::from("/abs/other"),
"an absolute path is untouched"
);
let bare = build_dd_manifest(&cfg, &options("T")).expect("builds");
assert_eq!(bare.repositories[0].path, PathBuf::from("./small-repo"));
}