Skip to main content

objects/store/
store_compliance.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Backend-agnostic compliance test suite for [`ObjectStore`] implementations.
3//!
4//! Call [`run_compliance_tests`] from any `#[test]` or `#[tokio::test]` that
5//! has a concrete [`ObjectStore`] to verify it satisfies the full contract.
6//! `InMemoryStore` is validated this way.
7
8use crate::{
9    object::{Attribution, Blob, ContentHash, Principal, State, Tree},
10    store::ObjectStore,
11};
12
13fn attribution() -> Attribution {
14    Attribution::human(Principal::new("Compliance Test", "test@example.com"))
15}
16
17/// Run the full ObjectStore compliance suite against `store`.
18///
19/// Panics on the first assertion failure. Designed to be called from unit or
20/// integration tests.
21pub fn run_compliance_tests<S: ObjectStore>(store: &S) {
22    blob_round_trip(store);
23    blob_missing_returns_none(store);
24    blob_has(store);
25    blob_list(store);
26    tree_round_trip(store);
27    tree_missing_returns_none(store);
28    state_round_trip(store);
29    state_has(store);
30    state_list(store);
31}
32
33// ── Blob ─────────────────────────────────────────────────────────────────────
34
35fn blob_round_trip<S: ObjectStore>(store: &S) {
36    let blob = Blob::from("compliance: blob round-trip");
37    let hash = store.put_blob(&blob).expect("put_blob failed");
38    let got = store
39        .get_blob(&hash)
40        .expect("get_blob failed")
41        .expect("blob missing after put");
42    assert_eq!(
43        got.content(),
44        blob.content(),
45        "blob content changed after round-trip"
46    );
47}
48
49fn blob_missing_returns_none<S: ObjectStore>(store: &S) {
50    let hash = ContentHash::compute(b"compliance-nonexistent-blob");
51    let result = store
52        .get_blob(&hash)
53        .expect("get_blob error on missing key");
54    assert!(
55        result.is_none(),
56        "get_blob should return None for unknown hash"
57    );
58}
59
60fn blob_has<S: ObjectStore>(store: &S) {
61    let blob = Blob::from("compliance: has_blob");
62    let hash = store.put_blob(&blob).expect("put_blob failed");
63    assert!(
64        store.has_blob(&hash).expect("has_blob failed"),
65        "has_blob returned false immediately after put"
66    );
67}
68
69fn blob_list<S: ObjectStore>(store: &S) {
70    let blob = Blob::from("compliance: list_blobs");
71    let hash = store.put_blob(&blob).expect("put_blob failed");
72    let list = store.list_blobs().expect("list_blobs failed");
73    assert!(
74        list.contains(&hash),
75        "list_blobs does not contain hash after put"
76    );
77}
78
79// ── Tree ──────────────────────────────────────────────────────────────────────
80
81fn tree_round_trip<S: ObjectStore>(store: &S) {
82    let tree = Tree::new();
83    let hash = store.put_tree(&tree).expect("put_tree failed");
84    let got = store
85        .get_tree(&hash)
86        .expect("get_tree failed")
87        .expect("tree missing after put");
88    assert_eq!(got.hash(), hash, "tree hash changed after round-trip");
89}
90
91fn tree_missing_returns_none<S: ObjectStore>(store: &S) {
92    let hash = ContentHash::compute(b"compliance-nonexistent-tree");
93    let result = store
94        .get_tree(&hash)
95        .expect("get_tree error on missing key");
96    assert!(
97        result.is_none(),
98        "get_tree should return None for unknown hash"
99    );
100}
101
102// ── State ─────────────────────────────────────────────────────────────────────
103
104fn state_round_trip<S: ObjectStore>(store: &S) {
105    let tree = Tree::new();
106    let tree_hash = store
107        .put_tree(&tree)
108        .expect("put_tree in state test failed");
109    let state = State::new(tree_hash, vec![], attribution());
110    let id = state.change_id;
111
112    store.put_state(&state).expect("put_state failed");
113
114    let got = store
115        .get_state(&id)
116        .expect("get_state failed")
117        .expect("state missing after put");
118
119    assert_eq!(got.change_id, id, "change_id changed after round-trip");
120    assert_eq!(got.tree, tree_hash, "tree hash changed after round-trip");
121}
122
123fn state_has<S: ObjectStore>(store: &S) {
124    let tree = Tree::new();
125    let tree_hash = store
126        .put_tree(&tree)
127        .expect("put_tree in state test failed");
128    let state = State::new(tree_hash, vec![], attribution());
129    let id = state.change_id;
130    store.put_state(&state).expect("put_state failed");
131    assert!(
132        store.has_state(&id).expect("has_state failed"),
133        "has_state returned false immediately after put"
134    );
135}
136
137fn state_list<S: ObjectStore>(store: &S) {
138    let tree = Tree::new();
139    let tree_hash = store
140        .put_tree(&tree)
141        .expect("put_tree in state test failed");
142    let state = State::new(tree_hash, vec![], attribution());
143    let id = state.change_id;
144    store.put_state(&state).expect("put_state failed");
145    let ids = store.list_states().expect("list_states failed");
146    assert!(
147        ids.contains(&id),
148        "list_states does not contain id after put"
149    );
150}