664 lines
23 KiB
Rust
664 lines
23 KiB
Rust
// By Nic Weyand!
|
|
//! Source poisoning, repeatability and metadata regression cases.
|
|
#[allow(dead_code)] // Same source-shaped helpers as the native lifecycle suite.
|
|
mod common;
|
|
use argand_site_registry::{
|
|
download::CachedSource,
|
|
model::{Compression, Format, Source},
|
|
query::Registry,
|
|
store::{self, ImportLimits},
|
|
};
|
|
use serde_json::json;
|
|
use std::{fs, io::Write, process::Command};
|
|
|
|
#[test]
|
|
fn version_one_store_migrates_without_losing_review_history() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let path = root.path().join("v1.sqlite");
|
|
let db = rusqlite::Connection::open(&path)?;
|
|
db.execute_batch(include_str!("../migrations/001.sql"))?;
|
|
db.execute("INSERT INTO reviews VALUES(1,?1,'revoke','legacy','reason','evidence','2026-01-01T00:00:00Z','2026-01-01T00:00:00Z','unspecified','','')", ["0".repeat(64)])?;
|
|
drop(db);
|
|
|
|
let migrated = store::open(&path)?;
|
|
assert_eq!(
|
|
migrated.query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))?,
|
|
5
|
|
);
|
|
assert_eq!(
|
|
migrated.query_row("SELECT rules FROM registry_metadata", [], |row| row
|
|
.get::<_, String>(0))?,
|
|
store::RULE_VERSION
|
|
);
|
|
assert_eq!(
|
|
migrated.query_row("SELECT count(*) FROM reviews", [], |row| row
|
|
.get::<_, i64>(0))?,
|
|
1
|
|
);
|
|
assert_eq!(
|
|
migrated.query_row("SELECT count(*) FROM review_auth", [], |row| row
|
|
.get::<_, i64>(0))?,
|
|
0
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn every_prior_writer_schema_migrates_to_v04() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
for version in 1..=4 {
|
|
let path = root.path().join(format!("v{version}.sqlite"));
|
|
let db = rusqlite::Connection::open(&path)?;
|
|
db.execute_batch(include_str!("../migrations/001.sql"))?;
|
|
if version >= 2 {
|
|
db.execute_batch(include_str!("../migrations/002.sql"))?;
|
|
}
|
|
if version >= 3 {
|
|
db.execute_batch(include_str!("../migrations/003.sql"))?;
|
|
}
|
|
if version >= 4 {
|
|
db.execute_batch(include_str!("../migrations/004.sql"))?;
|
|
}
|
|
drop(db);
|
|
let migrated = store::open(&path)?;
|
|
assert_eq!(
|
|
migrated.query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))?,
|
|
5
|
|
);
|
|
for table in ["votes", "vote_auth", "observation_batches", "observations"] {
|
|
let exists: bool = migrated.query_row(
|
|
"SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name=?1)",
|
|
[table],
|
|
|row| row.get(0),
|
|
)?;
|
|
assert!(exists, "{table} missing after schema {version} migration");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn externally_signed_unauthenticated_reviews_are_rejected() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let initial = common::build(&db, root.path(), "initial")?;
|
|
common::approve(&db, &initial, "FB", "https://facebook.com/", "primary", "")?;
|
|
let generation = common::build(&db, root.path(), "generation")?;
|
|
let key = root.path().join("key");
|
|
assert!(
|
|
Command::new("ssh-keygen")
|
|
.args(["-q", "-t", "ed25519", "-N", "", "-f"])
|
|
.arg(&key)
|
|
.status()?
|
|
.success()
|
|
);
|
|
fs::write(
|
|
root.path().join("allowed"),
|
|
format!("fixture {}", fs::read_to_string(key.with_extension("pub"))?),
|
|
)?;
|
|
assert!(
|
|
argand_site_registry::release::sign(
|
|
&root.path().join("generation"),
|
|
&key,
|
|
&generation.identity,
|
|
&root.path().join("allowed")
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
Command::new("ssh-keygen")
|
|
.args(["-Y", "sign", "-n", "argand-site-registry", "-f"])
|
|
.arg(&key)
|
|
.arg(root.path().join("generation/COMPLETE.json"))
|
|
.status()?
|
|
.success()
|
|
);
|
|
assert!(
|
|
argand_site_registry::release::verify_signed(
|
|
&root.path().join("generation"),
|
|
&root.path().join("allowed"),
|
|
"fixture",
|
|
&root.path().join("allowed")
|
|
)
|
|
.is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_sqlite_sidecars_and_generation_symlinks_are_rejected() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let generation = common::build(&db, root.path(), "sealed")?;
|
|
let scratch = root.path().join("scratch.sqlite");
|
|
fs::copy(root.path().join("sealed/registry.sqlite"), &scratch)?;
|
|
let attacker = rusqlite::Connection::open(&scratch)?;
|
|
attacker.execute_batch("PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0;")?;
|
|
attacker.execute(
|
|
"UPDATE properties SET url='https://unsigned.example.org/' WHERE url='https://facebook.com/'",
|
|
[],
|
|
)?;
|
|
fs::copy(
|
|
scratch.with_extension("sqlite-wal"),
|
|
root.path().join("sealed/registry.sqlite-wal"),
|
|
)?;
|
|
let error = Registry::open(&root.path().join("sealed"), &generation.identity)
|
|
.err()
|
|
.ok_or_else(|| anyhow::anyhow!("unsigned sidecar was accepted"))?;
|
|
assert!(error.to_string().contains("unexpected generation entry"));
|
|
drop(attacker);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use std::os::unix::fs::symlink;
|
|
let link = root.path().join("generation-link");
|
|
symlink(root.path().join("sealed"), &link)?;
|
|
assert!(Registry::open(&link, &generation.identity).is_err());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn opened_registry_uses_private_authenticated_database_bytes() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let registry = common::build(&db, root.path(), "private-snapshot")?;
|
|
let mutable = rusqlite::Connection::open(root.path().join("private-snapshot/registry.sqlite"))?;
|
|
mutable.execute(
|
|
"UPDATE properties SET url='https://changed.invalid/' WHERE url='https://facebook.com/'",
|
|
[],
|
|
)?;
|
|
assert_eq!(
|
|
registry.lookup("FB", 1)?.candidates[0].url,
|
|
"https://facebook.com/"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn import_limits_rollback_all_partial_source_rows() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = store::open(&root.path().join("limited.sqlite"))?;
|
|
let compressed = common::gzip(common::CRUX.as_bytes())?;
|
|
let input = root.path().join("limited.csv.gz");
|
|
fs::write(&input, &compressed)?;
|
|
let mut manifest = common::manifest(Source::Crux, Format::CruxCsv, &compressed)?;
|
|
manifest.compression = Compression::Gzip;
|
|
assert!(
|
|
store::import_with_limits(
|
|
&mut db,
|
|
&manifest,
|
|
&input,
|
|
ImportLimits {
|
|
maximum_expanded_bytes: 16,
|
|
maximum_records: 100,
|
|
maximum_database_growth_bytes: 1024 * 1024,
|
|
},
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(
|
|
db.query_row("SELECT count(*) FROM sources", [], |row| row
|
|
.get::<_, i64>(0))?,
|
|
0
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn retired_reviewer_keys_verify_history_only_with_a_validity_epoch() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let generation = common::build(&db, root.path(), "reviewer-epoch")?;
|
|
let fingerprint = generation.lookup("FB", 1)?.candidates[0]
|
|
.fingerprint
|
|
.clone();
|
|
let key = root.path().join("reviewer");
|
|
assert!(
|
|
Command::new("ssh-keygen")
|
|
.args(["-q", "-t", "ed25519", "-N", "", "-f"])
|
|
.arg(&key)
|
|
.status()?
|
|
.success()
|
|
);
|
|
let decision = root.path().join("decision.json");
|
|
fs::write(
|
|
&decision,
|
|
serde_json::to_vec(&json!({
|
|
"fingerprint":fingerprint,"decision":"revoke","reviewer":"retired",
|
|
"reason":"synthetic key epoch test","evidence":"synthetic:key-epoch",
|
|
"reviewed_at":"2026-09-12T12:00:00Z","expires_at":"2026-09-12T12:00:00Z",
|
|
"role":"unspecified","locale":"","country":""
|
|
}))?,
|
|
)?;
|
|
assert!(
|
|
Command::new("ssh-keygen")
|
|
.args(["-Y", "sign", "-n", "argand-site-registry-review", "-f"])
|
|
.arg(&key)
|
|
.arg(&decision)
|
|
.status()?
|
|
.success()
|
|
);
|
|
let public = fs::read_to_string(key.with_extension("pub"))?;
|
|
let allowed = root.path().join("allowed");
|
|
fs::write(&allowed, format!("retired {public}"))?;
|
|
let (review, authentication) = argand_site_registry::review::authenticate(
|
|
&decision,
|
|
&decision.with_extension("json.sig"),
|
|
&allowed,
|
|
"retired",
|
|
)?;
|
|
argand_site_registry::review::record_authenticated(&db, &generation, &review, &authentication)?;
|
|
fs::write(
|
|
&allowed,
|
|
format!("retired valid-before=\"20260913000000Z\" {public}"),
|
|
)?;
|
|
assert!(argand_site_registry::review::verify_all(&db, &allowed).is_ok());
|
|
fs::write(
|
|
&allowed,
|
|
format!("retired valid-before=\"20260912000000Z\" {public}"),
|
|
)?;
|
|
assert!(argand_site_registry::review::verify_all(&db, &allowed).is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn activation_accepts_a_pinned_v1_previous_generation() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let old = common::build(&db, root.path(), "legacy-current")?;
|
|
drop(old);
|
|
let receipt_path = root.path().join("legacy-current/COMPLETE.json");
|
|
let mut receipt: serde_json::Value = argand_site_registry::read_json(&receipt_path)?;
|
|
receipt["schema"] = json!("argand.site-registry/v1");
|
|
receipt["rules"] = json!("argand.site-rules/v1");
|
|
fs::write(&receipt_path, serde_json::to_vec_pretty(&receipt)?)?;
|
|
let old_pin = argand_site_registry::file_digest(&receipt_path)?;
|
|
let current = root.path().join("current.json");
|
|
fs::write(
|
|
¤t,
|
|
serde_json::to_vec_pretty(&json!({
|
|
"schema":"argand.site-current/v1",
|
|
"generation":root.path().join("legacy-current").canonicalize()?,
|
|
"receipt_sha256":old_pin,
|
|
"signer":"legacy-publisher",
|
|
"revocation_sequence":0
|
|
}))?,
|
|
)?;
|
|
|
|
let new = common::build(&db, root.path(), "current-rules")?;
|
|
let key = root.path().join("publisher");
|
|
assert!(
|
|
Command::new("ssh-keygen")
|
|
.args(["-q", "-t", "ed25519", "-N", "", "-f"])
|
|
.arg(&key)
|
|
.status()?
|
|
.success()
|
|
);
|
|
let allowed = root.path().join("allowed-publisher");
|
|
fs::write(
|
|
&allowed,
|
|
format!(
|
|
"publisher {}",
|
|
fs::read_to_string(key.with_extension("pub"))?
|
|
),
|
|
)?;
|
|
argand_site_registry::release::sign(
|
|
&root.path().join("current-rules"),
|
|
&key,
|
|
&new.identity,
|
|
&allowed,
|
|
)?;
|
|
argand_site_registry::release::activate(
|
|
&root.path().join("current-rules"),
|
|
¤t,
|
|
&allowed,
|
|
"publisher",
|
|
&allowed,
|
|
)?;
|
|
let activated: serde_json::Value = argand_site_registry::read_json(¤t)?;
|
|
assert_eq!(activated["receipt_sha256"], new.identity);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn removed_entity_websites_retire_the_previous_selection() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = common::fixture(root.path())?;
|
|
let before = common::build(&db, root.path(), "before-retirement")?;
|
|
common::approve(&db, &before, "FB", "https://facebook.com/", "primary", "")?;
|
|
let bytes = br#"{"entities":{"Q355":{"id":"Q355","missing":""}}}"#;
|
|
let mut source = common::manifest(Source::Wikidata, Format::WikidataEntities, bytes)?;
|
|
source.retrieved_at += chrono::Duration::days(1);
|
|
let input = root.path().join("retirement.json");
|
|
fs::write(&input, bytes)?;
|
|
store::import(&mut db, &source, &input)?;
|
|
let retired = common::build(&db, root.path(), "retired")?;
|
|
assert_eq!(retired.lookup("FB", 1)?.total_entities, 0);
|
|
assert!(
|
|
retired
|
|
.resolve("FB", None, None, common::timestamp()?)?
|
|
.is_none()
|
|
);
|
|
assert_eq!(before.lookup("FB", 1)?.total_entities, 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn import_order_does_not_change_generation_and_psl_refresh_preserves_review() -> anyhow::Result<()>
|
|
{
|
|
let root = tempfile::tempdir()?;
|
|
let a = common::fixture(root.path())?;
|
|
let mut b = store::open(&root.path().join("other.sqlite"))?;
|
|
for (source, format, bytes) in [
|
|
(Source::Curlie, Format::CurlieTarGz, common::curlie()?),
|
|
(
|
|
Source::Crux,
|
|
Format::CruxCsv,
|
|
common::CRUX.as_bytes().to_vec(),
|
|
),
|
|
(
|
|
Source::Majestic,
|
|
Format::MajesticCsv,
|
|
common::MAJESTIC.as_bytes().to_vec(),
|
|
),
|
|
(
|
|
Source::Wikidata,
|
|
Format::WikidataEntities,
|
|
serde_json::to_vec(&common::wikidata())?,
|
|
),
|
|
(
|
|
Source::Psl,
|
|
Format::PslText,
|
|
common::PSL.as_bytes().to_vec(),
|
|
),
|
|
] {
|
|
common::import(&mut b, root.path(), source, format, &bytes)?;
|
|
}
|
|
let first = common::build(&a, root.path(), "a")?;
|
|
let second = common::build(&b, root.path(), "b")?;
|
|
assert_eq!(first.identity, second.identity);
|
|
common::approve(&b, &second, "FB", "https://facebook.com/", "primary", "")?;
|
|
let input = root.path().join("psl");
|
|
let comment_only = format!("{}// Comment-only refresh\n", common::PSL);
|
|
fs::write(&input, &comment_only)?;
|
|
let mut manifest = common::manifest(Source::Psl, Format::PslText, comment_only.as_bytes())?;
|
|
manifest.retrieved_at += chrono::Duration::days(1);
|
|
store::import(&mut b, &manifest, &input)?;
|
|
let refreshed = common::build(&b, root.path(), "refresh")?;
|
|
assert!(
|
|
refreshed
|
|
.resolve("FB", None, None, common::timestamp()?)?
|
|
.is_some()
|
|
);
|
|
assert_eq!(
|
|
second.lookup("FB", 1)?.candidates[0].fingerprint,
|
|
refreshed.lookup("FB", 1)?.candidates[0].fingerprint
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn psl_semantic_change_invalidates_affected_review_only() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = common::fixture(root.path())?;
|
|
let original = common::build(&db, root.path(), "original")?;
|
|
common::approve(
|
|
&db,
|
|
&original,
|
|
"Atlas",
|
|
"https://atlas.example.co.uk/",
|
|
"regional",
|
|
"GB",
|
|
)?;
|
|
let original_fingerprint = original
|
|
.lookup("Atlas", 10)?
|
|
.candidates
|
|
.into_iter()
|
|
.find(|candidate| candidate.url == "https://atlas.example.co.uk/")
|
|
.ok_or_else(|| anyhow::anyhow!("fixture regional property missing"))?
|
|
.fingerprint;
|
|
|
|
let changed = common::PSL.replace("co.uk\n", "");
|
|
let input = root.path().join("changed-psl");
|
|
fs::write(&input, &changed)?;
|
|
let mut manifest = common::manifest(Source::Psl, Format::PslText, changed.as_bytes())?;
|
|
manifest.retrieved_at += chrono::Duration::days(1);
|
|
store::import(&mut db, &manifest, &input)?;
|
|
let rebuilt = common::build(&db, root.path(), "changed")?;
|
|
let changed_candidate = rebuilt
|
|
.lookup("Atlas", 10)?
|
|
.candidates
|
|
.into_iter()
|
|
.find(|candidate| candidate.url == "https://atlas.example.co.uk/")
|
|
.ok_or_else(|| anyhow::anyhow!("changed property missing"))?;
|
|
assert_ne!(original_fingerprint, changed_candidate.fingerprint);
|
|
assert_eq!(
|
|
changed_candidate.web_property["domain"]["registrable_domain"],
|
|
"co.uk"
|
|
);
|
|
assert!(
|
|
rebuilt
|
|
.resolve("Atlas", None, Some("GB"), common::timestamp()?)?
|
|
.is_none()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn typed_diff_reports_review_only_changes() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let db = common::fixture(root.path())?;
|
|
let before = common::build(&db, root.path(), "before")?;
|
|
common::approve(&db, &before, "FB", "https://facebook.com/", "primary", "")?;
|
|
let after = common::build(&db, root.path(), "after")?;
|
|
let mut output = Vec::new();
|
|
argand_site_registry::release::diff(&before, &after, &mut output)?;
|
|
let rows = String::from_utf8(output)?
|
|
.lines()
|
|
.map(serde_json::from_str::<serde_json::Value>)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
assert!(rows.iter().any(|row| {
|
|
row["type"] == "change" && row["subject"] == "review" && row["change"] == "added"
|
|
}));
|
|
assert_eq!(rows[0]["descriptions_included"], false);
|
|
assert!(rows[0]["attribution"]["curlie"].is_object());
|
|
assert_eq!(rows.last().and_then(|row| row["changes"].as_u64()), Some(1));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_archives_and_incomplete_refresh_never_replace_sources() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = common::fixture(root.path())?;
|
|
let mut archive = tar::Builder::new(flate2::write::GzEncoder::new(
|
|
Vec::new(),
|
|
flate2::Compression::default(),
|
|
));
|
|
let mut header = tar::Header::new_gnu();
|
|
header.set_entry_type(tar::EntryType::Symlink);
|
|
header.set_size(0);
|
|
header.set_mode(0o777);
|
|
archive.append_link(&mut header, "curlie-rdf/evil-c.tsv", "/etc/passwd")?;
|
|
let link = archive.into_inner()?.finish()?;
|
|
let mut truncated = common::curlie()?;
|
|
truncated.truncate(truncated.len() - 5);
|
|
for bytes in [&link, &truncated] {
|
|
assert!(
|
|
common::import(
|
|
&mut db,
|
|
root.path(),
|
|
Source::Curlie,
|
|
Format::CurlieTarGz,
|
|
bytes
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
let registry = common::build(&db, root.path(), "complete")?;
|
|
assert_eq!(registry.receipt.sources.len(), 5);
|
|
assert_eq!(
|
|
registry
|
|
.lookup("Facebook directory listing", 1)?
|
|
.total_entities,
|
|
1
|
|
);
|
|
fs::write(root.path().join("complete/ATTRIBUTION.json"), b"{}")?;
|
|
assert!(Registry::open(&root.path().join("complete"), ®istry.identity).is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn temporal_deprecated_and_nonvalue_assertions_keep_evidence_without_admission()
|
|
-> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = store::open(&root.path().join("data.sqlite"))?;
|
|
common::import(
|
|
&mut db,
|
|
root.path(),
|
|
Source::Psl,
|
|
Format::PslText,
|
|
common::PSL.as_bytes(),
|
|
)?;
|
|
let mut entity = common::entity(
|
|
"Q100",
|
|
"Historical",
|
|
&[],
|
|
&[
|
|
"https://old.example.com",
|
|
"https://ancient.example.com",
|
|
"https://unknown.example.com",
|
|
],
|
|
);
|
|
entity["claims"]["P856"][0]["qualifiers"] =
|
|
json!({"P582":[{"datavalue":{"value":{"time":"+2001-01-01T00:00:00Z"}}}]});
|
|
entity["claims"]["P856"][1]["rank"] = json!("deprecated");
|
|
entity["claims"]["P856"][2]["mainsnak"] = json!({"property":"P856","snaktype":"novalue"});
|
|
let bytes = serde_json::to_vec(&json!({"entities":{"Q100":entity}}))?;
|
|
common::import(
|
|
&mut db,
|
|
root.path(),
|
|
Source::Wikidata,
|
|
Format::WikidataEntities,
|
|
&bytes,
|
|
)?;
|
|
let registry = common::build(&db, root.path(), "generation")?;
|
|
assert_eq!(registry.receipt.rejected, 1);
|
|
assert!(
|
|
registry
|
|
.lookup("Historical", 100)?
|
|
.candidates
|
|
.iter()
|
|
.all(|c| !c.eligible)
|
|
);
|
|
assert!(
|
|
common::approve(
|
|
&db,
|
|
®istry,
|
|
"Historical",
|
|
"https://old.example.com/",
|
|
"primary",
|
|
""
|
|
)
|
|
.is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bzip2_multistream_and_property_scope_metadata() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut db = store::open(&root.path().join("data.sqlite"))?;
|
|
let data = serde_json::to_vec(&common::wikidata())?;
|
|
let mut bytes = Vec::new();
|
|
for half in data.chunks(data.len().div_ceil(2)) {
|
|
let mut bz = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::fast());
|
|
bz.write_all(half)?;
|
|
bytes.extend(bz.finish()?);
|
|
}
|
|
let input = root.path().join("entities.bz2");
|
|
fs::write(&input, &bytes)?;
|
|
let mut manifest = common::manifest(Source::Wikidata, Format::WikidataEntities, &bytes)?;
|
|
manifest.compression = Compression::Bzip2;
|
|
store::import(&mut db, &manifest, &input)?;
|
|
common::import(
|
|
&mut db,
|
|
root.path(),
|
|
Source::Psl,
|
|
Format::PslText,
|
|
common::PSL.as_bytes(),
|
|
)?;
|
|
let registry = common::build(&db, root.path(), "generation")?;
|
|
let candidates = registry.lookup("Atlas", 10)?.candidates;
|
|
let regional = candidates
|
|
.iter()
|
|
.find(|c| c.url.contains("co.uk"))
|
|
.ok_or_else(|| anyhow::anyhow!("regional missing"))?;
|
|
assert_eq!(regional.property_scopes[0].jurisdiction_entities, ["Q145"]);
|
|
assert_eq!(regional.property_scopes[0].language_entities, ["Q1860"]);
|
|
assert_eq!(regional.property_scopes[0].country, None);
|
|
assert!(
|
|
regional
|
|
.entity
|
|
.names
|
|
.iter()
|
|
.any(|n| n["value"]["text"] == "Atlas" && n["source"]["license"] == "CC0-1.0")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn repeated_update_reuses_generation_and_failure_preserves_it() -> anyhow::Result<()> {
|
|
let root = tempfile::tempdir()?;
|
|
let mut inputs = Vec::new();
|
|
for (source, format, bytes) in [
|
|
(
|
|
Source::Psl,
|
|
Format::PslText,
|
|
common::PSL.as_bytes().to_vec(),
|
|
),
|
|
(
|
|
Source::Wikidata,
|
|
Format::WikidataEntities,
|
|
serde_json::to_vec(&common::wikidata())?,
|
|
),
|
|
] {
|
|
let input = root.path().join(source.key());
|
|
let manifest = input.with_extension("json");
|
|
fs::write(&input, &bytes)?;
|
|
fs::write(
|
|
&manifest,
|
|
serde_json::to_vec(&common::manifest(source, format, &bytes)?)?,
|
|
)?;
|
|
inputs.push(CachedSource { input, manifest });
|
|
}
|
|
let review_policy = root.path().join("policy.json");
|
|
fs::write(
|
|
&review_policy,
|
|
serde_json::to_vec(&argand_site_registry::policy::ReviewPolicy::legacy_compatible())?,
|
|
)?;
|
|
let config = argand_site_registry::update::Config {
|
|
cache: root.path().join("cache"),
|
|
database: root.path().join("data.sqlite"),
|
|
generations: root.path().join("generations"),
|
|
downloads: vec![],
|
|
inputs,
|
|
crux: vec![],
|
|
review_policy: Some(review_policy),
|
|
reviewer_trust: None,
|
|
auto_supersede_typed_snapshots: false,
|
|
};
|
|
let first = argand_site_registry::update::run(&config).await?;
|
|
let second = argand_site_registry::update::run(&config).await?;
|
|
assert_eq!(first, second);
|
|
let pin = argand_site_registry::file_digest(&first.join("COMPLETE.json"))?;
|
|
fs::write(&config.inputs[1].input, "truncated")?;
|
|
assert!(argand_site_registry::update::run(&config).await.is_err());
|
|
assert_eq!(
|
|
Registry::open(&first, &pin)?
|
|
.lookup("FB", 1)?
|
|
.total_entities,
|
|
1
|
|
);
|
|
Ok(())
|
|
}
|