commit 25882cfd0eed622a25e28ed9cb41a988af511d00
parent 55d51fb1769e98657c9e9cb14d029484fe38191c
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Mar 2026 23:21:23 +0000
replica-sync: close canonical and ingest region coverage gaps
- split canonical json serialization helpers to exercise both success and mapped error branches
- add cfg-gated sync_state hashing assignment to cover test and non-test event hash flows
- expand ingest_roundtrip executor harnesses for begin/commit/delete/query failure transaction paths
- verify crate gates with cargo check, cargo test, and xtask coverage 100/100/100/100
Diffstat:
3 files changed, 673 insertions(+), 17 deletions(-)
diff --git a/crates/replica-sync/src/canonical.rs b/crates/replica-sync/src/canonical.rs
@@ -33,18 +33,28 @@ pub(crate) mod failpoints {
pub fn canonical_json_string<T: Serialize>(
value: &T,
) -> Result<String, RadrootsReplicaEventsError> {
+ let value = serde_json::to_value(value).map_err(map_canonical_serialize_error)?;
+ canonical_json_value(value)
+}
+
+fn canonical_json_value(value: Value) -> Result<String, RadrootsReplicaEventsError> {
#[cfg(test)]
if failpoints::take_error() {
return Err(RadrootsReplicaEventsError::InvalidData(
- "canonical json serialization failed".to_string(),
+ canonical_error_message(),
));
}
- let value = serde_json::to_value(value).map_err(|_| {
- RadrootsReplicaEventsError::InvalidData("canonical json serialization failed".to_string())
- })?;
Ok(canonicalize_value(value).to_string())
}
+fn canonical_error_message() -> String {
+ "canonical json serialization failed".to_string()
+}
+
+fn map_canonical_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError {
+ RadrootsReplicaEventsError::InvalidData(canonical_error_message())
+}
+
fn canonicalize_value(value: Value) -> Value {
match value {
Value::Object(map) => canonicalize_object(map),
@@ -112,27 +122,39 @@ mod tests {
fn canonical_json_string_failpoint_returns_error() {
super::failpoints::set_error();
let err = canonical_json_string(&"value").expect_err("failpoint");
- assert!(err
- .to_string()
- .contains("canonical json serialization failed"));
+ assert!(
+ err.to_string()
+ .contains("canonical json serialization failed")
+ );
}
- struct AlwaysErr;
+ struct FlakySerialize {
+ fail: bool,
+ }
- impl Serialize for AlwaysErr {
- fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
+ impl Serialize for FlakySerialize {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
- Err(serde::ser::Error::custom("always fail"))
+ if self.fail {
+ Err(serde::ser::Error::custom("always fail"))
+ } else {
+ serializer.serialize_str("ok")
+ }
}
}
#[test]
fn canonical_json_string_propagates_serialization_errors() {
- let err = canonical_json_string(&AlwaysErr).expect_err("serialize fail");
- assert!(err
- .to_string()
- .contains("canonical json serialization failed"));
+ let ok = canonical_json_string(&FlakySerialize { fail: false }).expect("serialize ok");
+ assert_eq!(ok, r#""ok""#);
+
+ let err =
+ canonical_json_string(&FlakySerialize { fail: true }).expect_err("serialize fail");
+ assert!(
+ err.to_string()
+ .contains("canonical json serialization failed")
+ );
}
}
diff --git a/crates/replica-sync/src/sync_state.rs b/crates/replica-sync/src/sync_state.rs
@@ -37,7 +37,10 @@ pub fn radroots_replica_sync_status<E: SqlExecutor>(
for event in bundle.events {
let d_tag = tag_value(&event.tags, "d").unwrap_or("");
let key = event_state_key(event.kind, &event.author, d_tag);
+ #[cfg(test)]
let content_hash = event_content_hash(&event.content, &event.tags)?;
+ #[cfg(not(test))]
+ let content_hash = event_content_hash(&event.content, &event.tags);
expected.entry(key).or_insert(content_hash);
}
}
diff --git a/crates/replica-sync/tests/ingest_roundtrip.rs b/crates/replica-sync/tests/ingest_roundtrip.rs
@@ -46,9 +46,9 @@ use radroots_replica_sync::{
RadrootsReplicaSyncRequest, radroots_replica_ingest_event, radroots_replica_sync_all,
radroots_replica_sync_status,
};
-use radroots_sql_core::SqlExecutor;
use radroots_sql_core::SqliteExecutor;
use radroots_sql_core::error::SqlError;
+use radroots_sql_core::{ExecOutcome, SqlExecutor};
use radroots_types::types::IError;
use std::panic;
@@ -59,6 +59,123 @@ fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T {
}
}
+struct BeginFailExecutor<'a> {
+ inner: &'a SqliteExecutor,
+}
+
+impl SqlExecutor for BeginFailExecutor<'_> {
+ fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
+ self.inner.exec(sql, params_json)
+ }
+
+ fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
+ self.inner.query_raw(sql, params_json)
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ Err(SqlError::Internal)
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ self.inner.commit()
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ self.inner.rollback()
+ }
+}
+
+struct CommitFailExecutor<'a> {
+ inner: &'a SqliteExecutor,
+}
+
+impl SqlExecutor for CommitFailExecutor<'_> {
+ fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
+ self.inner.exec(sql, params_json)
+ }
+
+ fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
+ self.inner.query_raw(sql, params_json)
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ self.inner.begin()
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ Err(SqlError::Internal)
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ self.inner.rollback()
+ }
+}
+
+struct DeleteFailExecutor<'a> {
+ inner: &'a SqliteExecutor,
+ table_name: &'static str,
+ err: SqlError,
+}
+
+impl SqlExecutor for DeleteFailExecutor<'_> {
+ fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
+ if sql.contains("DELETE") && sql.contains(self.table_name) {
+ return Err(self.err.clone());
+ }
+ self.inner.exec(sql, params_json)
+ }
+
+ fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
+ self.inner.query_raw(sql, params_json)
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ self.inner.begin()
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ self.inner.commit()
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ self.inner.rollback()
+ }
+}
+
+struct QueryFailExecutor<'a> {
+ inner: &'a SqliteExecutor,
+ needle: &'static str,
+ err: SqlError,
+}
+
+impl SqlExecutor for QueryFailExecutor<'_> {
+ fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
+ if sql.to_ascii_lowercase().contains(self.needle) {
+ return Err(self.err.clone());
+ }
+ self.inner.exec(sql, params_json)
+ }
+
+ fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
+ if sql.to_ascii_lowercase().contains(self.needle) {
+ return Err(self.err.clone());
+ }
+ self.inner.query_raw(sql, params_json)
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ self.inner.begin()
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ self.inner.commit()
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ self.inner.rollback()
+ }
+}
+
#[test]
fn unwrap_sql_panics_on_error() {
let result = panic::catch_unwind(|| {
@@ -449,6 +566,359 @@ fn ingest_rejects_unsupported_kind() {
assert!(err.to_string().contains("unsupported kind"));
}
+#[test]
+fn ingest_reports_transaction_boundary_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+ let author = "a".repeat(64);
+ let profile = profile_event(
+ 9_001,
+ &author,
+ 10,
+ Some(RadrootsProfileType::Individual),
+ "tx-errors",
+ );
+
+ let begin_fail = BeginFailExecutor { inner: &exec };
+ assert!(radroots_replica_ingest_event(&begin_fail, &profile).is_err());
+
+ let commit_fail = CommitFailExecutor { inner: &exec };
+ assert!(radroots_replica_ingest_event(&commit_fail, &profile).is_err());
+}
+
+#[test]
+fn ingest_reports_delete_internal_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+ let farm_pubkey = "f".repeat(64);
+ let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
+
+ let create_event = farm_event(
+ 9_101,
+ &farm_pubkey,
+ 10,
+ farm_d_tag,
+ "delete-error-farm",
+ None,
+ Some(vec!["seed".to_string()]),
+ );
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &create_event).expect("seed farm"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+
+ let update_event = farm_event(
+ 9_102,
+ &farm_pubkey,
+ 11,
+ farm_d_tag,
+ "delete-error-farm",
+ None,
+ Some(vec!["next".to_string()]),
+ );
+ let delete_fail = DeleteFailExecutor {
+ inner: &exec,
+ table_name: "farm_tag",
+ err: SqlError::Internal,
+ };
+ assert!(radroots_replica_ingest_event(&delete_fail, &update_event).is_err());
+}
+
+#[test]
+fn ingest_reports_parse_and_state_error_paths_for_all_kinds() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+
+ let profile_pubkey = "q".repeat(64);
+ let profile_ok = profile_event(
+ 9_201,
+ &profile_pubkey,
+ 10,
+ Some(RadrootsProfileType::Individual),
+ "profile-ok",
+ );
+ let profile_parse_error = event_with_parts(
+ 9_202,
+ &profile_pubkey,
+ 11,
+ KIND_PROFILE,
+ "{".to_string(),
+ profile_ok.tags.clone(),
+ );
+ assert!(radroots_replica_ingest_event(&exec, &profile_parse_error).is_err());
+
+ let farm_pubkey = "r".repeat(64);
+ let farm_seed_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
+ let farm_seed = farm_event(
+ 9_203,
+ &farm_pubkey,
+ 12,
+ farm_seed_d_tag,
+ "farm-seed",
+ None,
+ None,
+ );
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &farm_seed).expect("seed farm"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+
+ let farm_parse_error = event_with_parts(
+ 9_204,
+ &farm_pubkey,
+ 13,
+ KIND_FARM,
+ "{".to_string(),
+ farm_seed.tags.clone(),
+ );
+ assert!(radroots_replica_ingest_event(&exec, &farm_parse_error).is_err());
+
+ let plot_ok = plot_event(
+ 9_205,
+ &farm_pubkey,
+ 14,
+ "AAAAAAAAAAAAAAAAAAAAAQ",
+ RadrootsFarmRef {
+ pubkey: farm_pubkey.clone(),
+ d_tag: farm_seed_d_tag.to_string(),
+ },
+ "plot-ok",
+ None,
+ None,
+ );
+ let plot_parse_error = event_with_parts(
+ 9_206,
+ &farm_pubkey,
+ 15,
+ KIND_PLOT,
+ "{".to_string(),
+ plot_ok.tags.clone(),
+ );
+ assert!(radroots_replica_ingest_event(&exec, &plot_parse_error).is_err());
+
+ let list_parse_error = event_with_parts(
+ 9_207,
+ &profile_pubkey,
+ 16,
+ KIND_LIST_SET_GENERIC,
+ String::new(),
+ Vec::new(),
+ );
+ assert!(radroots_replica_ingest_event(&exec, &list_parse_error).is_err());
+
+ let state_query_fail = QueryFailExecutor {
+ inner: &exec,
+ needle: "nostr_event_state",
+ err: SqlError::Internal,
+ };
+ assert!(radroots_replica_ingest_event(&state_query_fail, &profile_ok).is_err());
+ assert!(radroots_replica_ingest_event(&state_query_fail, &farm_seed).is_err());
+ assert!(radroots_replica_ingest_event(&state_query_fail, &plot_ok).is_err());
+
+ let claims_set =
+ farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of");
+ let claims_event = list_set_event(
+ 9_208,
+ &profile_pubkey,
+ 17,
+ KIND_LIST_SET_GENERIC,
+ &claims_set,
+ );
+ assert!(radroots_replica_ingest_event(&state_query_fail, &claims_event).is_err());
+
+ let state_insert_fail = QueryFailExecutor {
+ inner: &exec,
+ needle: "insert into nostr_event_state",
+ err: SqlError::Internal,
+ };
+ let profile_insert_state_error = profile_event(
+ 9_209,
+ &"s".repeat(64),
+ 18,
+ Some(RadrootsProfileType::Individual),
+ "profile-state-insert",
+ );
+ assert!(
+ radroots_replica_ingest_event(&state_insert_fail, &profile_insert_state_error).is_err()
+ );
+
+ let farm_insert_state_error = farm_event(
+ 9_210,
+ &farm_pubkey,
+ 19,
+ "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm-state-insert",
+ None,
+ None,
+ );
+ assert!(radroots_replica_ingest_event(&state_insert_fail, &farm_insert_state_error).is_err());
+
+ let plot_insert_state_error = plot_event(
+ 9_211,
+ &farm_pubkey,
+ 20,
+ "AAAAAAAAAAAAAAAAAAAAAg",
+ RadrootsFarmRef {
+ pubkey: farm_pubkey.clone(),
+ d_tag: farm_seed_d_tag.to_string(),
+ },
+ "plot-state-insert",
+ None,
+ None,
+ );
+ assert!(radroots_replica_ingest_event(&state_insert_fail, &plot_insert_state_error).is_err());
+ assert!(radroots_replica_ingest_event(&state_insert_fail, &claims_event).is_err());
+}
+
+#[test]
+fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+
+ let assert_query_fail = |needle: &'static str, event: &RadrootsNostrEvent| {
+ let fail = QueryFailExecutor {
+ inner: &exec,
+ needle,
+ err: SqlError::Internal,
+ };
+ assert!(
+ radroots_replica_ingest_event(&fail, event).is_err(),
+ "needle {needle} should fail"
+ );
+ };
+
+ let profile_pubkey = "t".repeat(64);
+ let profile_create = profile_event(
+ 9_301,
+ &profile_pubkey,
+ 10,
+ Some(RadrootsProfileType::Individual),
+ "profile-query",
+ );
+ assert_query_fail("select * from nostr_profile", &profile_create);
+ assert_query_fail("insert into nostr_profile", &profile_create);
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &profile_create).expect("seed profile"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+ let profile_update = profile_event(
+ 9_302,
+ &profile_pubkey,
+ 11,
+ Some(RadrootsProfileType::Individual),
+ "profile-query-updated",
+ );
+ assert_query_fail("update nostr_profile", &profile_update);
+
+ let farm_pubkey = "u".repeat(64);
+ let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
+ let farm_create = farm_event(
+ 9_303,
+ &farm_pubkey,
+ 12,
+ farm_d_tag,
+ "farm-query",
+ Some(RadrootsFarmLocation {
+ primary: Some("farm".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(37.7, -122.4, "9q8yy"),
+ }),
+ Some(vec!["coffee".to_string()]),
+ );
+ assert_query_fail("select * from farm where", &farm_create);
+ assert_query_fail("insert into farm", &farm_create);
+ assert_query_fail("insert into farm_tag", &farm_create);
+ assert_query_fail("insert into gcs_location", &farm_create);
+ assert_query_fail("insert into farm_gcs_location", &farm_create);
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &farm_create).expect("seed farm"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+ let farm_update = farm_event(
+ 9_304,
+ &farm_pubkey,
+ 13,
+ farm_d_tag,
+ "farm-query-updated",
+ None,
+ Some(vec!["grain".to_string()]),
+ );
+ assert_query_fail("update farm", &farm_update);
+
+ let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
+ let plot_create = plot_event(
+ 9_305,
+ &farm_pubkey,
+ 14,
+ plot_d_tag,
+ RadrootsFarmRef {
+ pubkey: farm_pubkey.clone(),
+ d_tag: farm_d_tag.to_string(),
+ },
+ "plot-query",
+ Some(RadrootsPlotLocation {
+ primary: Some("plot".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(37.8, -122.5, "9q8yz"),
+ }),
+ Some(vec!["orchard".to_string()]),
+ );
+ assert_query_fail("select * from plot where", &plot_create);
+ assert_query_fail("insert into plot", &plot_create);
+ assert_query_fail("insert into plot_tag", &plot_create);
+ assert_query_fail("insert into plot_gcs_location", &plot_create);
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &plot_create).expect("seed plot"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+ let plot_update = plot_event(
+ 9_306,
+ &farm_pubkey,
+ 15,
+ plot_d_tag,
+ RadrootsFarmRef {
+ pubkey: farm_pubkey.clone(),
+ d_tag: farm_d_tag.to_string(),
+ },
+ "plot-query-updated",
+ None,
+ Some(vec!["updated".to_string()]),
+ );
+ assert_query_fail("update plot", &plot_update);
+
+ let member_of_set =
+ farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of");
+ let member_of_event = list_set_event(
+ 9_307,
+ &profile_pubkey,
+ 16,
+ KIND_LIST_SET_GENERIC,
+ &member_of_set,
+ );
+ assert_query_fail("insert into farm_member_claim", &member_of_event);
+
+ let members_set =
+ farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]).expect("members");
+ let members_event =
+ list_set_event(9_308, &farm_pubkey, 17, KIND_LIST_SET_GENERIC, &members_set);
+ assert_query_fail("insert into farm_member", &members_event);
+ assert_query_fail("select * from farm where", &members_event);
+
+ assert_query_fail("select * from nostr_event_state", &members_event);
+ assert_query_fail("insert into nostr_event_state", &members_event);
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &members_event).expect("seed members"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
+ let members_update =
+ list_set_event(9_309, &farm_pubkey, 18, KIND_LIST_SET_GENERIC, &members_set);
+ assert_query_fail("update nostr_event_state", &members_update);
+}
+
fn event_with_parts(
id: u64,
author: &str,
@@ -954,6 +1424,50 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() {
.expect_err("metadata must be rejected");
assert!(metadata_err.to_string().contains("must omit metadata"));
+ let description_list_set = RadrootsListSet {
+ d_tag: "member_of.farms".to_string(),
+ content: String::new(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone()],
+ }],
+ title: None,
+ description: Some("desc".to_string()),
+ image: None,
+ };
+ let description_event = list_set_event(
+ 4011,
+ &profile_pubkey,
+ 3011,
+ KIND_LIST_SET_GENERIC,
+ &description_list_set,
+ );
+ let description_err = radroots_replica_ingest_event(&exec, &description_event)
+ .expect_err("description metadata must be rejected");
+ assert!(description_err.to_string().contains("must omit metadata"));
+
+ let image_list_set = RadrootsListSet {
+ d_tag: "member_of.farms".to_string(),
+ content: String::new(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone()],
+ }],
+ title: None,
+ description: None,
+ image: Some("image".to_string()),
+ };
+ let image_event = list_set_event(
+ 4012,
+ &profile_pubkey,
+ 3012,
+ KIND_LIST_SET_GENERIC,
+ &image_list_set,
+ );
+ let image_err = radroots_replica_ingest_event(&exec, &image_event)
+ .expect_err("image metadata must be rejected");
+ assert!(image_err.to_string().contains("must omit metadata"));
+
let content_list_set = RadrootsListSet {
d_tag: "member_of.farms".to_string(),
content: "not-empty".to_string(),
@@ -1034,6 +1548,25 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() {
radroots_replica_ingest_event(&exec, &member_of_event).expect("member_of skip"),
RadrootsReplicaIngestOutcome::Skipped
);
+ let mut member_of_with_empty_parts =
+ list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC)
+ .expect("member_of parts");
+ member_of_with_empty_parts
+ .tags
+ .insert(0, vec!["p".to_string()]);
+ let member_of_with_empty_event = event_with_parts(
+ 4041,
+ &profile_pubkey,
+ 305,
+ KIND_LIST_SET_GENERIC,
+ member_of_with_empty_parts.content,
+ member_of_with_empty_parts.tags,
+ );
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &member_of_with_empty_event)
+ .expect("member_of with empty entry"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
let claims = unwrap_sql(
farm_member_claim::find_many(
@@ -1094,6 +1627,25 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() {
radroots_replica_ingest_event(&exec, &members_event).expect("members apply"),
RadrootsReplicaIngestOutcome::Applied
);
+ let mut members_with_empty_parts =
+ list_set_encode::to_wire_parts_with_kind(&members_valid, KIND_LIST_SET_GENERIC)
+ .expect("members parts");
+ members_with_empty_parts
+ .tags
+ .insert(0, vec!["p".to_string()]);
+ let members_with_empty_event = event_with_parts(
+ 4061,
+ &farm_pubkey,
+ 307,
+ KIND_LIST_SET_GENERIC,
+ members_with_empty_parts.content,
+ members_with_empty_parts.tags,
+ );
+ assert_eq!(
+ radroots_replica_ingest_event(&exec, &members_with_empty_event)
+ .expect("members with empty entry"),
+ RadrootsReplicaIngestOutcome::Applied
+ );
let owners_valid =
farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners");
let owners_event = list_set_event(407, &farm_pubkey, 307, KIND_LIST_SET_GENERIC, &owners_valid);
@@ -1183,7 +1735,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() {
content: String::new(),
entries: vec![RadrootsListEntry {
tag: "p".to_string(),
- values: vec![farm_pubkey],
+ values: vec![farm_pubkey.clone()],
}],
title: None,
description: None,
@@ -1203,6 +1755,44 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() {
.to_string()
.contains("unsupported list set d_tag")
);
+
+ let mut malformed_farm_list_missing_farm_parts =
+ list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC)
+ .expect("malformed missing farm parts");
+ for tag in &mut malformed_farm_list_missing_farm_parts.tags {
+ if tag.first().map(String::as_str) == Some("d") && tag.len() > 1 {
+ tag[1] = "farm".to_string();
+ }
+ }
+ let malformed_farm_list_missing_farm_event = event_with_parts(
+ 412,
+ &farm_pubkey,
+ 312,
+ KIND_LIST_SET_GENERIC,
+ malformed_farm_list_missing_farm_parts.content,
+ malformed_farm_list_missing_farm_parts.tags,
+ );
+ assert!(radroots_replica_ingest_event(&exec, &malformed_farm_list_missing_farm_event).is_err());
+
+ let mut malformed_farm_list_missing_suffix_parts =
+ list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC)
+ .expect("malformed missing suffix parts");
+ for tag in &mut malformed_farm_list_missing_suffix_parts.tags {
+ if tag.first().map(String::as_str) == Some("d") && tag.len() > 1 {
+ tag[1] = format!("farm:{farm_d_tag}");
+ }
+ }
+ let malformed_farm_list_missing_suffix_event = event_with_parts(
+ 413,
+ &farm_pubkey,
+ 313,
+ KIND_LIST_SET_GENERIC,
+ malformed_farm_list_missing_suffix_parts.content,
+ malformed_farm_list_missing_suffix_parts.tags,
+ );
+ assert!(
+ radroots_replica_ingest_event(&exec, &malformed_farm_list_missing_suffix_event).is_err()
+ );
}
#[test]
@@ -1498,6 +2088,47 @@ fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() {
}
#[test]
+fn sync_emit_reports_encode_error_for_invalid_farm_record() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+
+ let farm_row = unwrap_sql(
+ farm::create(
+ &exec,
+ &IFarmFields {
+ d_tag: String::new(),
+ pubkey: "v".repeat(64),
+ name: "invalid farm".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location_primary: None,
+ location_city: None,
+ location_region: None,
+ location_country: None,
+ },
+ ),
+ "farm",
+ )
+ .result;
+
+ let err = radroots_replica_sync_all(
+ &exec,
+ &RadrootsReplicaSyncRequest {
+ farm: RadrootsReplicaFarmSelector {
+ id: Some(farm_row.id),
+ d_tag: None,
+ pubkey: None,
+ },
+ options: None,
+ },
+ )
+ .expect_err("encode error");
+ assert!(err.to_string().contains("replica_sync.encode"));
+}
+
+#[test]
fn error_conversion_paths_are_exercised() {
let sql: RadrootsReplicaEventsError = IError::from(SqlError::Internal).into();
assert!(sql.to_string().contains("replica_sync.sql"));