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    tree_streaming_round_trip(store);
29    state_round_trip(store);
30    state_has(store);
31    state_list(store);
32}
33
34// ── Blob ─────────────────────────────────────────────────────────────────────
35
36fn blob_round_trip<S: ObjectStore>(store: &S) {
37    let blob = Blob::from("compliance: blob round-trip");
38    let hash = store.put_blob(&blob).expect("put_blob failed");
39    let got = store
40        .get_blob(&hash)
41        .expect("get_blob failed")
42        .expect("blob missing after put");
43    assert_eq!(
44        got.content(),
45        blob.content(),
46        "blob content changed after round-trip"
47    );
48}
49
50fn blob_missing_returns_none<S: ObjectStore>(store: &S) {
51    let hash = ContentHash::compute(b"compliance-nonexistent-blob");
52    let result = store
53        .get_blob(&hash)
54        .expect("get_blob error on missing key");
55    assert!(
56        result.is_none(),
57        "get_blob should return None for unknown hash"
58    );
59}
60
61fn blob_has<S: ObjectStore>(store: &S) {
62    let blob = Blob::from("compliance: has_blob");
63    let hash = store.put_blob(&blob).expect("put_blob failed");
64    assert!(
65        store.has_blob(&hash).expect("has_blob failed"),
66        "has_blob returned false immediately after put"
67    );
68}
69
70fn blob_list<S: ObjectStore>(store: &S) {
71    let blob = Blob::from("compliance: list_blobs");
72    let hash = store.put_blob(&blob).expect("put_blob failed");
73    let list = store.list_blobs().expect("list_blobs failed");
74    assert!(
75        list.contains(&hash),
76        "list_blobs does not contain hash after put"
77    );
78}
79
80// ── Tree ──────────────────────────────────────────────────────────────────────
81
82fn tree_round_trip<S: ObjectStore>(store: &S) {
83    let tree = Tree::new();
84    let hash = store.put_tree(&tree).expect("put_tree failed");
85    let got = store
86        .get_tree(&hash)
87        .expect("get_tree failed")
88        .expect("tree missing after put");
89    assert_eq!(got.hash(), hash, "tree hash changed after round-trip");
90}
91
92fn tree_streaming_round_trip<S: ObjectStore>(store: &S) {
93    use crate::error::HeddleError;
94    use crate::object::{TreeEntry, TreePageLimits, TreeStreamError};
95
96    let blob = ContentHash::compute(b"compliance-stream");
97    let tree = Tree::from_entries(vec![
98        TreeEntry::file("a.txt", blob, false).expect("a"),
99        TreeEntry::file("b.txt", blob, true).expect("b"),
100    ]);
101    let hash = store.put_tree(&tree).expect("put_tree failed");
102    let mut reader = store
103        .open_tree(&hash, None)
104        .expect("open_tree")
105        .expect("tree body");
106    let page = reader
107        .next_page(TreePageLimits::new(1, usize::MAX).expect("limits"))
108        .expect("page")
109        .expect("first page");
110    assert_eq!(page.entries.len(), 1);
111    assert_eq!(page.entries[0].name(), "a.txt");
112    let rest = reader
113        .next_page(TreePageLimits::new(8, usize::MAX).expect("limits"))
114        .expect("page")
115        .expect("rest");
116    assert_eq!(rest.entries[0].name(), "b.txt");
117    reader.finish_and_verify().expect("verify");
118
119    match store.open_tree(&hash, Some(&page.resume_cursor)) {
120        Ok(Some(mut resumed)) => {
121            let rest = resumed
122                .next_page(TreePageLimits::new(8, usize::MAX).expect("limits"))
123                .expect("page")
124                .expect("rest");
125            assert_eq!(rest.entries[0].name(), "b.txt");
126            resumed.finish_and_verify().expect("verify");
127        }
128        Err(HeddleError::TreeStream(TreeStreamError::UnverifiedRange)) => {}
129        other => panic!("resume must succeed on verified placement or fail closed: {other:?}"),
130    }
131}
132
133fn tree_missing_returns_none<S: ObjectStore>(store: &S) {
134    let hash = ContentHash::compute(b"compliance-nonexistent-tree");
135    let result = store
136        .get_tree(&hash)
137        .expect("get_tree error on missing key");
138    assert!(
139        result.is_none(),
140        "get_tree should return None for unknown hash"
141    );
142}
143
144// ── State ─────────────────────────────────────────────────────────────────────
145
146fn state_round_trip<S: ObjectStore>(store: &S) {
147    let tree = Tree::new();
148    let tree_hash = store
149        .put_tree(&tree)
150        .expect("put_tree in state test failed");
151    let state = State::new(tree_hash, vec![], attribution());
152    let id = state.id();
153
154    store.put_state(&state).expect("put_state failed");
155
156    let got = store
157        .get_state(&id)
158        .expect("get_state failed")
159        .expect("state missing after put");
160
161    assert_eq!(got.id(), id, "state id changed after round-trip");
162    assert_eq!(got.tree, tree_hash, "tree hash changed after round-trip");
163}
164
165fn state_has<S: ObjectStore>(store: &S) {
166    let tree = Tree::new();
167    let tree_hash = store
168        .put_tree(&tree)
169        .expect("put_tree in state test failed");
170    let state = State::new(tree_hash, vec![], attribution());
171    let id = state.id();
172    store.put_state(&state).expect("put_state failed");
173    assert!(
174        store.has_state(&id).expect("has_state failed"),
175        "has_state returned false immediately after put"
176    );
177}
178
179fn state_list<S: ObjectStore>(store: &S) {
180    let tree = Tree::new();
181    let tree_hash = store
182        .put_tree(&tree)
183        .expect("put_tree in state test failed");
184    let state = State::new(tree_hash, vec![], attribution());
185    let id = state.id();
186    store.put_state(&state).expect("put_state failed");
187    let ids = store.list_states().expect("list_states failed");
188    assert!(
189        ids.contains(&id),
190        "list_states does not contain id after put"
191    );
192}