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