Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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())
    );
}