1use crate::client::VaultClient;
25use crate::plugin::Vault;
26use crate::resolve::{BoxFuture, Secret, SecretSource};
27use rustlavel_client::FakeResponse;
28use rustlavel_core::{Json, Result};
29use std::time::Duration;
30
31pub const FAKE_ADDRESS: &str = "https://vault.fake:8200";
34
35#[derive(Default)]
42pub struct Fake {
43 routes: Vec<(String, FakeResponse)>,
44 fallback: Option<FakeResponse>,
45}
46
47impl Fake {
48 pub fn new() -> Fake {
49 Fake::default()
50 }
51
52 pub fn secret<K, V, I>(self, path: &str, fields: I) -> Fake
55 where
56 K: Into<String>,
57 V: Into<String>,
58 I: IntoIterator<Item = (K, V)>,
59 {
60 self.answers(path, kv2(fields, Json::Null, 0, false))
61 }
62
63 pub fn leased<K, V, I>(
68 self,
69 path: &str,
70 fields: I,
71 lease_id: &str,
72 duration: Duration,
73 ) -> Fake
74 where
75 K: Into<String>,
76 V: Into<String>,
77 I: IntoIterator<Item = (K, V)>,
78 {
79 self.answers(path, kv2(fields, Json::from(lease_id), duration.as_secs(), true))
80 }
81
82 pub fn answers(mut self, path: &str, body: Json) -> Fake {
85 self.routes.push((pattern(path), FakeResponse::json(body)));
86 self
87 }
88
89 pub fn missing(mut self, path: &str) -> Fake {
91 self.routes.push((pattern(path), FakeResponse::text(r#"{"errors":[]}"#).status(404)));
92 self
93 }
94
95 pub fn denies(mut self, path: &str) -> Fake {
97 self.routes.push((
98 pattern(path),
99 FakeResponse::text(r#"{"errors":["permission denied"]}"#).status(403),
100 ));
101 self
102 }
103
104 pub fn fails(mut self, path: &str, status: u16, message: &str) -> Fake {
107 let body = Json::object([("errors", Json::Array(vec![Json::from(message)]))]);
108 self.routes.push((pattern(path), FakeResponse::json(body).status(status)));
109 self
110 }
111
112 pub fn sealed(self) -> Fake {
118 Fake {
119 routes: Vec::new(),
120 fallback: Some(FakeResponse::text(r#"{"errors":["Vault is sealed"]}"#).status(503)),
121 }
122 }
123
124 pub fn store(self) -> FakeVault {
126 let mut routes = self.routes;
127 routes.sort_by_key(|(pattern, _)| std::cmp::Reverse(pattern.len()));
130
131 let mut script = rustlavel_client::Fake::new();
132 for (pattern, response) in routes {
133 script = script.on(&pattern, response);
134 }
135 if let Some(fallback) = self.fallback {
136 script = script.fallback(fallback);
137 }
138
139 let client = VaultClient::new(FAKE_ADDRESS).retries(0).faking(script);
142 client.set_token("fake-token");
143 FakeVault { client }
144 }
145}
146
147pub struct FakeVault {
150 client: VaultClient,
151}
152
153impl FakeVault {
154 pub fn client(&self) -> &VaultClient {
157 &self.client
158 }
159
160 pub fn reads(&self) -> Vec<String> {
163 self.client
164 .fake()
165 .expect("a faked client always has a script")
166 .recorded()
167 .iter()
168 .map(|request| read_path(&request.url))
169 .collect()
170 }
171
172 pub fn read_count(&self, path: &str) -> usize {
173 self.reads().iter().filter(|read| *read == path).count()
174 }
175
176 #[track_caller]
177 pub fn assert_read(&self, path: &str) {
178 assert!(
179 self.read_count(path) > 0,
180 "expected a read of {path:?}; the store was asked for {:?}",
181 self.reads()
182 );
183 }
184
185 #[track_caller]
190 pub fn assert_read_once(&self, path: &str) {
191 assert_eq!(
192 self.read_count(path),
193 1,
194 "expected exactly one read of {path:?}; the store was asked for {:?}",
195 self.reads()
196 );
197 }
198
199 #[track_caller]
200 pub fn assert_not_read(&self, path: &str) {
201 assert_eq!(self.read_count(path), 0, "did not expect a read of {path:?}");
202 }
203
204 #[track_caller]
205 pub fn assert_reads(&self, expected: usize) {
206 assert_eq!(
207 self.reads().len(),
208 expected,
209 "unexpected number of reads: {:?}",
210 self.reads()
211 );
212 }
213}
214
215impl SecretSource for FakeVault {
216 fn read<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<Secret>> {
217 self.client.read(path)
218 }
219}
220
221impl std::fmt::Debug for FakeVault {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "FakeVault({:?})", self.client)
225 }
226}
227
228impl Vault {
229 pub fn fake() -> FakeVault {
231 Fake::new().store()
232 }
233
234 pub fn fake_with(fake: Fake) -> FakeVault {
236 fake.store()
237 }
238}
239
240fn kv2<K, V, I>(fields: I, lease_id: Json, duration: u64, renewable: bool) -> Json
242where
243 K: Into<String>,
244 V: Into<String>,
245 I: IntoIterator<Item = (K, V)>,
246{
247 let data = Json::object(fields.into_iter().map(|(name, value)| (name, Json::from(value.into()))));
248
249 Json::object([
250 ("lease_id", lease_id),
251 ("lease_duration", Json::from(duration)),
252 ("renewable", Json::from(renewable)),
253 (
254 "data",
255 Json::object([("data", data), ("metadata", Json::object([("version", Json::from(1))]))]),
256 ),
257 ])
258}
259
260fn pattern(path: &str) -> String {
263 format!("/v1/{}", path.trim_start_matches('/'))
264}
265
266fn read_path(url: &str) -> String {
268 let path = url.split_once("/v1/").map(|(_, rest)| rest).unwrap_or(url);
269 path.split('?').next().unwrap_or(path).to_string()
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[tokio::test]
277 async fn answers_a_read_from_the_script() {
278 let store = Fake::new()
279 .secret("secret/data/myapp", [("password", "s3cr3t")])
280 .store();
281
282 let secret = store.read("secret/data/myapp").await.unwrap();
283
284 assert_eq!(secret.field("password"), Some("s3cr3t"));
285 assert_eq!(secret.path(), "secret/data/myapp");
286 }
287
288 #[tokio::test]
289 async fn records_which_paths_were_read() {
290 let store = Fake::new()
291 .secret("secret/data/one", [("a", "1")])
292 .secret("secret/data/two", [("b", "2")])
293 .store();
294
295 store.read("secret/data/one").await.unwrap();
296 store.read("secret/data/two").await.unwrap();
297 store.read("secret/data/one").await.unwrap();
298
299 assert_eq!(store.reads(), ["secret/data/one", "secret/data/two", "secret/data/one"]);
300 assert_eq!(store.read_count("secret/data/one"), 2);
301 store.assert_read("secret/data/two");
302 store.assert_not_read("secret/data/three");
303 store.assert_reads(3);
304 }
305
306 #[tokio::test]
307 async fn scripts_a_refusal_a_missing_path_and_a_sealed_store() {
308 let store = Fake::new().denies("secret/data/secret").missing("secret/data/gone").store();
309
310 let denied = store.read("secret/data/secret").await.unwrap_err().to_string();
311 assert!(denied.contains("refused"), "got {denied}");
312
313 let missing = store.read("secret/data/gone").await.unwrap_err().to_string();
314 assert!(missing.contains("nothing is stored at"), "got {missing}");
315
316 let sealed = Fake::new().sealed().store();
317 let error = sealed.read("secret/data/anything").await.unwrap_err().to_string();
318 assert!(error.contains("sealed"), "got {error}");
319 assert_eq!(sealed.reads().len(), 1, "a sealed store is not retried");
320 }
321
322 #[tokio::test]
323 async fn an_unscripted_path_fails_loudly_and_is_not_retried() {
324 let store = Fake::new().secret("secret/data/known", [("a", "1")]).store();
327
328 let error = store.read("secret/data/surprise").await.unwrap_err().to_string();
329
330 assert!(error.contains("no fake response is scripted"), "got {error}");
331 store.assert_reads(1);
332 }
333
334 #[tokio::test]
335 async fn scripts_a_secret_that_comes_with_a_lease() {
336 let store = Fake::new()
337 .leased(
338 "database/creds/app",
339 [("username", "v-token-app-x"), ("password", "generated")],
340 "database/creds/app/abc",
341 Duration::from_secs(3600),
342 )
343 .store();
344
345 let secret = store.read("database/creds/app").await.unwrap();
346
347 assert_eq!(secret.lease().id, "database/creds/app/abc");
348 assert_eq!(secret.lease().duration, Duration::from_secs(3600));
349 assert!(secret.lease().renewable);
350 }
351
352 #[tokio::test]
353 async fn a_path_is_not_answered_by_a_shorter_one_it_begins_with() {
354 let store = Fake::new()
355 .secret("secret/data/app", [("which", "app")])
356 .secret("secret/data/app2", [("which", "app2")])
357 .store();
358
359 assert_eq!(store.read("secret/data/app").await.unwrap().field("which"), Some("app"));
360 assert_eq!(store.read("secret/data/app2").await.unwrap().field("which"), Some("app2"));
361 }
362
363 #[tokio::test]
364 async fn a_status_can_be_scripted_for_any_path() {
365 let store = Fake::new().fails("secret/data/x", 429, "standby").store();
366
367 let error = store.read("secret/data/x").await.unwrap_err().to_string();
368
369 assert!(error.contains("standby"), "got {error}");
370 }
371
372 #[test]
373 fn a_faked_store_never_prints_its_token() {
374 let printed = format!("{:?}", Vault::fake());
375
376 assert!(!printed.contains("fake-token"), "got {printed}");
377 assert!(printed.contains("redacted"));
378 }
379}