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:
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));
+}