use std::path::Path;
use crate::config::definition::{
AuditDefinition, AuditEntry, AuditStrategy, LineAction, LineRule, RegexTarget,
};
use crate::diff::entry::{DiffEntry, DiffType};
use super::engine::AuditEngine;
use super::result::AuditStatus;
use super::strategy;
fn make_diff(path: &str, diff_type: DiffType, before: Option<&str>, after: Option<&str>) -> DiffEntry {
use sha2::{Digest, Sha256};
let before_sha256 = before.map(|t| hex::encode(Sha256::digest(t.as_bytes())));
let after_sha256 = after.map(|t| hex::encode(Sha256::digest(t.as_bytes())));
let stats = match (diff_type, before, after) {
(DiffType::Modified, Some(b), Some(a)) =>
Some(crate::diff::entry::DiffStats::compute(b, a)),
_ => None,
};
DiffEntry {
path: path.to_string(),
diff_type,
is_dir: false,
before_text: before.map(String::from),
after_text: after.map(String::from),
is_binary: false,
before_size: before.map(|t| t.len() as u64),
after_size: after.map(|t| t.len() as u64),
before_sha256,
after_sha256,
stats,
error_detail: None,
}
}
fn make_entry(path: &str, diff_type: DiffType, strategy: AuditStrategy) -> AuditEntry {
AuditEntry {
path: path.to_string(),
diff_type,
reason: "test reason".to_string(),
strategy,
enabled: true,
ticket: None,
approved_by: None,
approved_at: None,
expires_at: None,
note: None,
created_at: None,
updated_at: None,
}
}
fn make_def(entries: Vec<AuditEntry>) -> AuditDefinition {
let mut def = AuditDefinition::new_empty();
def.entries = entries;
def
}
#[test]
fn strategy_none_always_ok() {
let diff = make_diff("f.txt", DiffType::Added, None, Some("x"));
assert!(strategy::evaluate(&AuditStrategy::None, &diff).is_ok());
}
#[test]
fn checksum_matches() {
use sha2::{Digest, Sha256};
let content = "hello";
let sha = hex::encode(Sha256::digest(content.as_bytes()));
let diff = make_diff("f", DiffType::Modified, Some("old"), Some(content));
let strat = AuditStrategy::Checksum { expected_sha256: sha };
assert!(strategy::evaluate(&strat, &diff).is_ok());
}
#[test]
fn checksum_mismatch_fails() {
let diff = make_diff("f", DiffType::Modified, Some("old"), Some("hello"));
let strat = AuditStrategy::Checksum {
expected_sha256: "a".repeat(64),
};
assert!(strategy::evaluate(&strat, &diff).is_err());
}
#[test]
fn linematch_added_line_ok() {
let diff = make_diff("cfg.toml", DiffType::Modified,
Some("port = 80\n"),
Some("port = 8080\n"));
let strat = AuditStrategy::LineMatch {
rules: vec![
LineRule { action: LineAction::Removed, line: "port = 80".to_string() },
LineRule { action: LineAction::Added, line: "port = 8080".to_string() },
],
};
assert!(strategy::evaluate(&strat, &diff).is_ok());
}
#[test]
fn linematch_missing_line_fails() {
let diff = make_diff("cfg.toml", DiffType::Modified,
Some("port = 80\n"),
Some("port = 9999\n"));
let strat = AuditStrategy::LineMatch {
rules: vec![
LineRule { action: LineAction::Added, line: "port = 8080".to_string() },
],
};
let result = strategy::evaluate(&strat, &diff);
assert!(result.is_err());
assert!(result.unwrap_err().contains("port = 8080"));
}
#[test]
fn regex_added_lines_match() {
let diff = make_diff("ver.txt", DiffType::Modified,
Some("version = 1.0\n"),
Some("version = 2.0\n"));
let strat = AuditStrategy::Regex {
pattern: r"^version = \d+\.\d+$".to_string(),
target: RegexTarget::AddedLines,
};
assert!(strategy::evaluate(&strat, &diff).is_ok());
}
#[test]
fn regex_invalid_pattern_errors() {
let diff = make_diff("f", DiffType::Modified, Some("a\n"), Some("b\n"));
let strat = AuditStrategy::Regex {
pattern: "[invalid".to_string(),
target: RegexTarget::AddedLines,
};
assert!(strategy::evaluate(&strat, &diff).is_err());
}
#[test]
fn exact_match_ok() {
let expected = "exact content\n";
let diff = make_diff("f", DiffType::Modified, Some("old\n"), Some(expected));
let strat = AuditStrategy::Exact { expected_content: expected.to_string() };
assert!(strategy::evaluate(&strat, &diff).is_ok());
}
#[test]
fn exact_mismatch_fails() {
let diff = make_diff("f", DiffType::Modified, Some("old\n"), Some("actual\n"));
let strat = AuditStrategy::Exact { expected_content: "expected\n".to_string() };
assert!(strategy::evaluate(&strat, &diff).is_err());
}
#[test]
fn pending_when_no_entry() {
let diff = make_diff("unknown.txt", DiffType::Added, None, Some("x"));
let def = make_def(vec![]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Pending);
}
#[test]
fn ok_when_diff_type_matches_and_strategy_passes() {
let diff = make_diff("f.txt", DiffType::Added, None, Some("x"));
let entry = make_entry("f.txt", DiffType::Added, AuditStrategy::None);
let def = make_def(vec![entry]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Ok);
}
#[test]
fn failed_when_diff_type_mismatch() {
let diff = make_diff("f.txt", DiffType::Modified, Some("a\n"), Some("b\n"));
let entry = make_entry("f.txt", DiffType::Added, AuditStrategy::None);
let def = make_def(vec![entry]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Failed);
}
#[test]
fn ignored_when_entry_disabled() {
let diff = make_diff("f.txt", DiffType::Added, None, Some("x"));
let mut entry = make_entry("f.txt", DiffType::Added, AuditStrategy::None);
entry.enabled = false;
let def = make_def(vec![entry]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Ignored);
}
#[test]
fn error_for_unreadable_diff() {
let mut diff = make_diff("f.txt", DiffType::Unreadable, None, None);
diff.error_detail = Some("Permission denied".into());
let def = make_def(vec![]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Error);
}
#[test]
fn summary_counts_are_correct() {
let diffs = vec![
make_diff("ok.txt", DiffType::Added, None, Some("x")),
make_diff("pending.txt", DiffType::Added, None, Some("y")),
make_diff("failed.txt", DiffType::Modified, Some("a\n"), Some("b\n")),
];
let mut def = make_def(vec![
make_entry("ok.txt", DiffType::Added, AuditStrategy::None),
make_entry("failed.txt", DiffType::Added, AuditStrategy::None), ]);
let result = AuditEngine::evaluate(&diffs, &def);
assert_eq!(result.summary.ok, 1);
assert_eq!(result.summary.pending, 1);
assert_eq!(result.summary.failed, 1);
}
#[test]
fn empty_reason_is_pending_not_ok() {
let diff = make_diff("f.txt", DiffType::Added, None, Some("x"));
let mut entry = make_entry("f.txt", DiffType::Added, AuditStrategy::None);
entry.reason = " ".to_string(); let def = make_def(vec![entry]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Pending,
"empty reason must produce Pending, not OK");
}
#[test]
fn ok_requires_non_empty_reason() {
let diff = make_diff("f.txt", DiffType::Added, None, Some("x"));
let mut entry = make_entry("f.txt", DiffType::Added, AuditStrategy::None);
entry.reason = "Intentionally added".to_string();
let def = make_def(vec![entry]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Ok);
}
#[test]
fn unchanged_is_auto_ok_without_entry() {
let diff = make_diff("same.txt", DiffType::Unchanged, Some("x"), Some("x"));
let def = make_def(vec![]);
let result = AuditEngine::evaluate(&[diff], &def);
assert_eq!(result.results[0].status, AuditStatus::Ok,
"Unchanged entries should be auto-OK even without a rule");
}