use super::verify::payload_inventory;
use super::*;
fn entry(repo_did: &str) -> RepoEntry {
RepoEntry {
repo_did: repo_did.to_string(),
owner: "did:plc:owner".to_string(),
rkey: "fixture".to_string(),
name: None,
head: None,
public_refs: Vec::new(),
cob_refs: Vec::new(),
object_format: "sha1".to_string(),
fingerprint: "repo-fingerprint".to_string(),
git_export_hash: Some("export-fingerprint".to_string()),
release_id: None,
fetch_bases: Vec::new(),
shallow_branches: Vec::new(),
shallow_head: None,
moved_wants: Vec::new(),
}
}
fn fixture_manifest() -> GenerationManifest {
let mut manifest = GenerationManifest {
rebuilt_repos: Vec::new(),
generation_id: String::new(),
generator_profile: GENERATOR_PROFILE_VERSION.to_string(),
conf_template: crate::manifest::NGINX_TEMPLATE_VERSION.to_string(),
knot_did: "did:web:knot.example".to_string(),
hostname: "knot.example".to_string(),
owner: "did:plc:owner".to_string(),
static_version: "fixture".to_string(),
built_by: crate::identity::built_by().to_string(),
profile: serde_json::json!({}),
repos: vec![entry("did:plc:fixture")],
global_input_digest: None,
handles_digest: None,
stats: BuildStats::default(),
};
manifest.generation_id = manifest.compute_id();
manifest
}
fn fill_private_generation(
generation: &PrivateGeneration,
manifest: &GenerationManifest,
nginx: &[u8],
) -> anyhow::Result<()> {
let root = generation.path();
let export = root
.join("www/_git")
.join(plan::repo_token("did:plc:fixture"));
std::fs::create_dir_all(&export)?;
std::fs::write(export.join("export.fingerprint"), "export-fingerprint\n")?;
std::fs::create_dir_all(root.join("nginx/maps"))?;
std::fs::write(root.join("nginx/knot-static.conf"), nginx)?;
std::fs::write(root.join("nginx/maps/routes.conf"), b"map {}\n")?;
manifest.clone().finalize_and_write(root)
}
fn seal_release(output_root: &Path, entry: &mut RepoEntry) -> anyhow::Result<()> {
let releases = output_root
.join("repos")
.join(plan::repo_key(&entry.repo_did))
.join("releases");
let mut build = ReleaseBuild::new(&releases)?;
std::fs::create_dir_all(build.path().join("export"))?;
std::fs::write(build.path().join("export/HEAD"), b"ref: refs/heads/main\n")?;
build.record_input("export", "fixture");
build.finalize_tree("export")?;
let id = crate::release::release_id(&entry.repo_did, build.inputs());
build.publish(&releases, &id)?;
entry.release_id = Some(id);
Ok(())
}
#[test]
fn repeated_publication_reuses_generation_without_touching_files() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let generations = root.path().join("generations");
std::fs::create_dir(&generations)?;
let manifest = fixture_manifest();
let destination = generations.join(&manifest.generation_id);
let first = PrivateGeneration::new(&generations)?;
fill_private_generation(&first, &manifest, b"first config\n")?;
assert_eq!(
first.publish(&destination, &manifest)?,
Publication::Published
);
let config = destination.join("nginx/knot-static.conf");
let original = std::fs::read(&config)?;
#[cfg(unix)]
let original_inode = {
use std::os::unix::fs::MetadataExt as _;
std::fs::metadata(&config)?.ino()
};
let second = PrivateGeneration::new(&generations)?;
fill_private_generation(&second, &manifest, b"replacement config\n")?;
assert_eq!(
second.publish(&destination, &manifest)?,
Publication::Reused
);
assert_eq!(std::fs::read(&config)?, original);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
assert_eq!(std::fs::metadata(&config)?.ino(), original_inode);
}
Ok(())
}
#[test]
fn published_generation_files_refuse_writes() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let generations = root.path().join("generations");
std::fs::create_dir(&generations)?;
let manifest = fixture_manifest();
let destination = generations.join(&manifest.generation_id);
let staging = PrivateGeneration::new(&generations)?;
fill_private_generation(&staging, &manifest, b"config\n")?;
staging.publish(&destination, &manifest)?;
crate::cas::sealed::assert_sealed(&destination);
Ok(())
}
#[test]
fn corrupt_existing_generation_is_replaced_by_staging() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let generations = root.path().join("generations");
std::fs::create_dir(&generations)?;
let manifest = fixture_manifest();
let destination = generations.join(&manifest.generation_id);
let first = PrivateGeneration::new(&generations)?;
fill_private_generation(&first, &manifest, b"first config\n")?;
first.publish(&destination, &manifest)?;
let config = destination.join("nginx/knot-static.conf");
std::fs::remove_file(&config)?;
std::fs::write(&config, b"corrupt bytes\n")?;
let replacement = PrivateGeneration::new(&generations)?;
fill_private_generation(&replacement, &manifest, b"replacement config\n")?;
assert_eq!(
replacement.publish(&destination, &manifest)?,
Publication::Published
);
assert_eq!(std::fs::read(&config)?, b"replacement config\n");
Ok(())
}
#[test]
fn carried_candidates_preserve_sealed_repos_and_skip_the_hinted_one() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let mut manifest = fixture_manifest();
manifest.global_input_digest = Some("cfg".to_string());
seal_release(root.path(), &mut manifest.repos[0])?;
let global = GlobalInputs {
config_digest: "cfg".to_string(),
};
let hosted: BTreeSet<String> =
["did:plc:fixture".to_string(), "did:plc:other".to_string()].into();
let carried = carried_candidate_dids(
root.path(),
&manifest,
&global,
&hosted,
Some("did:plc:other"),
);
assert_eq!(carried, ["did:plc:fixture".to_string()].into());
let carried = carried_candidate_dids(
root.path(),
&manifest,
&global,
&hosted,
Some("did:plc:fixture"),
);
assert!(carried.is_empty());
Ok(())
}
#[test]
fn carried_candidates_require_matching_global_inputs() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let mut manifest = fixture_manifest();
manifest.global_input_digest = Some("old-config".to_string());
seal_release(root.path(), &mut manifest.repos[0])?;
let hosted: BTreeSet<String> = ["did:plc:fixture".to_string()].into();
let new_global = GlobalInputs {
config_digest: "new-config".to_string(),
};
assert!(carried_candidate_dids(root.path(), &manifest, &new_global, &hosted, None).is_empty());
let same_global = GlobalInputs {
config_digest: "old-config".to_string(),
};
assert_eq!(
carried_candidate_dids(root.path(), &manifest, &same_global, &hosted, None).len(),
1
);
Ok(())
}
#[test]
fn carried_candidates_drop_repos_no_longer_hosted_or_unsealed() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let mut manifest = fixture_manifest();
manifest.global_input_digest = Some("cfg".to_string());
manifest.repos.push(entry("did:plc:deleted"));
manifest.repos.push(entry("did:plc:unsealed"));
seal_release(root.path(), &mut manifest.repos[0])?;
seal_release(root.path(), &mut manifest.repos[1])?;
let global = GlobalInputs {
config_digest: "cfg".to_string(),
};
let hosted: BTreeSet<String> = [
"did:plc:fixture".to_string(),
"did:plc:unsealed".to_string(),
]
.into();
let carried = carried_candidate_dids(root.path(), &manifest, &global, &hosted, None);
assert_eq!(carried, ["did:plc:fixture".to_string()].into());
let hosted: BTreeSet<String> =
["did:plc:fixture".to_string(), "did:plc:deleted".to_string()].into();
let carried = carried_candidate_dids(root.path(), &manifest, &global, &hosted, None);
assert_eq!(
carried.len(),
2,
"both hosted sealed repos carry when not hinted"
);
Ok(())
}
#[test]
fn unchanged_shortcut_requires_hosted_set_to_match_active_generation() {
let global = GlobalInputs {
config_digest: "cfg".to_string(),
};
let snap = |did: &str| crate::snapshot::RepoSnapshot {
repo_did: knot_types::RepoDid::new(did).unwrap(),
owner: knot_types::OwnerDid::new("did:plc:owner").unwrap(),
rkey: knot_types::RepoRkey::new("r").unwrap(),
name: None,
head_capture: crate::snapshot::HeadCapture {
resolved: Some("1".repeat(40)),
symbolic: Some("refs/heads/main".to_string()),
unborn: false,
},
public_refs: vec![],
cob_refs: Vec::new(),
object_format: knot_types::ObjectFormat::SHA1,
};
let mut manifest = fixture_manifest();
manifest.generation_id = "a".repeat(64);
manifest.global_input_digest = Some(global.config_digest.clone());
manifest.repos[0].repo_did = "did:plc:a".to_string();
manifest.repos[0].fingerprint = plan::repo_identity(&snap("did:plc:a"), &global);
let active = plan::ActiveGeneration { manifest };
let outcome = plan::plan_repo(&snap("did:plc:a"), &global, Some(&active));
assert!(
matches!(outcome, RepoOutcome::Unchanged { .. }),
"fixture must model an unchanged repo"
);
let carried: BTreeSet<String> = BTreeSet::new();
let hosted: BTreeSet<String> = ["did:plc:a".to_string()].into();
assert!(unchanged_shortcut(
std::slice::from_ref(&snap("did:plc:a")),
std::slice::from_ref(&outcome),
&carried,
&hosted,
Some(&active)
));
// no hosted repos counts as all unchanged, but the active one is gone.
let empty: BTreeSet<String> = BTreeSet::new();
assert!(
!unchanged_shortcut(&[], &[], &carried, &empty, Some(&active)),
"a deleted repo must force a rebuilt generation, not a no-op"
);
let mut with_b = active.manifest.clone();
with_b.repos.push(entry("did:plc:b"));
let active_ab = plan::ActiveGeneration { manifest: with_b };
assert!(
!unchanged_shortcut(
std::slice::from_ref(&snap("did:plc:a")),
std::slice::from_ref(&outcome),
&carried,
&hosted,
Some(&active_ab)
),
"a removed active repo must not ghost through the unchanged check"
);
let mut older = active.manifest.clone();
older.built_by = "an older binary".to_string();
assert!(
!unchanged_shortcut(
std::slice::from_ref(&snap("did:plc:a")),
std::slice::from_ref(&outcome),
&carried,
&hosted,
Some(&plan::ActiveGeneration { manifest: older })
),
"a generation another binary built is built again"
);
}
#[cfg(unix)]
#[test]
fn release_export_mount_is_owned_sealed_and_excluded_from_generation_inventory()
-> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let releases = root.path().join("releases");
let mut build = ReleaseBuild::new(&releases)?;
std::fs::create_dir_all(build.path().join("export/objects/pack"))?;
std::fs::write(
build.path().join("export/objects/pack/test.pack"),
b"pack fixture",
)?;
build.record_input("export", "fixture");
build.finalize_tree("export")?;
let id = crate::release::release_id("did:plc:fixture", build.inputs());
let release = build.publish(&releases, &id)?;
let generation = root.path().join("generation");
let mount = generation.join("www/_git/token");
mount_release_export(release.path(), &mount)?;
assert!(std::fs::symlink_metadata(&mount)?.file_type().is_symlink());
assert_eq!(
std::fs::canonicalize(&mount)?,
std::fs::canonicalize(release.path().join("export"))?
);
assert_eq!(
std::fs::read(mount.join("objects/pack/test.pack"))?,
b"pack fixture"
);
assert!(payload_inventory(&generation)?.is_empty());
std::fs::remove_file(release.path().join("export/objects/pack/test.pack"))?;
assert!(mount_release_export(release.path(), &generation.join("www/_git/bad")).is_err());
Ok(())
}