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

//! **A `znippy-build-thing` repo, configured the way an operator configures
//! one, and answering over the real routing.**
//!
//! The backend crate's own tests drive `BuildThingRepoZnippy` directly. That
//! leaves the seam this file covers: `ron_repo_type: "znippy-build-thing"`
//! reaching that backend at all. A backend that exists and is never selected is
//! the same as no backend, and it is exactly what a missing match arm looks
//! like.

use std::path::Path;

use holger_server_lib::read_ron_config;

const SHA: &str = "1111111111111111111111111111111111111111111111111111111111111111";

/// Seal a minimal but real build-history archive.
fn seal(path: &Path) {
    let attestation = format!(
        r#"{{"attestation_version":1,"receipt":{{"receipt_version":1,"build_thing":{{"krate":"korp-build-thing","version":"0.3.1","hash":"a91f4c2e"}},"atom":"korp-server","target":"exe","settings":{{}},"inputs":{{}}}},"artifact":{{"sha256":"{SHA}","bytes":4096}}}}"#
    );
    let index = format!(
        r#"{{"kind":"znippy-build-thing","package_version":1,"attestations":[{{"artifact_sha256":"{SHA}","artifact_bytes":4096,"build_thing":"korp-build-thing:0.3.1:a91f4c2e","build_thing_krate":"korp-build-thing","build_thing_version":"0.3.1","atom":"korp-server","target":"exe","path":"attestations/{SHA}.json"}}]}}"#
    );
    let files = vec![
        ("index.json".to_string(), index.into_bytes()),
        (format!("attestations/{SHA}.json"), attestation.into_bytes()),
        (
            "by-recipe/korp-build-thing".to_string(),
            SHA.as_bytes().to_vec(),
        ),
        (
            "by-recipe/korp-build-thing/0.3.1".to_string(),
            SHA.as_bytes().to_vec(),
        ),
        ("by-atom/korp-server".to_string(), SHA.as_bytes().to_vec()),
    ];
    // Packed the way `graceful_backend_init` packs its fixtures — the stream API
    // this repo already uses in tests, rather than a second way to make an
    // archive. `compress_stream` appends the extension itself.
    let base = path.with_extension("");
    let compressor = znippy_compress::compress_stream(&base, true).expect("compressor");
    let sender = compressor.sender().clone();
    for (relative_path, data) in files {
        sender
            .send(znippy_compress::ArchiveEntry {
                relative_path,
                data,
                pkg_type: None,
                repo: None,
            })
            .expect("send entry");
    }
    drop(sender);
    compressor.finish().expect("seal");
    assert!(path.is_file(), "no archive at {}", path.display());
}

fn config(tmp: &Path, archive: &Path) -> String {
    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: "history", ron_repo_type: "znippy-build-thing", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_archive_path: Some("{archive}") ),
    ],
)"#,
        store = tmp.display(),
        archive = archive.display(),
    )
}

/// The repo type selects the build-history backend, and that backend answers
/// the question the package exists for.
#[test]
fn the_repo_type_reaches_the_build_history_backend_and_it_answers() {
    let tmp = tempfile::tempdir().unwrap();
    let archive = tmp.path().join("history.znippy");
    seal(&archive);

    let cfg = tmp.path().join("h.ron");
    std::fs::write(&cfg, config(tmp.path(), &archive)).unwrap();
    let mut holger = read_ron_config(&cfg).unwrap();
    holger
        .instantiate_backends()
        .expect("a znippy-build-thing repo must instantiate");

    let repo = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "history")
        .unwrap();
    let backend = repo
        .backend_repository
        .as_ref()
        .expect("the repo type must select a backend — an unselected backend is no backend");

    // Read-only: a provenance store anyone can append to attests nothing.
    assert!(!backend.is_writable());

    // What did this recipe build? — over the wired route, not the direct call.
    let (code, _headers, body) = backend
        .handle_http2_request("GET", "/history/recipe/korp-build-thing", b"")
        .expect("answer");
    assert_eq!(code, 200, "{}", String::from_utf8_lossy(&body));
    assert_eq!(String::from_utf8_lossy(&body).trim(), SHA);

    // One attestation, by the hash of what it attests.
    let (code, _headers, body) = backend
        .handle_http2_request("GET", &format!("/history/artifact/{SHA}"), b"")
        .expect("answer");
    assert_eq!(code, 200);
    assert!(String::from_utf8_lossy(&body).contains("korp-server"));

    // THE RED PROBE: the routing is the build-history routing and not a generic
    // path passthrough that would answer anything. A rust repo's sparse-index
    // route means nothing here and must miss.
    let (code, _headers, _) = backend
        .handle_http2_request("GET", "/history/index/config.json", b"")
        .expect("answer");
    assert_eq!(
        code, 404,
        "a build-history repo answered a cargo sparse-index route, so the repo type \
         is not selecting the backend under test"
    );

    assert_eq!(backend.list(None, 0).expect("list").len(), 1);
}

/// A corrupt build-history archive disables that repo rather than the server —
/// the same contract every other znippy-backed repo has, and the red probe for
/// the test above (which would pass just as happily if instantiation never
/// checked the archive at all).
#[test]
fn a_corrupt_build_history_archive_disables_only_that_repo() {
    let tmp = tempfile::tempdir().unwrap();
    let good = tmp.path().join("good.znippy");
    seal(&good);
    let bad = tmp.path().join("bad.znippy");
    std::fs::write(&bad, b"this is not a znippy archive").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: "history", ron_repo_type: "znippy-build-thing", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "s", ron_exposed_endpoint: "e" )),
          ron_archive_path: Some("{good}") ),
        ( ron_name: "rubble", ron_repo_type: "znippy-build-thing", 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(),
        good = good.display(),
        bad = bad.display(),
    );
    let cfg = tmp.path().join("h.ron");
    std::fs::write(&cfg, ron).unwrap();
    let mut holger = read_ron_config(&cfg).unwrap();
    holger
        .instantiate_backends()
        .expect("one corrupt archive must not fail the whole server");

    let live = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "history")
        .unwrap();
    let dead = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == "rubble")
        .unwrap();
    assert!(live.backend_repository.is_some());
    assert!(
        dead.backend_repository.is_none(),
        "a corrupt archive must disable its repo — otherwise instantiation never \
         opened the archive and the healthy case above proves nothing"
    );
}