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