argand-site-registry/crates/argand-site-registry/tests/cli.rs
nicweyand e83f43d00f
All checks were successful
Standalone registry checks / check (push) Successful in 3m43s
feat: harden reviewed registry releases
2026-09-13 01:19:27 -04:00

555 lines
15 KiB
Rust

// By Nic Weyand!
//! Fresh-process proof for all source imports and the signed release lifecycle.
#[allow(dead_code)] // Shared fixture helpers also support the library contract suite.
mod common;
use anyhow::{Context, ensure};
use argand_site_registry::model::{Format, Source};
use serde_json::{Value, json};
use std::{fs, path::Path, process::Command};
fn run(args: &[&str]) -> anyhow::Result<Value> {
let output = Command::new(env!("CARGO_BIN_EXE_argand-site-registry"))
.args(args)
.output()?;
ensure!(
output.status.success(),
"CLI failed: {:?}\n{}",
args,
String::from_utf8_lossy(&output.stderr)
);
Ok(serde_json::from_slice(&output.stdout)?)
}
fn text(path: &Path) -> anyhow::Result<&str> {
path.to_str().context("non-UTF8 fixture path")
}
#[test]
fn all_source_import_review_resolve_revoke_and_signed_rollback() -> anyhow::Result<()> {
let temporary = tempfile::tempdir()?;
let configured = std::env::var_os("ARGAND_REGISTRY_E2E_OUTPUT").map(std::path::PathBuf::from);
let root = configured.as_deref().unwrap_or(temporary.path());
if configured.is_some() {
fs::create_dir(root)?;
}
prepare_signer(root)?;
let database = root.join("store.sqlite");
import_sources(root, &database)?;
let candidate = root.join("candidate");
let built = run(&[
"build",
"--database",
text(&database)?,
"--output",
text(&candidate)?,
])?;
let pin = built["pin"].as_str().context("missing pin")?;
let lookup = run(&[
"lookup",
"--generation",
text(&candidate)?,
"--pin",
pin,
"--query",
"facebook",
])?;
assert_eq!(lookup["candidates"][0]["canonical_name"], "Facebook");
assert_eq!(
lookup["candidates"][0]["web_property"]["domain"]["registrable_domain"],
"facebook.com"
);
inspect_commands(&candidate, pin, &lookup)?;
let unresolved = run(&[
"resolve",
"--generation",
text(&candidate)?,
"--pin",
pin,
"--query",
"facebook",
])?;
assert!(unresolved["destination"].is_null());
assert_eq!(unresolved["status"], "no_active_review");
let now = chrono::Utc::now() - chrono::Duration::seconds(1);
let decision = json!({"fingerprint":lookup["candidates"][0]["fingerprint"],"decision":"approve","reviewer":"fixture","reason":"E2E test only, not actual site verification","evidence":"synthetic:fixture","reviewed_at":now,"expires_at":now+chrono::Duration::days(1),"role":"primary","locale":"","country":""});
let decision_path = root.join("review.json");
fs::write(&decision_path, serde_json::to_vec(&decision)?)?;
reject_tampered_review(root, &database, &candidate, pin, &decision)?;
record_review(root, &database, &candidate, pin, &decision_path)?;
let approved = root.join("approved");
review_identity(root, &database, &candidate, pin, &decision)?;
let built = run(&[
"build",
"--database",
text(&database)?,
"--output",
text(&approved)?,
])?;
let approved_pin = built["pin"].as_str().context("approved pin")?;
assert_eq!(
run(&[
"resolve",
"--generation",
text(&approved)?,
"--pin",
approved_pin,
"--query",
"FB"
])?["destination"]["url"],
"https://facebook.com/"
);
assert_eq!(
run(&[
"lookup",
"--generation",
text(&approved)?,
"--pin",
approved_pin,
"--query",
"FB",
])?["candidates"][0]["review"]["authentication"]["identity"],
"fixture"
);
let (revoked, revoked_pin) = release_lifecycle(
root,
&database,
&approved,
approved_pin,
decision,
&decision_path,
)?;
export_fixture(root, &revoked, &revoked_pin)?;
println!("Native fixture lifecycle passed: {}", root.display());
Ok(())
}
fn inspect_commands(generation: &Path, pin: &str, lookup: &Value) -> anyhow::Result<()> {
let entity_id = lookup["candidates"][0]["entity_id"]
.as_str()
.context("entity ID")?;
assert_eq!(
run(&[
"entity",
"--generation",
text(generation)?,
"--pin",
pin,
"--id",
entity_id,
])?["entity"]["canonical_name"],
"Facebook"
);
let reverse = run(&[
"lookup-web",
"--generation",
text(generation)?,
"--pin",
pin,
"--target",
"facebook.com",
])?;
assert_eq!(reverse["total_edges"], 2);
assert!(reverse["matches"].as_array().is_some_and(|matches| {
matches
.iter()
.any(|entry| entry["candidate"]["canonical_name"] == "Facebook")
}));
assert_eq!(
run(&[
"popularity",
"--generation",
text(generation)?,
"--pin",
pin,
"--target",
"facebook.com",
])?["total"],
2
);
let category = run(&[
"category",
"--generation",
text(generation)?,
"--pin",
pin,
"--id",
"42",
])?;
assert_eq!(category["total_members"], 1);
assert_eq!(
category["metadata"][0]["value"]["description_redacted"],
true
);
assert!(
category["metadata"][0]["value"]
.get("description")
.is_none()
);
let stats = run(&["stats", "--generation", text(generation)?, "--pin", pin])?;
assert_eq!(stats["selected_sources"], 5);
assert_eq!(stats["authenticated_reviews"], 0);
let cases = generation
.parent()
.context("generation parent")?
.join("cli-evaluation.jsonl");
fs::write(
&cases,
b"{\"id\":\"missing\",\"query\":\"not a fixture entity\",\"expected_status\":\"no_name_match\"}\n",
)?;
let report = run(&[
"evaluate",
"--generation",
text(generation)?,
"--pin",
pin,
"--cases",
text(&cases)?,
])?;
assert_eq!(report["passed"], 1);
assert_eq!(report["failed"], 0);
Ok(())
}
fn reject_tampered_review(
root: &Path,
database: &Path,
generation: &Path,
pin: &str,
decision: &Value,
) -> anyhow::Result<()> {
let path = root.join("tampered-review.json");
fs::write(&path, serde_json::to_vec(decision)?)?;
let signature = sign_review(root, &path)?;
let mut altered = decision.clone();
altered["reason"] = json!("changed after signature");
fs::write(&path, serde_json::to_vec(&altered)?)?;
assert!(
run(&[
"review",
"--database",
text(database)?,
"--generation",
text(generation)?,
"--pin",
pin,
"--decision",
text(&path)?,
"--signature",
text(&signature)?,
"--allowed-reviewers",
text(&root.join("allowed_signers"))?,
"--identity",
"fixture",
])
.is_err()
);
Ok(())
}
fn import_sources(root: &Path, database: &Path) -> anyhow::Result<()> {
let sources = [
(
Source::Psl,
Format::PslText,
common::PSL.as_bytes().to_vec(),
),
(
Source::Wikidata,
Format::WikidataEntities,
serde_json::to_vec(&common::wikidata())?,
),
(
Source::Majestic,
Format::MajesticCsv,
common::MAJESTIC.as_bytes().to_vec(),
),
(
Source::Crux,
Format::CruxCsv,
common::CRUX.as_bytes().to_vec(),
),
(Source::Curlie, Format::CurlieTarGz, common::curlie()?),
];
for (source, format, bytes) in sources {
let input = root.join(format!("{}.input", source.key()));
let manifest = root.join(format!("{}.json", source.key()));
fs::write(&input, &bytes)?;
fs::write(
&manifest,
serde_json::to_vec_pretty(&common::manifest(source, format, &bytes)?)?,
)?;
let args = [
"import",
"--database",
text(database)?,
"--input",
text(&input)?,
"--manifest",
text(&manifest)?,
];
assert_eq!(run(&args)?, run(&args)?);
}
Ok(())
}
fn release_lifecycle(
root: &Path,
database: &Path,
approved: &Path,
approved_pin: &str,
mut decision: Value,
decision_path: &Path,
) -> anyhow::Result<(std::path::PathBuf, String)> {
sign_and_activate(root, approved, approved_pin)?;
decision["decision"] = json!("revoke");
let revocation_path = decision_path.with_file_name("revocation.json");
fs::write(&revocation_path, serde_json::to_vec(&decision)?)?;
record_review(root, database, approved, approved_pin, &revocation_path)?;
let revoked = root.join("revoked");
let built = run(&[
"build",
"--database",
text(database)?,
"--output",
text(&revoked)?,
])?;
let revoked_pin = built["pin"].as_str().context("revoked pin")?;
assert!(
run(&[
"resolve",
"--generation",
text(&revoked)?,
"--pin",
revoked_pin,
"--query",
"FB"
])?["destination"]
.is_null()
);
sign_and_activate(root, &revoked, revoked_pin)?;
assert!(
run(&[
"activate",
"--generation",
text(approved)?,
"--current",
text(&root.join("current.json"))?,
"--allowed-signers",
text(&root.join("allowed_signers"))?,
"--identity",
"fixture"
])
.is_err()
);
Ok((revoked, revoked_pin.into()))
}
fn sign_and_activate(root: &Path, approved: &Path, approved_pin: &str) -> anyhow::Result<()> {
let key = root.join("signer");
let allowed = root.join("allowed_signers");
let wrong_reviewers = root.join("wrong_reviewers");
fs::write(
&wrong_reviewers,
format!(
"untrusted {}",
fs::read_to_string(key.with_extension("pub"))?
),
)?;
assert!(
run(&[
"sign",
"--generation",
text(approved)?,
"--pin",
approved_pin,
"--key",
text(&key)?,
"--allowed-reviewers",
text(&wrong_reviewers)?,
])
.is_err()
);
run(&[
"sign",
"--generation",
text(approved)?,
"--pin",
approved_pin,
"--key",
text(&key)?,
"--allowed-reviewers",
text(&allowed)?,
])?;
let current = root.join("current.json");
assert!(
run(&[
"activate",
"--generation",
text(approved)?,
"--current",
text(&current)?,
"--allowed-signers",
text(&allowed)?,
"--identity",
"untrusted"
])
.is_err()
);
run(&[
"activate",
"--generation",
text(approved)?,
"--current",
text(&current)?,
"--allowed-signers",
text(&allowed)?,
"--identity",
"fixture",
])?;
Ok(())
}
fn review_identity(
root: &Path,
database: &Path,
generation: &Path,
pin: &str,
template: &Value,
) -> anyhow::Result<()> {
let wiki = run(&[
"lookup",
"--generation",
text(generation)?,
"--pin",
pin,
"--query",
"FB",
])?;
let curlie = run(&[
"lookup",
"--generation",
text(generation)?,
"--pin",
pin,
"--query",
"Facebook directory listing",
])?;
let left = wiki["candidates"][0]["entity_id"]
.as_str()
.context("Wiki identity")?;
let right = curlie["candidates"][0]["entity_id"]
.as_str()
.context("Curlie identity")?;
let args = [
"equivalence",
"--generation",
text(generation)?,
"--pin",
pin,
"--left",
left,
"--right",
right,
];
let preview = run(&args)?;
let mut decision = template.clone();
decision["fingerprint"] = preview["fingerprint"].clone();
decision["role"] = json!("unspecified");
let path = root.join("identity-review.json");
fs::write(&path, serde_json::to_vec(&decision)?)?;
let mut args = args.to_vec();
let signature = sign_review(root, &path)?;
let allowed = root.join("allowed_signers");
args.extend([
"--database",
text(database)?,
"--decision",
text(&path)?,
"--signature",
text(&signature)?,
"--allowed-reviewers",
text(&allowed)?,
"--identity",
"fixture",
]);
run(&args)?;
Ok(())
}
fn prepare_signer(root: &Path) -> anyhow::Result<()> {
let key = root.join("signer");
let status = Command::new("ssh-keygen")
.args(["-q", "-t", "ed25519", "-N", "", "-f"])
.arg(&key)
.status()?;
ensure!(status.success(), "generate test key");
fs::write(
root.join("allowed_signers"),
format!("fixture {}", fs::read_to_string(key.with_extension("pub"))?),
)?;
Ok(())
}
fn sign_review(root: &Path, decision: &Path) -> anyhow::Result<std::path::PathBuf> {
let status = Command::new("ssh-keygen")
.args([
"-Y",
"sign",
"-n",
argand_site_registry::review::SIGNATURE_NAMESPACE,
"-f",
])
.arg(root.join("signer"))
.arg(decision)
.status()?;
ensure!(status.success(), "sign fixture review");
Ok(std::path::PathBuf::from(format!(
"{}.sig",
decision.display()
)))
}
fn record_review(
root: &Path,
database: &Path,
generation: &Path,
pin: &str,
decision: &Path,
) -> anyhow::Result<()> {
let signature = sign_review(root, decision)?;
let allowed = root.join("allowed_signers");
run(&[
"review",
"--database",
text(database)?,
"--generation",
text(generation)?,
"--pin",
pin,
"--decision",
text(decision)?,
"--signature",
text(&signature)?,
"--allowed-reviewers",
text(&allowed)?,
"--identity",
"fixture",
])?;
Ok(())
}
fn export_fixture(root: &Path, revoked: &Path, revoked_pin: &str) -> anyhow::Result<()> {
let export = root.join("registry.jsonl");
run(&[
"export",
"--generation",
text(revoked)?,
"--pin",
revoked_pin,
"--output",
text(&export)?,
])?;
assert!(fs::read_to_string(export)?.contains("CC-BY-4.0"));
Ok(())
}