commit 56391a96b622bc5d948a10678bc0c95f7e522a7b
parent 13f6e8a926429c9adcfc4057380e1e2de1ae9fe5
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Mar 2026 19:20:31 +0000
replica-sync: add sync status error-path coverage tests
- add sync_state tests for farm query, emit, content hash, and state query failure branches
- expose a test-only event_content_hash_fail_next helper for deterministic branch targeting
- make event_state failpoint thread-local to avoid cross-test interference under parallel execution
- run cargo check and cargo test for radroots-replica-sync after formatting updated files
Diffstat:
2 files changed, 98 insertions(+), 6 deletions(-)
diff --git a/crates/replica-sync/src/event_state.rs b/crates/replica-sync/src/event_state.rs
@@ -15,16 +15,22 @@ use crate::error::RadrootsReplicaEventsError;
#[cfg(test)]
mod failpoints {
- use core::sync::atomic::{AtomicBool, Ordering};
+ use std::cell::Cell;
- static FORCE_ERROR: AtomicBool = AtomicBool::new(false);
+ thread_local! {
+ static FORCE_ERROR: Cell<bool> = const { Cell::new(false) };
+ }
pub(crate) fn set_error() {
- FORCE_ERROR.store(true, Ordering::SeqCst);
+ FORCE_ERROR.with(|flag| flag.set(true));
}
pub(crate) fn take_error() -> bool {
- FORCE_ERROR.swap(false, Ordering::SeqCst)
+ FORCE_ERROR.with(|flag| {
+ let value = flag.get();
+ flag.set(false);
+ value
+ })
}
}
@@ -54,6 +60,11 @@ pub fn event_content_hash(
Ok(hex::encode(hasher.finalize()))
}
+#[cfg(test)]
+pub(crate) fn event_content_hash_fail_next() {
+ failpoints::set_error();
+}
+
pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> {
tags.iter()
.find(|tag| tag.get(0).map(|v| v.as_str()) == Some(key))
diff --git a/crates/replica-sync/src/sync_state.rs b/crates/replica-sync/src/sync_state.rs
@@ -72,12 +72,15 @@ pub fn radroots_replica_sync_status<E: SqlExecutor>(
mod tests {
use super::radroots_replica_sync_status;
use crate::emit::radroots_replica_sync_all_with_options;
- use crate::event_state::{event_content_hash, event_state_key, tag_value};
+ use crate::error::RadrootsReplicaEventsError;
+ use crate::event_state::{
+ event_content_hash, event_content_hash_fail_next, event_state_key, tag_value,
+ };
use crate::types::RadrootsReplicaFarmSelector;
use radroots_replica_db::{farm, migrations, nostr_event_state};
use radroots_replica_db_schema::farm::IFarmFields;
use radroots_replica_db_schema::nostr_event_state::INostrEventStateFields;
- use radroots_sql_core::SqliteExecutor;
+ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
#[test]
fn sync_status_empty_db_is_zero() {
@@ -139,4 +142,82 @@ mod tests {
assert_eq!(status.expected_count, expected_count);
assert_eq!(status.pending_count, expected_count.saturating_sub(1));
}
+
+ #[test]
+ fn sync_status_reports_farm_query_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ let err = radroots_replica_sync_status(&exec).expect_err("farm query error");
+ assert!(err.to_string().contains("invalid query"));
+ }
+
+ #[test]
+ fn sync_status_reports_emit_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+ let _ = farm::create(
+ &exec,
+ &IFarmFields {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ pubkey: "b".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,
+ },
+ )
+ .expect("farm");
+ let _ = exec
+ .exec("DROP TABLE farm_tag;", "[]")
+ .expect("drop farm_tag");
+ let err = radroots_replica_sync_status(&exec).expect_err("emit error");
+ assert!(err.to_string().contains("invalid query"));
+ }
+
+ #[test]
+ fn sync_status_reports_content_hash_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+ let _ = farm::create(
+ &exec,
+ &IFarmFields {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ pubkey: "c".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,
+ },
+ )
+ .expect("farm");
+ event_content_hash_fail_next();
+ let err = radroots_replica_sync_status(&exec).expect_err("content hash error");
+ assert!(
+ matches!(
+ err,
+ RadrootsReplicaEventsError::InvalidData(ref field) if field == "content_hash"
+ ),
+ "{err:?}"
+ );
+ }
+
+ #[test]
+ fn sync_status_reports_state_query_errors() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ migrations::run_all_up(&exec).expect("migrations");
+ let _ = exec
+ .exec("DROP TABLE nostr_event_state;", "[]")
+ .expect("drop nostr_event_state");
+ let err = radroots_replica_sync_status(&exec).expect_err("state query error");
+ assert!(err.to_string().contains("invalid query"));
+ }
}