use super::*;
use crate::policy_events::render_policy_events;
use crate::policy_registry::{PolicyId, Severity};
use crate::repository_facts::RepositoryFacts;
use crate::config_schema::{ForbiddenStringsOptions, RulesFileEntry};
use crate::scanner_test_support::{needle, run_isolated};
use crate::test_support::{fixture, git, remove};
use crate::wrapper_controls::no_controls;
use std::path::PathBuf;
const LANDED_CONTENT: &str = "landed content\n";
fn fixture_environment() -> Vec<(OsString, OsString)> {
let mut environment: Vec<(OsString, OsString)> = [
("PATH", "/usr/bin:/bin"),
("GIT_CONFIG_NOSYSTEM", "1"),
("GIT_CONFIG_GLOBAL", "/nonexistent-global-config"),
("GIT_AUTHOR_NAME", "Fixture"),
("GIT_AUTHOR_EMAIL", "fixture@example.invalid"),
("GIT_COMMITTER_NAME", "Fixture"),
("GIT_COMMITTER_EMAIL", "fixture@example.invalid"),
]
.iter()
.map(|(name, value)| {
return (OsString::from(name), OsString::from(value));
})
.collect();
environment.extend(crate::test_support::scratch_environment());
return environment;
}
fn landed_repository(name: &str, landed_bytes: &str) -> (PathBuf, String) {
return landed_file(name, "landed.txt", landed_bytes);
}
fn landed_file(name: &str, file: &str, landed_bytes: &str) -> (PathBuf, String) {
let root = fixture(name);
git(&root, &["init", "--quiet", "--initial-branch=main"]);
git(&root, &["config", "user.email", "cli-git@example.invalid"]);
git(&root, &["config", "user.name", "cli-git fixture"]);
std::fs::write(root.join("stable.txt"), "stable content\n").expect("stable file");
git(&root, &["add", "stable.txt"]);
git(&root, &["commit", "--quiet", "-m", "baseline"]);
std::fs::write(root.join(file), landed_bytes).expect("landed file");
git(&root, &["add", file]);
git(&root, &["commit", "--quiet", "-m", "landed"]);
let landed = String::from_utf8_lossy(&git(&root, &["rev-parse", "HEAD"]).stdout)
.trim()
.to_owned();
return (root, landed);
}
fn with_severity(policy: PolicyId, severity: Severity) -> PolicyConfig {
let mut config: PolicyConfig = PolicyConfig::defaults();
for setting in &mut config.settings {
if setting.id == policy {
setting.severity = severity;
}
}
return config;
}
fn request<'request>(
landed: &'request str,
global_prefix: &'request [OsString],
overlay: &'request [(OsString, OsString)],
config: PolicyConfig,
controls: Controls,
) -> PostCommitRequest<'request> {
return PostCommitRequest {
real_git: Path::new("/usr/bin/git"),
landed_oid: landed,
arguments: &[],
global_prefix,
overlay,
environment: overlay,
config,
controls,
};
}
fn prefix_for(root: &Path) -> Vec<OsString> {
return vec![OsString::from("-C"), root.as_os_str().to_os_string()];
}
#[test]
fn a_clean_landed_commit_passes_the_gate() {
let (root, landed) = landed_repository("pc-clean", LANDED_CONTENT);
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let outcome = run_post_commit_gate(&request(
&landed,
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Error),
no_controls(),
));
assert!(!outcome.blocked, "{outcome:?}");
assert!(outcome.events.is_empty(), "{outcome:?}");
assert_eq!(outcome.oid, landed);
remove(&root);
}
#[test]
fn a_blocked_landed_commit_ends_with_an_explicit_commit_landed_event() {
let (root, landed) = landed_repository("pc-block", "no final newline");
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let outcome = run_post_commit_gate(&request(
&landed,
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Error),
no_controls(),
));
assert!(outcome.blocked);
assert!(
matches!(outcome.events.first(), Some(PolicyEvent::Finding(_))),
"{outcome:?}"
);
assert_eq!(
outcome.events.last(),
Some(&PolicyEvent::CommitLanded {
oid: landed.clone()
})
);
let rendered = render_policy_events(0, outcome.events.as_slice());
assert!(
rendered.contains(&format!(
"\"message\":\"Commit {landed} remains local; post-commit gate blocked automatic backup.\""
)),
"{rendered}"
);
assert!(
rendered.contains("\"outcome\":\"post-commit-blocked\""),
"{rendered}"
);
remove(&root);
}
#[test]
fn a_warning_only_result_allows_the_backup() {
let (root, landed) = landed_repository("pc-warning", "no final newline");
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let outcome = run_post_commit_gate(&request(
&landed,
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Warn),
no_controls(),
));
assert!(!outcome.blocked, "{outcome:?}");
assert!(
matches!(outcome.events.as_slice(), [PolicyEvent::Finding(_)]),
"{outcome:?}"
);
remove(&root);
}
#[test]
fn the_full_lifecycle_escape_is_honored() {
let (root, landed) = landed_repository("pc-escape", "no final newline");
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let mut controls = no_controls();
controls.escaped = vec![PolicyId::FinalNewline];
let outcome = run_post_commit_gate(&request(
&landed,
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Error),
controls,
));
assert!(!outcome.blocked, "{outcome:?}");
assert!(outcome.events.is_empty(), "{outcome:?}");
remove(&root);
}
#[test]
fn an_engine_failure_retains_the_commit_and_reports_the_failure() {
let (root, _landed) = landed_repository("pc-failure", LANDED_CONTENT);
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let missing = "f".repeat(40);
let outcome = run_post_commit_gate(&request(
&missing,
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Error),
no_controls(),
));
assert!(outcome.blocked);
let kinds: Vec<&str> = outcome
.events
.iter()
.map(|event| {
return match event {
PolicyEvent::EngineFailure { .. } => "engine-failure",
PolicyEvent::CommitLanded { .. } => "commit-landed",
_ => "other",
};
})
.collect();
assert_eq!(kinds.last(), Some(&"commit-landed"), "{kinds:?}");
assert!(kinds.contains(&"engine-failure"), "{kinds:?}");
remove(&root);
}
#[test]
fn a_landed_name_that_is_not_an_object_name_is_an_engine_failure() {
let (root, _landed) = landed_repository("pc-bad-name", LANDED_CONTENT);
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let outcome = run_post_commit_gate(&request(
"not-an-oid",
&prefix,
&overlay,
with_severity(PolicyId::FinalNewline, Severity::Error),
no_controls(),
));
assert!(outcome.blocked);
assert!(matches!(
outcome.events.first(),
Some(PolicyEvent::EngineFailure {
code: EngineFailureCode::ContentUnavailable,
..
})
));
remove(&root);
}
#[test]
fn the_landed_delta_lists_only_the_changed_paths_as_added() {
let (root, landed) = landed_repository("pc-delta", LANDED_CONTENT);
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let mut facts = git_facts(Path::new("/usr/bin/git"), &prefix, &overlay);
let commit = parse_object_id(landed.as_bytes()).expect("object name");
let prepared = facts
.candidates(&CandidateRequest::Landed(commit))
.expect("landed candidates");
let paths: Vec<String> = prepared
.version
.candidates()
.iter()
.map(|candidate| {
return String::from_utf8_lossy(&candidate.path).into_owned();
})
.collect();
assert_eq!(paths, vec!["landed.txt".to_owned()], "{paths:?}");
remove(&root);
}
fn with_scanner(options: ForbiddenStringsOptions) -> PolicyConfig {
let mut config: PolicyConfig = with_severity(PolicyId::ForbiddenStrings, Severity::Error);
config.forbidden_strings = options;
return config;
}
fn reports_the_needle(outcome: &PostCommitGate) -> bool {
return outcome.events.iter().any(|event| {
return matches!(event, PolicyEvent::Finding(finding) if finding.policy == PolicyId::ForbiddenStrings);
});
}
fn landed_needle() -> (PathBuf, String) {
let (root, landed) = landed_repository("pc-scanner", format!("first\n{}\n", needle()).as_str());
std::fs::write(root.join("rules.txt"), format!("{}\n", needle())).expect("rules file");
return (root, landed);
}
#[test]
fn the_configured_rules_file_reaches_the_post_commit_scanner() {
run_isolated(
"post_commit::tests::the_configured_rules_file_reaches_the_post_commit_scanner",
"pc-config-rules",
configured_rules,
);
}
fn configured_rules(_work: &Path) {
let (root, landed) = landed_needle();
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let outcome = run_post_commit_gate(&request(
&landed,
&prefix,
&overlay,
with_scanner(ForbiddenStringsOptions {
builtin_rules: false,
rules_files: Some(vec![RulesFileEntry { path: String::from("rules.txt"), optional: false }]),
}),
no_controls(),
));
assert!(outcome.blocked, "{outcome:?}");
assert!(reports_the_needle(&outcome), "{outcome:?}");
remove(&root);
}
#[test]
fn the_rules_variable_reaches_the_post_commit_scanner() {
run_isolated(
"post_commit::tests::the_rules_variable_reaches_the_post_commit_scanner",
"pc-variable-rules",
variable_rules,
);
}
fn variable_rules(_work: &Path) {
let (root, landed) = landed_needle();
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let mut environment = fixture_environment();
environment.push((OsString::from("FORBIDDEN_STRINGS_RULES"), OsString::from("rules.txt")));
let mut asked = request(
&landed,
&prefix,
&overlay,
with_scanner(ForbiddenStringsOptions {
builtin_rules: false,
rules_files: None,
}),
no_controls(),
);
asked.environment = environment.as_slice();
let outcome = run_post_commit_gate(&asked);
assert!(outcome.blocked, "{outcome:?}");
assert!(reports_the_needle(&outcome), "{outcome:?}");
remove(&root);
}
fn with_markdown() -> PolicyConfig {
return with_severity(PolicyId::MarkdownAutofix, Severity::Warn);
}
#[test]
fn a_landed_markdown_file_is_checked_in_process() {
let stale: String = format!("\n", "b".repeat(64));
let (root, landed) = landed_file("pc-linter", "landed.md", stale.as_str());
std::fs::write(root.join(".lfsconfig"), b"[lfs]\n\turl = https://lfs.example.com/org/repo\n").expect("lfsconfig");
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let mut asked = request(&landed, &prefix, &overlay, with_markdown(), no_controls());
asked.environment = &[];
let outcome = run_post_commit_gate(&asked);
assert!(!outcome.blocked, "a warning does not block: {outcome:?}");
assert!(
outcome.events.iter().any(|event| {
return matches!(
event,
PolicyEvent::Finding(finding)
if finding.policy == PolicyId::MarkdownAutofix
&& finding.trigger == Trigger::PostCommit
&& finding.code == crate::policy_markdown::MARKDOWN_VIOLATION_CODE
);
}),
"{outcome:?}"
);
remove(&root);
}
#[test]
fn the_markdown_options_reach_the_post_commit_policy() {
let (root, landed) = landed_file("pc-markdown-options", "landed.md", "# T\n\nplain\n");
let prefix = prefix_for(&root);
let overlay = fixture_environment();
let mut broken: PolicyConfig = with_markdown();
broken.markdown_autofix.exclude = vec![String::from(UNCOMPILABLE_PATTERN)];
let unchecked = run_post_commit_gate(&request(&landed, &prefix, &overlay, broken, no_controls()));
assert!(unchecked.blocked, "{unchecked:?}");
assert!(
unchecked.events.iter().any(|event| {
return matches!(
event,
PolicyEvent::EngineFailure {
code: EngineFailureCode::PolicyIncomplete,
trigger: Some(Trigger::PostCommit),
policy: Some(PolicyId::MarkdownAutofix),
..
}
);
}),
"{unchecked:?}"
);
let mut config: PolicyConfig = with_markdown();
config.markdown_autofix.exclude = vec![String::from("landed.md")];
let excluded = run_post_commit_gate(&request(&landed, &prefix, &overlay, config, no_controls()));
assert!(!excluded.blocked, "{excluded:?}");
assert!(excluded.events.is_empty(), "{excluded:?}");
remove(&root);
}
const UNCOMPILABLE_PATTERN: &str = "a{b";