lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 13f6e8a926429c9adcfc4057380e1e2de1ae9fe5
parent e2b8ec089f0022a3cb7daa46ca8834ada2e3c91a
Author: triesap <tyson@radroots.org>
Date:   Fri,  6 Mar 2026 19:00:59 +0000

tests: add export and relation-path coverage assertions

- add export manifest unit tests for schema query failure, table count failure, and zero-row fallback
- add relation set and unset error-path tests for profile relay and trade product link tables
- expand mock executor path assertions to cover trait method and query fallback behavior
- increase replica-db region coverage with crate checks and tests passing

Diffstat:
Mcrates/replica-db/src/export.rs | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/replica-db/tests/error_paths.rs | 32++++++++++++++++++++++++++++++++
2 files changed, 174 insertions(+), 0 deletions(-)

diff --git a/crates/replica-db/src/export.rs b/crates/replica-db/src/export.rs @@ -75,3 +75,145 @@ fn schema_hash(schema: &[SchemaEntry]) -> Result<String, SqlError> { hasher.update(json.as_bytes()); Ok(hex::encode(hasher.finalize())) } + +#[cfg(test)] +mod tests { + use super::*; + use radroots_sql_core::ExecOutcome; + + struct MockExecutor { + query_rules: Vec<(String, String)>, + fail_query_contains: Option<String>, + } + + impl MockExecutor { + fn new(query_rules: Vec<(String, String)>, fail_query_contains: Option<String>) -> Self { + Self { + query_rules, + fail_query_contains, + } + } + } + + impl SqlExecutor for MockExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + Ok(ExecOutcome { + changes: 1, + last_insert_id: 1, + }) + } + + fn query_raw(&self, sql: &str, _params_json: &str) -> Result<String, SqlError> { + if let Some(needle) = &self.fail_query_contains { + if sql.contains(needle) { + return Err(SqlError::InvalidQuery(String::from("forced query failure"))); + } + } + for (needle, response) in &self.query_rules { + if sql.contains(needle) { + return Ok(response.clone()); + } + } + Ok(String::from("[]")) + } + + fn begin(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + Ok(()) + } + } + + #[test] + fn export_manifest_propagates_schema_query_errors() { + let executor = MockExecutor::new( + Vec::new(), + Some(String::from( + "select type, name, tbl_name as table_name, sql from sqlite_master", + )), + ); + let err = export_manifest(&executor).expect_err("export should fail"); + assert!(matches!(err, SqlError::InvalidQuery(_))); + } + + #[test] + fn export_manifest_propagates_table_count_query_errors() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + schema_rows, + )], + Some(String::from("select count(1) as count from \"tb_a\"")), + ); + let err = export_manifest(&executor).expect_err("export should fail"); + assert!(matches!(err, SqlError::InvalidQuery(_))); + } + + #[test] + fn export_manifest_defaults_missing_count_row_to_zero() { + let schema_rows = serde_json::json!([ + { + "type": "table", + "name": "tb_a", + "table_name": "tb_a", + "sql": "CREATE TABLE tb_a (id TEXT);" + } + ]) + .to_string(); + let executor = MockExecutor::new( + vec![ + ( + String::from("select type, name, tbl_name as table_name, sql from sqlite_master"), + schema_rows, + ), + (String::from("select count(1) as count from \"tb_a\""), String::from("[]")), + ], + None, + ); + let manifest = export_manifest(&executor).expect("export should succeed"); + assert_eq!(manifest.table_counts.len(), 1); + assert_eq!(manifest.table_counts[0].name, "tb_a"); + assert_eq!(manifest.table_counts[0].row_count, 0); + } + + #[test] + fn mock_executor_trait_and_query_paths_are_covered() { + let executor = MockExecutor::new( + vec![(String::from("select 1"), String::from("[{\"count\":1}]"))], + None, + ); + let outcome = executor.exec("select 1", "[]").expect("exec"); + assert_eq!(outcome.changes, 1); + assert_eq!(outcome.last_insert_id, 1); + + executor.begin().expect("begin"); + executor.commit().expect("commit"); + executor.rollback().expect("rollback"); + + let matched = executor.query_raw("select 1", "[]").expect("matched query"); + assert_eq!(matched, "[{\"count\":1}]"); + let fallback = executor.query_raw("select 2", "[]").expect("fallback query"); + assert_eq!(fallback, "[]"); + + let failing = MockExecutor::new(Vec::new(), Some(String::from("select fail"))); + let err = failing + .query_raw("select fail", "[]") + .expect_err("query should fail"); + assert!(matches!(err, SqlError::InvalidQuery(_))); + } +} diff --git a/crates/replica-db/tests/error_paths.rs b/crates/replica-db/tests/error_paths.rs @@ -32,6 +32,7 @@ use radroots_replica_db_schema::nostr_event_state::{ INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, INostrEventStateFindOne, INostrEventStateUpdate, }; +use radroots_replica_db_schema::nostr_profile_relay::INostrProfileRelayRelation; use radroots_replica_db_schema::nostr_profile::{ INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, INostrProfileUpdate, @@ -54,6 +55,8 @@ use radroots_replica_db_schema::trade_product::{ ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, ITradeProductUpdate, }; +use radroots_replica_db_schema::trade_product_location::ITradeProductLocationRelation; +use radroots_replica_db_schema::trade_product_media::ITradeProductMediaRelation; use radroots_sql_core::{SqlError, SqlExecutor, SqliteExecutor}; use radroots_types::types::IError; use serde::de::DeserializeOwned; @@ -940,3 +943,32 @@ fn trade_product_error_paths_cover_regions() { })); assert_invalid_query(db.trade_product_delete(&delete_id)); } + +#[test] +fn relation_set_unset_error_paths_cover_regions() { + let db = open_db(); + + drop_table(&db, "nostr_profile_relay"); + let profile_relay_rel: INostrProfileRelayRelation = parse_json(json!({ + "nostr_profile": { "id": "profile-1" }, + "nostr_relay": { "id": "relay-1" } + })); + assert_invalid_query(db.nostr_profile_relay_set(&profile_relay_rel)); + assert_invalid_query(db.nostr_profile_relay_unset(&profile_relay_rel)); + + drop_table(&db, "trade_product_location"); + let product_location_rel: ITradeProductLocationRelation = parse_json(json!({ + "trade_product": { "id": "product-1" }, + "gcs_location": { "id": "gcs-1" } + })); + assert_invalid_query(db.trade_product_location_set(&product_location_rel)); + assert_invalid_query(db.trade_product_location_unset(&product_location_rel)); + + drop_table(&db, "trade_product_media"); + let product_media_rel: ITradeProductMediaRelation = parse_json(json!({ + "trade_product": { "id": "product-1" }, + "media_image": { "id": "media-1" } + })); + assert_invalid_query(db.trade_product_media_set(&product_media_rel)); + assert_invalid_query(db.trade_product_media_unset(&product_media_rel)); +}