All checks were successful
Standalone registry checks / check (push) Successful in 3m47s
198 lines
7.1 KiB
Rust
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(())
|
|
}
|