diff --git a/px-auth/src/infrastructure/yaml_allowlist_store.rs b/px-auth/src/infrastructure/yaml_allowlist_store.rs index 852baa0..461b1e2 100644 --- a/px-auth/src/infrastructure/yaml_allowlist_store.rs +++ b/px-auth/src/infrastructure/yaml_allowlist_store.rs @@ -3,7 +3,7 @@ use crate::domain::allowlist_store::AllowlistStore; use async_trait::async_trait; use px_errors::AppError; use serde::Deserialize; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::path::Path; #[derive(Debug, Deserialize)] @@ -11,8 +11,10 @@ struct AllowlistFile { entries: Vec, } +/// `BTreeMap` keeps `list()` output in domain-sorted order so operator output +/// (`px-cli allowlist list`) and audit diffs are reproducible across reloads. pub struct YamlAllowlistStore { - by_domain: HashMap, + by_domain: BTreeMap, } impl YamlAllowlistStore { @@ -51,3 +53,44 @@ impl AllowlistStore for YamlAllowlistStore { Ok(self.by_domain.values().cloned().collect()) } } + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use super::*; + + fn entry(domain: &str) -> AllowlistEntry { + AllowlistEntry { + domain: domain.into(), + tos_reviewed: true, + justification: "test".into(), + handler: None, + } + } + + #[tokio::test] + async fn list_returns_entries_sorted_by_domain() { + let store = YamlAllowlistStore::from_entries(vec![ + entry("zeta.example"), + entry("alpha.example"), + entry("mu.example"), + ]); + let listed: Vec = store + .list() + .await + .expect("list") + .into_iter() + .map(|e| e.domain) + .collect(); + assert_eq!(listed, vec!["alpha.example", "mu.example", "zeta.example"]); + } + + #[tokio::test] + async fn lookup_finds_inserted_entry() { + let store = YamlAllowlistStore::from_entries(vec![entry("example.com")]); + let hit = store.lookup("example.com").await.expect("lookup"); + assert_eq!(hit.map(|e| e.domain).as_deref(), Some("example.com")); + let miss = store.lookup("missing.example").await.expect("lookup"); + assert!(miss.is_none()); + } +}