feat(gui): pure-logic helpers for Policy tab (mode mapping, violation grouping)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-04-27 11:13:53 -07:00
parent ec22188440
commit b2771e73fa
2 changed files with 93 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ mod enroll_dialog;
mod error; mod error;
mod firstrun; mod firstrun;
mod keys_page; mod keys_page;
mod policy_form;
const APP_ID: &str = "io.dangerousthings.AuthForge"; const APP_ID: &str = "io.dangerousthings.AuthForge";

92
gui/src/policy_form.rs Normal file
View File

@@ -0,0 +1,92 @@
//! Pure helpers backing the Policy tab. Mode <-> combo index, violation
//! grouping. Widget code calls these; nothing here touches GTK.
//!
//! `dead_code` allowances mirror `bus.rs` — these helpers are wired through
//! `policy_page.rs` in Task 4; without the allow, this commit's `-D warnings`
//! gate would reject the staged-but-unused symbols.
#![allow(dead_code)]
use authforge_common::types::{Mode, Violation};
use std::collections::BTreeMap;
pub(crate) const MODE_LABELS: &[(&str, Mode)] = &[
("Disabled", Mode::Disabled),
("Optional", Mode::Optional),
("Required", Mode::Required),
];
pub(crate) fn mode_to_combo_index(m: Mode) -> u32 {
match m {
Mode::Disabled => 0,
Mode::Optional => 1,
Mode::Required => 2,
}
}
pub(crate) fn combo_index_to_mode(idx: u32) -> Mode {
match idx {
0 => Mode::Disabled,
1 => Mode::Optional,
_ => Mode::Required,
}
}
/// Group violations by stack, joining reasons with "; ". Used to render the
/// inline AdwBanner content when `SetPolicy(force=false)` returns violations.
pub(crate) fn group_violations_by_stack(vs: &[Violation]) -> BTreeMap<String, String> {
let mut by_stack: BTreeMap<String, Vec<String>> = BTreeMap::new();
for v in vs {
by_stack
.entry(v.stack.clone())
.or_default()
.push(format!("{}: {}", v.user, v.reason));
}
by_stack
.into_iter()
.map(|(k, vs)| (k, vs.join("; ")))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_combo_roundtrips() {
for m in [Mode::Disabled, Mode::Optional, Mode::Required] {
assert_eq!(combo_index_to_mode(mode_to_combo_index(m)), m);
}
}
#[test]
fn out_of_range_index_clamps_to_required() {
assert_eq!(combo_index_to_mode(99), Mode::Required);
}
#[test]
fn violations_group_by_stack() {
let vs = vec![
Violation {
user: "alice".into(),
stack: "sudo".into(),
reason: "no fido2".into(),
},
Violation {
user: "bob".into(),
stack: "sudo".into(),
reason: "no fido2".into(),
},
Violation {
user: "alice".into(),
stack: "sshd".into(),
reason: "no fido2".into(),
},
];
let g = group_violations_by_stack(&vs);
assert_eq!(g.len(), 2);
assert!(g.get("sudo").unwrap().contains("alice"));
assert!(g.get("sudo").unwrap().contains("bob"));
assert_eq!(g.get("sshd").unwrap(), "alice: no fido2");
}
}