server/lib/tests/store_persistence.rs
//! **The stores survive a restart.** holger has no database; its "db" is a set of
//! on-disk stores — the append-only audit log, the content-addressed promotion
//! seal, the property and SBOM side stores, the raw blob bucket and the tenant
//! ledger. Each module's own unit tests exercise it through ONE live handle. This
//! file exercises the other half of the durability claim: what a SECOND process
//! sees.
//!
//! Every test here writes with one handle, drops it, opens a fresh handle over the
//! same directory (the shape a server restart has) and asserts the state is
//! byte-identical and that the guarantees still bite — a seal is still immutable,
//! a spent token is still spent, a listing still round-trips to a fetch.
use std::sync::Arc;
use holger_server_lib::audit::{default_audit_log, AuditAction, AuditEvent, FileAuditLog};
#[cfg(feature = "supply-chain")]
use holger_server_lib::promote::{PromoteError, PromotionEngine};
#[cfg(feature = "supply-chain")]
use holger_server_lib::properties::PropertyStore;
use holger_server_lib::raw::RawBackend;
use holger_server_lib::replication::{replicate_pairs, ReplicationOptions, RepoPair};
#[cfg(feature = "supply-chain")]
use holger_server_lib::sbom::SbomStore;
use holger_server_lib::search::{search_repos, SearchQuery};
use traits::{ArtifactId, RepositoryBackendTrait};
fn id(ns: Option<&str>, name: &str, version: &str) -> ArtifactId {
ArtifactId {
namespace: ns.map(str::to_string),
name: name.into(),
version: version.into(),
}
}
/// The audit trail is the one store that may never lose a row. Two "runs" over the
/// same directory each append to the file; a third reader replays BOTH, in
/// order, with every field intact. RED-when-broken if a restart ever truncates,
/// rotates away or reorders what an earlier run recorded.
#[test]
fn persist_audit_events_replay_across_two_runs() {
let dir = tempfile::tempdir().unwrap();
let first = AuditEvent::new(
"CN=alice",
AuditAction::Upload,
"rust-dev",
"serde@1.0.0",
"10.0.0.1:4444",
200,
1234,
)
.with_detail("first run");
let second = AuditEvent::new(
"anonymous",
AuditAction::Download,
"rust-dev",
"serde@1.0.0",
"10.0.0.2:5555",
200,
1234,
)
.with_detail("second run");
{
let log = default_audit_log(dir.path()).unwrap();
log.record(first.clone()).unwrap();
log.flush().unwrap();
}
{
let log = default_audit_log(dir.path()).unwrap();
log.record(second.clone()).unwrap();
}
let replayed = FileAuditLog::read_dir(dir.path()).unwrap();
assert_eq!(
replayed,
vec![first, second],
"both runs' rows replay, in order, byte-for-byte"
);
assert_eq!(
std::fs::read_dir(dir.path()).unwrap().count(),
1,
"both runs appended to the one file; neither rewrote the other"
);
}
/// A sealed coordinate stays sealed across a restart: a fresh engine over the same
/// PROD root still returns the bytes, still verifies them against the blake3 in the
/// ref, and still REFUSES to re-promote. The seal is on disk, not in memory.
#[cfg(feature = "supply-chain")]
#[test]
fn persist_a_promotion_seal_is_still_immutable_after_a_restart() {
let dir = tempfile::tempdir().unwrap();
let coord = id(Some("crates"), "serde", "1.0.0");
let bytes = b"the-sealed-crate-bytes";
let sealed = PromotionEngine::new(dir.path())
.seal(
"rust-dev",
"rust-prod",
&coord,
bytes,
"alice@corp",
1_700_000_000,
)
.expect("the first seal takes");
// A fresh engine — the shape a restart has.
let reopened = PromotionEngine::new(dir.path());
assert!(
reopened.is_promoted("rust-prod", &coord),
"the seal is on disk"
);
assert_eq!(
reopened.fetch("rust-prod", &coord).unwrap().as_deref(),
Some(bytes.as_slice()),
"the bytes survive and still verify against the sealed content address"
);
assert!(
matches!(
reopened
.seal(
"rust-dev",
"rust-prod",
&coord,
b"replacement",
"mallory",
1_700_000_001
)
.unwrap_err(),
PromoteError::AlreadyPromoted
),
"immutability is a property of the store, not of the handle that wrote it"
);
assert_eq!(
reopened.fetch("rust-prod", &coord).unwrap().as_deref(),
Some(bytes.as_slice()),
"and the refused overwrite changed nothing"
);
assert_eq!(
sealed.content_address,
blake3::hash(bytes).to_hex().to_string(),
"the record names the digest of the bytes it sealed"
);
}
/// The property and SBOM side stores are what the promotion gate, the retention pin
/// and the `GET /-/sbom` door read. A restart must not lose them, and a coordinate
/// that was never written must still read as unset rather than inheriting a
/// neighbour's document.
#[cfg(feature = "supply-chain")]
#[test]
fn persist_property_and_sbom_documents_survive_a_reopen() {
let dir = tempfile::tempdir().unwrap();
let props_root = dir.path().join("holger-props");
let sbom_root = dir.path().join("holger-sboms");
let coord = id(Some("crates"), "serde", "1.0.0");
let neighbour = id(Some("crates"), "serde", "1.0.1");
let document = br#"{"bomFormat":"CycloneDX","specVersion":"1.5"}"#;
{
let props = PropertyStore::new(&props_root);
props
.set("rust-prod", &coord, "signed", vec!["true".into()])
.unwrap();
props
.set(
"rust-prod",
&coord,
"env",
vec!["prod".into(), "staging".into()],
)
.unwrap();
SbomStore::new(&sbom_root)
.attach("rust-prod", &coord, document)
.unwrap();
}
let props = PropertyStore::new(&props_root);
let sboms = SbomStore::new(&sbom_root);
assert!(
props.matches("rust-prod", &coord, "signed", "true"),
"the promotion pin survives"
);
assert_eq!(
props.get("rust-prod", &coord).get("env"),
Some(&vec!["prod".to_string(), "staging".to_string()]),
"multi-value keys survive in order"
);
assert_eq!(
sboms.fetch("rust-prod", &coord).unwrap().as_deref(),
Some(document.as_slice()),
"the hosted SBOM is served byte-identical after a restart"
);
assert!(
props.get("rust-prod", &neighbour).is_empty(),
"a neighbour coordinate is unset"
);
assert!(sboms.fetch("rust-prod", &neighbour).unwrap().is_none());
assert!(
props.get("rust-dev", &coord).is_empty(),
"and the repo is part of the key"
);
}
/// A raw bucket written by one backend handle is fully visible to the next, and the
/// listing it reports still round-trips: `list` → `fetch` → the same bytes. That
/// round-trip is what retention, GC, search and replication act on after a restart,
/// so it is the contract that matters — not merely "the files are still there".
#[test]
fn persist_raw_blobs_and_their_listing_round_trip_after_a_reopen() {
let dir = tempfile::tempdir().unwrap();
let blobs: [(&str, &[u8]); 3] = [
("reports/2024/q1.pdf", b"PDF-BYTES"),
("firmware/device.bin", b"\x00\xff binary \x00"),
("räksmörgås.txt", "smörgås".as_bytes()),
];
{
let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
for (path, bytes) in blobs {
repo.put(&id(None, path, ""), bytes).unwrap();
}
}
let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
let listed = repo.list(None, 0).unwrap();
assert_eq!(
listed.len(),
blobs.len(),
"every blob is enumerated after the reopen"
);
for entry in &listed {
let want = blobs
.iter()
.find(|(p, _)| *p == entry.id.name)
.unwrap_or_else(|| panic!("unexpected blob {:?}", entry.id.name));
assert_eq!(
repo.fetch(&entry.id).unwrap().as_deref(),
Some(want.1),
"the listed coordinate re-fetches the same bytes"
);
assert_eq!(entry.size_bytes, want.1.len() as i64);
}
// The read engines see the same store through the same trait.
let repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = vec![(
"raw".into(),
Arc::new(RawBackend::new("raw".into(), dir.path().to_path_buf())),
)];
let hits = search_repos(
&repos,
&SearchQuery {
name: Some("firmware".into()),
..Default::default()
},
);
assert_eq!(hits.artifacts.len(), 1, "search finds the persisted blob");
assert_eq!(hits.artifacts[0].id.name, "firmware/device.bin");
}
/// Replication between two on-disk raw buckets converges the destination and is
/// idempotent ACROSS restarts: a second run over fresh handles copies nothing,
/// because the dedup key is the content on disk, not anything the first run kept in
/// memory.
#[test]
fn persist_replication_between_two_disk_stores_is_idempotent_across_restarts() {
let src_dir = tempfile::tempdir().unwrap();
let dst_dir = tempfile::tempdir().unwrap();
let pair = || {
vec![RepoPair {
source_name: "raw-src".into(),
source: Arc::new(RawBackend::new(
"raw-src".into(),
src_dir.path().to_path_buf(),
)),
dest_name: "raw-dst".into(),
dest: Arc::new(RawBackend::new(
"raw-dst".into(),
dst_dir.path().to_path_buf(),
)),
gate: Default::default(),
}]
};
{
let src = RawBackend::new("raw-src".into(), src_dir.path().to_path_buf());
src.put(&id(None, "a/one.bin", ""), b"one").unwrap();
src.put(&id(None, "two.bin", ""), b"two").unwrap();
}
let first = replicate_pairs(&pair(), &ReplicationOptions::executing());
assert_eq!(first.copied(), 2, "the first run copies both blobs");
assert!(first.is_complete(), "and nothing failed");
// A fresh pair of handles over the same two directories.
let second = replicate_pairs(&pair(), &ReplicationOptions::executing());
assert_eq!(second.copied(), 0, "a re-run copies nothing");
assert_eq!(
second.skipped(),
2,
"both are already present BY CONTENT ID"
);
let dst = RawBackend::new("raw-dst".into(), dst_dir.path().to_path_buf());
assert_eq!(
dst.fetch(&id(None, "a/one.bin", "")).unwrap().as_deref(),
Some(b"one".as_slice())
);
assert_eq!(
dst.fetch(&id(None, "two.bin", "")).unwrap().as_deref(),
Some(b"two".as_slice())
);
}