1use serde::{Deserialize, Serialize};
35
36use crate::storage::{MlsStorage, StorageError};
37use crate::GroupId;
38
39pub const CONFORMANCE_KEY_TYPE: &str = "mls_storage_conformance_probe";
44
45pub const CONFORMANCE_KEY_TYPE_OTHER: &str = "mls_storage_conformance_probe_other";
47
48const CONFORMANCE_KEY_TYPE_UNUSED: &str = "mls_storage_conformance_probe_unused";
51
52const CONFORMANCE_KEY_TYPE_SUFFIXED: &str = "suffixed_mls_storage_conformance_probe";
59
60const ISOLATION_PROBE_ID: &str = "mls_storage_conformance_isolation";
64
65const ISOLATION_LISTING_PROBE_ID: &str = "mls_storage_conformance_isolation_listing";
68
69pub const LARGE_RECORD_BYTES: usize = 512 * 1024;
78
79const CONCURRENT_WRITERS: usize = 8;
81const WRITES_PER_WRITER: usize = 16;
83const CONCURRENT_VALUE_BYTES: usize = 4096;
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ConformanceFailure {
90 pub check: String,
92 pub detail: String,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct ConformanceReport {
99 pub passed: Vec<String>,
101 pub failures: Vec<ConformanceFailure>,
103}
104
105impl ConformanceReport {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn is_green(&self) -> bool {
113 self.failures.is_empty()
114 }
115
116 pub fn summary(&self) -> String {
118 if self.is_green() {
119 format!("{} checks passed", self.passed.len())
120 } else {
121 let names: Vec<&str> = self
122 .failures
123 .iter()
124 .map(|failure| failure.check.as_str())
125 .collect();
126 format!(
127 "{} of {} checks failed: {}",
128 self.failures.len(),
129 self.passed.len() + self.failures.len(),
130 names.join(", ")
131 )
132 }
133 }
134
135 pub fn pass(&mut self, check: &str) {
137 self.passed.push(check.to_string());
138 }
139
140 pub fn fail(&mut self, check: &str, detail: impl Into<String>) {
142 self.failures.push(ConformanceFailure {
143 check: check.to_string(),
144 detail: detail.into(),
145 });
146 }
147
148 pub fn check(&mut self, name: &str, outcome: Result<(), String>) {
150 match outcome {
151 Ok(()) => self.pass(name),
152 Err(detail) => self.fail(name, detail),
153 }
154 }
155}
156
157fn expect_load(
158 storage: &dyn MlsStorage,
159 key_type: &str,
160 key_id: &str,
161 expected: &[u8],
162) -> Result<(), String> {
163 match storage.load(key_type, key_id) {
164 Ok(Some(actual)) if actual == expected => Ok(()),
165 Ok(Some(actual)) => Err(format!(
166 "loaded {} bytes, expected {} (contents differ)",
167 actual.len(),
168 expected.len()
169 )),
170 Ok(None) => Err("stored value loaded back as missing".to_string()),
171 Err(err) => Err(format!("load failed: {err}")),
172 }
173}
174
175fn store_and_read_back(
176 storage: &dyn MlsStorage,
177 key_id: &str,
178 value: &[u8],
179 what: &str,
180) -> Result<(), String> {
181 storage
182 .store(CONFORMANCE_KEY_TYPE, key_id, value)
183 .map_err(|err| format!("store of {what} failed: {err}"))?;
184 expect_load(storage, CONFORMANCE_KEY_TYPE, key_id, value)
185 .map_err(|detail| format!("{what}: {detail}"))
186}
187
188pub fn run(storage: &dyn MlsStorage) -> ConformanceReport {
194 let mut report = ConformanceReport::new();
195
196 let isolation = key_types_are_isolated(storage);
206 let isolated = isolation.is_ok();
207 report.check("key_types_are_separate_namespaces", isolation);
208 if !isolated {
209 return report;
210 }
211
212 cleanup(storage);
215
216 report.check(
218 "store_then_load",
219 store_and_read_back(storage, "basic", b"hello", "a value"),
220 );
221
222 let binary: Vec<u8> = (0u8..=255).collect();
226 report.check(
227 "binary_values_round_trip",
228 store_and_read_back(storage, "binary", &binary, "a binary value"),
229 );
230
231 report.check(
233 "empty_value_is_not_missing",
234 store_and_read_back(storage, "empty", b"", "an empty value"),
235 );
236
237 let outcome = storage
241 .store(CONFORMANCE_KEY_TYPE, "overwrite", b"first")
242 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, "overwrite", b"second"))
243 .map_err(|err| format!("store failed: {err}"))
244 .and_then(|()| expect_load(storage, CONFORMANCE_KEY_TYPE, "overwrite", b"second"));
245 report.check("store_overwrites", outcome);
246
247 let outcome = match storage.load(CONFORMANCE_KEY_TYPE, "never_written") {
251 Ok(None) => Ok(()),
252 Ok(Some(_)) => Err("a key that was never written returned a value".to_string()),
253 Err(StorageError::KeyNotFound(_)) => {
254 Err("absent keys must load as Ok(None), not KeyNotFound".to_string())
255 }
256 Err(err) => Err(format!("load of an absent key failed: {err}")),
257 };
258 report.check("absent_key_loads_as_none", outcome);
259
260 let outcome = storage
264 .store(CONFORMANCE_KEY_TYPE, "deletable", b"x")
265 .and_then(|()| storage.delete(CONFORMANCE_KEY_TYPE, "deletable"))
266 .map_err(|err| format!("store/delete failed: {err}"))
267 .and_then(|()| match storage.load(CONFORMANCE_KEY_TYPE, "deletable") {
268 Ok(None) => Ok(()),
269 Ok(Some(_)) => Err("value survived a delete".to_string()),
270 Err(err) => Err(format!("load after delete failed: {err}")),
271 })
272 .and_then(
273 |()| match storage.delete(CONFORMANCE_KEY_TYPE, "deletable") {
274 Ok(()) => Ok(()),
275 Err(err) => Err(format!("second delete must succeed, got: {err}")),
276 },
277 );
278 report.check("delete_removes_and_is_idempotent", outcome);
279
280 let outcome = (|| -> Result<(), String> {
284 storage
285 .store(CONFORMANCE_KEY_TYPE, "shared_id", b"mine")
286 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs"))
287 .map_err(|err| format!("store failed: {err}"))?;
288 let keys = storage
289 .list_keys(CONFORMANCE_KEY_TYPE)
290 .map_err(|err| format!("list_keys failed: {err}"))?;
291 for expected in ["basic", "binary", "empty", "overwrite", "shared_id"] {
292 if !keys.iter().any(|key| key == expected) {
293 return Err(format!("list_keys omitted {expected:?}"));
294 }
295 }
296 if keys.iter().any(|key| key == "deletable") {
297 return Err("list_keys named a deleted record".to_string());
298 }
299 let other = storage
300 .list_keys(CONFORMANCE_KEY_TYPE_OTHER)
301 .map_err(|err| format!("list_keys failed: {err}"))?;
302 if other.len() != 1 || other.first().map(String::as_str) != Some("shared_id") {
303 return Err(format!("list_keys leaked across key types: {other:?}"));
304 }
305 Ok(())
306 })();
307 report.check("list_keys_is_accurate_and_scoped", outcome);
308
309 let outcome = match storage.list_keys(CONFORMANCE_KEY_TYPE_UNUSED) {
312 Ok(keys) if keys.is_empty() => Ok(()),
313 Ok(keys) => Err(format!("an unused key type listed {} keys", keys.len())),
314 Err(err) => Err(format!("list_keys on an unused key type failed: {err}")),
315 };
316 report.check("unused_key_type_lists_empty", outcome);
317
318 let session_id = "session:off1qq6k7tx3adkz9gkvwt6c5c4v8lkh5g66ru6jm3s0dm\
327 :off1qq7wtkw2pmld2pcj3zn5fvhhj3kj5sgnlwq0mnh2yg7d";
328 let ids = [
329 ("a key package id", "8f2c4d3a-4b6e-4c1f-9d0a-1e2f3a4b5c6d"),
330 (
331 "an OpenMLS record id",
332 "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
333 ),
334 ("a session id", session_id),
335 ];
336 let outcome = (|| -> Result<(), String> {
337 for (what, id) in ids {
338 store_and_read_back(storage, id, what.as_bytes(), what)?;
339 }
340 let keys = storage
341 .list_keys(CONFORMANCE_KEY_TYPE)
342 .map_err(|err| format!("list_keys failed: {err}"))?;
343 for (what, id) in ids {
344 if !keys.iter().any(|key| key == id) {
345 return Err(format!("list_keys did not return {what} verbatim"));
346 }
347 }
348 Ok(())
349 })();
350 report.check("key_ids_as_the_sdk_writes_them", outcome);
351
352 let exact_ids: [(&str, &[u8]); 7] = [
359 ("group:Team", b"upper case"),
360 ("group:team", b"lower case"),
361 ("group:caf\u{e9}", b"composed"),
362 ("group:cafe\u{301}", b"decomposed"),
363 ("group:CON", b"a reserved device name"),
364 ("group:a*b?", b"wildcards"),
365 ("group:abc.", b"a trailing dot"),
366 ];
367 let outcome = (|| -> Result<(), String> {
368 for (id, _) in exact_ids {
369 if GroupId::new(id).is_err() {
370 return Err(format!("{id:?} is not a valid group id; fix the suite"));
371 }
372 }
373 for (id, value) in exact_ids {
374 storage
375 .store(CONFORMANCE_KEY_TYPE, id, value)
376 .map_err(|err| format!("store of {id:?} failed: {err}"))?;
377 }
378 for (id, value) in exact_ids {
379 expect_load(storage, CONFORMANCE_KEY_TYPE, id, value)
380 .map_err(|detail| format!("{id:?}: {detail}"))?;
381 }
382 let keys = storage
383 .list_keys(CONFORMANCE_KEY_TYPE)
384 .map_err(|err| format!("list_keys failed: {err}"))?;
385 for (id, _) in exact_ids {
386 if !keys.iter().any(|key| key == id) {
387 return Err(format!("list_keys did not return {id:?} verbatim"));
388 }
389 }
390 Ok(())
391 })();
392 report.check("key_ids_are_byte_exact", outcome);
393
394 let long_key = format!(
400 "session:{}",
401 "a".repeat(GroupId::MAX_LEN - "session:".len())
402 );
403 let outcome = (|| -> Result<(), String> {
404 if GroupId::new(long_key.as_str()).is_err() {
405 return Err("the probe id is not a valid group id; fix the suite".to_string());
406 }
407 store_and_read_back(storage, &long_key, b"long", "a maximum-length key id")
408 })();
409 report.check("long_key_ids_round_trip", outcome);
410
411 let large = vec![0xa5u8; LARGE_RECORD_BYTES];
416 report.check(
417 "large_records_round_trip",
418 store_and_read_back(storage, "large", &large, "a large record"),
419 );
420
421 let outcome = (|| -> Result<(), String> {
427 let describe = |err: StorageError| format!("exists failed: {err}");
428 if storage
429 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
430 .map_err(describe)?
431 {
432 return Err("exists reported a key that was never written".to_string());
433 }
434 storage
435 .store(CONFORMANCE_KEY_TYPE, "exists_probe", b"x")
436 .map_err(|err| format!("store failed: {err}"))?;
437 if !storage
438 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
439 .map_err(describe)?
440 {
441 return Err("exists missed a key that was just stored".to_string());
442 }
443 if storage
444 .exists(CONFORMANCE_KEY_TYPE_OTHER, "exists_probe")
445 .map_err(describe)?
446 {
447 return Err("exists found a key under the wrong key type".to_string());
448 }
449 storage
450 .delete(CONFORMANCE_KEY_TYPE, "exists_probe")
451 .map_err(|err| format!("delete failed: {err}"))?;
452 if storage
453 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
454 .map_err(describe)?
455 {
456 return Err("exists reported a deleted key".to_string());
457 }
458 Ok(())
459 })();
460 report.check("exists_agrees_with_load", outcome);
461
462 let outcome = (|| -> Result<(), String> {
469 storage
470 .store(CONFORMANCE_KEY_TYPE_OTHER, "survivor", b"kept")
471 .map_err(|err| format!("store failed: {err}"))?;
472 storage
473 .clear_type(CONFORMANCE_KEY_TYPE)
474 .map_err(|err| format!("clear_type failed: {err}"))?;
475 let cleared = storage
476 .list_keys(CONFORMANCE_KEY_TYPE)
477 .map_err(|err| format!("list_keys failed: {err}"))?;
478 if !cleared.is_empty() {
479 return Err(format!(
480 "{} records survived clear_type on their own key type",
481 cleared.len()
482 ));
483 }
484 expect_load(storage, CONFORMANCE_KEY_TYPE_OTHER, "survivor", b"kept")
485 .map_err(|_| "clear_type reached into another key type".to_string())?;
486 expect_load(storage, CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs")
487 .map_err(|_| "clear_type reached into another key type".to_string())?;
488 storage
489 .clear_type(CONFORMANCE_KEY_TYPE)
490 .map_err(|err| format!("clear_type on an emptied key type failed: {err}"))?;
491 storage
492 .clear_type(CONFORMANCE_KEY_TYPE_UNUSED)
493 .map_err(|err| format!("clear_type on an unused key type failed: {err}"))?;
494 Ok(())
495 })();
496 report.check("clear_type_is_scoped_and_idempotent", outcome);
497
498 report.check("concurrent_writes_land_whole", concurrent_writes(storage));
504
505 let outcome = (|| -> Result<(), String> {
508 cleanup(storage);
509 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
510 let remaining = storage
511 .list_keys(key_type)
512 .map_err(|err| format!("list_keys failed: {err}"))?;
513 if !remaining.is_empty() {
514 return Err(format!(
515 "{} records survived a listed delete in {key_type}",
516 remaining.len()
517 ));
518 }
519 }
520 Ok(())
521 })();
522 report.check("listed_records_can_all_be_deleted", outcome);
523
524 report
525}
526
527fn key_types_are_isolated(storage: &dyn MlsStorage) -> Result<(), String> {
536 let outcome = (|| -> Result<(), String> {
537 storage
538 .store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID, b"theirs")
539 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID, b"mine"))
540 .and_then(|()| {
541 storage.store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID, b"x")
542 })
543 .and_then(|()| {
544 storage.store(CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID, b"x")
545 })
546 .map_err(|err| {
547 format!("store failed, so isolation could not be checked and nothing was deleted: {err}")
548 })?;
549 if let Ok(Some(other)) = storage.load(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID) {
550 if other == b"mine" {
551 return Err(
552 "the same key id in two key types collided: a write under one \
553 replaced the other"
554 .to_string(),
555 );
556 }
557 }
558 if let Ok(listed) = storage.list_keys(CONFORMANCE_KEY_TYPE) {
559 if listed.iter().any(|key| key == ISOLATION_LISTING_PROBE_ID) {
560 return Err("list_keys named a record written under another key type".to_string());
561 }
562 }
563 Ok(())
564 })();
565 for (key_type, key_id) in [
566 (CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID),
567 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID),
568 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID),
569 (CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID),
570 ] {
571 let _ = storage.delete(key_type, key_id);
572 }
573 outcome
574}
575
576fn concurrent_writes(storage: &dyn MlsStorage) -> Result<(), String> {
582 const SHARED_KEY: &str = "concurrent_shared";
583 let shared_values: Vec<Vec<u8>> = (0..CONCURRENT_WRITERS)
584 .map(|writer| vec![writer as u8 + 1; CONCURRENT_VALUE_BYTES])
585 .collect();
586
587 let own_value = |writer: usize, round: usize| -> Vec<u8> {
588 let mut value = vec![writer as u8 + 1; CONCURRENT_VALUE_BYTES];
589 value[0] = round as u8;
590 value
591 };
592
593 let outcomes: Vec<Result<(), String>> = std::thread::scope(|scope| {
594 let handles: Vec<_> = (0..CONCURRENT_WRITERS)
595 .map(|writer| {
596 let shared_values = &shared_values;
597 let own_value = &own_value;
598 std::thread::Builder::new().spawn_scoped(scope, move || -> Result<(), String> {
599 let own_key = format!("concurrent_writer_{writer}");
600 for round in 0..WRITES_PER_WRITER {
601 storage
602 .store(CONFORMANCE_KEY_TYPE, &own_key, &own_value(writer, round))
603 .map_err(|err| format!("writer {writer}: store failed: {err}"))?;
604 storage
605 .store(CONFORMANCE_KEY_TYPE, SHARED_KEY, &shared_values[writer])
606 .map_err(|err| format!("writer {writer}: store failed: {err}"))?;
607 match storage.load(CONFORMANCE_KEY_TYPE, SHARED_KEY) {
608 Ok(Some(seen)) if shared_values.contains(&seen) => {}
609 Ok(Some(seen)) => {
610 return Err(format!(
611 "writer {writer} read a torn value ({} bytes) from a key \
612 under concurrent overwrite",
613 seen.len()
614 ))
615 }
616 Ok(None) => {
617 return Err(format!(
618 "writer {writer} found a key it had just written missing \
619 under concurrent overwrite"
620 ))
621 }
622 Err(err) => {
623 return Err(format!(
624 "writer {writer}: load under concurrent writes failed: {err}"
625 ))
626 }
627 }
628 }
629 expect_load(
630 storage,
631 CONFORMANCE_KEY_TYPE,
632 &own_key,
633 &own_value(writer, WRITES_PER_WRITER - 1),
634 )
635 .map_err(|detail| format!("writer {writer}'s own key: {detail}"))
636 })
637 })
638 .collect();
639 handles
640 .into_iter()
641 .map(|spawned| match spawned {
642 Ok(handle) => handle.join().unwrap_or_else(|_| {
643 Err("a writer thread panicked inside the adapter".to_string())
644 }),
645 Err(err) => Err(format!("could not start a writer thread: {err}")),
646 })
647 .collect()
648 });
649 for outcome in outcomes {
650 outcome?;
651 }
652
653 match storage.load(CONFORMANCE_KEY_TYPE, SHARED_KEY) {
654 Ok(Some(seen)) if shared_values.contains(&seen) => Ok(()),
655 Ok(Some(seen)) => Err(format!(
656 "after all writers finished the shared key held a torn value ({} bytes)",
657 seen.len()
658 )),
659 Ok(None) => Err("after all writers finished the shared key was missing".to_string()),
660 Err(err) => Err(format!("load after concurrent writes failed: {err}")),
661 }
662}
663
664fn cleanup(storage: &dyn MlsStorage) {
666 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
667 if let Ok(keys) = storage.list_keys(key_type) {
668 for key in keys {
669 let _ = storage.delete(key_type, &key);
670 }
671 }
672 }
673}
674
675pub fn run_json(storage: &dyn MlsStorage) -> String {
680 let report = run(storage);
681 serde_json::to_string(&report).unwrap_or_else(|err| {
682 format!(
683 "{{\"passed\":[],\"failures\":[{{\"check\":\"report_serialization\",\"detail\":\"{}\"}}]}}",
684 err.to_string().replace('"', "'")
685 )
686 })
687}
688
689#[cfg(test)]
690mod tests {
691 use std::collections::HashMap;
692 use std::sync::Mutex;
693
694 use super::*;
695 use crate::storage::{InMemoryStorage, StorageResult};
696
697 #[test]
698 fn the_suite_is_green_against_the_in_memory_store() {
699 let storage = InMemoryStorage::new();
700 let report = run(&storage);
701 assert!(report.is_green(), "{}", report.summary());
702 assert_eq!(report.passed.len(), 17, "{:?}", report.passed);
703 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
704 assert!(
705 storage.list_keys(key_type).unwrap().is_empty(),
706 "the suite left records behind in {key_type}"
707 );
708 }
709 }
710
711 #[test]
712 fn the_suite_leaves_records_outside_its_probe_key_types_alone() {
713 let storage = InMemoryStorage::new();
714 storage.store("identity", "key_pair", b"secret").unwrap();
715 let report = run(&storage);
716 assert!(report.is_green(), "{}", report.summary());
717 assert_eq!(
718 storage.load("identity", "key_pair").unwrap(),
719 Some(b"secret".to_vec())
720 );
721 assert_eq!(storage.list_keys("identity").unwrap(), vec!["key_pair"]);
722 }
723
724 #[test]
725 fn the_report_renders_the_documented_json_shape() {
726 let json = run_json(&InMemoryStorage::new());
727 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
728 assert!(parsed["passed"].as_array().is_some_and(|p| p.len() == 17));
729 assert!(parsed["failures"].as_array().is_some_and(Vec::is_empty));
730
731 let mut report = ConformanceReport::new();
732 report.fail("x", "why");
733 let parsed: serde_json::Value =
734 serde_json::from_str(&serde_json::to_string(&report).unwrap()).unwrap();
735 assert_eq!(parsed["failures"][0]["check"], "x");
736 assert_eq!(parsed["failures"][0]["detail"], "why");
737 }
738
739 #[derive(Debug, Clone, Copy)]
742 enum Fault {
743 None,
744 CorruptsValues,
745 StringTypedValues,
746 EmptyReadsAsMissing,
747 KeepsFirstWrite,
748 AbsentIsAnError,
749 DeleteOfAbsentFails,
750 IgnoresKeyType,
751 ListsBySuffix,
752 ListReturnsFirstPageOnly,
753 ListOfUnknownTypeFails,
754 KeepsIdUpToFirstColon,
755 CaseFoldsKeyIds,
756 CapsKeyIdAt255,
757 TruncatesValuesAt64KiB,
758 ExistsAlwaysFalse,
759 ClearTypeClearsEverything,
760 TearsOverwrites,
761 DeleteIsANoOp,
762 }
763
764 struct Faulty {
765 fault: Fault,
766 records: Mutex<HashMap<(String, String), Vec<u8>>>,
767 previous: Mutex<HashMap<(String, String), Vec<u8>>>,
768 }
769
770 impl Faulty {
771 fn new(fault: Fault) -> Self {
772 Self {
773 fault,
774 records: Mutex::default(),
775 previous: Mutex::default(),
776 }
777 }
778
779 fn key(&self, key_type: &str, key_id: &str) -> (String, String) {
780 let key_type = match self.fault {
781 Fault::IgnoresKeyType => "",
782 _ => key_type,
783 };
784 let key_id = match self.fault {
785 Fault::KeepsIdUpToFirstColon => key_id.split(':').next().unwrap_or("").to_string(),
786 Fault::CaseFoldsKeyIds => key_id.to_lowercase(),
787 _ => key_id.to_string(),
788 };
789 (key_type.to_string(), key_id)
790 }
791 }
792
793 impl MlsStorage for Faulty {
794 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()> {
795 if matches!(self.fault, Fault::CapsKeyIdAt255) && key_id.len() > 255 {
796 return Err(StorageError::StoreFailed("key too long".to_string()));
797 }
798 let mut data = data.to_vec();
799 if matches!(self.fault, Fault::TruncatesValuesAt64KiB) {
800 data.truncate(64 * 1024);
801 }
802 if matches!(self.fault, Fault::StringTypedValues) {
803 data = String::from_utf8_lossy(&data).into_owned().into_bytes();
804 }
805 let key = self.key(key_type, key_id);
806 let mut records = self.records.lock().unwrap();
807 if matches!(self.fault, Fault::KeepsFirstWrite) {
808 records.entry(key).or_insert(data);
809 return Ok(());
810 }
811 if let Some(old) = records.insert(key.clone(), data) {
812 self.previous.lock().unwrap().insert(key, old);
813 }
814 Ok(())
815 }
816
817 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>> {
818 let key = self.key(key_type, key_id);
819 let current = self.records.lock().unwrap().get(&key).cloned();
820 match (self.fault, current) {
821 (Fault::AbsentIsAnError, None) => {
822 Err(StorageError::KeyNotFound(key_id.to_string()))
823 }
824 (Fault::EmptyReadsAsMissing, Some(current)) if current.is_empty() => Ok(None),
825 (Fault::CorruptsValues, Some(mut current)) if !current.is_empty() => {
826 current[0] ^= 0xff;
827 Ok(Some(current))
828 }
829 (Fault::TearsOverwrites, Some(current)) => {
830 let previous = self.previous.lock().unwrap().get(&key).cloned();
833 match previous {
834 Some(previous)
835 if previous != current && previous.len() == current.len() =>
836 {
837 let half = current.len() / 2;
838 let mut torn = previous[..half].to_vec();
839 torn.extend_from_slice(¤t[half..]);
840 Ok(Some(torn))
841 }
842 _ => Ok(Some(current)),
843 }
844 }
845 (_, current) => Ok(current),
846 }
847 }
848
849 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()> {
850 if matches!(self.fault, Fault::DeleteIsANoOp) {
851 return Ok(());
852 }
853 let key = self.key(key_type, key_id);
854 let removed = self.records.lock().unwrap().remove(&key);
855 if removed.is_none() && matches!(self.fault, Fault::DeleteOfAbsentFails) {
856 return Err(StorageError::KeyNotFound(key_id.to_string()));
857 }
858 Ok(())
859 }
860
861 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>> {
862 let (key_type, _) = self.key(key_type, "");
863 let keys: Vec<String> = self
864 .records
865 .lock()
866 .unwrap()
867 .keys()
868 .filter(|(kt, _)| match self.fault {
869 Fault::ListsBySuffix => kt.ends_with(key_type.as_str()),
872 _ => *kt == key_type,
873 })
874 .map(|(_, id)| id.clone())
875 .collect();
876 match self.fault {
877 Fault::ListReturnsFirstPageOnly => Ok(keys.into_iter().take(4).collect()),
879 Fault::ListOfUnknownTypeFails if keys.is_empty() => Err(StorageError::LoadFailed(
882 format!("no such key type: {key_type}"),
883 )),
884 _ => Ok(keys),
885 }
886 }
887
888 fn exists(&self, key_type: &str, key_id: &str) -> StorageResult<bool> {
889 if matches!(self.fault, Fault::ExistsAlwaysFalse) {
890 return Ok(false);
891 }
892 Ok(self.load(key_type, key_id)?.is_some())
893 }
894
895 fn clear_type(&self, key_type: &str) -> StorageResult<()> {
896 let mut records = self.records.lock().unwrap();
897 if matches!(self.fault, Fault::ClearTypeClearsEverything) {
898 records.clear();
899 return Ok(());
900 }
901 let (key_type, _) = self.key(key_type, "");
902 records.retain(|(kt, _), _| *kt != key_type);
903 Ok(())
904 }
905 }
906
907 #[test]
908 fn the_fault_injecting_store_is_green_with_no_fault() {
909 let report = run(&Faulty::new(Fault::None));
911 assert!(report.is_green(), "{}", report.summary());
912 }
913
914 #[test]
915 fn the_suite_catches_each_defect_class() {
916 let cases = [
919 (Fault::IgnoresKeyType, "key_types_are_separate_namespaces"),
920 (Fault::ListsBySuffix, "key_types_are_separate_namespaces"),
921 (Fault::CorruptsValues, "store_then_load"),
922 (Fault::StringTypedValues, "binary_values_round_trip"),
923 (Fault::EmptyReadsAsMissing, "empty_value_is_not_missing"),
924 (Fault::KeepsFirstWrite, "store_overwrites"),
925 (Fault::AbsentIsAnError, "absent_key_loads_as_none"),
926 (
927 Fault::DeleteOfAbsentFails,
928 "delete_removes_and_is_idempotent",
929 ),
930 (
931 Fault::ListReturnsFirstPageOnly,
932 "list_keys_is_accurate_and_scoped",
933 ),
934 (Fault::ListOfUnknownTypeFails, "unused_key_type_lists_empty"),
935 (
936 Fault::KeepsIdUpToFirstColon,
937 "key_ids_as_the_sdk_writes_them",
938 ),
939 (Fault::CaseFoldsKeyIds, "key_ids_are_byte_exact"),
940 (Fault::CapsKeyIdAt255, "long_key_ids_round_trip"),
941 (Fault::TruncatesValuesAt64KiB, "large_records_round_trip"),
942 (Fault::ExistsAlwaysFalse, "exists_agrees_with_load"),
943 (
944 Fault::ClearTypeClearsEverything,
945 "clear_type_is_scoped_and_idempotent",
946 ),
947 (Fault::TearsOverwrites, "concurrent_writes_land_whole"),
948 (Fault::DeleteIsANoOp, "listed_records_can_all_be_deleted"),
949 ];
950 let covered: std::collections::BTreeSet<&str> =
953 cases.iter().map(|(_, check)| *check).collect();
954 let checks: std::collections::BTreeSet<String> =
955 run(&InMemoryStorage::new()).passed.into_iter().collect();
956 let uncovered: Vec<&String> = checks
957 .iter()
958 .filter(|check| !covered.contains(check.as_str()))
959 .collect();
960 assert!(
961 uncovered.is_empty(),
962 "checks with no negative control: {uncovered:?}"
963 );
964
965 for (fault, expected) in cases {
966 let report = run(&Faulty::new(fault));
967 assert!(!report.is_green(), "{fault:?} passed the suite");
968 assert!(
969 report.failures.iter().any(|f| f.check == expected),
970 "{fault:?}: expected {expected} to fail, got: {}",
971 report.summary()
972 );
973 }
974 }
975
976 #[test]
977 fn the_absent_key_check_names_the_wrong_error_variant() {
978 let report = run(&Faulty::new(Fault::AbsentIsAnError));
979 let failure = report
980 .failures
981 .iter()
982 .find(|f| f.check == "absent_key_loads_as_none")
983 .expect("the check failed");
984 assert!(failure.detail.contains("KeyNotFound"), "{}", failure.detail);
985 }
986 #[test]
987 fn a_backend_that_lists_by_suffix_stops_the_run_before_any_delete() {
988 let storage = Faulty::new(Fault::ListsBySuffix);
992 storage
993 .store("prefixed_mls_storage_conformance_probe", "real", b"kept")
994 .unwrap();
995 let report = run(&storage);
996 assert!(report.passed.is_empty(), "{:?}", report.passed);
997 assert_eq!(
998 report.failures[0].check,
999 "key_types_are_separate_namespaces"
1000 );
1001 assert_eq!(
1002 storage
1003 .load("prefixed_mls_storage_conformance_probe", "real")
1004 .unwrap(),
1005 Some(b"kept".to_vec())
1006 );
1007 }
1008
1009 #[test]
1010 fn a_backend_that_merges_key_types_stops_the_run_before_any_delete() {
1011 let storage = Faulty::new(Fault::IgnoresKeyType);
1016 storage.store("identity", "key_pair", b"secret").unwrap();
1017 storage.store("group_state", "group:abc", b"state").unwrap();
1018 let report = run(&storage);
1019 assert!(report.passed.is_empty(), "{:?}", report.passed);
1020 assert_eq!(report.failures.len(), 1, "{}", report.summary());
1021 assert_eq!(
1022 report.failures[0].check,
1023 "key_types_are_separate_namespaces"
1024 );
1025 assert_eq!(
1026 storage.load("identity", "key_pair").unwrap(),
1027 Some(b"secret".to_vec())
1028 );
1029 assert_eq!(
1030 storage.load("group_state", "group:abc").unwrap(),
1031 Some(b"state".to_vec())
1032 );
1033 }
1034}