1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::AtomicBool;
3use std::sync::{Arc, Mutex};
4
5pub struct VaultState {
7 pub cert_cache: HashMap<
10 String,
11 (
12 std::time::Instant,
13 crate::vault_ssh::CertStatus,
14 Option<std::time::SystemTime>,
15 ),
16 >,
17 pub cert_checks_in_flight: HashSet<String>,
19 pub cleanup_warning: Option<String>,
21 pub signing_cancel: Option<Arc<AtomicBool>>,
23 pub sign_thread: Option<std::thread::JoinHandle<()>>,
25 pub sign_in_flight: Arc<Mutex<HashSet<String>>>,
27 pub pending_config_write: bool,
29}
30
31impl Default for VaultState {
32 fn default() -> Self {
33 Self {
34 cert_cache: HashMap::new(),
35 cert_checks_in_flight: HashSet::new(),
36 cleanup_warning: None,
37 signing_cancel: None,
38 sign_thread: None,
39 sign_in_flight: Arc::new(Mutex::new(HashSet::new())),
40 pending_config_write: false,
41 }
42 }
43}
44
45impl VaultState {
46 pub(crate) fn mark_cert_check_started(&mut self, alias: String) {
50 self.cert_checks_in_flight.insert(alias);
51 }
52
53 pub(crate) fn record_cert_check(
58 &mut self,
59 alias: String,
60 status: crate::vault_ssh::CertStatus,
61 mtime: Option<std::time::SystemTime>,
62 ) {
63 self.cert_checks_in_flight.remove(&alias);
64 self.cert_cache
65 .insert(alias, (std::time::Instant::now(), status, mtime));
66 }
67
68 pub(crate) fn cancel_signing_run(&mut self) -> Option<std::thread::JoinHandle<()>> {
73 if let Some(ref cancel) = self.signing_cancel {
74 cancel.store(true, std::sync::atomic::Ordering::Relaxed);
75 }
76 self.signing_cancel = None;
77 self.sign_thread.take()
78 }
79
80 pub(crate) fn finalize_signing_run(&mut self) -> Option<std::thread::JoinHandle<()>> {
87 self.signing_cancel = None;
88 self.sign_thread.take()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use std::sync::atomic::Ordering;
96
97 #[test]
98 fn mark_cert_check_started_inserts_alias() {
99 let mut v = VaultState::default();
100 v.mark_cert_check_started("web".to_string());
101 assert!(v.cert_checks_in_flight.contains("web"));
102 }
103
104 #[test]
105 fn mark_cert_check_started_is_idempotent() {
106 let mut v = VaultState::default();
109 v.mark_cert_check_started("web".to_string());
110 v.mark_cert_check_started("web".to_string());
111 assert_eq!(v.cert_checks_in_flight.len(), 1);
112 assert!(v.cert_checks_in_flight.contains("web"));
113 }
114
115 #[test]
116 fn record_cert_check_clears_in_flight_and_writes_cache() {
117 let mut v = VaultState::default();
118 v.mark_cert_check_started("web".to_string());
119 v.record_cert_check(
120 "web".to_string(),
121 crate::vault_ssh::CertStatus::Missing,
122 None,
123 );
124 assert!(!v.cert_checks_in_flight.contains("web"));
125 assert!(v.cert_cache.contains_key("web"));
126 let (_, status, mtime) = v.cert_cache.get("web").unwrap();
127 assert!(matches!(status, crate::vault_ssh::CertStatus::Missing));
128 assert!(mtime.is_none());
129 }
130
131 #[test]
132 fn record_cert_check_caches_even_without_prior_start() {
133 let mut v = VaultState::default();
138 v.record_cert_check(
139 "web".to_string(),
140 crate::vault_ssh::CertStatus::Invalid("nope".to_string()),
141 None,
142 );
143 assert!(v.cert_cache.contains_key("web"));
144 assert!(v.cert_checks_in_flight.is_empty());
145 }
146
147 #[test]
148 fn cancel_signing_run_with_no_active_run_returns_none() {
149 let mut v = VaultState::default();
150 let handle = v.cancel_signing_run();
151 assert!(handle.is_none());
152 assert!(v.signing_cancel.is_none());
153 assert!(v.sign_thread.is_none());
154 }
155
156 #[test]
157 fn cancel_signing_run_signals_cancel_and_clears_handle() {
158 let mut v = VaultState::default();
162 let cancel = Arc::new(AtomicBool::new(false));
163 v.signing_cancel = Some(cancel.clone());
164 v.sign_thread = Some(std::thread::spawn(|| {}));
165
166 let handle = v
167 .cancel_signing_run()
168 .expect("returned thread handle for joining");
169 let _ = handle.join();
170
171 assert!(
172 cancel.load(Ordering::Relaxed),
173 "cancel must be signalled so a long-running worker exits"
174 );
175 assert!(v.signing_cancel.is_none());
176 assert!(v.sign_thread.is_none());
177 }
178
179 #[test]
180 fn finalize_signing_run_does_not_signal_cancel() {
181 let mut v = VaultState::default();
187 let cancel = Arc::new(AtomicBool::new(false));
188 v.signing_cancel = Some(cancel.clone());
189 v.sign_thread = Some(std::thread::spawn(|| {}));
190
191 let handle = v
192 .finalize_signing_run()
193 .expect("returned thread handle for joining");
194 let _ = handle.join();
195
196 assert!(
197 !cancel.load(Ordering::Relaxed),
198 "finalize must not signal cancel: a racing newer run's Arc could be hit"
199 );
200 assert!(v.signing_cancel.is_none());
201 assert!(v.sign_thread.is_none());
202 }
203
204 #[test]
205 fn finalize_signing_run_with_cancel_but_no_thread_clears_cancel() {
206 let mut v = VaultState::default();
212 let cancel = Arc::new(AtomicBool::new(false));
213 v.signing_cancel = Some(cancel.clone());
214
215 let handle = v.finalize_signing_run();
216 assert!(handle.is_none());
217 assert!(v.signing_cancel.is_none());
218 assert!(!cancel.load(Ordering::Relaxed));
219 }
220}