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/graceful_backend_init.rs

//! A repository whose backend fails to instantiate (e.g. a corrupt or
//! version-incompatible znippy archive) must be *disabled*, not fatal — the
//! server still serves every other repo. Only an all-repos-failed config
//! hard-fails. Regression cover for the WISHLIST "graceful archive errors" item.

use holger_server_lib::read_ron_config;

fn load(ron: &str, tmp: &std::path::Path) -> holger_server_lib::Holger {
    let path = tmp.join("h.ron");
    std::fs::write(&path, ron).unwrap();
    read_ron_config(&path).unwrap()
}

#[test]
fn corrupt_archive_disables_only_that_repo() {
    let tmp = tempfile::tempdir().unwrap();
    // A garbage file that is NOT a valid znippy archive.
    let bad = tmp.path().join("bad.znippy");
    std::fs::write(&bad, b"this is not a znippy archive").unwrap();
    let data_dir = tmp.path().join("rust-dev");

    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0" ) ],
    storage_endpoints: [ ( ron_name: "s", ron_storage_type: "file", ron_path: "{store}" ) ],
    repositories: [
        ( ron_name: "good", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_data_dir: Some("{dd}") ),
        ( ron_name: "bad", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_archive_path: Some("{bad}") ),
    ],
)"#,
        store = tmp.path().display(),
        dd = data_dir.display(),
        bad = bad.display(),
    );

    let mut holger = load(&ron, tmp.path());
    // Must NOT bail despite the one corrupt archive.
    holger
        .instantiate_backends()
        .expect("one corrupt archive must not fail the whole server");

    let good = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "good")
        .unwrap();
    let bad_repo = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "bad")
        .unwrap();
    assert!(
        good.backend_repository.is_some(),
        "good repo should be live"
    );
    assert!(
        bad_repo.backend_repository.is_none(),
        "corrupt-archive repo should be disabled"
    );
}

#[test]
fn all_repos_failing_hard_fails() {
    let tmp = tempfile::tempdir().unwrap();
    let bad = tmp.path().join("bad.znippy");
    std::fs::write(&bad, b"garbage").unwrap();

    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0" ) ],
    storage_endpoints: [ ( ron_name: "s", ron_storage_type: "file", ron_path: "{store}" ) ],
    repositories: [
        ( ron_name: "bad", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_archive_path: Some("{bad}") ),
    ],
)"#,
        store = tmp.path().display(),
        bad = bad.display(),
    );

    let mut holger = load(&ron, tmp.path());
    let err = holger
        .instantiate_backends()
        .expect_err("a config where every repo fails must hard-fail");
    assert!(err.to_string().contains("nothing to serve"), "got: {err}");
}

/// A znippy archive written by a **newer znippy than this build reads** must be
/// treated exactly like any other unusable archive: the repo backing it is
/// disabled, every other repo stays live, and the server keeps serving. This is
/// the server-level half of the format-version pin (`traits::znippy_format`) —
/// the backend-level half lives in
/// `server/repository/znippy-rust/tests/format_version_gate.rs`.
///
/// RED-when-broken: without holger's own pin an archive from an unknown future
/// format reaches the reader, and the failure mode this guards is precisely that
/// it takes the process down rather than one repo.
#[test]
fn future_format_archive_disables_only_that_repo_and_the_server_serves_on() {
    let tmp = tempfile::tempdir().unwrap();

    // A REAL archive, packed by znippy, then edited to claim a format version from
    // the future. The edit is length-preserving (see the crafting note in
    // `format_version_gate.rs`): value slot `[len][ascii][NUL][pad → 8]` followed
    // by `[21]["znippy_format_version"]`, so `"3"` → `"39"` shifts no offset and
    // the archive differs in exactly one thing — the format it declares.
    let base = tmp.path().join("future");
    let compressor = znippy_compress::compress_stream(&base, true).unwrap();
    let sender = compressor.sender().clone();
    sender
        .send(znippy_compress::ArchiveEntry {
            relative_path: "serde-1.0.200.crate".into(),
            data: vec![7u8; 4096],
            pkg_type: None,
            repo: None,
        })
        .unwrap();
    drop(sender);
    compressor.finish().unwrap();

    let future_archive = base.with_extension("znippy");
    let current = traits::recorded_format_version(&future_archive)
        .expect("a packed archive records its format version");
    let future = format!("{current}9");
    let slot = |value: &str| -> Vec<u8> {
        let key = traits::ZNIPPY_FORMAT_VERSION_KEY;
        let mut v = Vec::new();
        v.extend_from_slice(&(value.len() as u32).to_le_bytes());
        v.extend_from_slice(value.as_bytes());
        while v.len() % 8 != 0 {
            v.push(0);
        }
        v.extend_from_slice(&(key.len() as u32).to_le_bytes());
        v.extend_from_slice(key.as_bytes());
        v
    };
    let (needle, patch) = (slot(&current), slot(&future));
    let mut bytes = std::fs::read(&future_archive).unwrap();
    let mut hits = 0usize;
    let mut i = 0usize;
    while i + needle.len() <= bytes.len() {
        if bytes[i..i + needle.len()] == needle[..] {
            bytes[i..i + patch.len()].copy_from_slice(&patch);
            hits += 1;
            i += needle.len();
        } else {
            i += 1;
        }
    }
    assert!(
        hits > 0,
        "no recorded format version to patch — update the crafting recipe"
    );
    std::fs::write(&future_archive, &bytes).unwrap();
    assert!(
        traits::ensure_supported_format(&future_archive).is_err(),
        "the crafted archive must be one holger refuses"
    );

    let data_dir = tmp.path().join("rust-dev");
    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0" ) ],
    storage_endpoints: [ ( ron_name: "s", ron_storage_type: "file", ron_path: "{store}" ) ],
    repositories: [
        ( ron_name: "good", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_data_dir: Some("{dd}") ),
        ( ron_name: "fromthefuture", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_archive_path: Some("{bad}") ),
    ],
)"#,
        store = tmp.path().display(),
        dd = data_dir.display(),
        bad = future_archive.display(),
    );

    let mut holger = load(&ron, tmp.path());
    holger
        .instantiate_backends()
        .expect("an archive from a future znippy format must not fail the whole server");

    let good = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "good")
        .unwrap();
    let future_repo = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "fromthefuture")
        .unwrap();
    assert!(
        good.backend_repository.is_some(),
        "the good repo must still be live"
    );
    assert!(
        future_repo.backend_repository.is_none(),
        "a repo on an unreadable-format archive must be disabled, not fatal"
    );

    // AND IT STILL SERVES — the live repo answers a real request afterwards.
    let backend = good.backend_repository.as_ref().unwrap();
    let (status, _h, body) = backend
        .handle_http2_request("GET", "/good/index/config.json", b"")
        .expect("the surviving repo must still answer requests");
    assert_eq!(
        status, 200,
        "the server serves on after refusing the future archive"
    );
    assert!(!body.is_empty());
}