feat(daemon): add AppState with fixture-backed in-memory store

This commit is contained in:
michael
2026-04-26 20:48:56 -07:00
parent c78ad0dc29
commit 0dec2a1236
2 changed files with 126 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
use anyhow::Result;
use tracing::info;
mod state;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()

124
daemon/src/state.rs Normal file
View File

@@ -0,0 +1,124 @@
use authforge_common::types::{Credential, Method, PendingFlag, Policy, Transport};
use std::collections::HashMap;
use tokio::sync::RwLock;
#[derive(Default)]
pub struct AppState {
inner: RwLock<StateInner>,
}
#[derive(Default)]
struct StateInner {
credentials: HashMap<String, Vec<Credential>>,
policy: Policy,
pending: HashMap<String, PendingFlag>,
}
// `dead_code` allow lifts in Task 1.8 when dbus.rs starts calling these methods.
#[allow(dead_code)]
impl AppState {
pub fn with_fixtures() -> Self {
let mut inner = StateInner::default();
inner.credentials.insert(
"alice".to_string(),
vec![Credential {
id: "fixture-cred-1".to_string(),
nickname: "Alice's Yubikey".to_string(),
method: Method::Fido2,
transport: Transport::Usb,
created_unix: 1_700_000_000,
}],
);
Self { inner: RwLock::new(inner) }
}
pub async fn list_credentials(&self, user: &str) -> Vec<Credential> {
self.inner.read().await.credentials.get(user).cloned().unwrap_or_default()
}
pub async fn add_credential(&self, user: &str, c: Credential) {
self.inner.write().await.credentials.entry(user.to_string()).or_default().push(c);
}
pub async fn remove_credential(&self, user: &str, cred_id: &str) -> bool {
let mut g = self.inner.write().await;
let Some(list) = g.credentials.get_mut(user) else { return false };
let before = list.len();
list.retain(|c| c.id != cred_id);
list.len() != before
}
pub async fn get_policy(&self) -> Policy {
self.inner.read().await.policy.clone()
}
pub async fn set_policy(&self, p: Policy) {
self.inner.write().await.policy = p;
}
pub async fn set_pending(&self, user: &str, f: PendingFlag) {
self.inner.write().await.pending.insert(user.to_string(), f);
}
pub async fn clear_pending(&self, user: &str) -> bool {
self.inner.write().await.pending.remove(user).is_some()
}
pub async fn has_pending(&self, user: &str) -> bool {
self.inner.read().await.pending.contains_key(user)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn fixtures_seeded_for_alice() {
let s = AppState::with_fixtures();
let creds = s.list_credentials("alice").await;
assert_eq!(creds.len(), 1);
assert_eq!(creds[0].nickname, "Alice's Yubikey");
}
#[tokio::test]
async fn unknown_user_has_no_creds() {
let s = AppState::with_fixtures();
assert!(s.list_credentials("nobody").await.is_empty());
}
#[tokio::test]
async fn add_then_remove_credential() {
let s = AppState::default();
let c = Credential {
id: "id1".to_string(),
nickname: "x".to_string(),
method: Method::Fido2,
transport: Transport::Usb,
created_unix: 0,
};
s.add_credential("bob", c).await;
assert_eq!(s.list_credentials("bob").await.len(), 1);
assert!(s.remove_credential("bob", "id1").await);
assert!(s.list_credentials("bob").await.is_empty());
}
#[tokio::test]
async fn pending_set_clear_roundtrip() {
let s = AppState::default();
assert!(!s.has_pending("alice").await);
s.set_pending(
"alice",
PendingFlag {
required_methods: vec![Method::Fido2],
created_unix: 1,
deadline_unix: 0,
re_enroll: false,
},
)
.await;
assert!(s.has_pending("alice").await);
assert!(s.clear_pending("alice").await);
assert!(!s.has_pending("alice").await);
}
}