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//! Both [`InMemoryStore`] and [`S3Store`] are 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 — including from `spawn_blocking` when testing async
21/// backends like [`S3Store`].
22pub fn run_compliance_tests(store: &dyn ObjectStore) {
23    blob_round_trip(store);
24    blob_missing_returns_none(store);
25    blob_has(store);
26    blob_list(store);
27    tree_round_trip(store);
28    tree_missing_returns_none(store);
29    state_round_trip(store);
30    state_has(store);
31    state_list(store);
32}
33
34// ── Blob ─────────────────────────────────────────────────────────────────────
35
36fn blob_round_trip(store: &dyn ObjectStore) {
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(store: &dyn ObjectStore) {
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(store: &dyn ObjectStore) {
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(store: &dyn ObjectStore) {
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(store: &dyn ObjectStore) {
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_missing_returns_none(store: &dyn ObjectStore) {
93    let hash = ContentHash::compute(b"compliance-nonexistent-tree");
94    let result = store
95        .get_tree(&hash)
96        .expect("get_tree error on missing key");
97    assert!(
98        result.is_none(),
99        "get_tree should return None for unknown hash"
100    );
101}
102
103// ── State ─────────────────────────────────────────────────────────────────────
104
105fn state_round_trip(store: &dyn ObjectStore) {
106    let tree = Tree::new();
107    let tree_hash = store
108        .put_tree(&tree)
109        .expect("put_tree in state test failed");
110    let state = State::new(tree_hash, vec![], attribution());
111    let id = state.change_id;
112
113    store.put_state(&state).expect("put_state failed");
114
115    let got = store
116        .get_state(&id)
117        .expect("get_state failed")
118        .expect("state missing after put");
119
120    assert_eq!(got.change_id, id, "change_id changed after round-trip");
121    assert_eq!(got.tree, tree_hash, "tree hash changed after round-trip");
122}
123
124fn state_has(store: &dyn ObjectStore) {
125    let tree = Tree::new();
126    let tree_hash = store
127        .put_tree(&tree)
128        .expect("put_tree in state test failed");
129    let state = State::new(tree_hash, vec![], attribution());
130    let id = state.change_id;
131    store.put_state(&state).expect("put_state failed");
132    assert!(
133        store.has_state(&id).expect("has_state failed"),
134        "has_state returned false immediately after put"
135    );
136}
137
138fn state_list(store: &dyn ObjectStore) {
139    let tree = Tree::new();
140    let tree_hash = store
141        .put_tree(&tree)
142        .expect("put_tree in state test failed");
143    let state = State::new(tree_hash, vec![], attribution());
144    let id = state.change_id;
145    store.put_state(&state).expect("put_state failed");
146    let ids = store.list_states().expect("list_states failed");
147    assert!(
148        ids.contains(&id),
149        "list_states does not contain id after put"
150    );
151}