1#[cfg(feature = "secret-store")]
4use std::collections::hash_map::Entry;
5
6#[cfg(feature = "secret-store")]
7use chrono::Utc;
8#[cfg(feature = "secret-store")]
9use uuid::Uuid;
10
11#[cfg(feature = "secret-store")]
12use tracing::error;
13
14#[cfg(feature = "secret-store")]
15use crate::crypto::{decrypt, encrypt, join_versions};
16use crate::entities::{
17 KeyVersionStatus, Page, RotationBatch, RotationRequest, Secret, SecretMetadata,
18};
19use crate::error::StoreError;
20use crate::secret_store::SecretStore;
21use crate::store::StoreFuture;
22
23use super::InMemoryStore;
24
25#[cfg(feature = "secret-store")]
26impl InMemoryStore {
27 fn require_key_ring(&self) -> Result<&crate::crypto::KeyRing, StoreError> {
29 self.key_ring
30 .as_deref()
31 .ok_or_else(|| StoreError::Crypto("no master key configured".to_string()))
32 }
33}
34
35impl SecretStore for InMemoryStore {
36 fn get_secret(&self, key: &str) -> StoreFuture<'_, Option<Secret>> {
37 let key = key.to_string();
38 Box::pin(async move {
39 #[cfg(feature = "secret-store")]
40 {
41 let ring = self.require_key_ring()?;
42
43 let state = self.state.read().await;
44 let Some(encrypted) = state.secrets.get(&key) else {
45 return Ok(None);
46 };
47
48 let master_key = ring.key_for(encrypted.key_version).ok_or_else(|| {
49 StoreError::Crypto(format!(
50 "secret {key:?} uses key version {} which is not configured",
51 encrypted.key_version
52 ))
53 })?;
54
55 let plaintext = decrypt(master_key, &encrypted.encrypted_value, &encrypted.nonce)
56 .map_err(|e| StoreError::Crypto(e.to_string()))?;
57
58 let value = String::from_utf8(plaintext)
59 .map_err(|e| StoreError::Crypto(format!("invalid UTF-8: {e}")))?;
60
61 Ok(Some(Secret {
62 id: encrypted.id,
63 key: encrypted.key.clone(),
64 value,
65 created_at: encrypted.created_at,
66 updated_at: encrypted.updated_at,
67 }))
68 }
69 #[cfg(not(feature = "secret-store"))]
70 {
71 let _ = key;
72 Err(StoreError::Crypto(
73 "secret-store feature not enabled".to_string(),
74 ))
75 }
76 })
77 }
78
79 fn set_secret(&self, key: &str, value: &str) -> StoreFuture<'_, Secret> {
80 let key = key.to_string();
81 let value = value.to_string();
82 Box::pin(async move {
83 #[cfg(feature = "secret-store")]
84 {
85 let ring = self.require_key_ring()?;
86
87 let (encrypted_value, nonce) = encrypt(ring.active_key(), value.as_bytes())
88 .map_err(|e| StoreError::Crypto(e.to_string()))?;
89 let key_version = ring.active_version();
90
91 let now = Utc::now();
92 let mut state = self.state.write().await;
93
94 let entry = state.secrets.entry(key.clone());
95 let encrypted = match entry {
96 Entry::Occupied(mut occ) => {
97 let existing = occ.get_mut();
98 existing.encrypted_value = encrypted_value;
99 existing.nonce = nonce;
100 existing.key_version = key_version;
101 existing.updated_at = now;
102 existing.clone()
103 }
104 Entry::Vacant(vac) => {
105 let new = super::EncryptedSecret {
106 id: Uuid::now_v7(),
107 key: key.clone(),
108 encrypted_value,
109 nonce,
110 key_version,
111 created_at: now,
112 updated_at: now,
113 };
114 vac.insert(new.clone());
115 new
116 }
117 };
118
119 Ok(Secret {
120 id: encrypted.id,
121 key: encrypted.key,
122 value,
123 created_at: encrypted.created_at,
124 updated_at: encrypted.updated_at,
125 })
126 }
127 #[cfg(not(feature = "secret-store"))]
128 {
129 let _ = (key, value);
130 Err(StoreError::Crypto(
131 "secret-store feature not enabled".to_string(),
132 ))
133 }
134 })
135 }
136
137 fn delete_secret(&self, key: &str) -> StoreFuture<'_, bool> {
138 let key = key.to_string();
139 Box::pin(async move {
140 let mut state = self.state.write().await;
141 Ok(state.secrets.remove(&key).is_some())
142 })
143 }
144
145 fn list_secret_keys(&self, prefix: &str) -> StoreFuture<'_, Vec<String>> {
146 let prefix = prefix.to_string();
147 Box::pin(async move {
148 let state = self.state.read().await;
149 let keys: Vec<String> = state
150 .secrets
151 .keys()
152 .filter(|k| k.starts_with(&prefix))
153 .cloned()
154 .collect();
155 Ok(keys)
156 })
157 }
158
159 fn list_secrets(
160 &self,
161 prefix: &str,
162 page: u32,
163 per_page: u32,
164 ) -> StoreFuture<'_, Page<SecretMetadata>> {
165 let prefix = prefix.to_string();
166 Box::pin(async move {
167 let state = self.state.read().await;
168 let mut metadata: Vec<SecretMetadata> = state
169 .secrets
170 .values()
171 .filter(|s| s.key.starts_with(&prefix))
172 .map(|s| SecretMetadata {
173 id: s.id,
174 key: s.key.clone(),
175 created_at: s.created_at,
176 updated_at: s.updated_at,
177 })
178 .collect();
179
180 metadata.sort_by(|a, b| a.key.cmp(&b.key));
181
182 let total = metadata.len() as u64;
183 let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
184 let items: Vec<SecretMetadata> = metadata
185 .into_iter()
186 .skip(offset)
187 .take(per_page as usize)
188 .collect();
189
190 Ok(Page {
191 items,
192 total,
193 page,
194 per_page,
195 })
196 })
197 }
198
199 fn secret_key_status(&self) -> StoreFuture<'_, KeyVersionStatus> {
200 Box::pin(async move {
201 #[cfg(feature = "secret-store")]
202 {
203 let ring = self.require_key_ring()?;
204
205 let state = self.state.read().await;
206 let mut in_use: Vec<i32> = state.secrets.values().map(|s| s.key_version).collect();
207 in_use.sort_unstable();
208 in_use.dedup();
209
210 Ok(KeyVersionStatus {
211 active: ring.active_version(),
212 configured: ring.versions(),
213 missing: ring.missing_versions(&in_use),
214 retirable: ring.retirable_versions(&in_use),
215 in_use,
216 })
217 }
218 #[cfg(not(feature = "secret-store"))]
219 {
220 Err(StoreError::Crypto(
221 "secret-store feature not enabled".to_string(),
222 ))
223 }
224 })
225 }
226
227 fn rotate_secrets(&self, request: RotationRequest) -> StoreFuture<'_, RotationBatch> {
228 Box::pin(async move {
229 #[cfg(feature = "secret-store")]
230 {
231 let ring = self.require_key_ring()?;
232 let target = ring.key_for(request.to_version).ok_or_else(|| {
233 StoreError::Crypto(format!(
234 "target key version {} is not configured (available: {})",
235 request.to_version,
236 join_versions(&ring.versions())
237 ))
238 })?;
239
240 let batch_size = request.effective_batch_size() as usize;
241 let mut state = self.state.write().await;
242
243 let mut candidates: Vec<(Uuid, String)> = state
246 .secrets
247 .values()
248 .filter(|s| s.key_version != request.to_version)
249 .filter(|s| request.after_id.is_none_or(|after| s.id > after))
250 .map(|s| (s.id, s.key.clone()))
251 .collect();
252 candidates.sort_unstable_by_key(|(id, _)| *id);
253
254 let remaining = candidates.len().saturating_sub(batch_size) as u64;
255 let mut rotated = 0u64;
256 let mut failed = 0u64;
257 let mut last_id = None;
258
259 for (id, key) in candidates.into_iter().take(batch_size) {
260 last_id = Some(id);
261
262 let Some(secret) = state.secrets.get(&key) else {
263 continue;
264 };
265 let source_version = secret.key_version;
266
267 let plaintext = match ring
268 .key_for(source_version)
269 .ok_or_else(|| format!("key version {source_version} is not configured"))
270 .and_then(|source| {
271 decrypt(source, &secret.encrypted_value, &secret.nonce)
272 .map_err(|e| e.to_string())
273 }) {
274 Ok(plaintext) => plaintext,
275 Err(reason) => {
276 failed += 1;
277 error!(
278 secret_key = %key,
279 key_version = source_version,
280 reason = %reason,
281 "secret rotation failed"
282 );
283 continue;
284 }
285 };
286
287 let (encrypted_value, nonce) = match encrypt(target, &plaintext) {
288 Ok(pair) => pair,
289 Err(e) => {
290 failed += 1;
291 error!(
292 secret_key = %key,
293 reason = %e,
294 "secret rotation failed"
295 );
296 continue;
297 }
298 };
299
300 let entry = state
301 .secrets
302 .get_mut(&key)
303 .expect("secret was present a moment ago under the same write lock");
304 entry.encrypted_value = encrypted_value;
305 entry.nonce = nonce;
306 entry.key_version = request.to_version;
307 rotated += 1;
310 }
311
312 Ok(RotationBatch {
313 to_version: request.to_version,
314 rotated,
315 failed,
316 remaining,
317 last_id,
318 })
319 }
320 #[cfg(not(feature = "secret-store"))]
321 {
322 let _ = request;
323 Err(StoreError::Crypto(
324 "secret-store feature not enabled".to_string(),
325 ))
326 }
327 })
328 }
329}
330
331#[cfg(all(test, feature = "secret-store"))]
332mod tests {
333 use crate::crypto::MasterKey;
334 use crate::memory::InMemoryStore;
335 use crate::secret_store::SecretStore;
336
337 fn test_store() -> InMemoryStore {
338 let key = MasterKey::from_bytes(&[42u8; 32]).unwrap();
339 let mut store = InMemoryStore::new();
340 store.set_master_key(key);
341 store
342 }
343
344 #[tokio::test]
345 async fn set_and_get_secret() {
346 let store = test_store();
347 let secret = store.set_secret("my/key", "my-value").await.unwrap();
348 assert_eq!(secret.key, "my/key");
349 assert_eq!(secret.value, "my-value");
350
351 let fetched = store.get_secret("my/key").await.unwrap().unwrap();
352 assert_eq!(fetched.value, "my-value");
353 assert_eq!(fetched.id, secret.id);
354 }
355
356 #[tokio::test]
357 async fn get_missing_secret_returns_none() {
358 let store = test_store();
359 let result = store.get_secret("does/not/exist").await.unwrap();
360 assert!(result.is_none());
361 }
362
363 #[tokio::test]
364 async fn set_secret_updates_existing() {
365 let store = test_store();
366 let first = store.set_secret("token", "v1").await.unwrap();
367 let second = store.set_secret("token", "v2").await.unwrap();
368
369 assert_eq!(first.id, second.id);
370 assert_eq!(second.value, "v2");
371
372 let fetched = store.get_secret("token").await.unwrap().unwrap();
373 assert_eq!(fetched.value, "v2");
374 }
375
376 #[tokio::test]
377 async fn delete_existing_secret() {
378 let store = test_store();
379 store.set_secret("to-delete", "val").await.unwrap();
380
381 let deleted = store.delete_secret("to-delete").await.unwrap();
382 assert!(deleted);
383
384 let fetched = store.get_secret("to-delete").await.unwrap();
385 assert!(fetched.is_none());
386 }
387
388 #[tokio::test]
389 async fn delete_missing_secret_returns_false() {
390 let store = test_store();
391 let deleted = store.delete_secret("nope").await.unwrap();
392 assert!(!deleted);
393 }
394
395 #[tokio::test]
396 async fn list_keys_with_prefix() {
397 let store = test_store();
398 store.set_secret("wf/inbox/token_a", "a").await.unwrap();
399 store.set_secret("wf/inbox/token_b", "b").await.unwrap();
400 store.set_secret("wf/veille/token_c", "c").await.unwrap();
401
402 let mut keys = store.list_secret_keys("wf/inbox/").await.unwrap();
403 keys.sort();
404 assert_eq!(keys, vec!["wf/inbox/token_a", "wf/inbox/token_b"]);
405 }
406
407 #[tokio::test]
408 async fn list_keys_empty_prefix_returns_all() {
409 let store = test_store();
410 store.set_secret("a", "1").await.unwrap();
411 store.set_secret("b", "2").await.unwrap();
412
413 let keys = store.list_secret_keys("").await.unwrap();
414 assert_eq!(keys.len(), 2);
415 }
416
417 #[tokio::test]
418 async fn operations_without_master_key_fail() {
419 let store = InMemoryStore::new();
420 let err = store.get_secret("key").await.unwrap_err();
421 assert!(err.to_string().contains("no master key"));
422
423 let err = store.set_secret("key", "val").await.unwrap_err();
424 assert!(err.to_string().contains("no master key"));
425 }
426
427 #[tokio::test]
428 async fn secret_value_is_encrypted_at_rest() {
429 let store = test_store();
430 store
431 .set_secret("sensitive", "plaintext-value")
432 .await
433 .unwrap();
434
435 let state = store.state.read().await;
436 let encrypted = state.secrets.get("sensitive").unwrap();
437 let as_str = String::from_utf8(encrypted.encrypted_value.clone());
438 assert!(
439 as_str.is_err() || as_str.unwrap() != "plaintext-value",
440 "value must be encrypted at rest"
441 );
442 }
443
444 #[tokio::test]
445 async fn set_secret_with_empty_value() {
446 let store = test_store();
447 let secret = store.set_secret("empty", "").await.unwrap();
448 assert_eq!(secret.value, "");
449
450 let fetched = store.get_secret("empty").await.unwrap().unwrap();
451 assert_eq!(fetched.value, "");
452 }
453
454 #[tokio::test]
455 async fn list_secrets_paginated() {
456 let store = test_store();
457 store.set_secret("a/1", "v").await.unwrap();
458 store.set_secret("a/2", "v").await.unwrap();
459 store.set_secret("a/3", "v").await.unwrap();
460 store.set_secret("b/1", "v").await.unwrap();
461
462 let page = store.list_secrets("a/", 1, 2).await.unwrap();
463 assert_eq!(page.total, 3);
464 assert_eq!(page.items.len(), 2);
465 assert_eq!(page.items[0].key, "a/1");
466 assert_eq!(page.items[1].key, "a/2");
467
468 let page2 = store.list_secrets("a/", 2, 2).await.unwrap();
469 assert_eq!(page2.items.len(), 1);
470 assert_eq!(page2.items[0].key, "a/3");
471 }
472
473 use crate::crypto::KeyRing;
476 use crate::entities::RotationRequest;
477
478 fn hex_key(byte: u8) -> String {
479 format!("{byte:02x}").repeat(32)
480 }
481
482 fn two_version_store(active: i32) -> InMemoryStore {
484 let spec = format!("1:{},2:{}", hex_key(0xaa), hex_key(0xbb));
485 let mut store = InMemoryStore::new();
486 store.set_key_ring(KeyRing::from_spec(&spec, Some(active)).unwrap());
487 store
488 }
489
490 fn rekey(store: &mut InMemoryStore, active: i32) {
492 let spec = format!("1:{},2:{}", hex_key(0xaa), hex_key(0xbb));
493 store.set_key_ring(KeyRing::from_spec(&spec, Some(active)).unwrap());
494 }
495
496 #[tokio::test]
497 async fn set_master_key_is_legacy_version_one() {
498 let store = test_store();
499 store.set_secret("k", "v").await.unwrap();
500
501 let status = store.secret_key_status().await.unwrap();
502 assert_eq!(status.active, 1);
503 assert_eq!(status.configured, vec![1]);
504 assert_eq!(status.in_use, vec![1]);
505 assert!(status.is_consistent());
506 }
507
508 #[tokio::test]
509 async fn new_secret_uses_active_version() {
510 let store = two_version_store(2);
511 store.set_secret("k", "v").await.unwrap();
512
513 let status = store.secret_key_status().await.unwrap();
514 assert_eq!(status.in_use, vec![2]);
515 assert_eq!(status.retirable, vec![1]);
516 }
517
518 #[tokio::test]
519 async fn secret_written_with_old_version_stays_readable() {
520 let mut store = two_version_store(1);
521 store.set_secret("legacy", "old-value").await.unwrap();
522
523 rekey(&mut store, 2);
524 store.set_secret("fresh", "new-value").await.unwrap();
525
526 assert_eq!(
527 store.get_secret("legacy").await.unwrap().unwrap().value,
528 "old-value"
529 );
530 assert_eq!(
531 store.get_secret("fresh").await.unwrap().unwrap().value,
532 "new-value"
533 );
534
535 let status = store.secret_key_status().await.unwrap();
536 assert_eq!(status.in_use, vec![1, 2]);
537 assert!(status.retirable.is_empty());
538 }
539
540 #[tokio::test]
541 async fn secret_key_status_reports_missing_version() {
542 let mut store = two_version_store(2);
543 store.set_secret("k", "v").await.unwrap();
544
545 store.set_key_ring(KeyRing::from_spec(&format!("1:{}", hex_key(0xaa)), Some(1)).unwrap());
547
548 let status = store.secret_key_status().await.unwrap();
549 assert_eq!(status.missing, vec![2]);
550 assert!(!status.is_consistent());
551 }
552
553 #[tokio::test]
554 async fn secret_with_unconfigured_version_fails_to_read() {
555 let mut store = two_version_store(2);
556 store.set_secret("k", "v").await.unwrap();
557
558 store.set_key_ring(KeyRing::from_spec(&format!("1:{}", hex_key(0xaa)), Some(1)).unwrap());
559
560 let err = store.get_secret("k").await.unwrap_err().to_string();
561 assert!(err.contains("key version 2"));
562 assert!(err.contains("not configured"));
563 }
564
565 #[tokio::test]
566 async fn rotate_moves_whole_stock_to_target_version() {
567 let mut store = two_version_store(1);
568 for i in 0..5 {
569 store
570 .set_secret(&format!("k{i}"), &format!("v{i}"))
571 .await
572 .unwrap();
573 }
574 rekey(&mut store, 2);
575
576 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
577 assert_eq!(batch.rotated, 5);
578 assert_eq!(batch.failed, 0);
579 assert_eq!(batch.remaining, 0);
580 assert!(batch.is_complete());
581
582 let status = store.secret_key_status().await.unwrap();
583 assert_eq!(status.in_use, vec![2]);
584 assert_eq!(status.retirable, vec![1]);
585
586 for i in 0..5 {
587 assert_eq!(
588 store
589 .get_secret(&format!("k{i}"))
590 .await
591 .unwrap()
592 .unwrap()
593 .value,
594 format!("v{i}")
595 );
596 }
597 }
598
599 #[tokio::test]
600 async fn rotate_is_idempotent() {
601 let mut store = two_version_store(1);
602 store.set_secret("k", "v").await.unwrap();
603 rekey(&mut store, 2);
604
605 let first = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
606 assert_eq!(first.rotated, 1);
607
608 let second = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
609 assert_eq!(second.rotated, 0);
610 assert_eq!(second.remaining, 0);
611 assert!(second.last_id.is_none());
612 assert!(second.is_complete());
613
614 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
615 }
616
617 #[tokio::test]
618 async fn rotate_on_empty_store_is_a_noop() {
619 let store = two_version_store(2);
620 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
621
622 assert_eq!(batch.rotated, 0);
623 assert_eq!(batch.failed, 0);
624 assert_eq!(batch.remaining, 0);
625 assert!(batch.last_id.is_none());
626 assert!(batch.is_complete());
627 }
628
629 #[tokio::test]
630 async fn rotate_batch_by_batch_with_cursor() {
631 let mut store = two_version_store(1);
632 for i in 0..3 {
633 store
634 .set_secret(&format!("k{i}"), &format!("v{i}"))
635 .await
636 .unwrap();
637 }
638 rekey(&mut store, 2);
639
640 let mut cursor = None;
641 let mut batches = 0;
642 loop {
643 let mut request = RotationRequest::new(2).with_batch_size(1);
644 if let Some(id) = cursor {
645 request = request.after(id);
646 }
647 let batch = store.rotate_secrets(request).await.unwrap();
648 batches += 1;
649 assert_eq!(batch.rotated, 1);
650
651 for i in 0..3 {
654 assert_eq!(
655 store
656 .get_secret(&format!("k{i}"))
657 .await
658 .unwrap()
659 .unwrap()
660 .value,
661 format!("v{i}")
662 );
663 }
664
665 if batch.is_complete() {
666 break;
667 }
668 cursor = batch.last_id;
669 }
670
671 assert_eq!(batches, 3);
672 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![2]);
673 }
674
675 #[tokio::test]
676 async fn interrupted_rotation_resumes_from_scratch() {
677 let mut store = two_version_store(1);
678 for i in 0..4 {
679 store
680 .set_secret(&format!("k{i}"), &format!("v{i}"))
681 .await
682 .unwrap();
683 }
684 rekey(&mut store, 2);
685
686 let partial = store
688 .rotate_secrets(RotationRequest::new(2).with_batch_size(2))
689 .await
690 .unwrap();
691 assert_eq!(partial.rotated, 2);
692 assert_eq!(partial.remaining, 2);
693
694 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1, 2]);
696 for i in 0..4 {
697 assert_eq!(
698 store
699 .get_secret(&format!("k{i}"))
700 .await
701 .unwrap()
702 .unwrap()
703 .value,
704 format!("v{i}")
705 );
706 }
707
708 let resumed = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
710 assert_eq!(resumed.rotated, 2);
711 assert_eq!(resumed.remaining, 0);
712 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![2]);
713 }
714
715 #[tokio::test]
716 async fn rotate_preserves_identity_and_timestamps() {
717 let mut store = two_version_store(1);
718 let before = store.set_secret("k", "v").await.unwrap();
719 rekey(&mut store, 2);
720
721 store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
722
723 let after = store.get_secret("k").await.unwrap().unwrap();
724 assert_eq!(after.id, before.id);
725 assert_eq!(after.key, before.key);
726 assert_eq!(after.value, before.value);
727 assert_eq!(after.created_at, before.created_at);
728 assert_eq!(after.updated_at, before.updated_at);
729 }
730
731 #[tokio::test]
732 async fn rotate_changes_the_ciphertext() {
733 let mut store = two_version_store(1);
734 store.set_secret("k", "v").await.unwrap();
735
736 let before = {
737 let state = store.state.read().await;
738 state.secrets["k"].encrypted_value.clone()
739 };
740
741 rekey(&mut store, 2);
742 store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
743
744 let state = store.state.read().await;
745 assert_ne!(state.secrets["k"].encrypted_value, before);
746 assert_eq!(state.secrets["k"].key_version, 2);
747 }
748
749 #[tokio::test]
750 async fn rotate_to_unconfigured_version_fails_without_touching_anything() {
751 let store = two_version_store(1);
752 store.set_secret("k", "v").await.unwrap();
753
754 let err = store
755 .rotate_secrets(RotationRequest::new(9))
756 .await
757 .unwrap_err()
758 .to_string();
759 assert!(err.contains("key version 9"));
760 assert!(err.contains("1, 2"));
761
762 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1]);
763 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
764 }
765
766 #[tokio::test]
767 async fn rotate_without_key_ring_fails() {
768 let store = InMemoryStore::new();
769 let err = store
770 .rotate_secrets(RotationRequest::new(1))
771 .await
772 .unwrap_err();
773 assert!(err.to_string().contains("no master key"));
774
775 let err = store.secret_key_status().await.unwrap_err();
776 assert!(err.to_string().contains("no master key"));
777 }
778
779 #[tokio::test]
780 async fn rotate_batch_size_is_clamped() {
781 let mut store = two_version_store(1);
782 for i in 0..3 {
783 store.set_secret(&format!("k{i}"), "v").await.unwrap();
784 }
785 rekey(&mut store, 2);
786
787 let batch = store
789 .rotate_secrets(RotationRequest::new(2).with_batch_size(0))
790 .await
791 .unwrap();
792 assert_eq!(batch.rotated, 1);
793 assert_eq!(batch.remaining, 2);
794 }
795
796 #[tokio::test]
797 async fn undecryptable_secret_is_counted_as_failed_and_skipped() {
798 let mut store = two_version_store(1);
799 store.set_secret("broken", "v").await.unwrap();
800
801 let wrong = format!("1:{},2:{}", hex_key(0xcc), hex_key(0xbb));
804 store.set_key_ring(KeyRing::from_spec(&wrong, Some(2)).unwrap());
805
806 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
807 assert_eq!(batch.rotated, 0);
808 assert_eq!(batch.failed, 1);
809 assert!(batch.last_id.is_some());
811 assert_eq!(batch.remaining, 0);
812 assert!(batch.is_complete());
813
814 let state = store.state.read().await;
816 assert_eq!(state.secrets["broken"].key_version, 1);
817 }
818
819 #[tokio::test]
820 async fn failed_secret_does_not_block_the_rest_of_the_batch() {
821 let mut store = two_version_store(1);
822 store.set_secret("a", "va").await.unwrap();
823 store.set_secret("b", "vb").await.unwrap();
824 store.set_secret("c", "vc").await.unwrap();
825
826 rekey(&mut store, 2);
828 store.set_secret("b", "vb").await.unwrap();
829
830 let wrong = format!("1:{},2:{}", hex_key(0xcc), hex_key(0xbb));
833 store.set_key_ring(KeyRing::from_spec(&wrong, Some(2)).unwrap());
834
835 let batch = store.rotate_secrets(RotationRequest::new(2)).await.unwrap();
836 assert_eq!(batch.failed, 2);
837 assert_eq!(batch.rotated, 0);
838 assert_eq!(batch.remaining, 0);
839
840 assert_eq!(store.get_secret("b").await.unwrap().unwrap().value, "vb");
841 }
842
843 #[tokio::test]
844 async fn rotate_backwards_to_an_older_version() {
845 let mut store = two_version_store(2);
846 store.set_secret("k", "v").await.unwrap();
847 rekey(&mut store, 1);
848
849 let batch = store.rotate_secrets(RotationRequest::new(1)).await.unwrap();
850 assert_eq!(batch.rotated, 1);
851 assert_eq!(store.secret_key_status().await.unwrap().in_use, vec![1]);
852 assert_eq!(store.get_secret("k").await.unwrap().unwrap().value, "v");
853 }
854}