kcode-k1-web-cache-resolution 0.1.1

Build one frozen K1 Web cache resolution view
Documentation
use kcode_k1_transaction_id::TxId;
use kcode_k1_web_cache_resolution::{ResolutionEntry, ResolutionView};
use kcode_k1_web_package::{
    AuthorityId, DependencySelector, SourceFile, SourcePackage, WebFamily, WebId,
};
use semver::Version;
use std::fs;
use std::sync::Arc;

fn tx(byte: u8) -> TxId {
    TxId::from_bytes([byte; 12])
}
fn family(byte: u8, name: &str) -> WebFamily {
    WebFamily::new(AuthorityId::new(tx(byte)), name).unwrap()
}

fn package(
    authority: u8,
    name: &str,
    version: &str,
    body: &str,
    dependencies: &str,
) -> SourcePackage {
    let id = WebId::new(family(authority, name), Version::parse(version).unwrap()).unwrap();
    let manifest = format!(
        r#"{{"name":"{name}","version":"{version}","entry":"index.js","tests":"tests.js","dependencies":{dependencies}}}"#
    );
    SourcePackage::new(
        id,
        vec![
            SourceFile::new("Documentation.md", name.as_bytes().to_vec()),
            SourceFile::new("index.js", body.as_bytes().to_vec()),
            SourceFile::new("k1-web.json", manifest.into_bytes()),
            SourceFile::new("tests.js", b"export {};".to_vec()),
        ],
    )
    .unwrap()
}

fn selection(
    selector: &str,
    package: Arc<SourcePackage>,
    winner: u8,
) -> (ResolutionEntry, Arc<SourcePackage>) {
    let resolved = package.id().clone();
    (
        ResolutionEntry {
            family: resolved.family().clone(),
            selector: DependencySelector::parse(selector).unwrap(),
            resolved,
            winning: tx(winner),
        },
        package,
    )
}

fn view() -> ResolutionView {
    let dependencies = format!(
        r#"[{{"authority":"{}","name":"dep","selector":"*"}},{{"authority":"{}","name":"dep","selector":"1"}}]"#,
        tx(3),
        tx(3)
    );
    let candidate = package(1, "app", "1.0.0", "candidate", &dependencies);
    let one = Arc::new(package(3, "dep", "1.0.0", "one", "[]"));
    ResolutionView::from_snapshot(
        &candidate,
        vec![selection("1", Arc::clone(&one), 7), selection("*", one, 7)],
    )
    .unwrap()
}

fn replace_once(text: String, from: &str, to: &str) -> String {
    assert!(text.contains(from));
    text.replacen(from, to, 1)
}

fn reject_manifest(edit: impl FnOnce(String) -> String) {
    let view = view();
    let root = tempfile::tempdir().unwrap();
    view.materialize(root.path()).unwrap();
    let path = root.path().join("manifest");
    let original = fs::read_to_string(&path).unwrap();
    let changed = edit(original.clone());
    assert_ne!(changed, original);
    fs::write(path, changed).unwrap();
    assert!(ResolutionView::inspect(root.path()).is_err());
}

fn replace_first_digest(text: String, replacement: &str) -> String {
    let line = text
        .lines()
        .find(|line| line.starts_with("f\t"))
        .unwrap()
        .to_owned();
    let prefix = line.rsplit_once('\t').unwrap().0;
    replace_once(text, &line, &format!("{prefix}\t{replacement}"))
}

#[test]
fn validates_materializes_and_inspects_canonical_view() {
    let view = view();
    assert_eq!(view.entries()[0].selector.to_string(), "*");
    let root = tempfile::tempdir().unwrap();
    view.materialize(root.path()).unwrap();
    assert_eq!(
        ResolutionView::inspect(root.path()).unwrap(),
        Some(view.digest())
    );
    assert_eq!(
        fs::read(
            root.path()
                .join(format!("web-libs/{}/dep/1.0.0/index.js", tx(3)))
        )
        .unwrap(),
        b"one"
    );
}

#[test]
fn roundtrips_canonical_empty_view() {
    let candidate = package(1, "app", "1.0.0", "candidate", "[]");
    let view = ResolutionView::from_snapshot(&candidate, Vec::new()).unwrap();
    let root = tempfile::tempdir().unwrap();
    view.materialize(root.path()).unwrap();
    assert!(view.entries().is_empty());
    assert_eq!(
        fs::read(root.path().join("manifest")).unwrap(),
        b"K1WEBRESOLUTION3\n"
    );
    assert_eq!(
        ResolutionView::inspect(root.path()).unwrap(),
        Some(view.digest())
    );
    let mut names = fs::read_dir(root.path())
        .unwrap()
        .map(|entry| entry.unwrap().file_name().into_string().unwrap())
        .collect::<Vec<_>>();
    names.sort();
    assert_eq!(names, vec!["manifest".to_owned(), "web-libs".to_owned()]);
    assert!(
        fs::read_dir(root.path().join("web-libs"))
            .unwrap()
            .next()
            .is_none()
    );
}

#[test]
fn rejects_incomplete_or_inconsistent_snapshot() {
    let dependencies = format!(
        r#"[{{"authority":"{}","name":"dep","selector":"*"}},{{"authority":"{}","name":"dep","selector":"1"}}]"#,
        tx(3),
        tx(3)
    );
    let candidate = package(1, "app", "1.0.0", "candidate", &dependencies);
    let one = Arc::new(package(3, "dep", "1.0.0", "one", "[]"));
    let different = Arc::new(package(3, "dep", "1.0.0", "different", "[]"));
    assert!(
        ResolutionView::from_snapshot(
            &candidate,
            vec![selection("*", one, 7), selection("1", different, 7)]
        )
        .is_err()
    );
}

#[test]
fn rejects_tampered_extra_and_misplaced_files() {
    let view = view();
    let root = tempfile::tempdir().unwrap();
    view.materialize(root.path()).unwrap();
    let package = root.path().join(format!("web-libs/{}/dep/1.0.0", tx(3)));
    fs::write(package.join("index.js"), "tampered").unwrap();
    assert!(ResolutionView::inspect(root.path()).is_err());
    fs::write(package.join("index.js"), "one").unwrap();
    fs::write(package.join("extra.js"), "extra").unwrap();
    assert!(ResolutionView::inspect(root.path()).is_err());
    fs::remove_file(package.join("extra.js")).unwrap();
    let misplaced = root.path().join(format!("web-libs/{}/wrong/1.0.0", tx(3)));
    fs::create_dir_all(&misplaced).unwrap();
    fs::write(misplaced.join("index.js"), "one").unwrap();
    assert!(ResolutionView::inspect(root.path()).is_err());
}

#[test]
fn rejects_noncanonical_manifest_identity_fields() {
    let authority = tx(3).to_string();
    let winner = tx(7).to_string();
    for (from, to) in [
        (
            format!("r\t{authority}\tdep\t"),
            String::from("r\tABCDEFABCDEFABCDEFABCDEF\tdep\t"),
        ),
        (String::from("\tdep\t*\t"), String::from("\tBad\t*\t")),
        (
            String::from("\tdep\t*\t1.0.0\t"),
            String::from("\tdep\t01\t1.0.0\t"),
        ),
        (
            String::from("\tdep\t*\t1.0.0\t"),
            String::from("\tdep\t*\t01.0.0\t"),
        ),
        (
            format!("\t{winner}\n"),
            String::from("\tABCDEFABCDEFABCDEFABCDEF\n"),
        ),
        (String::from("/dep/1.0.0/"), String::from("/dep/01.0.0/")),
    ] {
        reject_manifest(|text| replace_once(text, &from, &to));
    }
    reject_manifest(|text| replace_first_digest(text, &"A".repeat(64)));
}

#[test]
fn rejects_duplicate_out_of_order_and_mismatched_manifest_records() {
    reject_manifest(|text| {
        let route = text
            .lines()
            .rev()
            .find(|line| line.starts_with("r\t"))
            .unwrap()
            .to_owned();
        format!("{text}{route}\n")
    });
    reject_manifest(|text| {
        let mut lines = text.lines().map(str::to_owned).collect::<Vec<_>>();
        let routes = lines
            .iter()
            .enumerate()
            .filter_map(|(index, line)| line.starts_with("r\t").then_some(index))
            .collect::<Vec<_>>();
        assert_eq!(routes.len(), 2);
        lines.swap(routes[0], routes[1]);
        format!("{}\n", lines.join("\n"))
    });
    reject_manifest(|text| replace_once(text, "\tdep\t1\t1.0.0\t", "\tdep\t1\t2.0.0\t"));
    reject_manifest(|text| replace_first_digest(text, &"0".repeat(64)));
}