facett-security/src/tests.rs
//! FC compliance tests for the security view: the state layer round-trips and
//! folds correctly (FC-1/FC-3), the `state_json` introspection dump exposes the
//! **Block** verdict + findings as DATA (the headless-assertion contract), and a
//! real `egui_kittest` render proves the painted verdict pill + finding rows ride
//! into the AccessKit tree as queryable, value-carrying nodes (FC-4/FC-6).
use super::*;
// ── FC-1 / FC-3: the whole observable state is serializable + round-trips ──────
#[test]
fn state_round_trips_through_serde() {
let v = SecurityView::local();
let json = serde_json::to_string(v.state()).expect("state serializes");
let back: SecurityState = serde_json::from_str(&json).expect("state deserializes");
assert_eq!(v.state(), &back, "SecurityState is a faithful round-trip (FC-3)");
}
#[test]
fn verdict_and_severity_fold_by_max() {
// Declaration order is strictness order, so folding is `max()` — the same
// contract a modgunn consumer relies on.
assert!(Verdict::Pass < Verdict::Warn && Verdict::Warn < Verdict::Block);
assert!(Severity::Info < Severity::Critical);
let rep = SecurityView::local().report().clone();
assert_eq!(rep.max_severity(), Some(Severity::Critical), "worst finding is Critical");
assert_eq!(rep.findings.iter().map(|f| f.severity).max(), rep.max_severity());
}
#[test]
fn verdict_and_severity_parse_round_trip() {
for v in [Verdict::Pass, Verdict::Warn, Verdict::Block] {
assert_eq!(Verdict::parse(v.tag()), Some(v));
}
for s in [Severity::Info, Severity::Low, Severity::Medium, Severity::High, Severity::Critical] {
assert_eq!(Severity::parse(s.tag()), Some(s));
}
assert_eq!(Verdict::parse("FAILED"), Some(Verdict::Block));
assert_eq!(Severity::parse("moderate"), Some(Severity::Medium));
assert_eq!(Verdict::parse("nonsense"), None);
}
// ── FC-2 / FC-9: mutation only via Msg/update; render produces no hidden state ──
#[test]
fn select_msg_toggles_and_filter_narrows() {
let mut v = SecurityView::local();
assert_eq!(v.selected(), None);
let eff = v.update(Msg::Select(Some("CVE-2024-31337".into())));
assert!(eff.is_empty(), "FC-8: the view emits no effects");
assert_eq!(v.selected(), Some("CVE-2024-31337"));
// Re-selecting the open finding clears it (toggle).
v.update(Msg::Select(Some("CVE-2024-31337".into())));
assert_eq!(v.selected(), None);
// The severity floor narrows the visible set, worst-first.
v.update(Msg::SetMinSeverity(Some(Severity::High)));
let vis = v.state().visible_findings();
assert_eq!(vis.len(), 2, "only Critical + High clear the High floor");
assert_eq!(vis[0].severity, Severity::Critical, "sorted worst-first");
assert!(vis.iter().all(|f| f.severity >= Severity::High));
}
#[test]
fn scale_is_clamped() {
let mut v = SecurityView::local();
v.update(Msg::SetScale(99.0));
assert_eq!(v.scale(), 4.0, "scale clamps to the 0.25..=4.0 band");
v.update(Msg::SetScale(0.0));
assert_eq!(v.scale(), 0.25);
}
// ── The headless-assertion contract: state_json exposes a BLOCK verdict +
// the findings AS DATA (not pixels). ──────────────────────────────────────────
#[test]
fn state_json_exposes_block_verdict_and_findings_as_data() {
let v = SecurityView::local();
let js = v.state_json();
assert_eq!(js["verdict"], "BLOCK", "the demo report is a Block");
assert_eq!(js["max_severity"], "CRIT");
assert_eq!(js["kev_count"], 1, "one KEV-listed finding");
assert_eq!(js["findings_total"], 4);
assert_eq!(js["findings_visible"], 4, "no filter → all visible");
assert_eq!(js["severity_counts"]["critical"], 1);
assert_eq!(js["severity_counts"]["high"], 1);
// The findings ride into state_json as structured rows, worst-first.
let findings = js["findings"].as_array().expect("findings array");
assert_eq!(findings.len(), 4);
let crit = &findings[0];
assert_eq!(crit["id"], "CVE-2024-31337");
assert_eq!(crit["severity"], "CRIT");
assert_eq!(crit["severity_rank"], 4);
assert_eq!(crit["package"], "openssl-sys@0.9.90");
assert_eq!(crit["fix"], "0.9.92");
assert_eq!(crit["kev"], true);
assert!((crit["epss"].as_f64().unwrap() - 0.94).abs() < 1e-9);
// Worst-first ordering holds across the whole list.
let ranks: Vec<u64> = findings.iter().map(|f| f["severity_rank"].as_u64().unwrap()).collect();
assert!(ranks.windows(2).all(|w| w[0] >= w[1]), "findings are sorted worst-first: {ranks:?}");
}
#[test]
fn selection_json_reflects_drilled_in_finding() {
let mut v = SecurityView::local();
assert_eq!(v.selection_json(), serde_json::Value::Null, "nothing selected → Null");
v.update(Msg::Select(Some("RUSTSEC-2023-0071".into())));
let sel = v.selection_json();
assert_eq!(sel["id"], "RUSTSEC-2023-0071");
assert_eq!(sel["severity"], "HIGH");
assert_eq!(sel["package"], "rsa@0.9.2");
}
// ── The canonical `Elm` trait + headless harness (facett-core), proven against
// THIS reference component: apply a Vec<Msg>, snapshot state() — no egui/GPU. ──
#[test]
fn elm_harness_drives_msgs_and_snapshots_state() {
use facett_core::harness;
// The security view drives through the shared `Elm` trait exactly as it does
// through its inherent methods (the trait just delegates). Feed inputs, read
// state — the FC-2 → FC-3 property, headless.
let mut v = SecurityView::local();
let snap = harness::snapshot(
&mut v,
[
Msg::SetMinSeverity(Some(Severity::High)),
Msg::Select(Some("CVE-2024-31337".into())),
Msg::SetScale(2.0),
],
);
assert_eq!(snap.min_severity, Some(Severity::High));
assert_eq!(snap.selected.as_deref(), Some("CVE-2024-31337"));
assert_eq!(snap.scale, 2.0);
// Only Critical + High clear the High floor (state is the source of truth).
assert_eq!(snap.visible_findings().len(), 2);
// `snapshot_json` is the same state, serialized (FC-3) — no render in the loop.
let mut v2 = SecurityView::local();
let js = harness::snapshot_json(&mut v2, [Msg::SetMinSeverity(Some(Severity::Critical))]);
assert_eq!(js["min_severity"], "critical");
}
#[test]
fn elm_harness_proptest_style_over_msg_sequences() {
use facett_core::harness;
// proptest-style (dependency-free deterministic LCG): generate many Msg
// sequences, drive the reference component headlessly, and assert the FC
// invariants hold — no panic, state stays well-formed, serde round-trips, and
// the selection-toggle / clamp laws match an independent reference. This is
// contract §5.1 (property layer over `update`), driven via the shared harness.
let ids = ["CVE-2024-31337", "RUSTSEC-2023-0071", "GHSA-wxr5-93ch-7f92", "RUSTSEC-2024-0006"];
let sevs = [Severity::Info, Severity::Low, Severity::Medium, Severity::High, Severity::Critical];
let mut rng: u64 = 0xD1B54A32D192ED03;
let mut next = || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for _ in 0..300 {
let mut v = SecurityView::local();
let mut expect_sel: Option<String> = None;
let len = (next() % 16) as usize;
let msgs: Vec<Msg> = (0..len)
.map(|_| match next() % 3 {
0 => {
let id = ids[(next() % ids.len() as u64) as usize].to_string();
let new = Some(id.clone());
// Reference toggle law: re-selecting the open id clears it.
expect_sel = if expect_sel == new { None } else { new };
Msg::Select(Some(id))
}
1 => {
let choice = next() % (sevs.len() as u64 + 1);
Msg::SetMinSeverity((choice < sevs.len() as u64).then(|| sevs[choice as usize]))
}
_ => Msg::SetScale((next() % 1000) as f32 / 100.0),
})
.collect();
let snap = harness::snapshot(&mut v, msgs);
// Invariant 1: selection tracks the independent toggle reference.
assert_eq!(snap.selected, expect_sel);
// Invariant 2: scale stays inside the clamp band whatever was fed.
assert!((0.25..=4.0).contains(&snap.scale), "scale escaped clamp: {}", snap.scale);
// Invariant 3: the filter never invents findings; visible ⊆ all, worst-first.
let vis = snap.visible_findings();
assert!(vis.len() <= snap.report.findings.len());
assert!(vis.windows(2).all(|w| w[0].severity >= w[1].severity), "visible not worst-first");
// Invariant 4: state round-trips through serde (FC-3).
let json = serde_json::to_string(&snap).unwrap();
let back: SecurityState = serde_json::from_str(&json).unwrap();
assert_eq!(back, snap);
}
}
// ── FC-4 / FC-6: a real headless render — the painted verdict pill + finding
// rows must be queryable, labelled, value-carrying AccessKit nodes. ───────────
#[test]
fn robot_sees_block_verdict_and_finding_rows_as_nodes() {
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
let mut view = SecurityView::local();
let mut harness = Harness::new_ui(move |ui| {
view.ui(ui);
});
harness.run();
// The painter-drawn verdict pill is now a queryable node carrying the verdict
// tag as its label and the finding COUNT (4) as numeric_value (FC-4).
let pill = harness.get_by_label("verdict: BLOCK").accesskit_node();
assert_eq!(pill.role(), Role::Label, "the verdict pill exposes a Label node");
assert_eq!(pill.numeric_value(), Some(4.0), "pill carries the finding count as value");
// The Critical finding row is a selectable node whose numeric_value is the
// severity rank (4 = Critical) — a robot reads the severity back as a number.
let crit = harness.get_by_label("CVE-2024-31337: CRIT").accesskit_node();
assert_eq!(crit.role(), Role::Button, "selectable finding row is a Button-role node");
assert_eq!(crit.numeric_value(), Some(4.0), "row carries the severity rank");
// The severity histogram (facett-stat sparkline lane) entered the tree too,
// as an Image-role node whose value is the total finding count.
let hist = harness.get_by_label("severity histogram · 4 findings · modal critical").accesskit_node();
assert_eq!(hist.role(), Role::Image, "the histogram is an Image-role node");
assert_eq!(hist.numeric_value(), Some(4.0), "histogram carries the total finding count");
}
/// **RED-when-broken: the uniform scale knob reaches the PIXELS.**
///
/// Everything else here is a state round-trip — set a value, read it back, see it in
/// `state_json`. Measured: deleting the scale application from the painter outright
/// left this whole crate's suite green, and `RenderReport::drew()` cannot see it
/// either (a glyph is four vertices at 6pt and at 60pt). So this asserts APPLIED
/// geometry: the area of the glyph triangles the painter really emitted, which must
/// grow roughly 4x when the scale doubles.
#[test]
fn scale_is_applied_to_the_drawn_text_not_just_stored() {
use facett_core::harness;
let ink = |s: f32| {
let mut v = SecurityView::local();
<_ as facett_core::Facet>::set_scale(&mut v, s);
harness::headless_render(&mut v).textured_ink_area
};
let (small, big) = (ink(1.0), ink(2.0));
assert!(small > 0.0, "the facet draws text at 1.0x ({small})");
assert!(
big > small * 2.0,
"doubling the scale must roughly QUADRUPLE the glyph ink, got {small:.0} -> \
{big:.0} pt^2 ({:.2}x). A ratio near 1.0 means the knob is stored, clamped and \
serialised but never applied to anything drawn.",
big / small,
);
}