argand-site-registry/crates/argand-site-registry/tests/identity.rs
nicweyand 9705b01fe4
All checks were successful
Standalone registry checks / check (push) Successful in 3m47s
security: harden registry trust and ingestion
2026-09-13 02:31:51 -04:00

198 lines
7.1 KiB
Rust

// By Nic Weyand!
//! Cross-source collisions need an exact, revocable identity decision.
#[allow(dead_code)]
mod common;
use argand_site_registry::{
identity,
model::{Format, Source},
review::{self, Review},
store,
};
use serde_json::json;
fn decision(fingerprint: String) -> anyhow::Result<Review> {
Ok(Review {
fingerprint,
decision: "approve".into(),
reviewer: "synthetic reviewer".into(),
reason: "Synthetic identity test, not actual ownership".into(),
evidence: "synthetic:identity".into(),
reviewed_at: common::timestamp()?,
expires_at: common::timestamp()? + chrono::Duration::days(2),
role: "unspecified".into(),
locale: String::new(),
country: String::new(),
})
}
#[test]
fn explicit_equivalence_resolves_cross_source_aliases_and_expires_or_revokes() -> anyhow::Result<()>
{
let root = tempfile::tempdir()?;
let db = common::fixture(root.path())?;
let baseline = common::build(&db, root.path(), "baseline")?;
common::approve(&db, &baseline, "FB", "https://facebook.com/", "primary", "")?;
let wiki = &baseline.lookup("FB", 1)?.candidates[0].entity_id;
let curlie = &baseline.lookup("Facebook directory listing", 1)?.candidates[0].entity_id;
let pair = identity::propose(&baseline, wiki, curlie)?;
let mut review = decision(pair.fingerprint.clone())?;
identity::record(&db, &baseline, &pair, &review)?;
let linked = common::build(&db, root.path(), "linked")?;
let resolved = linked
.resolve(
"Facebook directory listing",
None,
None,
common::timestamp()?,
)?
.ok_or_else(|| anyhow::anyhow!("approved alias not linked"))?;
assert_eq!(resolved.entity_id, *wiki);
assert_eq!(resolved.identity_provenance.len(), 1);
assert!(
linked
.resolve("Facebook directory listing", None, None, review.expires_at)?
.is_none()
);
review.decision = "revoke".into();
identity::record(&db, &linked, &pair, &review)?;
let revoked = common::build(&db, root.path(), "revoked")?;
assert!(
revoked
.resolve(
"Facebook directory listing",
None,
None,
common::timestamp()?
)?
.is_none()
);
assert!(
revoked
.resolve("FB", None, None, common::timestamp()?)?
.is_some()
);
Ok(())
}
#[test]
fn name_collision_remains_ambiguous_until_review_and_changes_invalidate_link() -> anyhow::Result<()>
{
let root = tempfile::tempdir()?;
let mut db = common::fixture(root.path())?;
let mut entity = common::entity(
"Q355",
"Facebook",
&["FB", "Facebook directory listing"],
&["https://facebook.com/"],
);
let bytes = serde_json::to_vec(&json!({"entities":{"Q355":entity}}))?;
let mut source = common::manifest(Source::Wikidata, Format::WikidataEntities, &bytes)?;
source.retrieved_at += chrono::Duration::hours(1);
let input = root.path().join("changed");
std::fs::write(&input, &bytes)?;
store::import(&mut db, &source, &input)?;
let baseline = common::build(&db, root.path(), "baseline")?;
common::approve(&db, &baseline, "FB", "https://facebook.com/", "primary", "")?;
let approved = common::build(&db, root.path(), "approved")?;
assert_eq!(
approved
.lookup("Facebook directory listing", 1)?
.total_entities,
2
);
assert!(
approved
.resolve(
"Facebook directory listing",
None,
None,
common::timestamp()?
)?
.is_none()
);
let candidates = baseline
.lookup("Facebook directory listing", 10)?
.candidates;
let pair = identity::propose(
&baseline,
&candidates[0].entity_id,
&candidates[1].entity_id,
)?;
identity::record(&db, &baseline, &pair, &decision(pair.fingerprint.clone())?)?;
let linked = common::build(&db, root.path(), "linked")?;
assert!(
linked
.resolve(
"Facebook directory listing",
None,
None,
common::timestamp()?
)?
.is_some()
);
assert_eq!(
linked
.lookup("Facebook directory listing", 1)?
.total_entities,
2
);
// Even renewing a destination review does not silently renew identity evidence.
entity["aliases"]["en"]
.as_array_mut()
.ok_or_else(|| anyhow::anyhow!("aliases"))?
.push(json!({"language":"en","value":"new unreviewed alias"}));
let bytes = serde_json::to_vec(&json!({"entities":{"Q355":entity}}))?;
source.sha256 = argand_site_registry::digest(&bytes);
source.bytes = u64::try_from(bytes.len())?;
source.retrieved_at += chrono::Duration::hours(1);
std::fs::write(&input, &bytes)?;
store::import(&mut db, &source, &input)?;
let changed = common::build(&db, root.path(), "changed-generation")?;
common::approve(&db, &changed, "FB", "https://facebook.com/", "primary", "")?;
let renewed = common::build(&db, root.path(), "renewed-destination")?;
assert!(
renewed
.resolve(
"Facebook directory listing",
None,
None,
common::timestamp()?
)?
.is_none()
);
assert!(
renewed
.resolve("FB", None, None, common::timestamp()?)?
.is_some()
);
let mut tampered = decision(pair.fingerprint.clone())?;
tampered.role = "primary".into();
assert!(identity::record(&db, &baseline, &pair, &tampered).is_err());
assert!(review::record(&db, &baseline, &tampered).is_err());
Ok(())
}
#[test]
fn metadata_changes_invalidate_identity_fingerprint() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let mut db = common::fixture(root.path())?;
let baseline = common::build(&db, root.path(), "metadata-baseline")?;
let wiki = baseline.lookup("FB", 1)?.candidates[0].entity_id.clone();
let curlie = baseline.lookup("Facebook directory listing", 1)?.candidates[0]
.entity_id
.clone();
let before = identity::propose(&baseline, &wiki, &curlie)?;
let mut entity = common::entity("Q355", "Facebook", &["FB"], &["https://facebook.com/"]);
entity["claims"]["P17"] = json!([{"id":"Q355$country","rank":"normal","mainsnak":{"property":"P17","snaktype":"value","datavalue":{"type":"wikibase-entityid","value":{"id":"Q30"}}}}]);
let bytes = serde_json::to_vec(&json!({"entities":{"Q355":entity}}))?;
let mut source = common::manifest(Source::Wikidata, Format::WikidataEntities, &bytes)?;
source.retrieved_at += chrono::Duration::hours(1);
let input = root.path().join("metadata-change.json");
std::fs::write(&input, &bytes)?;
store::import(&mut db, &source, &input)?;
let changed = common::build(&db, root.path(), "metadata-changed")?;
let after = identity::propose(&changed, &wiki, &curlie)?;
assert_ne!(before.fingerprint, after.fingerprint);
Ok(())
}