commit 4b850fac00ba4b114d8064c4aa60513ead2afbcd
parent 2c472afa5479a895616eb50f238198a02f7dd54d
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Mar 2026 03:20:55 +0000
replica-sync: close coverage gaps
- add emit executors and option coverage paths
- add ingest/error assertions and event-state failpoint hook
- fix emit test d_tag fixtures for valid ids
- tests: cargo check, cargo test
Diffstat:
4 files changed, 307 insertions(+), 18 deletions(-)
diff --git a/crates/replica-sync/src/emit.rs b/crates/replica-sync/src/emit.rs
@@ -843,7 +843,62 @@ mod tests {
IPlotGcsLocationFields, IPlotGcsLocationFindMany,
};
use radroots_replica_db_schema::plot_tag::IPlotTagFields;
- use radroots_sql_core::SqliteExecutor;
+ use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor, SqliteExecutor};
+
+ struct ErrorExecutor;
+
+ impl SqlExecutor for ErrorExecutor {
+ fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> {
+ Err(SqlError::Internal)
+ }
+
+ fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> {
+ Err(SqlError::Internal)
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ Ok(())
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ Ok(())
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ Ok(())
+ }
+ }
+
+ 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> {
+ 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()
+ }
+ }
fn seed(exec: &SqliteExecutor) -> (Farm, Plot, Plot) {
migrations::run_all_up(exec).expect("migrations");
@@ -1414,4 +1469,213 @@ mod tests {
.expect("plots lookup");
assert_eq!(plots_lookup.results.len(), 2);
}
+
+ #[test]
+ fn emit_option_toggles_and_empty_rows_cover_branches() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+
+ let farm = farm::create(
+ &exec,
+ &IFarmFields {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ pubkey: "b".repeat(64),
+ name: "farm-empty".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location_primary: None,
+ location_city: None,
+ location_region: None,
+ location_country: None,
+ },
+ )
+ .expect("farm")
+ .result;
+
+ let _plot = plot::create(
+ &exec,
+ &IPlotFields {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ farm_id: farm.id.clone(),
+ name: "plot-empty".to_string(),
+ about: None,
+ location_primary: None,
+ location_city: None,
+ location_region: None,
+ location_country: None,
+ },
+ )
+ .expect("plot");
+
+ let selector = RadrootsReplicaFarmSelector {
+ id: Some(farm.id.clone()),
+ d_tag: None,
+ pubkey: None,
+ };
+ let bundle = radroots_replica_sync_all_with_options(
+ &exec,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(false),
+ include_list_sets: Some(false),
+ include_membership_claims: Some(false),
+ }),
+ )
+ .expect("sync");
+ assert_eq!(bundle.events.len(), 2);
+
+ let farm_event = radroots_replica_farm_event(&exec, &farm).expect("farm event");
+ assert_eq!(farm_event.kind, KIND_FARM);
+ let plot_events = radroots_replica_plot_events(&exec, &farm).expect("plot events");
+ assert_eq!(plot_events.len(), 1);
+
+ let claims = radroots_replica_membership_claim_events(&exec, &"z".repeat(64))
+ .expect("empty claims");
+ assert!(claims.is_empty());
+
+ let by_pair = resolve_farm(
+ &exec,
+ &RadrootsReplicaFarmSelector {
+ id: None,
+ d_tag: Some(farm.d_tag.clone()),
+ pubkey: Some(farm.pubkey.clone()),
+ },
+ )
+ .expect("resolve by pair");
+ assert_eq!(by_pair.id, farm.id);
+ }
+
+ #[test]
+ fn emit_profile_variants_and_missing_profiles_are_handled() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ let (farm_row, _, _) = seed(&exec);
+
+ let _ = farm_member::create(
+ &exec,
+ &IFarmMemberFields {
+ farm_id: farm_row.id.clone(),
+ member_pubkey: "z".repeat(64),
+ role: ROLE_MEMBER.to_string(),
+ },
+ )
+ .expect("member");
+
+ let profiles = radroots_replica_profile_events(&exec, &farm_row).expect("profiles");
+ assert!(!profiles.is_empty());
+
+ let profile_coop = profile_event(
+ &"c".repeat(64),
+ radroots_replica_db_schema::nostr_profile::NostrProfile {
+ id: "00000000-0000-0000-0000-0000000000c0".to_string(),
+ created_at: "2024-01-01T00:00:00.000Z".to_string(),
+ updated_at: "2024-01-01T00:00:00.000Z".to_string(),
+ public_key: "c".repeat(64),
+ profile_type: "coop".to_string(),
+ name: "coop".to_string(),
+ display_name: None,
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ nip05: None,
+ lud06: None,
+ lud16: None,
+ },
+ )
+ .expect("profile coop");
+ assert!(!profile_coop.tags.is_empty());
+
+ let profile_any = profile_event(
+ &"a".repeat(64),
+ radroots_replica_db_schema::nostr_profile::NostrProfile {
+ id: "00000000-0000-0000-0000-0000000000a0".to_string(),
+ created_at: "2024-01-01T00:00:00.000Z".to_string(),
+ updated_at: "2024-01-01T00:00:00.000Z".to_string(),
+ public_key: "a".repeat(64),
+ profile_type: "any".to_string(),
+ name: "any".to_string(),
+ display_name: None,
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ nip05: None,
+ lud06: None,
+ lud16: None,
+ },
+ )
+ .expect("profile any");
+ assert!(!profile_any.tags.is_empty());
+
+ let profile_sparse = serialize_profile_content(&RadrootsProfile {
+ name: "sparse".to_string(),
+ display_name: None,
+ nip05: None,
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ lud06: None,
+ lud16: None,
+ bot: None,
+ })
+ .expect("serialize");
+ assert!(profile_sparse.contains("\"name\""));
+ }
+
+ #[test]
+ fn emit_query_error_paths_are_reported() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+
+ let farm = Farm {
+ id: "farm".to_string(),
+ created_at: "now".to_string(),
+ updated_at: "now".to_string(),
+ d_tag: "d".to_string(),
+ pubkey: "p".repeat(64),
+ name: "farm".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location_primary: None,
+ location_city: None,
+ location_region: None,
+ location_country: None,
+ };
+ let plot = Plot {
+ id: "plot".to_string(),
+ created_at: "now".to_string(),
+ updated_at: "now".to_string(),
+ d_tag: "plot".to_string(),
+ farm_id: farm.id.clone(),
+ name: "plot".to_string(),
+ about: None,
+ location_primary: None,
+ location_city: None,
+ location_region: None,
+ location_country: None,
+ };
+
+ let err_exec = ErrorExecutor;
+ assert!(collect_farm_tags(&err_exec, "id").is_err());
+ assert!(collect_plot_tags(&err_exec, "id").is_err());
+ assert!(load_farm_members(&err_exec, "id").is_err());
+ assert!(load_plots(&err_exec, "id").is_err());
+ assert!(load_farm_location(&err_exec, &farm).is_err());
+ assert!(load_plot_location(&err_exec, &plot).is_err());
+ assert!(load_profile(&err_exec, "p").is_err());
+ assert!(load_member_claims(&err_exec, "p").is_err());
+ assert!(load_member_claims_for_member(&err_exec, "p").is_err());
+
+ let claims_fail = QueryFailExecutor {
+ inner: &exec,
+ needle: "farm_member_claim",
+ err: SqlError::Internal,
+ };
+ assert!(collect_profile_pubkeys(&claims_fail, &farm).is_err());
+ }
}
diff --git a/crates/replica-sync/src/error.rs b/crates/replica-sync/src/error.rs
@@ -78,14 +78,14 @@ mod tests {
#[test]
fn from_impls_map_into_expected_variants() {
let sql_from: RadrootsReplicaEventsError = IError::from(SqlError::Internal).into();
- assert!(matches!(sql_from, RadrootsReplicaEventsError::Sql(_)));
+ assert!(sql_from.to_string().contains("replica_sync.sql"));
let encode_from: RadrootsReplicaEventsError = EventEncodeError::Json.into();
- assert!(matches!(encode_from, RadrootsReplicaEventsError::Encode(_)));
+ assert!(encode_from.to_string().contains("replica_sync.encode"));
let parse_number_err = "invalid".parse::<u32>().expect_err("parse int should fail");
let parse_from: RadrootsReplicaEventsError =
EventParseError::InvalidNumber("k", parse_number_err).into();
- assert!(matches!(parse_from, RadrootsReplicaEventsError::Parse(_)));
+ assert!(parse_from.to_string().contains("replica_sync.parse"));
}
}
diff --git a/crates/replica-sync/src/event_state.rs b/crates/replica-sync/src/event_state.rs
@@ -13,6 +13,21 @@ use sha2::{Digest, Sha256};
use crate::error::RadrootsReplicaEventsError;
+#[cfg(test)]
+mod failpoints {
+ use core::sync::atomic::{AtomicBool, Ordering};
+
+ static FORCE_ERROR: AtomicBool = AtomicBool::new(false);
+
+ pub(crate) fn set_error() {
+ FORCE_ERROR.store(true, Ordering::SeqCst);
+ }
+
+ pub(crate) fn take_error() -> bool {
+ FORCE_ERROR.swap(false, Ordering::SeqCst)
+ }
+}
+
pub fn event_state_key(kind: u32, pubkey: &str, d_tag: &str) -> String {
format!("{kind}:{pubkey}:{d_tag}")
}
@@ -21,6 +36,12 @@ pub fn event_content_hash(
content: &str,
tags: &[Vec<String>],
) -> Result<String, RadrootsReplicaEventsError> {
+ #[cfg(test)]
+ if failpoints::take_error() {
+ return Err(RadrootsReplicaEventsError::InvalidData(
+ "content_hash".to_string(),
+ ));
+ }
let tags_json = Value::Array(
tags.iter()
.map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect()))
diff --git a/crates/replica-sync/src/ingest.rs b/crates/replica-sync/src/ingest.rs
@@ -1432,18 +1432,19 @@ mod tests {
let begin_err =
radroots_replica_ingest_event_with_factory(&begin_executor, &event, &FixedFactory)
.expect_err("begin");
- assert!(matches!(begin_err, RadrootsReplicaEventsError::Sql(_)));
+ assert!(begin_err.to_string().contains("replica_sync.sql"));
assert!(begin_executor.commit().is_ok());
- assert!(matches!(
- begin_executor.exec("select 1", "[]").expect_err("exec"),
- SqlError::UnsupportedPlatform
- ));
- assert!(matches!(
+ assert_eq!(
+ begin_executor.exec("select 1", "[]").expect_err("exec").code(),
+ "ERR_UNSUPPORTED_PLATFORM"
+ );
+ assert_eq!(
begin_executor
.query_raw("select 1", "[]")
- .expect_err("query"),
- SqlError::UnsupportedPlatform
- ));
+ .expect_err("query")
+ .code(),
+ "ERR_UNSUPPORTED_PLATFORM"
+ );
let rollback_count = Arc::new(AtomicUsize::new(0));
let commit_executor = TxnExecutor {
@@ -1454,7 +1455,7 @@ mod tests {
let commit_err =
radroots_replica_ingest_event_with_factory(&commit_executor, &event, &FixedFactory)
.expect_err("commit");
- assert!(matches!(commit_err, RadrootsReplicaEventsError::Sql(_)));
+ assert!(commit_err.to_string().contains("replica_sync.sql"));
assert_eq!(rollback_count.load(Ordering::SeqCst), 0);
let rollback_executor = TxnExecutor {
@@ -1855,10 +1856,13 @@ mod tests {
assert!(not_found_claims.commit().is_ok());
let _ = not_found_claims.rollback();
assert!(not_found_claims.query_raw("SELECT 1", "[]").is_ok());
- assert!(matches!(
- not_found_claims.exec("DELETE FROM farm_member_claim WHERE id = 1", "[]"),
- Err(SqlError::NotFound(_))
- ));
+ assert_eq!(
+ not_found_claims
+ .exec("DELETE FROM farm_member_claim WHERE id = 1", "[]")
+ .expect_err("exec not found")
+ .code(),
+ "ERR_NOT_FOUND"
+ );
let _ = not_found_claims.exec("DELETE FROM other_table WHERE id = 1", "[]");
let internal_farm_tags = DeleteErrorExecutor {