All checks were successful
Standalone registry checks / check (push) Successful in 3m43s
555 lines
15 KiB
Rust
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(¤t)?,
|
|
"--allowed-signers",
|
|
text(&allowed)?,
|
|
"--identity",
|
|
"untrusted"
|
|
])
|
|
.is_err()
|
|
);
|
|
run(&[
|
|
"activate",
|
|
"--generation",
|
|
text(approved)?,
|
|
"--current",
|
|
text(¤t)?,
|
|
"--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(())
|
|
}
|