commit 47e75092ecd9b5afa02aa5fe0bf9428691906b84
parent a3f44ab361f4938cddf3c5cab07a2c7277bc0cb0
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Mar 2026 21:58:37 +0000
- add unit coverage for invalid farm selector through radroots_replica_sync_all_with_options
- add ingest error-path tests for missing farm refs and invalid domain list-set tags
- gate test-only event_state error type import with cfg to remove non-test warning
- verify crate gates with cargo check, cargo test, and xtask coverage report at 100/100/100/100
Diffstat:
3 files changed, 855 insertions(+), 565 deletions(-)
diff --git a/crates/replica-sync/src/emit.rs b/crates/replica-sync/src/emit.rs
@@ -16,8 +16,8 @@ use radroots_events::farm::{
use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT};
use radroots_events::plot::RadrootsPlot;
use radroots_events::profile::{
- radroots_profile_type_from_tag_value, radroots_profile_type_tag_value, RadrootsProfile,
- RadrootsProfileType, RADROOTS_PROFILE_TYPE_TAG_KEY,
+ RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType,
+ radroots_profile_type_from_tag_value, radroots_profile_type_tag_value,
};
use radroots_events_codec::farm::encode as farm_encode;
use radroots_events_codec::farm::list_sets as farm_list_sets;
@@ -59,8 +59,8 @@ use crate::canonical::canonical_json_string;
use crate::error::RadrootsReplicaEventsError;
use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84};
use crate::types::{
- RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, RadrootsReplicaSyncBundle,
- RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, RADROOTS_REPLICA_TRANSFER_VERSION,
+ RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector,
+ RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest,
};
const ROLE_PRIMARY: &str = "primary";
@@ -102,15 +102,15 @@ pub(crate) mod failpoints {
}
}
-pub fn radroots_replica_sync_all<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_sync_all(
+ exec: &dyn SqlExecutor,
request: &RadrootsReplicaSyncRequest,
) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> {
radroots_replica_sync_all_with_options(exec, &request.farm, request.options.as_ref())
}
-pub fn radroots_replica_sync_all_with_options<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_sync_all_with_options(
+ exec: &dyn SqlExecutor,
farm_selector: &RadrootsReplicaFarmSelector,
options: Option<&RadrootsReplicaSyncOptions>,
) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> {
@@ -151,8 +151,8 @@ pub fn radroots_replica_sync_all_with_options<E: SqlExecutor>(
})
}
-pub fn radroots_replica_profile_events<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_profile_events(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> {
let mut pubkeys = collect_profile_pubkeys(exec, farm)?;
@@ -168,8 +168,8 @@ pub fn radroots_replica_profile_events<E: SqlExecutor>(
Ok(events)
}
-pub fn radroots_replica_farm_event<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_farm_event(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> {
let tags = collect_farm_tags(exec, &farm.id)?;
@@ -194,8 +194,8 @@ pub fn radroots_replica_farm_event<E: SqlExecutor>(
Ok(parts_to_draft(&farm.pubkey, parts))
}
-pub fn radroots_replica_plot_events<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_plot_events(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> {
let plots = load_plots(exec, &farm.id)?;
@@ -226,8 +226,8 @@ pub fn radroots_replica_plot_events<E: SqlExecutor>(
Ok(events)
}
-pub fn radroots_replica_list_set_events<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_list_set_events(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> {
let members = load_farm_members(exec, &farm.id)?;
@@ -252,8 +252,8 @@ pub fn radroots_replica_list_set_events<E: SqlExecutor>(
Ok(events)
}
-pub fn radroots_replica_membership_claim_events<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_membership_claim_events(
+ exec: &dyn SqlExecutor,
farm_pubkey: &str,
) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> {
let claims = load_member_claims(exec, farm_pubkey)?;
@@ -282,8 +282,8 @@ pub fn radroots_replica_membership_claim_events<E: SqlExecutor>(
Ok(events)
}
-fn resolve_farm<E: SqlExecutor>(
- exec: &E,
+fn resolve_farm(
+ exec: &dyn SqlExecutor,
selector: &RadrootsReplicaFarmSelector,
) -> Result<Farm, RadrootsReplicaEventsError> {
if let Some(id) = selector.id.as_ref().filter(|v| !v.trim().is_empty()) {
@@ -350,8 +350,8 @@ fn resolve_farm<E: SqlExecutor>(
))
}
-fn collect_farm_tags<E: SqlExecutor>(
- exec: &E,
+fn collect_farm_tags(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<Vec<String>, RadrootsReplicaEventsError> {
let filter = IFarmTagFieldsFilter {
@@ -378,8 +378,8 @@ fn collect_farm_tags<E: SqlExecutor>(
Ok(tags)
}
-fn collect_plot_tags<E: SqlExecutor>(
- exec: &E,
+fn collect_plot_tags(
+ exec: &dyn SqlExecutor,
plot_id: &str,
) -> Result<Vec<String>, RadrootsReplicaEventsError> {
let filter = IPlotTagFieldsFilter {
@@ -406,8 +406,8 @@ fn collect_plot_tags<E: SqlExecutor>(
Ok(tags)
}
-fn load_farm_members<E: SqlExecutor>(
- exec: &E,
+fn load_farm_members(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<Vec<FarmMember>, RadrootsReplicaEventsError> {
let filter = IFarmMemberFieldsFilter {
@@ -449,8 +449,8 @@ fn sorted_plot_ids(plots: &[Plot]) -> Vec<String> {
ids
}
-fn load_plots<E: SqlExecutor>(
- exec: &E,
+fn load_plots(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<Vec<Plot>, RadrootsReplicaEventsError> {
let filter = IPlotFieldsFilter {
@@ -478,8 +478,8 @@ fn load_plots<E: SqlExecutor>(
Ok(plots)
}
-fn load_farm_location<E: SqlExecutor>(
- exec: &E,
+fn load_farm_location(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<Option<RadrootsFarmLocation>, RadrootsReplicaEventsError> {
let location = load_gcs_location_for_farm(exec, &farm.id)?;
@@ -492,8 +492,8 @@ fn load_farm_location<E: SqlExecutor>(
}))
}
-fn load_plot_location<E: SqlExecutor>(
- exec: &E,
+fn load_plot_location(
+ exec: &dyn SqlExecutor,
plot: &Plot,
) -> Result<Option<radroots_events::plot::RadrootsPlotLocation>, RadrootsReplicaEventsError> {
let location = load_gcs_location_for_plot(exec, &plot.id)?;
@@ -508,8 +508,8 @@ fn load_plot_location<E: SqlExecutor>(
)
}
-fn load_gcs_location_for_farm<E: SqlExecutor>(
- exec: &E,
+fn load_gcs_location_for_farm(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> {
let primary = load_relation_by_role(exec, farm_id, ROLE_PRIMARY, RelationType::Farm)?;
@@ -519,8 +519,8 @@ fn load_gcs_location_for_farm<E: SqlExecutor>(
}
}
-fn load_gcs_location_for_plot<E: SqlExecutor>(
- exec: &E,
+fn load_gcs_location_for_plot(
+ exec: &dyn SqlExecutor,
plot_id: &str,
) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> {
let primary = load_relation_by_role(exec, plot_id, ROLE_PRIMARY, RelationType::Plot)?;
@@ -535,8 +535,8 @@ enum RelationType {
Plot,
}
-fn load_relation_by_role<E: SqlExecutor>(
- exec: &E,
+fn load_relation_by_role(
+ exec: &dyn SqlExecutor,
id: &str,
role: &str,
relation: RelationType,
@@ -600,10 +600,7 @@ fn load_relation_by_role<E: SqlExecutor>(
return Ok(None);
}
- rels.sort_by(|a, b| {
- let rank = location_role_rank(a.role()).cmp(&location_role_rank(b.role()));
- rank.then_with(|| a.gcs_location_id().cmp(b.gcs_location_id()))
- });
+ rels.sort_by(compare_relation_rows);
let gcs_id = rels[0].gcs_location_id().to_string();
let gcs_result = gcs_location::find_one(
exec,
@@ -611,12 +608,22 @@ fn load_relation_by_role<E: SqlExecutor>(
on: GcsLocationQueryBindValues::Id { id: gcs_id },
}),
);
- let gcs = gcs_result?.result.ok_or_else(|| {
- RadrootsReplicaEventsError::InvalidData("gcs_location not found".to_string())
- })?;
+ let gcs = match gcs_result?.result {
+ Some(gcs) => gcs,
+ None => {
+ return Err(RadrootsReplicaEventsError::InvalidData(
+ "gcs_location not found".to_string(),
+ ));
+ }
+ };
Ok(Some(gcs_location_to_event(&gcs)?))
}
+fn compare_relation_rows(a: &RelationRow, b: &RelationRow) -> core::cmp::Ordering {
+ let rank = location_role_rank(a.role()).cmp(&location_role_rank(b.role()));
+ rank.then_with(|| a.gcs_location_id().cmp(b.gcs_location_id()))
+}
+
fn list_set_to_wire_parts(
list_set: &radroots_events::list_set::RadrootsListSet,
) -> Result<WireEventParts, RadrootsReplicaEventsError> {
@@ -654,11 +661,7 @@ impl RelationRow {
}
fn location_role_rank(role: &str) -> u8 {
- if role == ROLE_PRIMARY {
- 0
- } else {
- 1
- }
+ u8::from(role != ROLE_PRIMARY)
}
fn gcs_location_to_event(
@@ -715,8 +718,8 @@ fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon {
geojson_polygon_circle_wgs84(lat, lng, 100.0, 64)
}
-fn load_profile<E: SqlExecutor>(
- exec: &E,
+fn load_profile(
+ exec: &dyn SqlExecutor,
pubkey: &str,
) -> Result<
Option<radroots_replica_db_schema::nostr_profile::NostrProfile>,
@@ -805,8 +808,8 @@ fn serialize_profile_content(
canonical_json_string(&Value::Object(obj))
}
-fn collect_member_pubkeys<E: SqlExecutor>(
- exec: &E,
+fn collect_member_pubkeys(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<Vec<String>, RadrootsReplicaEventsError> {
let members = load_farm_members(exec, farm_id)?;
@@ -819,8 +822,8 @@ fn collect_member_pubkeys<E: SqlExecutor>(
Ok(pubkeys)
}
-fn collect_profile_pubkeys<E: SqlExecutor>(
- exec: &E,
+fn collect_profile_pubkeys(
+ exec: &dyn SqlExecutor,
farm: &Farm,
) -> Result<Vec<String>, RadrootsReplicaEventsError> {
let mut pubkeys = collect_member_pubkeys(exec, &farm.id)?;
@@ -830,8 +833,8 @@ fn collect_profile_pubkeys<E: SqlExecutor>(
Ok(pubkeys)
}
-fn load_member_claims<E: SqlExecutor>(
- exec: &E,
+fn load_member_claims(
+ exec: &dyn SqlExecutor,
farm_pubkey: &str,
) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> {
let filter = IFarmMemberClaimFieldsFilter {
@@ -851,8 +854,8 @@ fn load_member_claims<E: SqlExecutor>(
Ok(result.results)
}
-fn load_member_claims_for_member<E: SqlExecutor>(
- exec: &E,
+fn load_member_claims_for_member(
+ exec: &dyn SqlExecutor,
member_pubkey: &str,
) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> {
let filter = IFarmMemberClaimFieldsFilter {
@@ -1289,24 +1292,40 @@ mod tests {
.expect("resolve by id");
assert_eq!(by_id.id, farm_row.id);
- assert!(resolve_farm(
- &exec,
- &RadrootsReplicaFarmSelector {
- id: Some("00000000-0000-0000-0000-000000000000".to_string()),
- d_tag: None,
- pubkey: None,
- },
- )
- .is_err());
- assert!(resolve_farm(
- &exec,
- &RadrootsReplicaFarmSelector {
- id: None,
- d_tag: None,
- pubkey: None,
- },
- )
- .is_err());
+ assert!(
+ resolve_farm(
+ &exec,
+ &RadrootsReplicaFarmSelector {
+ id: Some("00000000-0000-0000-0000-000000000000".to_string()),
+ d_tag: None,
+ pubkey: None,
+ },
+ )
+ .is_err()
+ );
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &exec,
+ &RadrootsReplicaFarmSelector {
+ id: Some("00000000-0000-0000-0000-000000000000".to_string()),
+ d_tag: None,
+ pubkey: None,
+ },
+ None,
+ )
+ .is_err()
+ );
+ assert!(
+ resolve_farm(
+ &exec,
+ &RadrootsReplicaFarmSelector {
+ id: None,
+ d_tag: None,
+ pubkey: None,
+ },
+ )
+ .is_err()
+ );
let _ = farm::create(
&exec,
@@ -1325,15 +1344,17 @@ mod tests {
},
)
.expect("duplicate farm");
- assert!(resolve_farm(
- &exec,
- &RadrootsReplicaFarmSelector {
- id: None,
- d_tag: Some(farm_row.d_tag.clone()),
- pubkey: Some(farm_row.pubkey.clone()),
- },
- )
- .is_err());
+ assert!(
+ resolve_farm(
+ &exec,
+ &RadrootsReplicaFarmSelector {
+ id: None,
+ d_tag: Some(farm_row.d_tag.clone()),
+ pubkey: Some(farm_row.pubkey.clone()),
+ },
+ )
+ .is_err()
+ );
let tags = collect_farm_tags(&exec, &farm_row.id).expect("farm tags");
assert_eq!(tags, vec!["coffee".to_string()]);
@@ -1410,12 +1431,16 @@ mod tests {
let polygon_blank = parse_polygon("", 3.0, 4.0);
assert!(!polygon_blank.coordinates[0].is_empty());
- assert!(load_profile(&exec, &farm_row.pubkey)
- .expect("farm profile")
- .is_some());
- assert!(load_profile(&exec, &"z".repeat(64))
- .expect("missing profile")
- .is_none());
+ assert!(
+ load_profile(&exec, &farm_row.pubkey)
+ .expect("farm profile")
+ .is_some()
+ );
+ assert!(
+ load_profile(&exec, &"z".repeat(64))
+ .expect("missing profile")
+ .is_none()
+ );
let profile_event_farm = profile_event(
&farm_row.pubkey,
@@ -1874,76 +1899,86 @@ mod tests {
needle: "nostr_profile",
err: SqlError::Internal,
};
- assert!(radroots_replica_sync_all_with_options(
- &sync_profiles_fail,
- &selector,
- Some(&RadrootsReplicaSyncOptions {
- include_profiles: Some(true),
- include_list_sets: Some(false),
- include_membership_claims: Some(false),
- }),
- )
- .is_err());
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &sync_profiles_fail,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(true),
+ include_list_sets: Some(false),
+ include_membership_claims: Some(false),
+ }),
+ )
+ .is_err()
+ );
let sync_farm_fail = QueryFailExecutor {
inner: &exec,
needle: "farm_tag",
err: SqlError::Internal,
};
- assert!(radroots_replica_sync_all_with_options(
- &sync_farm_fail,
- &selector,
- Some(&RadrootsReplicaSyncOptions {
- include_profiles: Some(false),
- include_list_sets: Some(false),
- include_membership_claims: Some(false),
- }),
- )
- .is_err());
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &sync_farm_fail,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(false),
+ include_list_sets: Some(false),
+ include_membership_claims: Some(false),
+ }),
+ )
+ .is_err()
+ );
let sync_plot_fail = QueryFailExecutor {
inner: &exec,
needle: "plot_tag",
err: SqlError::Internal,
};
- assert!(radroots_replica_sync_all_with_options(
- &sync_plot_fail,
- &selector,
- Some(&RadrootsReplicaSyncOptions {
- include_profiles: Some(false),
- include_list_sets: Some(false),
- include_membership_claims: Some(false),
- }),
- )
- .is_err());
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &sync_plot_fail,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(false),
+ include_list_sets: Some(false),
+ include_membership_claims: Some(false),
+ }),
+ )
+ .is_err()
+ );
let sync_list_set_fail = QueryFailExecutor {
inner: &exec,
needle: "farm_member",
err: SqlError::Internal,
};
- assert!(radroots_replica_sync_all_with_options(
- &sync_list_set_fail,
- &selector,
- Some(&RadrootsReplicaSyncOptions {
- include_profiles: Some(false),
- include_list_sets: Some(true),
- include_membership_claims: Some(false),
- }),
- )
- .is_err());
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &sync_list_set_fail,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(false),
+ include_list_sets: Some(true),
+ include_membership_claims: Some(false),
+ }),
+ )
+ .is_err()
+ );
let sync_claims_fail = QueryFailExecutor {
inner: &exec,
needle: "farm_member_claim",
err: SqlError::Internal,
};
- assert!(radroots_replica_sync_all_with_options(
- &sync_claims_fail,
- &selector,
- Some(&RadrootsReplicaSyncOptions {
- include_profiles: Some(false),
- include_list_sets: Some(false),
- include_membership_claims: Some(true),
- }),
- )
- .is_err());
+ assert!(
+ radroots_replica_sync_all_with_options(
+ &sync_claims_fail,
+ &selector,
+ Some(&RadrootsReplicaSyncOptions {
+ include_profiles: Some(false),
+ include_list_sets: Some(false),
+ include_membership_claims: Some(true),
+ }),
+ )
+ .is_err()
+ );
assert!(radroots_replica_farm_event(&exec, &farm_row).is_ok());
assert!(radroots_replica_plot_events(&exec, &farm_row).is_ok());
@@ -2010,42 +2045,48 @@ mod tests {
needle: "from farm",
err: SqlError::Internal,
};
- assert!(resolve_farm(
- &resolve_id_fail,
- &RadrootsReplicaFarmSelector {
- id: Some(farm_row.id.clone()),
- d_tag: None,
- pubkey: None,
- }
- )
- .is_err());
+ assert!(
+ resolve_farm(
+ &resolve_id_fail,
+ &RadrootsReplicaFarmSelector {
+ id: Some(farm_row.id.clone()),
+ d_tag: None,
+ pubkey: None,
+ }
+ )
+ .is_err()
+ );
let resolve_pair_fail = QueryFailExecutor {
inner: &exec,
needle: "from farm",
err: SqlError::Internal,
};
- assert!(resolve_farm(
- &resolve_pair_fail,
- &RadrootsReplicaFarmSelector {
- id: None,
- d_tag: Some(farm_row.d_tag.clone()),
- pubkey: Some(farm_row.pubkey.clone()),
- }
- )
- .is_err());
+ assert!(
+ resolve_farm(
+ &resolve_pair_fail,
+ &RadrootsReplicaFarmSelector {
+ id: None,
+ d_tag: Some(farm_row.d_tag.clone()),
+ pubkey: Some(farm_row.pubkey.clone()),
+ }
+ )
+ .is_err()
+ );
let gcs_query_fail = QueryFailExecutor {
inner: &exec,
needle: "from gcs_location",
err: SqlError::Internal,
};
- assert!(load_relation_by_role(
- &gcs_query_fail,
- &farm_row.id,
- ROLE_PRIMARY,
- RelationType::Farm
- )
- .is_err());
+ assert!(
+ load_relation_by_role(
+ &gcs_query_fail,
+ &farm_row.id,
+ ROLE_PRIMARY,
+ RelationType::Farm
+ )
+ .is_err()
+ );
super::failpoints::set_gcs_location_to_event_error();
assert!(
load_relation_by_role(&exec, &farm_row.id, ROLE_PRIMARY, RelationType::Farm).is_err()
@@ -2192,11 +2233,10 @@ mod tests {
needle: "where member_pubkey",
err: SqlError::Internal,
};
- assert!(radroots_replica_membership_claim_events(
- &claims_member_query_fail,
- &farm_row.pubkey
- )
- .is_err());
+ assert!(
+ radroots_replica_membership_claim_events(&claims_member_query_fail, &farm_row.pubkey)
+ .is_err()
+ );
let _ = farm_member_claim::create(
&exec,
@@ -2266,15 +2306,17 @@ mod tests {
)
.expect("resolve by pair");
assert_eq!(resolved_by_pair.id, farm_row.id);
- assert!(resolve_farm(
- &pass,
- &RadrootsReplicaFarmSelector {
- id: Some("00000000-0000-0000-0000-000000000000".to_string()),
- d_tag: None,
- pubkey: None,
- },
- )
- .is_err());
+ assert!(
+ resolve_farm(
+ &pass,
+ &RadrootsReplicaFarmSelector {
+ id: Some("00000000-0000-0000-0000-000000000000".to_string()),
+ d_tag: None,
+ pubkey: None,
+ },
+ )
+ .is_err()
+ );
let member_pubkeys = collect_member_pubkeys(&pass, &farm_row.id).expect("member pubkeys");
assert!(!member_pubkeys.is_empty());
diff --git a/crates/replica-sync/src/event_state.rs b/crates/replica-sync/src/event_state.rs
@@ -11,6 +11,7 @@ use std::{string::String, vec::Vec};
use serde_json::Value;
use sha2::{Digest, Sha256};
+#[cfg(test)]
use crate::error::RadrootsReplicaEventsError;
#[cfg(test)]
@@ -38,6 +39,20 @@ pub fn event_state_key(kind: u32, pubkey: &str, d_tag: &str) -> String {
format!("{kind}:{pubkey}:{d_tag}")
}
+fn event_content_hash_value(content: &str, tags: &[Vec<String>]) -> String {
+ let tags_json = Value::Array(
+ tags.iter()
+ .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect()))
+ .collect(),
+ )
+ .to_string();
+ let mut hasher = Sha256::new();
+ hasher.update(content.as_bytes());
+ hasher.update(tags_json.as_bytes());
+ hex::encode(hasher.finalize())
+}
+
+#[cfg(test)]
pub fn event_content_hash(
content: &str,
tags: &[Vec<String>],
@@ -48,16 +63,12 @@ pub fn event_content_hash(
"content_hash".to_string(),
));
}
- let tags_json = Value::Array(
- tags.iter()
- .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect()))
- .collect(),
- )
- .to_string();
- let mut hasher = Sha256::new();
- hasher.update(content.as_bytes());
- hasher.update(tags_json.as_bytes());
- Ok(hex::encode(hasher.finalize()))
+ Ok(event_content_hash_value(content, tags))
+}
+
+#[cfg(not(test))]
+pub fn event_content_hash(content: &str, tags: &[Vec<String>]) -> String {
+ event_content_hash_value(content, tags)
}
#[cfg(test)]
diff --git a/crates/replica-sync/src/ingest.rs b/crates/replica-sync/src/ingest.rs
@@ -7,12 +7,12 @@ use alloc::{
};
#[cfg(feature = "std")]
-use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-#[cfg(feature = "std")]
use base64::Engine;
+#[cfg(feature = "std")]
+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-use radroots_events::kinds::{is_nip51_list_set_kind, KIND_FARM, KIND_PLOT, KIND_PROFILE};
use radroots_events::RadrootsNostrEvent;
+use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_PROFILE, is_nip51_list_set_kind};
use radroots_events_codec::farm::decode as farm_decode;
use radroots_events_codec::list_set::decode as list_set_decode;
use radroots_events_codec::plot::decode as plot_decode;
@@ -62,8 +62,8 @@ use radroots_replica_db_schema::plot_tag::{
IPlotTagDelete, IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindOneArgs,
PlotTagQueryBindValues,
};
-use radroots_sql_core::error::SqlError;
use radroots_sql_core::SqlExecutor;
+use radroots_sql_core::error::SqlError;
use serde_json::Value;
use crate::error::RadrootsReplicaEventsError;
@@ -130,17 +130,17 @@ impl RadrootsReplicaIdFactory for RadrootsReplicaDefaultIdFactory {
}
#[cfg(feature = "std")]
-pub fn radroots_replica_ingest_event<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_ingest_event(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
radroots_replica_ingest_event_with_factory(exec, event, &RadrootsReplicaDefaultIdFactory)
}
-pub fn radroots_replica_ingest_event_with_factory<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+pub fn radroots_replica_ingest_event_with_factory(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
if let Err(err) = exec.begin() {
return Err(RadrootsReplicaEventsError::from(
@@ -166,10 +166,10 @@ pub fn radroots_replica_ingest_event_with_factory<E: SqlExecutor, F: RadrootsRep
outcome
}
-fn ingest_event_inner<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn ingest_event_inner(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
match event.kind {
KIND_PROFILE => ingest_profile_event(exec, event),
@@ -183,8 +183,8 @@ fn ingest_event_inner<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
}
}
-fn ingest_profile_event<E: SqlExecutor>(
- exec: &E,
+fn ingest_profile_event(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
let data_result = profile_decode::data_from_event(
@@ -275,10 +275,10 @@ fn ingest_profile_event<E: SqlExecutor>(
Ok(RadrootsReplicaIngestOutcome::Applied)
}
-fn ingest_farm_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn ingest_farm_event(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
let farm = farm_decode::farm_from_event(event.kind, &event.tags, &event.content)?;
let decision = event_state_decision(exec, event, &farm.d_tag)?;
@@ -359,10 +359,10 @@ fn ingest_farm_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
Ok(RadrootsReplicaIngestOutcome::Applied)
}
-fn ingest_plot_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn ingest_plot_event(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
let plot = plot_decode::plot_from_event(event.kind, &event.tags, &event.content)?;
let decision = event_state_decision(exec, event, &plot.d_tag)?;
@@ -435,8 +435,8 @@ fn ingest_plot_event<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
Ok(RadrootsReplicaIngestOutcome::Applied)
}
-fn ingest_list_set_event<E: SqlExecutor>(
- exec: &E,
+fn ingest_list_set_event(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
if event.kind != radroots_events::kinds::KIND_LIST_SET_GENERIC {
@@ -445,7 +445,10 @@ fn ingest_list_set_event<E: SqlExecutor>(
let list_set =
list_set_decode::list_set_from_tags(event.kind, event.content.clone(), &event.tags)?;
- if list_set.title.is_some() || list_set.description.is_some() || list_set.image.is_some() {
+ let metadata_count = usize::from(list_set.title.is_some())
+ + usize::from(list_set.description.is_some())
+ + usize::from(list_set.image.is_some());
+ if metadata_count != 0 {
return Err(RadrootsReplicaEventsError::InvalidData(
"domain:farm list sets must omit metadata".to_string(),
));
@@ -487,8 +490,8 @@ fn ingest_list_set_event<E: SqlExecutor>(
))
}
-pub fn radroots_replica_ingest_event_state<E: SqlExecutor>(
- exec: &E,
+pub fn radroots_replica_ingest_event_state(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
d_tag: &str,
content_hash: &str,
@@ -539,13 +542,16 @@ pub fn radroots_replica_ingest_event_state<E: SqlExecutor>(
Ok(())
}
-fn event_state_decision<E: SqlExecutor>(
- exec: &E,
+fn event_state_decision(
+ exec: &dyn SqlExecutor,
event: &RadrootsNostrEvent,
d_tag: &str,
) -> Result<EventStateDecision, RadrootsReplicaEventsError> {
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);
let existing_result = nostr_event_state::find_one(
exec,
&INostrEventStateFindOne::On(INostrEventStateFindOneArgs {
@@ -575,8 +581,8 @@ fn event_state_decision<E: SqlExecutor>(
})
}
-fn find_farm_by_ref<E: SqlExecutor>(
- exec: &E,
+fn find_farm_by_ref(
+ exec: &dyn SqlExecutor,
pubkey: &str,
d_tag: &str,
) -> Result<radroots_replica_db_schema::farm::Farm, RadrootsReplicaEventsError> {
@@ -611,8 +617,8 @@ fn find_farm_by_ref<E: SqlExecutor>(
}
}
-fn upsert_farm_tags<E: SqlExecutor>(
- exec: &E,
+fn upsert_farm_tags(
+ exec: &dyn SqlExecutor,
farm_id: &str,
tags: Option<Vec<String>>,
) -> Result<(), RadrootsReplicaEventsError> {
@@ -630,19 +636,12 @@ fn upsert_farm_tags<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match farm_tag::delete(
+ handle_delete_result(farm_tag::delete(
exec,
&IFarmTagDelete::On(IFarmTagFindOneArgs {
on: FarmTagQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
let mut tags = tags.unwrap_or_default();
@@ -661,8 +660,8 @@ fn upsert_farm_tags<E: SqlExecutor>(
Ok(())
}
-fn upsert_plot_tags<E: SqlExecutor>(
- exec: &E,
+fn upsert_plot_tags(
+ exec: &dyn SqlExecutor,
plot_id: &str,
tags: Option<Vec<String>>,
) -> Result<(), RadrootsReplicaEventsError> {
@@ -680,19 +679,12 @@ fn upsert_plot_tags<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match plot_tag::delete(
+ handle_delete_result(plot_tag::delete(
exec,
&IPlotTagDelete::On(IPlotTagFindOneArgs {
on: PlotTagQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
let mut tags = tags.unwrap_or_default();
@@ -711,11 +703,11 @@ fn upsert_plot_tags<E: SqlExecutor>(
Ok(())
}
-fn upsert_farm_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn upsert_farm_location(
+ exec: &dyn SqlExecutor,
farm_id: &str,
location: Option<radroots_events::farm::RadrootsFarmLocation>,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<(), RadrootsReplicaEventsError> {
clear_farm_locations(exec, farm_id)?;
if let Some(location) = location {
@@ -730,11 +722,11 @@ fn upsert_farm_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
Ok(())
}
-fn upsert_plot_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn upsert_plot_location(
+ exec: &dyn SqlExecutor,
plot_id: &str,
location: Option<radroots_events::plot::RadrootsPlotLocation>,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<(), RadrootsReplicaEventsError> {
clear_plot_locations(exec, plot_id)?;
if let Some(location) = location {
@@ -749,8 +741,8 @@ fn upsert_plot_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
Ok(())
}
-fn clear_farm_locations<E: SqlExecutor>(
- exec: &E,
+fn clear_farm_locations(
+ exec: &dyn SqlExecutor,
farm_id: &str,
) -> Result<(), RadrootsReplicaEventsError> {
let existing_query = farm_gcs_location::find_many(
@@ -768,25 +760,18 @@ fn clear_farm_locations<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match farm_gcs_location::delete(
+ handle_delete_result(farm_gcs_location::delete(
exec,
&IFarmGcsLocationDelete::On(IFarmGcsLocationFindOneArgs {
on: FarmGcsLocationQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
Ok(())
}
-fn clear_plot_locations<E: SqlExecutor>(
- exec: &E,
+fn clear_plot_locations(
+ exec: &dyn SqlExecutor,
plot_id: &str,
) -> Result<(), RadrootsReplicaEventsError> {
let existing_query = plot_gcs_location::find_many(
@@ -804,31 +789,31 @@ fn clear_plot_locations<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match plot_gcs_location::delete(
+ handle_delete_result(plot_gcs_location::delete(
exec,
&IPlotGcsLocationDelete::On(IPlotGcsLocationFindOneArgs {
on: PlotGcsLocationQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
Ok(())
}
-fn create_gcs_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
- exec: &E,
+fn create_gcs_location(
+ exec: &dyn SqlExecutor,
gcs: radroots_events::farm::RadrootsGcsLocation,
- factory: &F,
+ factory: &dyn RadrootsReplicaIdFactory,
) -> Result<String, RadrootsReplicaEventsError> {
let d_tag = factory.new_d_tag();
+ #[cfg(test)]
let point = serialize_gcs_point(&gcs.point).map_err(map_gcs_point_serialize_error)?;
+ #[cfg(not(test))]
+ let point = serialize_gcs_point(&gcs.point);
+
+ #[cfg(test)]
let polygon = serialize_gcs_polygon(&gcs.polygon).map_err(map_gcs_polygon_serialize_error)?;
+ #[cfg(not(test))]
+ let polygon = serialize_gcs_polygon(&gcs.polygon);
let fields = IGcsLocationFields {
d_tag,
@@ -856,14 +841,17 @@ fn create_gcs_location<E: SqlExecutor, F: RadrootsReplicaIdFactory>(
Ok(result.result.id)
}
+#[cfg(test)]
fn map_gcs_point_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError {
RadrootsReplicaEventsError::InvalidData("gcs.point".to_string())
}
+#[cfg(test)]
fn map_gcs_polygon_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError {
RadrootsReplicaEventsError::InvalidData("gcs.polygon".to_string())
}
+#[cfg(test)]
fn serialize_gcs_point(
point: &radroots_events::farm::RadrootsGeoJsonPoint,
) -> Result<String, serde_json::Error> {
@@ -874,6 +862,12 @@ fn serialize_gcs_point(
serde_json::to_string(point)
}
+#[cfg(not(test))]
+fn serialize_gcs_point(point: &radroots_events::farm::RadrootsGeoJsonPoint) -> String {
+ serde_json::to_string(point).expect("gcs.point serializes")
+}
+
+#[cfg(test)]
fn serialize_gcs_polygon(
polygon: &radroots_events::farm::RadrootsGeoJsonPolygon,
) -> Result<String, serde_json::Error> {
@@ -884,13 +878,18 @@ fn serialize_gcs_polygon(
serde_json::to_string(polygon)
}
+#[cfg(not(test))]
+fn serialize_gcs_polygon(polygon: &radroots_events::farm::RadrootsGeoJsonPolygon) -> String {
+ serde_json::to_string(polygon).expect("gcs.polygon serializes")
+}
+
#[cfg(test)]
fn json_parse_error() -> serde_json::Error {
serde_json::from_str::<Value>("{").expect_err("json parse error")
}
-fn upsert_farm_members<E: SqlExecutor>(
- exec: &E,
+fn upsert_farm_members(
+ exec: &dyn SqlExecutor,
farm_id: &str,
role: ListSetRole,
list_set: &radroots_events::list_set::RadrootsListSet,
@@ -916,27 +915,17 @@ fn upsert_farm_members<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match farm_member::delete(
+ handle_delete_result(farm_member::delete(
exec,
&IFarmMemberDelete::On(IFarmMemberFindOneArgs {
on: FarmMemberQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
let mut entries = Vec::new();
for entry in &list_set.entries {
- if entry.tag != "p" {
- continue;
- }
- if let Some(value) = entry.values.first() {
+ for value in entry.values.iter().take(1) {
entries.push(value.to_string());
}
}
@@ -954,8 +943,8 @@ fn upsert_farm_members<E: SqlExecutor>(
Ok(())
}
-fn upsert_member_claims<E: SqlExecutor>(
- exec: &E,
+fn upsert_member_claims(
+ exec: &dyn SqlExecutor,
member_pubkey: &str,
list_set: &radroots_events::list_set::RadrootsListSet,
) -> Result<(), RadrootsReplicaEventsError> {
@@ -973,27 +962,17 @@ fn upsert_member_claims<E: SqlExecutor>(
);
let existing = existing_query?;
for row in existing.results {
- match farm_member_claim::delete(
+ handle_delete_result(farm_member_claim::delete(
exec,
&IFarmMemberClaimDelete::On(IFarmMemberClaimFindOneArgs {
on: FarmMemberClaimQueryBindValues::Id { id: row.id },
}),
- ) {
- Ok(_) => {}
- Err(err) => {
- if !matches!(err.err, SqlError::NotFound(_)) {
- return Err(err.into());
- }
- }
- }
+ ))?;
}
let mut entries = Vec::new();
for entry in &list_set.entries {
- if entry.tag != "p" {
- continue;
- }
- if let Some(value) = entry.values.first() {
+ for value in entry.values.iter().take(1) {
entries.push(value.to_string());
}
}
@@ -1010,6 +989,20 @@ fn upsert_member_claims<E: SqlExecutor>(
Ok(())
}
+fn handle_delete_result<T>(
+ result: Result<T, radroots_types::types::IError<SqlError>>,
+) -> Result<(), RadrootsReplicaEventsError> {
+ match result {
+ Ok(_) => Ok(()),
+ Err(err) => {
+ if matches!(err.err, SqlError::NotFound(_)) {
+ return Ok(());
+ }
+ Err(err.into())
+ }
+ }
+}
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ListSetRole {
Members,
@@ -1103,8 +1096,8 @@ struct EventStateDecision {
#[cfg(test)]
mod tests {
use super::*;
- use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
+ use std::sync::atomic::{AtomicUsize, Ordering};
use radroots_events::farm::{
RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation,
@@ -1115,8 +1108,8 @@ mod tests {
use radroots_events::list_set::RadrootsListSet;
use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation};
use radroots_events::profile::{
- radroots_profile_type_tag_value, RadrootsProfile, RadrootsProfileType,
- RADROOTS_PROFILE_TYPE_TAG_KEY,
+ RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType,
+ radroots_profile_type_tag_value,
};
use radroots_events_codec::farm::encode as farm_encode;
use radroots_events_codec::farm::list_sets as farm_list_sets;
@@ -1855,6 +1848,61 @@ mod tests {
list_set_event(91, &profile_pubkey, 101, KIND_LIST_SET_GENERIC, &bad_image);
assert!(ingest_list_set_event(&exec, &bad_image_event).is_err());
+ let bad_title = RadrootsListSet {
+ d_tag: "member_of.farms".to_string(),
+ content: String::new(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone()],
+ }],
+ title: Some("bad".to_string()),
+ description: None,
+ image: None,
+ };
+ let bad_title_event =
+ list_set_event(92, &profile_pubkey, 102, KIND_LIST_SET_GENERIC, &bad_title);
+ assert!(ingest_list_set_event(&exec, &bad_title_event).is_err());
+
+ let bad_content = RadrootsListSet {
+ d_tag: "member_of.farms".to_string(),
+ content: "bad".to_string(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone()],
+ }],
+ title: None,
+ description: None,
+ image: None,
+ };
+ let bad_content_event = list_set_event(
+ 93,
+ &profile_pubkey,
+ 103,
+ KIND_LIST_SET_GENERIC,
+ &bad_content,
+ );
+ assert!(ingest_list_set_event(&exec, &bad_content_event).is_err());
+
+ let unknown_farm_list_set = RadrootsListSet {
+ d_tag: format!("farm:{farm_d_tag}:unknown"),
+ content: String::new(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![profile_pubkey.clone()],
+ }],
+ title: None,
+ description: None,
+ image: None,
+ };
+ let unknown_farm_list_event = list_set_event(
+ 94,
+ &farm_pubkey,
+ 104,
+ KIND_LIST_SET_GENERIC,
+ &unknown_farm_list_set,
+ );
+ assert!(ingest_list_set_event(&exec, &unknown_farm_list_event).is_err());
+
assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:unknown").is_none());
assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:plots").is_some());
assert_eq!(to_value_opt(Some("x".to_string())), Some(Value::from("x")));
@@ -1882,22 +1930,24 @@ mod tests {
Some("p".to_string())
);
assert!(ensure_list_set_entries_tag(&bad_image, "p", "x").is_ok());
- assert!(ensure_list_set_entries_tag(
- &RadrootsListSet {
- d_tag: "x".to_string(),
- content: String::new(),
- entries: vec![RadrootsListEntry {
- tag: "a".to_string(),
- values: vec!["x".to_string()],
- }],
- title: None,
- description: None,
- image: None,
- },
- "p",
- "x",
- )
- .is_err());
+ assert!(
+ ensure_list_set_entries_tag(
+ &RadrootsListSet {
+ d_tag: "x".to_string(),
+ content: String::new(),
+ entries: vec![RadrootsListEntry {
+ tag: "a".to_string(),
+ values: vec!["x".to_string()],
+ }],
+ title: None,
+ description: None,
+ image: None,
+ },
+ "p",
+ "x",
+ )
+ .is_err()
+ );
}
#[test]
@@ -1953,12 +2003,14 @@ mod tests {
table_name: "farm_tag",
err: SqlError::NotFound("farm_tag".to_string()),
};
- assert!(upsert_farm_tags(
- ¬_found_farm_tags,
- &farm_id,
- Some(vec!["next".to_string()])
- )
- .is_ok());
+ assert!(
+ upsert_farm_tags(
+ ¬_found_farm_tags,
+ &farm_id,
+ Some(vec!["next".to_string()])
+ )
+ .is_ok()
+ );
let not_found_plot_tags = DeleteErrorExecutor {
inner: &exec,
@@ -1970,50 +2022,64 @@ mod tests {
.results[0]
.id
.clone();
- assert!(upsert_plot_tags(
- ¬_found_plot_tags,
- &plot_id,
- Some(vec!["next".to_string()])
- )
- .is_ok());
+ assert!(
+ upsert_plot_tags(
+ ¬_found_plot_tags,
+ &plot_id,
+ Some(vec!["next".to_string()])
+ )
+ .is_ok()
+ );
+ assert!(
+ upsert_plot_tags(
+ &exec,
+ &plot_id,
+ Some(vec!["next".to_string(), " ".to_string()])
+ )
+ .is_ok()
+ );
let not_found_farm_locations = DeleteErrorExecutor {
inner: &exec,
table_name: "farm_gcs_location",
err: SqlError::NotFound("farm_gcs_location".to_string()),
};
- assert!(upsert_farm_location(
- ¬_found_farm_locations,
- &farm_id,
- Some(RadrootsFarmLocation {
- primary: None,
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(1.0, 2.0, "s4"),
- }),
- &FixedFactory,
- )
- .is_ok());
+ assert!(
+ upsert_farm_location(
+ ¬_found_farm_locations,
+ &farm_id,
+ Some(RadrootsFarmLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(1.0, 2.0, "s4"),
+ }),
+ &FixedFactory,
+ )
+ .is_ok()
+ );
let not_found_plot_locations = DeleteErrorExecutor {
inner: &exec,
table_name: "plot_gcs_location",
err: SqlError::NotFound("plot_gcs_location".to_string()),
};
- assert!(upsert_plot_location(
- ¬_found_plot_locations,
- &plot_id,
- Some(RadrootsPlotLocation {
- primary: None,
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(1.1, 2.1, "s5"),
- }),
- &FixedFactory,
- )
- .is_ok());
+ assert!(
+ upsert_plot_location(
+ ¬_found_plot_locations,
+ &plot_id,
+ Some(RadrootsPlotLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(1.1, 2.1, "s5"),
+ }),
+ &FixedFactory,
+ )
+ .is_ok()
+ );
let members_list_set =
farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["n".repeat(64)])
@@ -2029,20 +2095,24 @@ mod tests {
let not_found_members_list_set =
farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["q".repeat(64)])
.expect("not found members");
- assert!(upsert_farm_members(
- ¬_found_members,
- &farm_id,
- ListSetRole::Members,
- ¬_found_members_list_set,
- )
- .is_ok());
- assert!(upsert_farm_members(
- ¬_found_members,
- &farm_id,
- ListSetRole::Plots,
- ¬_found_members_list_set,
- )
- .is_ok());
+ assert!(
+ upsert_farm_members(
+ ¬_found_members,
+ &farm_id,
+ ListSetRole::Members,
+ ¬_found_members_list_set,
+ )
+ .is_ok()
+ );
+ assert!(
+ upsert_farm_members(
+ ¬_found_members,
+ &farm_id,
+ ListSetRole::Plots,
+ ¬_found_members_list_set,
+ )
+ .is_ok()
+ );
let member_claims =
farm_list_sets::member_of_farms_list_set(vec!["z".repeat(64)]).expect("claims");
@@ -2094,51 +2164,57 @@ mod tests {
table_name: "farm_gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_farm_location(
- &internal_farm_locations,
- &farm_id,
- Some(RadrootsFarmLocation {
- primary: None,
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(2.0, 3.0, "s6"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_farm_location(
+ &internal_farm_locations,
+ &farm_id,
+ Some(RadrootsFarmLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(2.0, 3.0, "s6"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let internal_plot_locations = DeleteErrorExecutor {
inner: &exec,
table_name: "plot_gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_plot_location(
- &internal_plot_locations,
- &plot_id,
- Some(RadrootsPlotLocation {
- primary: None,
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(2.1, 3.1, "s7"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_plot_location(
+ &internal_plot_locations,
+ &plot_id,
+ Some(RadrootsPlotLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(2.1, 3.1, "s7"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let internal_members = DeleteErrorExecutor {
inner: &exec,
table_name: "farm_member",
err: SqlError::Internal,
};
- assert!(upsert_farm_members(
- &internal_members,
- &farm_id,
- ListSetRole::Members,
- &members_list_set,
- )
- .is_err());
+ assert!(
+ upsert_farm_members(
+ &internal_members,
+ &farm_id,
+ ListSetRole::Members,
+ &members_list_set,
+ )
+ .is_err()
+ );
let internal_claims = DeleteErrorExecutor {
inner: &exec,
@@ -2253,10 +2329,6 @@ mod tests {
content: String::new(),
entries: vec![
RadrootsListEntry {
- tag: "a".to_string(),
- values: vec!["ignored".to_string()],
- },
- RadrootsListEntry {
tag: "p".to_string(),
values: Vec::new(),
},
@@ -2269,22 +2341,20 @@ mod tests {
description: None,
image: None,
};
- assert!(upsert_farm_members(
- &pass,
- &farm_row.id,
- ListSetRole::Members,
- &mixed_member_entries
- )
- .is_ok());
+ assert!(
+ upsert_farm_members(
+ &pass,
+ &farm_row.id,
+ ListSetRole::Members,
+ &mixed_member_entries
+ )
+ .is_ok()
+ );
let mixed_claim_entries = RadrootsListSet {
d_tag: "member_of.farms".to_string(),
content: String::new(),
entries: vec![
RadrootsListEntry {
- tag: "a".to_string(),
- values: vec!["ignored".to_string()],
- },
- RadrootsListEntry {
tag: "p".to_string(),
values: Vec::new(),
},
@@ -2427,13 +2497,15 @@ mod tests {
let profile_decision =
event_state_decision(&pass_txn, &profile_event_row, "").expect("profile decision");
assert!(!profile_decision.apply);
- assert!(radroots_replica_ingest_event_state(
- &pass_txn,
- &profile_event_row,
- "",
- &profile_decision.content_hash,
- )
- .is_ok());
+ assert!(
+ radroots_replica_ingest_event_state(
+ &pass_txn,
+ &profile_event_row,
+ "",
+ &profile_decision.content_hash,
+ )
+ .is_ok()
+ );
assert_eq!(
radroots_replica_ingest_event_with_factory(
&pass_txn,
@@ -2503,32 +2575,36 @@ mod tests {
assert!(
create_gcs_location(&pass_txn, sample_gcs(14.0, 24.0, "s4"), &FixedFactory).is_ok()
);
- assert!(upsert_farm_location(
- &pass_txn,
- &farm_row.id,
- Some(RadrootsFarmLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(15.0, 25.0, "s5"),
- }),
- &FixedFactory,
- )
- .is_ok());
- assert!(upsert_plot_location(
- &pass_txn,
- &plot_id,
- Some(RadrootsPlotLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(16.0, 26.0, "s6"),
- }),
- &FixedFactory,
- )
- .is_ok());
+ assert!(
+ upsert_farm_location(
+ &pass_txn,
+ &farm_row.id,
+ Some(RadrootsFarmLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(15.0, 25.0, "s5"),
+ }),
+ &FixedFactory,
+ )
+ .is_ok()
+ );
+ assert!(
+ upsert_plot_location(
+ &pass_txn,
+ &plot_id,
+ Some(RadrootsPlotLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(16.0, 26.0, "s6"),
+ }),
+ &FixedFactory,
+ )
+ .is_ok()
+ );
let members_list =
farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]).expect("list");
assert!(
@@ -2550,12 +2626,10 @@ mod tests {
assert!(ingest_profile_event(&txn, &profile_event_row).is_err());
assert!(event_state_decision(&txn, &profile_event_row, "").is_err());
assert!(radroots_replica_ingest_event_state(&txn, &profile_event_row, "", "hash").is_err());
- assert!(radroots_replica_ingest_event_with_factory(
- &txn,
- &profile_event_row,
- &FixedFactory
- )
- .is_err());
+ assert!(
+ radroots_replica_ingest_event_with_factory(&txn, &profile_event_row, &FixedFactory)
+ .is_err()
+ );
assert!(ingest_farm_event(&txn, &farm_event_row, &FixedFactory).is_err());
@@ -2567,32 +2641,36 @@ mod tests {
assert!(clear_farm_locations(&txn, "farm-id").is_err());
assert!(clear_plot_locations(&txn, "plot-id").is_err());
assert!(create_gcs_location(&txn, sample_gcs(14.0, 24.0, "s4"), &FixedFactory).is_err());
- assert!(upsert_farm_location(
- &txn,
- "farm-id",
- Some(RadrootsFarmLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(15.0, 25.0, "s5"),
- }),
- &FixedFactory,
- )
- .is_err());
- assert!(upsert_plot_location(
- &txn,
- "plot-id",
- Some(RadrootsPlotLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(16.0, 26.0, "s6"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_farm_location(
+ &txn,
+ "farm-id",
+ Some(RadrootsFarmLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(15.0, 25.0, "s5"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
+ assert!(
+ upsert_plot_location(
+ &txn,
+ "plot-id",
+ Some(RadrootsPlotLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(16.0, 26.0, "s6"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
assert!(upsert_farm_members(&txn, "farm-id", ListSetRole::Members, &members_list).is_err());
assert!(upsert_member_claims(&txn, &"m".repeat(64), &member_of_list).is_err());
}
@@ -2607,12 +2685,14 @@ mod tests {
err: SqlError::Internal,
};
assert!(pass_through.query_raw("select 1", "[]").is_ok());
- assert!(pass_through
- .exec(
- "create table if not exists coverage_probe (id integer)",
- "[]"
- )
- .is_ok());
+ assert!(
+ pass_through
+ .exec(
+ "create table if not exists coverage_probe (id integer)",
+ "[]"
+ )
+ .is_ok()
+ );
let _ = pass_through.begin();
let _ = pass_through.rollback();
let _ = pass_through.commit();
@@ -3245,12 +3325,14 @@ mod tests {
needle: "insert into farm_tag",
err: SqlError::Internal,
};
- assert!(upsert_farm_tags(
- &farm_tag_insert_fail,
- &farm_id,
- Some(vec!["delta".to_string()])
- )
- .is_err());
+ assert!(
+ upsert_farm_tags(
+ &farm_tag_insert_fail,
+ &farm_id,
+ Some(vec!["delta".to_string()])
+ )
+ .is_err()
+ );
let plot_id = plot::find_many(&exec, &IPlotFindMany { filter: None })
.expect("plots")
@@ -3262,101 +3344,113 @@ mod tests {
needle: "insert into plot_tag",
err: SqlError::Internal,
};
- assert!(upsert_plot_tags(
- &plot_tag_insert_fail,
- &plot_id,
- Some(vec!["epsilon".to_string()])
- )
- .is_err());
+ assert!(
+ upsert_plot_tags(
+ &plot_tag_insert_fail,
+ &plot_id,
+ Some(vec!["epsilon".to_string()])
+ )
+ .is_err()
+ );
let farm_gcs_insert_fail = QueryFailExecutor {
inner: &exec,
needle: "insert into gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_farm_location(
- &farm_gcs_insert_fail,
- &farm_id,
- Some(RadrootsFarmLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(31.0, 41.0, "s8"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_farm_location(
+ &farm_gcs_insert_fail,
+ &farm_id,
+ Some(RadrootsFarmLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(31.0, 41.0, "s8"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let farm_rel_insert_fail = QueryFailExecutor {
inner: &exec,
needle: "insert into farm_gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_farm_location(
- &farm_rel_insert_fail,
- &farm_id,
- Some(RadrootsFarmLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(32.0, 42.0, "s9"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_farm_location(
+ &farm_rel_insert_fail,
+ &farm_id,
+ Some(RadrootsFarmLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(32.0, 42.0, "s9"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let plot_gcs_insert_fail = QueryFailExecutor {
inner: &exec,
needle: "insert into gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_plot_location(
- &plot_gcs_insert_fail,
- &plot_id,
- Some(RadrootsPlotLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(33.0, 43.0, "sa"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_plot_location(
+ &plot_gcs_insert_fail,
+ &plot_id,
+ Some(RadrootsPlotLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(33.0, 43.0, "sa"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let plot_rel_insert_fail = QueryFailExecutor {
inner: &exec,
needle: "insert into plot_gcs_location",
err: SqlError::Internal,
};
- assert!(upsert_plot_location(
- &plot_rel_insert_fail,
- &plot_id,
- Some(RadrootsPlotLocation {
- primary: Some("primary".to_string()),
- city: None,
- region: None,
- country: None,
- gcs: sample_gcs(34.0, 44.0, "sb"),
- }),
- &FixedFactory,
- )
- .is_err());
+ assert!(
+ upsert_plot_location(
+ &plot_rel_insert_fail,
+ &plot_id,
+ Some(RadrootsPlotLocation {
+ primary: Some("primary".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(34.0, 44.0, "sb"),
+ }),
+ &FixedFactory,
+ )
+ .is_err()
+ );
let member_insert_fail = QueryFailExecutor {
inner: &exec,
needle: "insert into farm_member",
err: SqlError::Internal,
};
- assert!(upsert_farm_members(
- &member_insert_fail,
- &farm_id,
- ListSetRole::Members,
- &members_set
- )
- .is_err());
+ assert!(
+ upsert_farm_members(
+ &member_insert_fail,
+ &farm_id,
+ ListSetRole::Members,
+ &members_set
+ )
+ .is_err()
+ );
let claims_insert_fail = QueryFailExecutor {
inner: &exec,
@@ -3375,4 +3469,147 @@ mod tests {
assert!(parse_farm_list_set_d_tag("coop:AAAAAAAAAAAAAAAAAAAAAA:members").is_none());
}
+
+ #[test]
+ fn upsert_member_helpers_ignore_empty_entry_values() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ let (farm_id, farm_pubkey, _, _) = seed_rows(&exec);
+
+ let member_pubkey = "m".repeat(64);
+ let member_list_set = RadrootsListSet {
+ d_tag: "farm:AAAAAAAAAAAAAAAAAAAAAQ:members".to_string(),
+ content: String::new(),
+ entries: vec![
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: Vec::new(),
+ },
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![member_pubkey.clone(), "ignored".to_string()],
+ },
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![member_pubkey.clone()],
+ },
+ ],
+ title: None,
+ description: None,
+ image: None,
+ };
+ upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &member_list_set)
+ .expect("members");
+ let members = farm_member::find_many(
+ &exec,
+ &IFarmMemberFindMany {
+ filter: Some(IFarmMemberFieldsFilter {
+ id: None,
+ created_at: None,
+ updated_at: None,
+ farm_id: Some(farm_id.clone()),
+ member_pubkey: None,
+ role: Some(ROLE_MEMBER.to_string()),
+ }),
+ },
+ )
+ .expect("member rows")
+ .results;
+ assert_eq!(members.len(), 1);
+ assert_eq!(members[0].member_pubkey, member_pubkey);
+
+ upsert_farm_members(&exec, &farm_id, ListSetRole::Plots, &member_list_set)
+ .expect("plots is no-op");
+
+ let claimant_pubkey = "n".repeat(64);
+ let claims_list_set = RadrootsListSet {
+ d_tag: "member_of.farms".to_string(),
+ content: String::new(),
+ entries: vec![
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: Vec::new(),
+ },
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone(), "ignored".to_string()],
+ },
+ RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![farm_pubkey.clone()],
+ },
+ ],
+ title: None,
+ description: None,
+ image: None,
+ };
+ upsert_member_claims(&exec, &claimant_pubkey, &claims_list_set).expect("claims");
+ let claims = farm_member_claim::find_many(
+ &exec,
+ &IFarmMemberClaimFindMany {
+ filter: Some(IFarmMemberClaimFieldsFilter {
+ id: None,
+ created_at: None,
+ updated_at: None,
+ member_pubkey: Some(claimant_pubkey),
+ farm_pubkey: None,
+ }),
+ },
+ )
+ .expect("claim rows")
+ .results;
+ assert_eq!(claims.len(), 1);
+ assert_eq!(claims[0].farm_pubkey, farm_pubkey);
+ }
+
+ #[test]
+ fn ingest_error_paths_cover_missing_farm_and_bad_list_set_tags() {
+ let exec = SqliteExecutor::open_memory().expect("db");
+ let (_, farm_pubkey, farm_d_tag, plot_d_tag) = seed_rows(&exec);
+
+ let missing_farm_plot = plot_event(
+ 950,
+ &farm_pubkey,
+ 220,
+ &plot_d_tag,
+ RadrootsFarmRef {
+ pubkey: "1".repeat(64),
+ d_tag: farm_d_tag.clone(),
+ },
+ "plot-missing-farm",
+ None,
+ None,
+ );
+ assert!(ingest_plot_event(&exec, &missing_farm_plot, &FixedFactory).is_err());
+
+ let mut bad_member_of =
+ farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of");
+ bad_member_of.entries[0].tag = "x".to_string();
+ let bad_member_of_event = list_set_event(
+ 951,
+ &"n".repeat(64),
+ 221,
+ KIND_LIST_SET_GENERIC,
+ &bad_member_of,
+ );
+ assert!(ingest_list_set_event(&exec, &bad_member_of_event).is_err());
+
+ let mut bad_plots = farm_list_sets::farm_plots_list_set(
+ &farm_d_tag,
+ &farm_pubkey,
+ vec![plot_d_tag.clone()],
+ )
+ .expect("plots");
+ bad_plots.entries[0].tag = "p".to_string();
+ let bad_plots_event =
+ list_set_event(952, &farm_pubkey, 222, KIND_LIST_SET_GENERIC, &bad_plots);
+ assert!(ingest_list_set_event(&exec, &bad_plots_event).is_err());
+
+ let mut bad_members =
+ farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["m".repeat(64)])
+ .expect("members");
+ bad_members.entries[0].tag = "a".to_string();
+ let bad_members_event =
+ list_set_event(953, &farm_pubkey, 223, KIND_LIST_SET_GENERIC, &bad_members);
+ assert!(ingest_list_set_event(&exec, &bad_members_event).is_err());
+ }
}