use std::path::PathBuf;
use chrono::NaiveDate;
use proptest::prelude::*;
use secunit_core::model::{
Cadence, Control, EvidenceRequirement, InventoryEntry, ResolvedSystem, RunStatus, Schedule,
ScheduleEntry, ScheduleInsert, ScheduleSkip, Scope, State, StateEntry, Weekday,
};
use secunit_core::registry::{
loader,
resolver::{self, DueReason, DueResolution},
};
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("testdata/orgs")
.join(name)
.canonicalize()
.unwrap()
}
fn d(s: &str) -> NaiveDate {
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
}
fn skeleton(id: &str, cadence: Cadence) -> Control {
Control {
id: id.into(),
title: id.into(),
policy: "p.md".into(),
nist: vec![],
owner: "cto".into(),
cadence,
weekday: None,
due_by: None,
skill: id.into(),
skill_args: None,
scope: None,
evidence_required: vec![],
remediation_thresholds: Default::default(),
outputs: None,
references: vec![],
}
}
#[test]
fn weekly_default_monday_from_midweek() {
let c = skeleton("c", Cadence::Weekly);
let next = resolver::next_due(&c, &Default::default(), None, d("2026-05-06"), None);
assert_eq!(next, Some(d("2026-05-11"))); }
#[test]
fn weekly_with_weekday_override_thursday() {
let mut c = skeleton("c", Cadence::Weekly);
c.weekday = Some(Weekday::Thursday);
let next = resolver::next_due(&c, &Default::default(), None, d("2026-05-06"), None);
assert_eq!(next, Some(d("2026-05-07"))); }
#[test]
fn quarterly_anchors_to_first_of_quarter_business_day() {
let c = skeleton("c", Cadence::Quarterly);
let next = resolver::next_due(&c, &Default::default(), None, d("2026-04-01"), None);
assert_eq!(next, Some(d("2026-04-01")));
let next = resolver::next_due(&c, &Default::default(), None, d("2027-01-01"), None);
assert_eq!(next, Some(d("2027-01-01")));
}
#[test]
fn annual_with_due_by_december_31() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("december-31".into());
let next = resolver::next_due(&c, &Default::default(), None, d("2026-05-01"), None);
assert_eq!(next, Some(d("2026-12-31")));
let next = resolver::next_due(&c, &Default::default(), None, d("2027-01-15"), None);
assert_eq!(next, Some(d("2027-12-31")));
}
#[test]
fn overdue_after_grace_window() {
let c = skeleton("c", Cadence::Weekly);
let due = d("2026-05-04");
assert!(!resolver::is_overdue(&c, due, d("2026-05-04")));
assert!(!resolver::is_overdue(&c, due, d("2026-05-07"))); assert!(resolver::is_overdue(&c, due, d("2026-05-08")));
}
#[test]
fn resolve_scope_filters_by_tag_and_lifecycle() {
let (reg, report) = loader::load(&fixture("multi-system"));
assert!(report.is_clean(), "{:?}", report.errors);
let ctrl = ®.controls["sca-weekly-dependency-scan"];
let resolved = resolver::resolve_scope(ctrl, ®.inventory, d("2026-05-04"));
let names: Vec<_> = resolved.iter().map(|r| r.name.clone()).collect();
assert!(names.contains(&"app-api".to_string()));
assert!(names.contains(&"app-ui".to_string()));
assert!(names.contains(&"data-pipeline".to_string())); assert!(!names.contains(&"marketing-site".to_string())); }
#[test]
fn resolve_scope_drops_retired_entries() {
let (reg, _) = loader::load(&fixture("multi-system"));
let ctrl = ®.controls["sca-weekly-dependency-scan"];
let resolved = resolver::resolve_scope(ctrl, ®.inventory, d("2026-10-01"));
let names: Vec<_> = resolved.iter().map(|r| r.name.clone()).collect();
assert!(!names.contains(&"data-pipeline".to_string())); }
#[test]
fn inline_scope_passes_through() {
let (reg, _) = loader::load(&fixture("multi-system"));
let ctrl = ®.controls["cp-annual-bcp-test"];
let resolved = resolver::resolve_scope(ctrl, ®.inventory, d("2026-08-01"));
assert_eq!(
resolved.iter().map(|r| r.name.as_str()).collect::<Vec<_>>(),
vec!["prod", "app-api"]
);
}
#[test]
fn inventory_entry_active_window() {
let entry = InventoryEntry {
name: "x".into(),
tags: vec![],
in_scope_since: Some(d("2025-06-01")),
retired_on: Some(d("2026-09-01")),
aliases: vec![],
excludes: vec![],
extras: Default::default(),
};
assert!(!entry.is_active_on(d("2025-05-31")));
assert!(entry.is_active_on(d("2025-06-01")));
assert!(entry.is_active_on(d("2026-08-31")));
assert!(!entry.is_active_on(d("2026-09-01"))); }
fn entry_for(control_id: &str) -> ScheduleEntry {
ScheduleEntry {
control_id: control_id.into(),
due: None,
weekday: None,
note: None,
reason: None,
skip: None,
insert: None,
}
}
#[test]
fn reason_is_cadence_when_no_overrides_apply() {
let c = skeleton("c", Cadence::Weekly);
let r = resolver::next_due_with_reason(&c, &Default::default(), None, d("2026-05-06"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-05-11"),
reason: DueReason::Cadence,
note: None,
})
);
}
#[test]
fn reason_is_override_due_when_pinned_by_date() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("december-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-06-30")),
note: Some("auditor-on-site".into()),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-05-01"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-06-30"),
reason: DueReason::OverrideDue,
note: Some("auditor-on-site".into()),
})
);
}
#[test]
fn dated_override_can_defer_past_the_cadence_date() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("july-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2027-06-01")),
note: Some("certs valid to 2027-07-08".into()),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-07-27"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2027-06-01"),
reason: DueReason::OverrideDue,
note: Some("certs valid to 2027-07-08".into()),
})
);
}
#[test]
fn reason_is_override_insert_when_a_one_off_lands_first() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("december-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
insert: Some(ScheduleInsert {
run_at: d("2026-05-20"),
reason: Some("re-test after remediation".into()),
}),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-05-01"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-05-20"),
reason: DueReason::OverrideInsert,
note: Some("re-test after remediation".into()),
})
);
}
#[test]
fn reason_is_override_weekday_when_weekly_override_set() {
let c = skeleton("c", Cadence::Weekly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
weekday: Some(Weekday::Thursday),
note: Some("staff meeting on Mondays".into()),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-05-04"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-05-07"),
reason: DueReason::OverrideWeekday,
note: Some("staff meeting on Mondays".into()),
})
);
}
#[test]
fn skip_drops_cadence_firing_and_returns_next_insert() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![
ScheduleEntry {
skip: Some(ScheduleSkip {
quarter: Some("2026-q2".into()),
year: None,
reason: Some("frozen during migration".into()),
}),
..entry_for("c")
},
ScheduleEntry {
insert: Some(ScheduleInsert {
run_at: d("2026-08-15"),
reason: Some("post-migration".into()),
}),
..entry_for("c")
},
],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-05-01"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-08-15"),
reason: DueReason::OverrideInsert,
note: Some("post-migration".into()),
})
);
}
#[test]
fn insert_beats_cadence_on_same_date() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
insert: Some(ScheduleInsert {
run_at: d("2026-04-01"),
reason: Some("ride along with quarterly".into()),
}),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-04-01"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-04-01"),
reason: DueReason::OverrideInsert,
note: Some("ride along with quarterly".into()),
})
);
}
#[test]
fn next_due_facade_matches_with_reason() {
let c = skeleton("c", Cadence::Weekly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
insert: Some(ScheduleInsert {
run_at: d("2026-05-20"),
reason: None,
}),
..entry_for("c")
}],
};
let today = d("2026-05-04");
assert_eq!(
resolver::next_due(&c, &schedule, None, today, None),
resolver::next_due_with_reason(&c, &schedule, None, today, None).map(|r| r.date)
);
}
fn state_due(next_due: &str) -> StateEntry {
StateEntry {
last_run_id: None,
last_run_path: None,
last_run_at: None,
last_status: RunStatus::Complete,
next_due: Some(d(next_due)),
}
}
#[test]
fn stale_cached_due_date_is_not_rolled_forward() {
let w = skeleton("w", Cadence::Weekly);
let st = state_due("2026-05-04");
let next = resolver::next_due(&w, &Default::default(), Some(&st), d("2026-06-01"), None);
assert_eq!(next, Some(d("2026-05-04")));
assert!(resolver::is_overdue(&w, next.unwrap(), d("2026-06-01")));
let m = skeleton("m", Cadence::Monthly);
let st = state_due("2026-05-01");
let next = resolver::next_due(&m, &Default::default(), Some(&st), d("2026-06-15"), None);
assert_eq!(next, Some(d("2026-05-01")));
assert!(resolver::is_overdue(&m, next.unwrap(), d("2026-06-15")));
let st = state_due("2026-05-04");
let next = resolver::next_due(&w, &Default::default(), Some(&st), d("2026-05-06"), None);
assert_eq!(next, Some(d("2026-05-04")));
assert!(!resolver::is_overdue(&w, next.unwrap(), d("2026-05-06")));
}
#[test]
fn skip_covering_the_missed_date_forgives_the_miss() {
let w = skeleton("w", Cadence::Weekly);
let mut entry = entry_for("w");
entry.skip = Some(ScheduleSkip {
quarter: Some("2026-q2".into()),
year: None,
reason: Some("audit prep".into()),
});
let schedule = Schedule {
overrides: vec![entry],
};
let st = state_due("2026-05-04");
let next = resolver::next_due(&w, &schedule, Some(&st), d("2026-07-06"), None);
assert_eq!(next, Some(d("2026-07-06")), "2026-07-06 is a Monday");
}
#[test]
fn dated_override_survives_a_stale_next_due() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("july-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2027-06-01")),
..entry_for("c")
}],
};
let state = state_due("2026-07-31");
let r = resolver::next_due_with_reason(&c, &schedule, Some(&state), d("2026-09-01"), None);
assert_eq!(r.unwrap().date, d("2027-06-01"));
}
#[test]
fn missed_dated_override_is_reported_as_the_missed_obligation() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("july-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2027-06-01")),
note: Some("certs valid to 2027-07-08".into()),
..entry_for("c")
}],
};
let state = state_due("2026-07-31");
let r =
resolver::next_due_with_reason(&c, &schedule, Some(&state), d("2027-08-01"), None).unwrap();
assert_eq!(r.date, d("2027-06-01"));
assert_eq!(r.reason, DueReason::OverrideDue);
assert_eq!(r.note, Some("certs valid to 2027-07-08".into()));
assert!(resolver::is_overdue(&c, r.date, d("2027-08-01")));
}
#[test]
fn far_pin_on_a_monthly_control_leaves_nearer_firings_alone() {
let c = skeleton("c", Cadence::Monthly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-12-04")),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-08-03"), None).unwrap();
assert_eq!(r.date, d("2026-08-03"));
assert_eq!(r.reason, DueReason::Cadence);
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-12-01"), None).unwrap();
assert_eq!(r.date, d("2026-12-04"));
assert_eq!(r.reason, DueReason::OverrideDue);
}
#[test]
fn far_pin_on_a_weekly_control_leaves_nearer_firings_alone() {
let c = skeleton("c", Cadence::Weekly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-12-25")),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-08-04"), None).unwrap();
assert_eq!(r.date, d("2026-08-10"));
assert_eq!(r.reason, DueReason::Cadence);
}
#[test]
fn annual_pin_can_postpone_within_the_year() {
let mut c = skeleton("c", Cadence::Annual);
c.due_by = Some("july-31".into());
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-08-15")),
..entry_for("c")
}],
};
let r = resolver::next_due_with_reason(&c, &schedule, None, d("2026-07-27"), None).unwrap();
assert_eq!(r.date, d("2026-08-15"));
assert_eq!(r.reason, DueReason::OverrideDue);
}
#[test]
fn future_pin_does_not_forgive_a_miss_of_an_earlier_firing() {
let c = skeleton("c", Cadence::Monthly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-12-04")),
..entry_for("c")
}],
};
let state = state_due("2026-07-01");
let r =
resolver::next_due_with_reason(&c, &schedule, Some(&state), d("2026-07-27"), None).unwrap();
assert_eq!(r.date, d("2026-07-01"));
assert_eq!(r.reason, DueReason::Cadence);
assert!(resolver::is_overdue(&c, r.date, d("2026-07-27")));
}
#[test]
fn stale_miss_is_not_rebranded_by_an_unrelated_later_pin() {
let c = skeleton("c", Cadence::Monthly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-11-02")),
..entry_for("c")
}],
};
let state = state_due("2026-06-01");
let r =
resolver::next_due_with_reason(&c, &schedule, Some(&state), d("2026-11-06"), None).unwrap();
assert_eq!(r.date, d("2026-06-01"));
assert_eq!(r.reason, DueReason::Cadence);
assert!(resolver::is_overdue(&c, r.date, d("2026-11-06")));
}
proptest! {
#[test]
fn weekly_next_due_monotonic_in_today(
a in 0i64..3650,
b in 0i64..3650,
) {
let c = skeleton("c", Cadence::Weekly);
let base = d("2025-01-06"); let day_a = base + chrono::Duration::days(a);
let day_b = base + chrono::Duration::days(b);
let na = resolver::next_due(&c, &Default::default(), None, day_a, None);
let nb = resolver::next_due(&c, &Default::default(), None, day_b, None);
if let (Some(na), Some(nb)) = (na, nb) {
prop_assert!(na >= day_a);
prop_assert!(nb >= day_b);
if day_a <= day_b {
prop_assert!(nb >= na || nb >= day_a);
}
}
}
}
#[allow(dead_code)]
fn _keep_imports(_: &ResolvedSystem, _: &EvidenceRequirement, _: &State, _: &Scope) {}
#[test]
fn cached_future_next_due_is_honoured_for_month_anchored_cadences() {
for cadence in [Cadence::Monthly, Cadence::Quarterly, Cadence::SemiAnnual] {
let c = skeleton("c", cadence);
let st = state_due("2026-10-01");
let n = resolver::next_due(&c, &Default::default(), Some(&st), d("2026-08-15"), None);
assert_eq!(n, Some(d("2026-10-01")), "cadence {cadence:?}");
}
}
#[test]
fn insert_still_beats_a_cached_next_due() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
insert: Some(ScheduleInsert {
run_at: d("2026-09-01"),
reason: Some("extra scan".into()),
}),
..entry_for("c")
}],
};
let st = state_due("2026-10-01");
let r = resolver::next_due_with_reason(&c, &schedule, Some(&st), d("2026-08-15"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-09-01"),
reason: DueReason::OverrideInsert,
note: Some("extra scan".into()),
})
);
}
#[test]
fn next_firing_after_rolls_to_the_next_period() {
let cases = [
(Cadence::Weekly, "2026-07-27"), (Cadence::Monthly, "2026-08-03"), (Cadence::Quarterly, "2026-10-01"), (Cadence::SemiAnnual, "2027-01-01"), ];
for (cadence, want) in cases {
let c = skeleton("c", cadence);
let n = resolver::next_firing_after(&c, &Default::default(), d("2026-07-21"), None);
assert_eq!(n, Some(d(want)), "cadence {cadence:?}");
}
let mut a = skeleton("a", Cadence::Annual);
a.due_by = Some("december-31".into());
let n = resolver::next_firing_after(&a, &Default::default(), d("2026-12-31"), None);
assert_eq!(n, Some(d("2027-12-31")));
}
#[test]
fn next_firing_after_rolls_past_skipped_windows() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
skip: Some(ScheduleSkip {
quarter: Some("2026-q4".into()),
year: None,
reason: Some("frozen".into()),
}),
..entry_for("c")
}],
};
let n = resolver::next_firing_after(&c, &schedule, d("2026-07-21"), None);
assert_eq!(n, Some(d("2027-01-01")));
}
#[test]
fn pin_earlier_than_a_cached_next_due_wins() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-09-15")),
note: Some("board asked for it early".into()),
..entry_for("c")
}],
};
let st = state_due("2026-10-01");
let r = resolver::next_due_with_reason(&c, &schedule, Some(&st), d("2026-08-15"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-09-15"),
reason: DueReason::OverrideDue,
note: Some("board asked for it early".into()),
})
);
}
#[test]
fn pin_can_defer_a_cached_next_due() {
let c = skeleton("c", Cadence::Quarterly);
let schedule = Schedule {
overrides: vec![ScheduleEntry {
due: Some(d("2026-11-15")),
note: Some("auditor on site mid-November".into()),
..entry_for("c")
}],
};
let st = state_due("2026-10-01");
let r = resolver::next_due_with_reason(&c, &schedule, Some(&st), d("2026-08-15"), None);
assert_eq!(
r,
Some(DueResolution {
date: d("2026-11-15"),
reason: DueReason::OverrideDue,
note: Some("auditor on site mid-November".into()),
})
);
}