ingest.rs (168084B)
1 #[cfg(not(feature = "std"))] 2 use alloc::format; 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 string::{String, ToString}, 6 vec::Vec, 7 }; 8 9 #[cfg(feature = "std")] 10 use base64::Engine; 11 #[cfg(feature = "std")] 12 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 13 14 use radroots_core::Decimal; 15 use radroots_event::contract::EventClass; 16 #[cfg(test)] 17 use radroots_event::envelope::EventEnvelopeParts; 18 use radroots_event::envelope::event_head::{ 19 CurrentEventHead, EventHeadCandidateResult, EventHeadCoordinate, 20 EventHeadDecision as ProtocolEventHeadDecision, event_head_candidate_for_class, 21 event_head_candidate_for_event, select_event_head, 22 }; 23 use radroots_event::envelope::kind::{ 24 KIND_CALENDAR, KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_PLOT, KIND_PROFILE, 25 is_nip51_list_set_kind, 26 }; 27 use radroots_event::id::EventId; 28 use radroots_event::listing::operational::{ 29 OperationalListing, OperationalListingAvailability, OperationalListingBin, 30 OperationalListingStatus, 31 }; 32 use radroots_event::profile::{ 33 ProfileType, RADROOTS_PROFILE_TYPE_TAG_KEY, radroots_profile_type_from_tag_value, 34 }; 35 use radroots_event::{ 36 envelope::EventEnvelope, 37 listing::classified::{ClassifiedListingPartition, classify_classified_listing_tags}, 38 }; 39 use radroots_event_codec::admission::profile::admit_verified_profile_event; 40 use radroots_event_codec::decode::farm as farm_decode; 41 use radroots_event_codec::decode::food_availability::{ 42 RadrootsFoodAvailabilityProjectionOutcome, project_verified_food_availability_event, 43 }; 44 use radroots_event_codec::decode::list_set as list_set_decode; 45 use radroots_event_codec::decode::operational_listing as listing_decode; 46 use radroots_event_codec::decode::plot as plot_decode; 47 use radroots_event_codec::verify::{RadrootsSignatureVerifiedEvent, verify_nip01_event}; 48 use radroots_replica_schema::ReplicaSchemaError; 49 use radroots_replica_schema::farm::{ 50 FarmQueryBindValues, IFarmFields, IFarmFieldsFilter, IFarmFieldsPartial, IFarmFindMany, 51 IFarmUpdate, 52 }; 53 #[cfg(test)] 54 use radroots_replica_schema::farm_gcs_location::{ 55 FarmGcsLocationQueryBindValues, IFarmGcsLocationDelete, IFarmGcsLocationFieldsFilter, 56 IFarmGcsLocationFindMany, IFarmGcsLocationFindOneArgs, 57 }; 58 use radroots_replica_schema::farm_member::{ 59 FarmMemberQueryBindValues, IFarmMemberDelete, IFarmMemberFields, IFarmMemberFieldsFilter, 60 IFarmMemberFindMany, IFarmMemberFindOneArgs, 61 }; 62 use radroots_replica_schema::farm_member_claim::{ 63 FarmMemberClaimQueryBindValues, IFarmMemberClaimDelete, IFarmMemberClaimFields, 64 IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, IFarmMemberClaimFindOneArgs, 65 }; 66 use radroots_replica_schema::farm_tag::{ 67 FarmTagQueryBindValues, IFarmTagDelete, IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany, 68 IFarmTagFindOneArgs, 69 }; 70 use radroots_replica_schema::gcs_location::IGcsLocationFields; 71 use radroots_replica_schema::nostr_event_head::{ 72 INostrEventHeadFields, INostrEventHeadFieldsPartial, INostrEventHeadFindOne, 73 INostrEventHeadFindOneArgs, INostrEventHeadUpdate, NostrEventHead, 74 NostrEventHeadQueryBindValues, 75 }; 76 use radroots_replica_schema::nostr_profile::{ 77 INostrProfileFields, INostrProfileFieldsPartial, INostrProfileFindOne, 78 INostrProfileFindOneArgs, INostrProfileUpdate, NostrProfileQueryBindValues, 79 }; 80 use radroots_replica_schema::plot::{ 81 IPlotFields, IPlotFieldsFilter, IPlotFieldsPartial, IPlotFindMany, IPlotUpdate, 82 PlotQueryBindValues, 83 }; 84 use radroots_replica_schema::plot_gcs_location::{ 85 IPlotGcsLocationDelete, IPlotGcsLocationFields, IPlotGcsLocationFieldsFilter, 86 IPlotGcsLocationFindMany, IPlotGcsLocationFindOneArgs, PlotGcsLocationQueryBindValues, 87 }; 88 use radroots_replica_schema::plot_tag::{ 89 IPlotTagDelete, IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany, IPlotTagFindOneArgs, 90 PlotTagQueryBindValues, 91 }; 92 use radroots_replica_schema::trade_product::{ 93 ITradeProductFields, ITradeProductFieldsFilter, ITradeProductFieldsPartial, 94 ITradeProductFindMany, ITradeProductFindOne, ITradeProductFindOneArgs, ITradeProductUpdate, 95 TradeProductQueryBindValues, 96 }; 97 #[cfg(test)] 98 use radroots_replica_store::farm_gcs_location; 99 use radroots_replica_store::{ 100 farm, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_event_head, nostr_profile, 101 plot, plot_gcs_location, plot_tag, trade_product, 102 }; 103 use radroots_sql_core::SqlExecutor; 104 use radroots_sql_core::error::SqlError; 105 use serde_json::{Value, json}; 106 107 use crate::error::RadrootsReplicaEventsError; 108 use crate::event_head::{event_content_hash, event_head_key}; 109 const ROLE_PRIMARY: &str = "primary"; 110 const ROLE_MEMBER: &str = "member"; 111 const ROLE_OWNER: &str = "owner"; 112 const ROLE_WORKER: &str = "worker"; 113 114 #[cfg(test)] 115 pub(crate) mod failpoints { 116 use std::cell::Cell; 117 118 thread_local! { 119 static FORCE_GCS_POINT_SERIALIZE_ERROR: Cell<bool> = const { Cell::new(false) }; 120 static FORCE_GCS_POLYGON_SERIALIZE_ERROR: Cell<bool> = const { Cell::new(false) }; 121 } 122 123 pub(crate) fn set_gcs_point_serialize_error() { 124 FORCE_GCS_POINT_SERIALIZE_ERROR.with(|flag| flag.set(true)); 125 } 126 127 pub(crate) fn take_gcs_point_serialize_error() -> bool { 128 FORCE_GCS_POINT_SERIALIZE_ERROR.with(|flag| { 129 let value = flag.get(); 130 flag.set(false); 131 value 132 }) 133 } 134 135 pub(crate) fn set_gcs_polygon_serialize_error() { 136 FORCE_GCS_POLYGON_SERIALIZE_ERROR.with(|flag| flag.set(true)); 137 } 138 139 pub(crate) fn take_gcs_polygon_serialize_error() -> bool { 140 FORCE_GCS_POLYGON_SERIALIZE_ERROR.with(|flag| { 141 let value = flag.get(); 142 flag.set(false); 143 value 144 }) 145 } 146 } 147 148 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 149 pub enum RadrootsReplicaIngestOutcome { 150 /// The selected event updated its supported replica projection and raw head. 151 Applied, 152 /// The selected raw head belongs to a valid profile this replica excludes. 153 Excluded, 154 /// The selected raw head is invalid or ambiguous for its declared profile. 155 Rejected, 156 /// The event did not win NIP-01 replacement ordering. 157 Skipped, 158 } 159 160 pub trait RadrootsReplicaIdFactory { 161 fn new_d_tag(&self) -> String; 162 } 163 164 #[cfg(feature = "std")] 165 pub struct RadrootsReplicaDefaultIdFactory; 166 167 #[cfg(feature = "std")] 168 impl RadrootsReplicaIdFactory for RadrootsReplicaDefaultIdFactory { 169 fn new_d_tag(&self) -> String { 170 let uuid = uuid::Uuid::now_v7(); 171 let bytes = uuid.as_bytes(); 172 URL_SAFE_NO_PAD.encode(bytes) 173 } 174 } 175 176 #[cfg(feature = "std")] 177 /// Verifies and ingests an envelope through its supported replica projection. 178 pub fn radroots_replica_ingest_event( 179 exec: &dyn SqlExecutor, 180 event: &EventEnvelope, 181 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 182 radroots_replica_ingest_event_with_factory(exec, event, &RadrootsReplicaDefaultIdFactory) 183 } 184 185 /// Verifies and ingests an envelope with an explicit replica ID source. 186 pub fn radroots_replica_ingest_event_with_factory( 187 exec: &dyn SqlExecutor, 188 event: &EventEnvelope, 189 factory: &dyn RadrootsReplicaIdFactory, 190 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 191 let verified_event = match event.kind_u32() { 192 KIND_PROFILE | KIND_CLASSIFIED_LISTING => Some(verify_nip01_event(event.clone())?), 193 _ => None, 194 }; 195 196 if let Err(err) = exec.begin() { 197 return Err(RadrootsReplicaEventsError::from(ReplicaSchemaError::from( 198 err, 199 ))); 200 } 201 202 match ingest_event_inner(exec, event, factory, verified_event.as_ref()) { 203 Ok(outcome) => { 204 if let Err(err) = exec.commit() { 205 return Err(RadrootsReplicaEventsError::from(ReplicaSchemaError::from( 206 err, 207 ))); 208 } 209 Ok(outcome) 210 } 211 Err(err) => { 212 let _ = exec.rollback(); 213 Err(err) 214 } 215 } 216 } 217 218 fn ingest_event_inner( 219 exec: &dyn SqlExecutor, 220 event: &EventEnvelope, 221 factory: &dyn RadrootsReplicaIdFactory, 222 verified_event: Option<&RadrootsSignatureVerifiedEvent>, 223 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 224 match event.kind_u32() { 225 KIND_PROFILE => { 226 let verified_event = verified_event.ok_or_else(|| { 227 RadrootsReplicaEventsError::InvalidData( 228 "profile verification invariant missing".to_string(), 229 ) 230 })?; 231 ingest_profile_event(exec, verified_event) 232 } 233 KIND_FARM => ingest_farm_event(exec, event, factory), 234 KIND_PLOT => ingest_plot_event(exec, event, factory), 235 KIND_CLASSIFIED_LISTING => { 236 let verified_event = verified_event.ok_or_else(|| { 237 RadrootsReplicaEventsError::InvalidData( 238 "classified listing verification invariant missing".to_string(), 239 ) 240 })?; 241 ingest_listing_event(exec, verified_event) 242 } 243 kind if is_nip51_list_set_kind(kind) && kind != KIND_CALENDAR => { 244 ingest_list_set_event(exec, event) 245 } 246 _ => Err(RadrootsReplicaEventsError::InvalidData(format!( 247 "unsupported kind {}", 248 event.kind_u32() 249 ))), 250 } 251 } 252 253 fn ingest_profile_event( 254 exec: &dyn SqlExecutor, 255 verified_event: &RadrootsSignatureVerifiedEvent, 256 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 257 let admitted = admit_verified_profile_event(verified_event.clone()) 258 .map_err(|error| RadrootsReplicaEventsError::InvalidData(error.to_string()))?; 259 let event = admitted.event(); 260 let metadata = admitted.metadata(); 261 let profile_type = match profile_type_from_event(event) { 262 Some(profile_type) => profile_type, 263 None => { 264 return Err(RadrootsReplicaEventsError::InvalidData( 265 "profile_type required".to_string(), 266 )); 267 } 268 }; 269 270 let decision = event_head_decision(exec, event)?; 271 if !decision.apply { 272 return Ok(RadrootsReplicaIngestOutcome::Skipped); 273 } 274 275 let profile_type = match profile_type { 276 radroots_event::profile::ProfileType::Individual => "individual", 277 radroots_event::profile::ProfileType::Farm => "farm", 278 radroots_event::profile::ProfileType::Coop => "coop", 279 radroots_event::profile::ProfileType::Any => "any", 280 radroots_event::profile::ProfileType::Radrootsd => "radrootsd", 281 }; 282 283 let existing_result = nostr_profile::find_one( 284 exec, 285 &INostrProfileFindOne::On(INostrProfileFindOneArgs { 286 on: NostrProfileQueryBindValues::PublicKey { 287 public_key: event.author().to_hex().to_owned(), 288 }, 289 }), 290 ); 291 let existing = existing_result?.result; 292 293 match existing { 294 Some(profile) => { 295 let fields = INostrProfileFieldsPartial { 296 public_key: None, 297 profile_type: Some(Value::from(profile_type)), 298 name: Some(Value::from(required_profile_name(metadata)?)), 299 display_name: to_value_opt(metadata.display_name().map(str::to_owned)), 300 about: to_value_opt(metadata.about().map(str::to_owned)), 301 website: to_value_opt(profile_string_field(metadata, "website")), 302 picture: to_value_opt(metadata.picture().map(|value| value.as_str().to_owned())), 303 banner: to_value_opt(metadata.banner().map(|value| value.as_str().to_owned())), 304 nip05: to_value_opt(metadata.nip05().map(|value| value.as_str().to_owned())), 305 lud06: to_value_opt(profile_string_field(metadata, "lud06")), 306 lud16: to_value_opt(profile_string_field(metadata, "lud16")), 307 }; 308 let update_result = nostr_profile::update( 309 exec, 310 &INostrProfileUpdate { 311 on: NostrProfileQueryBindValues::Id { id: profile.id }, 312 fields, 313 }, 314 ); 315 let _updated = update_result?; 316 } 317 None => { 318 let fields = INostrProfileFields { 319 public_key: event.author().to_hex().to_owned(), 320 profile_type: profile_type.to_string(), 321 name: required_profile_name(metadata)?, 322 display_name: metadata.display_name().map(str::to_owned), 323 about: metadata.about().map(str::to_owned), 324 website: profile_string_field(metadata, "website"), 325 picture: metadata.picture().map(|value| value.as_str().to_owned()), 326 banner: metadata.banner().map(|value| value.as_str().to_owned()), 327 nip05: metadata.nip05().map(|value| value.as_str().to_owned()), 328 lud06: profile_string_field(metadata, "lud06"), 329 lud16: profile_string_field(metadata, "lud16"), 330 }; 331 let _ = nostr_profile::create(exec, &fields)?; 332 } 333 } 334 335 upsert_event_head(exec, &decision)?; 336 Ok(RadrootsReplicaIngestOutcome::Applied) 337 } 338 339 fn profile_type_from_event(event: &EventEnvelope) -> Option<ProfileType> { 340 event 341 .tags_as_vec() 342 .into_iter() 343 .filter(|tag| { 344 tag.first() 345 .is_some_and(|key| key == RADROOTS_PROFILE_TYPE_TAG_KEY) 346 }) 347 .filter_map(|tag| tag.get(1).cloned()) 348 .find_map(|value| radroots_profile_type_from_tag_value(&value)) 349 } 350 351 fn required_profile_name( 352 metadata: &radroots_event_codec::decode::profile::RadrootsInboundProfileMetadata, 353 ) -> Result<String, RadrootsReplicaEventsError> { 354 metadata 355 .name() 356 .map(str::to_owned) 357 .ok_or_else(|| RadrootsReplicaEventsError::InvalidData("profile name required".to_string())) 358 } 359 360 fn profile_string_field( 361 metadata: &radroots_event_codec::decode::profile::RadrootsInboundProfileMetadata, 362 field: &'static str, 363 ) -> Option<String> { 364 metadata 365 .raw_fields() 366 .get(field) 367 .and_then(Value::as_str) 368 .map(str::to_owned) 369 } 370 371 fn ingest_farm_event( 372 exec: &dyn SqlExecutor, 373 event: &EventEnvelope, 374 _factory: &dyn RadrootsReplicaIdFactory, 375 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 376 let farm = 377 farm_decode::farm_from_event(event.kind_u32(), &event.tags_as_vec(), event.content())?; 378 let decision = event_head_decision(exec, event)?; 379 if !decision.apply { 380 return Ok(RadrootsReplicaIngestOutcome::Skipped); 381 } 382 383 let filter = IFarmFieldsFilter { 384 id: None, 385 created_at: None, 386 updated_at: None, 387 d_tag: Some(farm.d_tag.clone()), 388 pubkey: Some(event.author().to_hex().to_owned()), 389 name: None, 390 about: None, 391 website: None, 392 picture: None, 393 banner: None, 394 location_primary: None, 395 location_city: None, 396 location_region: None, 397 location_country: None, 398 }; 399 let existing_result = farm::find_many( 400 exec, 401 &IFarmFindMany { 402 filter: Some(filter), 403 }, 404 ); 405 let existing = existing_result?; 406 let location = farm.location.clone(); 407 let (location_primary, location_city, location_region, location_country) = 408 unpack_farm_location_strings(location.as_ref()); 409 let farm_id = if let Some(row) = existing.results.first() { 410 let fields = IFarmFieldsPartial { 411 d_tag: Some(Value::from(farm.d_tag.clone())), 412 pubkey: Some(Value::from(event.author().to_hex().to_owned())), 413 name: Some(Value::from(farm.name.clone())), 414 about: to_value_opt(farm.about.clone()), 415 website: to_value_opt(farm.website.clone()), 416 picture: to_value_opt(farm.picture.clone()), 417 banner: to_value_opt(farm.banner.clone()), 418 location_primary: to_value_opt(location_primary), 419 location_city: to_value_opt(location_city), 420 location_region: to_value_opt(location_region), 421 location_country: to_value_opt(location_country), 422 }; 423 let update_result = farm::update( 424 exec, 425 &IFarmUpdate { 426 on: FarmQueryBindValues::Id { id: row.id.clone() }, 427 fields, 428 }, 429 ); 430 let _updated = update_result?; 431 row.id.clone() 432 } else { 433 let fields = IFarmFields { 434 d_tag: farm.d_tag.clone(), 435 pubkey: event.author().to_hex().to_owned(), 436 name: farm.name.clone(), 437 about: farm.about.clone(), 438 website: farm.website.clone(), 439 picture: farm.picture.clone(), 440 banner: farm.banner.clone(), 441 location_primary, 442 location_city, 443 location_region, 444 location_country, 445 }; 446 farm::create(exec, &fields)?.result.id 447 }; 448 449 upsert_farm_tags(exec, &farm_id, farm.tags)?; 450 451 upsert_event_head(exec, &decision)?; 452 Ok(RadrootsReplicaIngestOutcome::Applied) 453 } 454 455 fn ingest_plot_event( 456 exec: &dyn SqlExecutor, 457 event: &EventEnvelope, 458 factory: &dyn RadrootsReplicaIdFactory, 459 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 460 let plot = 461 plot_decode::plot_from_event(event.kind_u32(), &event.tags_as_vec(), event.content())?; 462 let decision = event_head_decision(exec, event)?; 463 if !decision.apply { 464 return Ok(RadrootsReplicaIngestOutcome::Skipped); 465 } 466 467 let farm = find_farm_by_ref(exec, &plot.farm.pubkey, &plot.farm.d_tag)?; 468 let filter = IPlotFieldsFilter { 469 id: None, 470 created_at: None, 471 updated_at: None, 472 d_tag: Some(plot.d_tag.clone()), 473 farm_id: Some(farm.id.clone()), 474 name: None, 475 about: None, 476 location_primary: None, 477 location_city: None, 478 location_region: None, 479 location_country: None, 480 }; 481 let existing_result = plot::find_many( 482 exec, 483 &IPlotFindMany { 484 filter: Some(filter), 485 }, 486 ); 487 let existing = existing_result?; 488 let location = plot.location.clone(); 489 let (location_primary, location_city, location_region, location_country) = 490 unpack_plot_location_strings(location.as_ref()); 491 let plot_id = if let Some(row) = existing.results.first() { 492 let fields = IPlotFieldsPartial { 493 d_tag: Some(Value::from(plot.d_tag.clone())), 494 farm_id: Some(Value::from(farm.id.clone())), 495 name: Some(Value::from(plot.name.clone())), 496 about: to_value_opt(plot.about.clone()), 497 location_primary: to_value_opt(location_primary), 498 location_city: to_value_opt(location_city), 499 location_region: to_value_opt(location_region), 500 location_country: to_value_opt(location_country), 501 }; 502 let update_result = plot::update( 503 exec, 504 &IPlotUpdate { 505 on: PlotQueryBindValues::Id { id: row.id.clone() }, 506 fields, 507 }, 508 ); 509 let _updated = update_result?; 510 row.id.clone() 511 } else { 512 let fields = IPlotFields { 513 d_tag: plot.d_tag.clone(), 514 farm_id: farm.id.clone(), 515 name: plot.name.clone(), 516 about: plot.about.clone(), 517 location_primary, 518 location_city, 519 location_region, 520 location_country, 521 }; 522 plot::create(exec, &fields)?.result.id 523 }; 524 525 upsert_plot_tags(exec, &plot_id, plot.tags)?; 526 upsert_plot_location(exec, &plot_id, location, factory)?; 527 528 upsert_event_head(exec, &decision)?; 529 Ok(RadrootsReplicaIngestOutcome::Applied) 530 } 531 532 fn ingest_listing_event( 533 exec: &dyn SqlExecutor, 534 verified_event: &RadrootsSignatureVerifiedEvent, 535 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 536 let event = verified_event.event(); 537 let decision = event_head_decision(exec, event)?; 538 if !decision.apply { 539 return Ok(RadrootsReplicaIngestOutcome::Skipped); 540 } 541 542 let partition = classify_classified_listing_tags(event.tags()); 543 if partition == ClassifiedListingPartition::FocusedFoodAvailability { 544 let outcome = match project_verified_food_availability_event(verified_event) { 545 Ok(RadrootsFoodAvailabilityProjectionOutcome::Focused(_)) => { 546 RadrootsReplicaIngestOutcome::Excluded 547 } 548 Ok(RadrootsFoodAvailabilityProjectionOutcome::Excluded(_)) | Err(_) => { 549 RadrootsReplicaIngestOutcome::Rejected 550 } 551 Ok(_) => RadrootsReplicaIngestOutcome::Rejected, 552 }; 553 return replace_listing_projection_with_raw_head(exec, &decision, outcome); 554 } 555 if partition == ClassifiedListingPartition::GenericNip99 { 556 return replace_listing_projection_with_raw_head( 557 exec, 558 &decision, 559 RadrootsReplicaIngestOutcome::Excluded, 560 ); 561 } 562 if partition == ClassifiedListingPartition::Ambiguous { 563 return replace_listing_projection_with_raw_head( 564 exec, 565 &decision, 566 RadrootsReplicaIngestOutcome::Rejected, 567 ); 568 } 569 570 let listing = match listing_decode::operational_listing_from_event( 571 event.kind_u32(), 572 &event.tags_as_vec(), 573 event.content(), 574 ) { 575 Ok(listing) => listing, 576 Err(_) => { 577 return replace_listing_projection_with_raw_head( 578 exec, 579 &decision, 580 RadrootsReplicaIngestOutcome::Rejected, 581 ); 582 } 583 }; 584 585 let listing_addr = decision.key.as_str(); 586 if listing_is_orderable(&listing) { 587 let fields = match trade_product_fields_from_listing(&listing, listing_addr) { 588 Ok(fields) => fields, 589 Err(_) => { 590 return replace_listing_projection_with_raw_head( 591 exec, 592 &decision, 593 RadrootsReplicaIngestOutcome::Rejected, 594 ); 595 } 596 }; 597 upsert_trade_product_for_listing_addr(exec, listing_addr, fields)?; 598 } else { 599 delete_trade_products_for_listing_addr(exec, listing_addr)?; 600 } 601 602 upsert_event_head(exec, &decision)?; 603 Ok(RadrootsReplicaIngestOutcome::Applied) 604 } 605 606 fn replace_listing_projection_with_raw_head( 607 exec: &dyn SqlExecutor, 608 decision: &EventHeadDecision, 609 outcome: RadrootsReplicaIngestOutcome, 610 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 611 debug_assert!(matches!( 612 outcome, 613 RadrootsReplicaIngestOutcome::Excluded | RadrootsReplicaIngestOutcome::Rejected 614 )); 615 delete_trade_products_for_listing_addr(exec, &decision.key)?; 616 upsert_event_head(exec, decision)?; 617 Ok(outcome) 618 } 619 620 fn ingest_list_set_event( 621 exec: &dyn SqlExecutor, 622 event: &EventEnvelope, 623 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 624 if event.kind_u32() != radroots_event::envelope::kind::KIND_LIST_SET_GENERIC { 625 return Ok(RadrootsReplicaIngestOutcome::Skipped); 626 } 627 let list_set = list_set_decode::list_set_from_tags( 628 event.kind_u32(), 629 event.content().to_owned(), 630 &event.tags_as_vec(), 631 )?; 632 633 let metadata_count = usize::from(list_set.title.is_some()) 634 + usize::from(list_set.description.is_some()) 635 + usize::from(list_set.image.is_some()); 636 if metadata_count != 0 { 637 return Err(RadrootsReplicaEventsError::InvalidData( 638 "domain:farm list sets must omit metadata".to_string(), 639 )); 640 } 641 if !list_set.content.is_empty() { 642 return Err(RadrootsReplicaEventsError::InvalidData( 643 "domain:farm list sets must not include content".to_string(), 644 )); 645 } 646 647 let d_tag = list_set.d_tag.clone(); 648 649 if d_tag == "member_of.farms" { 650 ensure_list_set_entries_tag(&list_set, "p", "member_of.farms")?; 651 let decision = event_head_decision(exec, event)?; 652 if !decision.apply { 653 return Ok(RadrootsReplicaIngestOutcome::Skipped); 654 } 655 upsert_member_claims(exec, &event.author().to_hex(), &list_set)?; 656 upsert_event_head(exec, &decision)?; 657 return Ok(RadrootsReplicaIngestOutcome::Applied); 658 } 659 660 if let Some((farm_d_tag, role)) = parse_farm_list_set_d_tag(&d_tag) { 661 if role == ListSetRole::Plots { 662 ensure_list_set_entries_tag(&list_set, "a", "farm plots")?; 663 let decision = event_head_decision(exec, event)?; 664 if !decision.apply { 665 return Ok(RadrootsReplicaIngestOutcome::Skipped); 666 } 667 upsert_event_head(exec, &decision)?; 668 return Ok(RadrootsReplicaIngestOutcome::Applied); 669 } 670 ensure_list_set_entries_tag(&list_set, "p", "farm members")?; 671 let decision = event_head_decision(exec, event)?; 672 if !decision.apply { 673 return Ok(RadrootsReplicaIngestOutcome::Skipped); 674 } 675 let farm = find_farm_by_ref(exec, &event.author().to_hex(), &farm_d_tag)?; 676 upsert_farm_members(exec, &farm.id, role, &list_set)?; 677 upsert_event_head(exec, &decision)?; 678 return Ok(RadrootsReplicaIngestOutcome::Applied); 679 } 680 681 Err(RadrootsReplicaEventsError::InvalidData( 682 "unsupported list set d_tag".to_string(), 683 )) 684 } 685 686 fn listing_is_orderable(listing: &OperationalListing) -> bool { 687 match listing.availability.as_ref() { 688 Some(OperationalListingAvailability::Status { status }) => { 689 matches!(status, OperationalListingStatus::Active) 690 } 691 Some(OperationalListingAvailability::Window { .. }) | None => true, 692 } 693 } 694 695 fn trade_product_fields_from_listing( 696 listing: &OperationalListing, 697 listing_addr: &str, 698 ) -> Result<ITradeProductFields, RadrootsReplicaEventsError> { 699 let bin = primary_listing_bin(listing)?; 700 let quantity_amount = bin.quantity.amount(); 701 let qty_amt = decimal_to_f64(&quantity_amount, "listing primary bin quantity")?; 702 let qty_amt_exact = quantity_amount.to_string(); 703 let qty_avail = listing 704 .inventory_available 705 .as_ref() 706 .map(|amount| decimal_to_i64(amount, "listing inventory")) 707 .transpose()?; 708 let price_source = bin 709 .display_price 710 .as_ref() 711 .unwrap_or(bin.price_per_canonical_unit.amount()); 712 let Some(price_amt) = price_source.amount().to_f64_lossy() else { 713 return Err(RadrootsReplicaEventsError::InvalidData( 714 "listing price amount out of range".to_string(), 715 )); 716 }; 717 let price_amt_exact = price_source.amount().to_string(); 718 let price_currency = price_source.currency().as_str().to_string(); 719 let price_qty_amt = if bin.display_price.is_some() { 720 1.0 721 } else { 722 decimal_to_f64( 723 &bin.price_per_canonical_unit.quantity().amount(), 724 "listing price quantity", 725 )? 726 }; 727 let price_qty_amt_exact = if bin.display_price.is_some() { 728 "1".to_string() 729 } else { 730 bin.price_per_canonical_unit.quantity().amount().to_string() 731 }; 732 let price_qty_unit = bin 733 .display_price_unit 734 .unwrap_or(bin.price_per_canonical_unit.quantity().unit()) 735 .to_string(); 736 737 Ok(ITradeProductFields { 738 key: listing.product.key.clone(), 739 category: listing.product.category.clone(), 740 title: listing.product.title.clone(), 741 summary: listing.product.summary.clone().unwrap_or_default(), 742 process: listing.product.process.clone().unwrap_or_default(), 743 lot: listing.product.lot.clone().unwrap_or_default(), 744 profile: listing.product.profile.clone().unwrap_or_default(), 745 year: listing 746 .product 747 .year 748 .as_deref() 749 .and_then(|value| value.parse::<i64>().ok()) 750 .unwrap_or_default(), 751 qty_amt, 752 qty_amt_exact, 753 qty_unit: bin.quantity.unit().to_string(), 754 qty_label: bin 755 .display_label 756 .clone() 757 .or_else(|| bin.quantity.label().map(ToOwned::to_owned)), 758 qty_avail, 759 price_amt, 760 price_amt_exact, 761 price_currency, 762 price_qty_amt, 763 price_qty_amt_exact, 764 price_qty_unit, 765 listing_addr: Some(listing_addr.to_string()), 766 primary_bin_id: Some(listing.primary_bin_id.to_string()), 767 verified_primary_bin_id: Some(listing.primary_bin_id.to_string()), 768 notes: trade_product_notes_from_listing(listing)?, 769 }) 770 } 771 772 fn trade_product_notes_from_listing( 773 listing: &OperationalListing, 774 ) -> Result<Option<String>, RadrootsReplicaEventsError> { 775 let Some(discounts) = listing 776 .discounts 777 .as_ref() 778 .filter(|discounts| !discounts.is_empty()) 779 else { 780 return Ok(None); 781 }; 782 match serde_json::to_string(&json!({ "listing_discounts": discounts })) { 783 Ok(notes) => Ok(Some(notes)), 784 Err(error) => Err(RadrootsReplicaEventsError::InvalidData(format!( 785 "listing discounts could not be serialized: {error}" 786 ))), 787 } 788 } 789 790 fn primary_listing_bin( 791 listing: &OperationalListing, 792 ) -> Result<&OperationalListingBin, RadrootsReplicaEventsError> { 793 listing 794 .bins 795 .iter() 796 .find(|bin| bin.bin_id == listing.primary_bin_id) 797 .ok_or_else(|| { 798 RadrootsReplicaEventsError::InvalidData( 799 "listing primary bin missing from bins".to_string(), 800 ) 801 }) 802 } 803 804 fn decimal_to_i64(value: &Decimal, field: &str) -> Result<i64, RadrootsReplicaEventsError> { 805 let value = decimal_to_u64(value, field)?; 806 match i64::try_from(value) { 807 Ok(value) => Ok(value), 808 Err(_) => Err(RadrootsReplicaEventsError::InvalidData(format!( 809 "{field} exceeds i64 range" 810 ))), 811 } 812 } 813 814 fn decimal_to_f64(value: &Decimal, field: &str) -> Result<f64, RadrootsReplicaEventsError> { 815 match value.to_f64_lossy() { 816 Some(value) => Ok(value), 817 None => Err(RadrootsReplicaEventsError::InvalidData(format!( 818 "{field} exceeds f64 range" 819 ))), 820 } 821 } 822 823 fn decimal_to_u64(value: &Decimal, field: &str) -> Result<u64, RadrootsReplicaEventsError> { 824 match value.to_u64_exact() { 825 Some(value) => Ok(value), 826 None => Err(RadrootsReplicaEventsError::InvalidData(format!( 827 "{field} must be a whole number" 828 ))), 829 } 830 } 831 832 fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsFilter { 833 ITradeProductFieldsFilter { 834 id: None, 835 created_at: None, 836 updated_at: None, 837 key: None, 838 category: None, 839 title: None, 840 summary: None, 841 process: None, 842 lot: None, 843 profile: None, 844 year: None, 845 qty_amt: None, 846 qty_amt_exact: None, 847 qty_unit: None, 848 qty_label: None, 849 qty_avail: None, 850 price_amt: None, 851 price_amt_exact: None, 852 price_currency: None, 853 price_qty_amt: None, 854 price_qty_amt_exact: None, 855 price_qty_unit: None, 856 listing_addr: Some(listing_addr.to_string()), 857 primary_bin_id: None, 858 verified_primary_bin_id: None, 859 notes: None, 860 } 861 } 862 863 fn upsert_trade_product_for_listing_addr( 864 exec: &dyn SqlExecutor, 865 listing_addr: &str, 866 fields: ITradeProductFields, 867 ) -> Result<(), RadrootsReplicaEventsError> { 868 let existing = trade_product::find_many( 869 exec, 870 &ITradeProductFindMany { 871 filter: Some(trade_product_listing_addr_filter(listing_addr)), 872 }, 873 )? 874 .results; 875 876 if let Some(row) = existing.first() { 877 let update = ITradeProductUpdate { 878 on: TradeProductQueryBindValues::Id { id: row.id.clone() }, 879 fields: trade_product_partial_from_fields(&fields), 880 }; 881 let _ = trade_product::update(exec, &update)?; 882 for duplicate in existing.iter().skip(1) { 883 delete_trade_product_by_id(exec, &duplicate.id)?; 884 } 885 } else { 886 let _ = trade_product::create(exec, &fields)?; 887 } 888 889 Ok(()) 890 } 891 892 fn delete_trade_products_for_listing_addr( 893 exec: &dyn SqlExecutor, 894 listing_addr: &str, 895 ) -> Result<(), RadrootsReplicaEventsError> { 896 let existing = trade_product::find_many( 897 exec, 898 &ITradeProductFindMany { 899 filter: Some(trade_product_listing_addr_filter(listing_addr)), 900 }, 901 )? 902 .results; 903 904 for row in existing { 905 delete_trade_product_by_id(exec, &row.id)?; 906 } 907 908 Ok(()) 909 } 910 911 fn delete_trade_product_by_id( 912 exec: &dyn SqlExecutor, 913 id: &str, 914 ) -> Result<(), RadrootsReplicaEventsError> { 915 let _ = trade_product::delete( 916 exec, 917 &ITradeProductFindOne::On(ITradeProductFindOneArgs { 918 on: TradeProductQueryBindValues::Id { id: id.to_string() }, 919 }), 920 )?; 921 Ok(()) 922 } 923 924 fn trade_product_partial_from_fields(fields: &ITradeProductFields) -> ITradeProductFieldsPartial { 925 ITradeProductFieldsPartial { 926 key: Some(Value::from(fields.key.clone())), 927 category: Some(Value::from(fields.category.clone())), 928 title: Some(Value::from(fields.title.clone())), 929 summary: Some(Value::from(fields.summary.clone())), 930 process: Some(Value::from(fields.process.clone())), 931 lot: Some(Value::from(fields.lot.clone())), 932 profile: Some(Value::from(fields.profile.clone())), 933 year: Some(Value::from(fields.year)), 934 qty_amt: Some(Value::from(fields.qty_amt)), 935 qty_amt_exact: Some(Value::from(fields.qty_amt_exact.clone())), 936 qty_unit: Some(Value::from(fields.qty_unit.clone())), 937 qty_label: to_value_opt(fields.qty_label.clone()), 938 qty_avail: fields.qty_avail.map(Value::from).or(Some(Value::Null)), 939 price_amt: Some(Value::from(fields.price_amt)), 940 price_amt_exact: Some(Value::from(fields.price_amt_exact.clone())), 941 price_currency: Some(Value::from(fields.price_currency.clone())), 942 price_qty_amt: Some(Value::from(fields.price_qty_amt)), 943 price_qty_amt_exact: Some(Value::from(fields.price_qty_amt_exact.clone())), 944 price_qty_unit: Some(Value::from(fields.price_qty_unit.clone())), 945 listing_addr: to_value_opt(fields.listing_addr.clone()), 946 primary_bin_id: to_value_opt(fields.primary_bin_id.clone()), 947 verified_primary_bin_id: to_value_opt(fields.verified_primary_bin_id.clone()), 948 notes: to_value_opt(fields.notes.clone()), 949 } 950 } 951 952 /// Advances a supported non-classified event head without running projection. 953 /// 954 /// Kind 30402 is rejected because its raw head and profile-aware projection 955 /// cleanup must be applied atomically through full replica ingestion. 956 pub fn radroots_replica_ingest_event_head( 957 exec: &dyn SqlExecutor, 958 event: &EventEnvelope, 959 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 960 if event.kind_u32() == KIND_CLASSIFIED_LISTING { 961 verify_nip01_event(event.clone())?; 962 return Err(RadrootsReplicaEventsError::InvalidData( 963 "classified listing heads require profile-aware replica ingestion".to_string(), 964 )); 965 } 966 let decision = event_head_decision(exec, event)?; 967 if !decision.apply { 968 return Ok(RadrootsReplicaIngestOutcome::Skipped); 969 } 970 upsert_event_head(exec, &decision)?; 971 Ok(RadrootsReplicaIngestOutcome::Applied) 972 } 973 974 fn upsert_event_head( 975 exec: &dyn SqlExecutor, 976 decision: &EventHeadDecision, 977 ) -> Result<(), RadrootsReplicaEventsError> { 978 let existing_result = nostr_event_head::find_one( 979 exec, 980 &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { 981 on: NostrEventHeadQueryBindValues::Key { 982 key: decision.key.clone(), 983 }, 984 }), 985 ); 986 let existing = existing_result?.result; 987 988 match existing { 989 Some(state) => { 990 let fields = INostrEventHeadFieldsPartial { 991 key: None, 992 kind: None, 993 pubkey: None, 994 d_tag: None, 995 last_event_id: Some(Value::from(decision.last_event_id.clone())), 996 last_created_at: Some(Value::from(decision.last_created_at)), 997 content_hash: Some(Value::from(decision.content_hash.clone())), 998 }; 999 let update_result = nostr_event_head::update( 1000 exec, 1001 &INostrEventHeadUpdate { 1002 on: NostrEventHeadQueryBindValues::Id { id: state.id }, 1003 fields, 1004 }, 1005 ); 1006 let _updated = update_result?; 1007 } 1008 None => { 1009 let fields = INostrEventHeadFields { 1010 key: decision.key.clone(), 1011 kind: decision.kind, 1012 pubkey: decision.pubkey.clone(), 1013 d_tag: decision.d_tag.clone(), 1014 last_event_id: decision.last_event_id.clone(), 1015 last_created_at: decision.last_created_at, 1016 content_hash: decision.content_hash.clone(), 1017 }; 1018 let _ = nostr_event_head::create(exec, &fields)?; 1019 } 1020 } 1021 1022 Ok(()) 1023 } 1024 1025 fn event_head_decision( 1026 exec: &dyn SqlExecutor, 1027 event: &EventEnvelope, 1028 ) -> Result<EventHeadDecision, RadrootsReplicaEventsError> { 1029 let candidate_result = if event.kind_u32() == KIND_CLASSIFIED_LISTING { 1030 event_head_candidate_for_class(event, EventClass::Addressable) 1031 } else { 1032 match event_head_candidate_for_event(event) { 1033 Ok(candidate) => candidate, 1034 Err(err) => { 1035 return Err(RadrootsReplicaEventsError::InvalidData(format!( 1036 "event head contract mismatch: {err:?}" 1037 ))); 1038 } 1039 } 1040 }; 1041 let candidate = match candidate_result { 1042 EventHeadCandidateResult::Candidate(candidate) => candidate, 1043 EventHeadCandidateResult::NotHeadSelected => { 1044 return Err(RadrootsReplicaEventsError::InvalidData( 1045 "event is not head-selected".to_string(), 1046 )); 1047 } 1048 EventHeadCandidateResult::NotPersisted => { 1049 return Ok(EventHeadDecision { 1050 apply: false, 1051 key: String::new(), 1052 kind: event.kind_u32(), 1053 pubkey: event.author().to_hex().to_owned(), 1054 d_tag: String::new(), 1055 last_event_id: event.id_hex(), 1056 last_created_at: event.created_at_u64(), 1057 content_hash: String::new(), 1058 }); 1059 } 1060 EventHeadCandidateResult::Malformed(err) => { 1061 return Err(RadrootsReplicaEventsError::InvalidData(format!( 1062 "malformed event head: {err:?}" 1063 ))); 1064 } 1065 }; 1066 let (key, kind, pubkey, d_tag) = event_head_coordinate_fields(&candidate.coordinate); 1067 #[cfg(test)] 1068 let content_hash = event_content_hash(event.content(), &event.tags_as_vec())?; 1069 #[cfg(not(test))] 1070 let content_hash = event_content_hash(event.content(), &event.tags_as_vec()); 1071 let existing_result = nostr_event_head::find_one( 1072 exec, 1073 &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { 1074 on: NostrEventHeadQueryBindValues::Key { key: key.clone() }, 1075 }), 1076 ); 1077 let existing = existing_result?.result; 1078 let current = existing 1079 .as_ref() 1080 .map(|state| current_event_head_from_row(state, &candidate.coordinate)) 1081 .transpose()?; 1082 1083 let decision = select_event_head(candidate, current.as_ref()); 1084 let apply = match decision { 1085 ProtocolEventHeadDecision::Applied(_) => true, 1086 ProtocolEventHeadDecision::SkippedDuplicate 1087 | ProtocolEventHeadDecision::SkippedOlder 1088 | ProtocolEventHeadDecision::SkippedSameTimestampHigherEventId => false, 1089 ProtocolEventHeadDecision::CoordinateMismatch => { 1090 return Err(RadrootsReplicaEventsError::InvalidData( 1091 "event head coordinate mismatch".to_string(), 1092 )); 1093 } 1094 }; 1095 1096 Ok(EventHeadDecision { 1097 apply, 1098 key, 1099 kind, 1100 pubkey, 1101 d_tag, 1102 last_event_id: event.id_hex(), 1103 last_created_at: event.created_at_u64(), 1104 content_hash, 1105 }) 1106 } 1107 1108 fn current_event_head_from_row( 1109 row: &NostrEventHead, 1110 coordinate: &EventHeadCoordinate, 1111 ) -> Result<CurrentEventHead, RadrootsReplicaEventsError> { 1112 let event_id = EventId::parse(&row.last_event_id).map_err(|err| { 1113 RadrootsReplicaEventsError::InvalidData(format!( 1114 "nostr event head last_event_id invalid: {err}" 1115 )) 1116 })?; 1117 Ok(CurrentEventHead { 1118 coordinate: coordinate.clone(), 1119 event_id, 1120 created_at: row.last_created_at, 1121 }) 1122 } 1123 1124 fn event_head_coordinate_fields(coordinate: &EventHeadCoordinate) -> (String, u32, String, String) { 1125 match coordinate { 1126 EventHeadCoordinate::Replaceable { kind, pubkey } => { 1127 let pubkey = pubkey.to_string(); 1128 ( 1129 event_head_key(*kind, &pubkey, ""), 1130 *kind, 1131 pubkey, 1132 String::new(), 1133 ) 1134 } 1135 EventHeadCoordinate::Addressable { 1136 kind, 1137 pubkey, 1138 d_tag, 1139 } => { 1140 let pubkey = pubkey.to_string(); 1141 let d_tag = d_tag.to_string(); 1142 (event_head_key(*kind, &pubkey, &d_tag), *kind, pubkey, d_tag) 1143 } 1144 } 1145 } 1146 1147 fn find_farm_by_ref( 1148 exec: &dyn SqlExecutor, 1149 pubkey: &str, 1150 d_tag: &str, 1151 ) -> Result<radroots_replica_schema::farm::Farm, RadrootsReplicaEventsError> { 1152 let filter = IFarmFieldsFilter { 1153 id: None, 1154 created_at: None, 1155 updated_at: None, 1156 d_tag: Some(d_tag.to_string()), 1157 pubkey: Some(pubkey.to_string()), 1158 name: None, 1159 about: None, 1160 website: None, 1161 picture: None, 1162 banner: None, 1163 location_primary: None, 1164 location_city: None, 1165 location_region: None, 1166 location_country: None, 1167 }; 1168 let result_query = farm::find_many( 1169 exec, 1170 &IFarmFindMany { 1171 filter: Some(filter), 1172 }, 1173 ); 1174 let result = result_query?; 1175 match result.results.into_iter().next() { 1176 Some(farm) => Ok(farm), 1177 None => Err(RadrootsReplicaEventsError::InvalidData( 1178 "farm not found".to_string(), 1179 )), 1180 } 1181 } 1182 1183 fn upsert_farm_tags( 1184 exec: &dyn SqlExecutor, 1185 farm_id: &str, 1186 tags: Option<Vec<String>>, 1187 ) -> Result<(), RadrootsReplicaEventsError> { 1188 let existing_query = farm_tag::find_many( 1189 exec, 1190 &IFarmTagFindMany { 1191 filter: Some(IFarmTagFieldsFilter { 1192 id: None, 1193 created_at: None, 1194 updated_at: None, 1195 farm_id: Some(farm_id.to_string()), 1196 tag: None, 1197 }), 1198 }, 1199 ); 1200 let existing = existing_query?; 1201 for row in existing.results { 1202 handle_delete_result(farm_tag::delete( 1203 exec, 1204 &IFarmTagDelete::On(IFarmTagFindOneArgs { 1205 on: FarmTagQueryBindValues::Id { id: row.id }, 1206 }), 1207 ))?; 1208 } 1209 1210 let mut tags = tags.unwrap_or_default(); 1211 tags.sort(); 1212 tags.dedup(); 1213 for tag in tags { 1214 if tag.trim().is_empty() { 1215 continue; 1216 } 1217 let fields = IFarmTagFields { 1218 farm_id: farm_id.to_string(), 1219 tag, 1220 }; 1221 let _ = farm_tag::create(exec, &fields)?; 1222 } 1223 Ok(()) 1224 } 1225 1226 fn upsert_plot_tags( 1227 exec: &dyn SqlExecutor, 1228 plot_id: &str, 1229 tags: Option<Vec<String>>, 1230 ) -> Result<(), RadrootsReplicaEventsError> { 1231 let existing_query = plot_tag::find_many( 1232 exec, 1233 &IPlotTagFindMany { 1234 filter: Some(IPlotTagFieldsFilter { 1235 id: None, 1236 created_at: None, 1237 updated_at: None, 1238 plot_id: Some(plot_id.to_string()), 1239 tag: None, 1240 }), 1241 }, 1242 ); 1243 let existing = existing_query?; 1244 for row in existing.results { 1245 handle_delete_result(plot_tag::delete( 1246 exec, 1247 &IPlotTagDelete::On(IPlotTagFindOneArgs { 1248 on: PlotTagQueryBindValues::Id { id: row.id }, 1249 }), 1250 ))?; 1251 } 1252 1253 let mut tags = tags.unwrap_or_default(); 1254 tags.sort(); 1255 tags.dedup(); 1256 for tag in tags { 1257 if tag.trim().is_empty() { 1258 continue; 1259 } 1260 let fields = IPlotTagFields { 1261 plot_id: plot_id.to_string(), 1262 tag, 1263 }; 1264 let _ = plot_tag::create(exec, &fields)?; 1265 } 1266 Ok(()) 1267 } 1268 1269 fn upsert_plot_location( 1270 exec: &dyn SqlExecutor, 1271 plot_id: &str, 1272 location: Option<radroots_event::farm::plot::PlotLocation>, 1273 factory: &dyn RadrootsReplicaIdFactory, 1274 ) -> Result<(), RadrootsReplicaEventsError> { 1275 clear_plot_locations(exec, plot_id)?; 1276 if let Some(location) = location { 1277 let gcs_id = create_gcs_location(exec, location.gcs, factory)?; 1278 let fields = IPlotGcsLocationFields { 1279 plot_id: plot_id.to_string(), 1280 gcs_location_id: gcs_id, 1281 role: ROLE_PRIMARY.to_string(), 1282 }; 1283 let _ = plot_gcs_location::create(exec, &fields)?; 1284 } 1285 Ok(()) 1286 } 1287 1288 #[cfg(test)] 1289 fn clear_farm_locations( 1290 exec: &dyn SqlExecutor, 1291 farm_id: &str, 1292 ) -> Result<(), RadrootsReplicaEventsError> { 1293 let existing_query = farm_gcs_location::find_many( 1294 exec, 1295 &IFarmGcsLocationFindMany { 1296 filter: Some(IFarmGcsLocationFieldsFilter { 1297 id: None, 1298 created_at: None, 1299 updated_at: None, 1300 farm_id: Some(farm_id.to_string()), 1301 gcs_location_id: None, 1302 role: None, 1303 }), 1304 }, 1305 ); 1306 let existing = existing_query?; 1307 for row in existing.results { 1308 handle_delete_result(farm_gcs_location::delete( 1309 exec, 1310 &IFarmGcsLocationDelete::On(IFarmGcsLocationFindOneArgs { 1311 on: FarmGcsLocationQueryBindValues::Id { id: row.id }, 1312 }), 1313 ))?; 1314 } 1315 Ok(()) 1316 } 1317 1318 fn clear_plot_locations( 1319 exec: &dyn SqlExecutor, 1320 plot_id: &str, 1321 ) -> Result<(), RadrootsReplicaEventsError> { 1322 let existing_query = plot_gcs_location::find_many( 1323 exec, 1324 &IPlotGcsLocationFindMany { 1325 filter: Some(IPlotGcsLocationFieldsFilter { 1326 id: None, 1327 created_at: None, 1328 updated_at: None, 1329 plot_id: Some(plot_id.to_string()), 1330 gcs_location_id: None, 1331 role: None, 1332 }), 1333 }, 1334 ); 1335 let existing = existing_query?; 1336 for row in existing.results { 1337 handle_delete_result(plot_gcs_location::delete( 1338 exec, 1339 &IPlotGcsLocationDelete::On(IPlotGcsLocationFindOneArgs { 1340 on: PlotGcsLocationQueryBindValues::Id { id: row.id }, 1341 }), 1342 ))?; 1343 } 1344 Ok(()) 1345 } 1346 1347 fn create_gcs_location( 1348 exec: &dyn SqlExecutor, 1349 gcs: radroots_event::farm::change_set::GcsLocation, 1350 factory: &dyn RadrootsReplicaIdFactory, 1351 ) -> Result<String, RadrootsReplicaEventsError> { 1352 let d_tag = factory.new_d_tag(); 1353 #[cfg(test)] 1354 let point = serialize_gcs_point(&gcs.point).map_err(map_gcs_point_serialize_error)?; 1355 #[cfg(not(test))] 1356 let point = serialize_gcs_point(&gcs.point); 1357 1358 #[cfg(test)] 1359 let polygon = serialize_gcs_polygon(&gcs.polygon).map_err(map_gcs_polygon_serialize_error)?; 1360 #[cfg(not(test))] 1361 let polygon = serialize_gcs_polygon(&gcs.polygon); 1362 1363 let fields = IGcsLocationFields { 1364 d_tag, 1365 lat: gcs.lat, 1366 lng: gcs.lng, 1367 geohash: gcs.geohash, 1368 point, 1369 polygon, 1370 accuracy: gcs.accuracy, 1371 altitude: gcs.altitude, 1372 tag_0: gcs.tag_0, 1373 label: gcs.label, 1374 area: gcs.area, 1375 elevation: gcs.elevation, 1376 soil: gcs.soil, 1377 climate: gcs.climate, 1378 gc_id: gcs.gc_id, 1379 gc_name: gcs.gc_name, 1380 gc_admin1_id: gcs.gc_admin1_id, 1381 gc_admin1_name: gcs.gc_admin1_name, 1382 gc_country_id: gcs.gc_country_id, 1383 gc_country_name: gcs.gc_country_name, 1384 }; 1385 let result = gcs_location::create(exec, &fields)?; 1386 Ok(result.result.id) 1387 } 1388 1389 #[cfg(test)] 1390 fn map_gcs_point_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError { 1391 RadrootsReplicaEventsError::InvalidData("gcs.point".to_string()) 1392 } 1393 1394 #[cfg(test)] 1395 fn map_gcs_polygon_serialize_error(_err: serde_json::Error) -> RadrootsReplicaEventsError { 1396 RadrootsReplicaEventsError::InvalidData("gcs.polygon".to_string()) 1397 } 1398 1399 #[cfg(test)] 1400 fn serialize_gcs_point( 1401 point: &radroots_event::farm::change_set::GeoJsonPoint, 1402 ) -> Result<String, serde_json::Error> { 1403 #[cfg(test)] 1404 if failpoints::take_gcs_point_serialize_error() { 1405 return Err(json_parse_error()); 1406 } 1407 serde_json::to_string(point) 1408 } 1409 1410 #[cfg(not(test))] 1411 fn serialize_gcs_point(point: &radroots_event::farm::change_set::GeoJsonPoint) -> String { 1412 serde_json::to_string(point).expect("gcs.point serializes") 1413 } 1414 1415 #[cfg(test)] 1416 fn serialize_gcs_polygon( 1417 polygon: &radroots_event::farm::change_set::GeoJsonPolygon, 1418 ) -> Result<String, serde_json::Error> { 1419 #[cfg(test)] 1420 if failpoints::take_gcs_polygon_serialize_error() { 1421 return Err(json_parse_error()); 1422 } 1423 serde_json::to_string(polygon) 1424 } 1425 1426 #[cfg(not(test))] 1427 fn serialize_gcs_polygon(polygon: &radroots_event::farm::change_set::GeoJsonPolygon) -> String { 1428 serde_json::to_string(polygon).expect("gcs.polygon serializes") 1429 } 1430 1431 #[cfg(test)] 1432 fn json_parse_error() -> serde_json::Error { 1433 serde_json::from_str::<Value>("{").expect_err("json parse error") 1434 } 1435 1436 fn upsert_farm_members( 1437 exec: &dyn SqlExecutor, 1438 farm_id: &str, 1439 role: ListSetRole, 1440 list_set: &radroots_event::social::list_set::ListSet, 1441 ) -> Result<(), RadrootsReplicaEventsError> { 1442 let role_value = match role { 1443 ListSetRole::Members => ROLE_MEMBER, 1444 ListSetRole::Owners => ROLE_OWNER, 1445 ListSetRole::Workers => ROLE_WORKER, 1446 ListSetRole::Plots => return Ok(()), 1447 }; 1448 let existing_query = farm_member::find_many( 1449 exec, 1450 &IFarmMemberFindMany { 1451 filter: Some(IFarmMemberFieldsFilter { 1452 id: None, 1453 created_at: None, 1454 updated_at: None, 1455 farm_id: Some(farm_id.to_string()), 1456 member_pubkey: None, 1457 role: Some(role_value.to_string()), 1458 }), 1459 }, 1460 ); 1461 let existing = existing_query?; 1462 for row in existing.results { 1463 handle_delete_result(farm_member::delete( 1464 exec, 1465 &IFarmMemberDelete::On(IFarmMemberFindOneArgs { 1466 on: FarmMemberQueryBindValues::Id { id: row.id }, 1467 }), 1468 ))?; 1469 } 1470 1471 let mut entries = Vec::new(); 1472 for entry in &list_set.entries { 1473 for value in entry.values.iter().take(1) { 1474 entries.push(value.to_string()); 1475 } 1476 } 1477 entries.sort(); 1478 entries.dedup(); 1479 1480 for pubkey in entries { 1481 let fields = IFarmMemberFields { 1482 farm_id: farm_id.to_string(), 1483 member_pubkey: pubkey, 1484 role: role_value.to_string(), 1485 }; 1486 let _ = farm_member::create(exec, &fields)?; 1487 } 1488 Ok(()) 1489 } 1490 1491 fn upsert_member_claims( 1492 exec: &dyn SqlExecutor, 1493 member_pubkey: &str, 1494 list_set: &radroots_event::social::list_set::ListSet, 1495 ) -> Result<(), RadrootsReplicaEventsError> { 1496 let existing_query = farm_member_claim::find_many( 1497 exec, 1498 &IFarmMemberClaimFindMany { 1499 filter: Some(IFarmMemberClaimFieldsFilter { 1500 id: None, 1501 created_at: None, 1502 updated_at: None, 1503 member_pubkey: Some(member_pubkey.to_string()), 1504 farm_pubkey: None, 1505 }), 1506 }, 1507 ); 1508 let existing = existing_query?; 1509 for row in existing.results { 1510 handle_delete_result(farm_member_claim::delete( 1511 exec, 1512 &IFarmMemberClaimDelete::On(IFarmMemberClaimFindOneArgs { 1513 on: FarmMemberClaimQueryBindValues::Id { id: row.id }, 1514 }), 1515 ))?; 1516 } 1517 1518 let mut entries = Vec::new(); 1519 for entry in &list_set.entries { 1520 for value in entry.values.iter().take(1) { 1521 entries.push(value.to_string()); 1522 } 1523 } 1524 entries.sort(); 1525 entries.dedup(); 1526 1527 for farm_pubkey in entries { 1528 let fields = IFarmMemberClaimFields { 1529 member_pubkey: member_pubkey.to_string(), 1530 farm_pubkey, 1531 }; 1532 let _ = farm_member_claim::create(exec, &fields)?; 1533 } 1534 Ok(()) 1535 } 1536 1537 fn handle_delete_result<T>( 1538 result: Result<T, ReplicaSchemaError<SqlError>>, 1539 ) -> Result<(), RadrootsReplicaEventsError> { 1540 match result { 1541 Ok(_) => Ok(()), 1542 Err(err) => { 1543 if matches!(err.error, SqlError::NotFound(_)) { 1544 return Ok(()); 1545 } 1546 Err(err.into()) 1547 } 1548 } 1549 } 1550 1551 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 1552 enum ListSetRole { 1553 Members, 1554 Owners, 1555 Workers, 1556 Plots, 1557 } 1558 1559 fn unpack_farm_location_strings( 1560 location: Option<&radroots_event::farm::FarmPublicLocation>, 1561 ) -> ( 1562 Option<String>, 1563 Option<String>, 1564 Option<String>, 1565 Option<String>, 1566 ) { 1567 match location { 1568 Some(location) => ( 1569 Some(location.primary.clone()), 1570 location.city.clone(), 1571 location.region.clone(), 1572 location.country.clone(), 1573 ), 1574 None => (None, None, None, None), 1575 } 1576 } 1577 1578 fn unpack_plot_location_strings( 1579 location: Option<&radroots_event::farm::plot::PlotLocation>, 1580 ) -> ( 1581 Option<String>, 1582 Option<String>, 1583 Option<String>, 1584 Option<String>, 1585 ) { 1586 match location { 1587 Some(location) => ( 1588 location.primary.clone(), 1589 location.city.clone(), 1590 location.region.clone(), 1591 location.country.clone(), 1592 ), 1593 None => (None, None, None, None), 1594 } 1595 } 1596 1597 fn ensure_list_set_entries_tag( 1598 list_set: &radroots_event::social::list_set::ListSet, 1599 expected: &str, 1600 label: &str, 1601 ) -> Result<(), RadrootsReplicaEventsError> { 1602 for entry in list_set.entries.iter() { 1603 if entry.tag != expected { 1604 return Err(RadrootsReplicaEventsError::InvalidData(format!( 1605 "domain:farm list set {label} must only include {expected} tags" 1606 ))); 1607 } 1608 } 1609 Ok(()) 1610 } 1611 1612 fn parse_farm_list_set_d_tag(d_tag: &str) -> Option<(String, ListSetRole)> { 1613 let mut parts = d_tag.splitn(3, ':'); 1614 if parts.next() != Some("farm") { 1615 return None; 1616 } 1617 let farm_d_tag = parts.next()?.to_string(); 1618 let suffix = parts.next()?; 1619 let role = match suffix { 1620 "members" => ListSetRole::Members, 1621 "members.owners" => ListSetRole::Owners, 1622 "members.workers" => ListSetRole::Workers, 1623 "plots" => ListSetRole::Plots, 1624 _ => return None, 1625 }; 1626 Some((farm_d_tag, role)) 1627 } 1628 1629 fn to_value_opt(value: Option<String>) -> Option<Value> { 1630 Some(match value { 1631 Some(value) => Value::from(value), 1632 None => Value::Null, 1633 }) 1634 } 1635 1636 struct EventHeadDecision { 1637 apply: bool, 1638 key: String, 1639 kind: u32, 1640 pubkey: String, 1641 d_tag: String, 1642 last_event_id: String, 1643 last_created_at: u64, 1644 content_hash: String, 1645 } 1646 1647 #[cfg(test)] 1648 mod tests { 1649 use super::*; 1650 use std::sync::Arc; 1651 use std::sync::atomic::{AtomicUsize, Ordering}; 1652 1653 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp}; 1654 use radroots_core::{Currency, Money, Quantity, QuantityPrice, Unit}; 1655 use radroots_event::envelope::kind::{KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC}; 1656 use radroots_event::farm::change_set::{GcsLocation, GeoJsonPoint, GeoJsonPolygon}; 1657 use radroots_event::farm::plot::{Plot, PlotLocation}; 1658 use radroots_event::farm::{Farm, FarmPublicLocation, FarmRef}; 1659 use radroots_event::listing::operational::OperationalListingProduct; 1660 use radroots_event::profile::{ 1661 ProfileType, RADROOTS_PROFILE_TYPE_TAG_KEY, radroots_profile_type_tag_value, 1662 }; 1663 use radroots_event::social::list::ListEntry; 1664 use radroots_event::social::list_set::ListSet; 1665 use radroots_event_codec::encode::farm as farm_encode; 1666 use radroots_event_codec::encode::farm as farm_list_sets; 1667 use radroots_event_codec::encode::list_set as list_set_encode; 1668 use radroots_event_codec::encode::plot as plot_encode; 1669 use radroots_nostr::event::from_nostr; 1670 use radroots_replica_schema::farm::IFarmFields; 1671 use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; 1672 use radroots_replica_schema::farm_member::IFarmMemberFields; 1673 use radroots_replica_schema::farm_member_claim::IFarmMemberClaimFields; 1674 use radroots_replica_schema::farm_tag::IFarmTagFields; 1675 use radroots_replica_schema::gcs_location::IGcsLocationFields; 1676 use radroots_replica_schema::plot::IPlotFields; 1677 use radroots_replica_schema::plot_gcs_location::IPlotGcsLocationFields; 1678 use radroots_replica_schema::plot_tag::IPlotTagFields; 1679 use radroots_replica_store::{ 1680 ReplicaSql, farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, 1681 gcs_location, migrations, nostr_event_head, plot, plot_gcs_location, plot_tag, 1682 trade_product, 1683 }; 1684 use radroots_sql_core::{ExecOutcome, SqlExecutor, SqlxSqliteExecutor}; 1685 use radroots_test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_ALICE_SECRET_KEY_HEX}; 1686 1687 fn fixture_public_key(seed: u8) -> String { 1688 Keys::parse(&format!("{seed:064x}")) 1689 .expect("fixture secret key") 1690 .public_key() 1691 .to_hex() 1692 } 1693 1694 fn test_event_envelope( 1695 id: u64, 1696 author: &str, 1697 created_at: u64, 1698 kind: u32, 1699 tags: Vec<Vec<String>>, 1700 content: String, 1701 ) -> EventEnvelope { 1702 EventEnvelope::new(EventEnvelopeParts { 1703 id: format!("{id:064x}"), 1704 author: author.to_string(), 1705 created_at, 1706 kind, 1707 tags, 1708 content, 1709 sig: "f".repeat(128), 1710 }) 1711 .expect("test event envelope") 1712 } 1713 1714 fn test_event_with_parts( 1715 event: &EventEnvelope, 1716 kind: u32, 1717 tags: Vec<Vec<String>>, 1718 content: String, 1719 ) -> EventEnvelope { 1720 EventEnvelope::new(EventEnvelopeParts { 1721 id: event.id_hex(), 1722 author: event.author().to_hex().to_owned(), 1723 created_at: event.created_at_u64(), 1724 kind, 1725 tags, 1726 content, 1727 sig: event.signature_hex(), 1728 }) 1729 .expect("test event envelope parts") 1730 } 1731 1732 fn test_event_with_content(event: &EventEnvelope, content: String) -> EventEnvelope { 1733 test_event_with_parts(event, event.kind_u32(), event.tags_as_vec(), content) 1734 } 1735 1736 fn test_event_with_id(event: &EventEnvelope, id: String) -> EventEnvelope { 1737 EventEnvelope::new(EventEnvelopeParts { 1738 id, 1739 author: event.author().to_hex().to_owned(), 1740 created_at: event.created_at_u64(), 1741 kind: event.kind_u32(), 1742 tags: event.tags_as_vec(), 1743 content: event.content().to_owned(), 1744 sig: event.signature_hex(), 1745 }) 1746 .expect("test event id") 1747 } 1748 1749 struct FixedFactory; 1750 1751 impl RadrootsReplicaIdFactory for FixedFactory { 1752 fn new_d_tag(&self) -> String { 1753 "AAAAAAAAAAAAAAAAAAAAAZ".to_string() 1754 } 1755 } 1756 1757 struct TxnExecutor<'a> { 1758 inner: Option<&'a SqlxSqliteExecutor>, 1759 begin_err: Option<SqlError>, 1760 commit_err: Option<SqlError>, 1761 rollback_count: Arc<AtomicUsize>, 1762 } 1763 1764 impl SqlExecutor for TxnExecutor<'_> { 1765 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 1766 match self.inner { 1767 Some(inner) => inner.exec(sql, params_json), 1768 None => Err(SqlError::UnsupportedPlatform), 1769 } 1770 } 1771 1772 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 1773 match self.inner { 1774 Some(inner) => inner.query_raw(sql, params_json), 1775 None => Err(SqlError::UnsupportedPlatform), 1776 } 1777 } 1778 1779 fn begin(&self) -> Result<(), SqlError> { 1780 match self.begin_err.clone() { 1781 Some(err) => Err(err), 1782 None => Ok(()), 1783 } 1784 } 1785 1786 fn commit(&self) -> Result<(), SqlError> { 1787 match self.commit_err.clone() { 1788 Some(err) => Err(err), 1789 None => Ok(()), 1790 } 1791 } 1792 1793 fn rollback(&self) -> Result<(), SqlError> { 1794 self.rollback_count.fetch_add(1, Ordering::SeqCst); 1795 Ok(()) 1796 } 1797 } 1798 1799 struct DeleteErrorExecutor<'a> { 1800 inner: &'a SqlxSqliteExecutor, 1801 table_name: &'static str, 1802 err: SqlError, 1803 } 1804 1805 impl SqlExecutor for DeleteErrorExecutor<'_> { 1806 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 1807 let normalized = sql.to_ascii_lowercase(); 1808 if normalized.contains("delete from") && normalized.contains(self.table_name) { 1809 return Err(self.err.clone()); 1810 } 1811 self.inner.exec(sql, params_json) 1812 } 1813 1814 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 1815 self.inner.query_raw(sql, params_json) 1816 } 1817 1818 fn begin(&self) -> Result<(), SqlError> { 1819 self.inner.begin() 1820 } 1821 1822 fn commit(&self) -> Result<(), SqlError> { 1823 self.inner.commit() 1824 } 1825 1826 fn rollback(&self) -> Result<(), SqlError> { 1827 self.inner.rollback() 1828 } 1829 } 1830 1831 struct PassExecutor<'a> { 1832 inner: &'a SqlxSqliteExecutor, 1833 } 1834 1835 impl SqlExecutor for PassExecutor<'_> { 1836 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 1837 self.inner.exec(sql, params_json) 1838 } 1839 1840 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 1841 self.inner.query_raw(sql, params_json) 1842 } 1843 1844 fn begin(&self) -> Result<(), SqlError> { 1845 self.inner.begin() 1846 } 1847 1848 fn commit(&self) -> Result<(), SqlError> { 1849 self.inner.commit() 1850 } 1851 1852 fn rollback(&self) -> Result<(), SqlError> { 1853 self.inner.rollback() 1854 } 1855 } 1856 1857 struct QueryFailExecutor<'a> { 1858 inner: &'a SqlxSqliteExecutor, 1859 needle: &'static str, 1860 err: SqlError, 1861 } 1862 1863 impl SqlExecutor for QueryFailExecutor<'_> { 1864 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 1865 let normalized = sql.to_ascii_lowercase(); 1866 if normalized.contains(self.needle) { 1867 return Err(self.err.clone()); 1868 } 1869 self.inner.exec(sql, params_json) 1870 } 1871 1872 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 1873 let normalized = sql.to_ascii_lowercase(); 1874 if normalized.contains(self.needle) { 1875 return Err(self.err.clone()); 1876 } 1877 self.inner.query_raw(sql, params_json) 1878 } 1879 1880 fn begin(&self) -> Result<(), SqlError> { 1881 self.inner.begin() 1882 } 1883 1884 fn commit(&self) -> Result<(), SqlError> { 1885 self.inner.commit() 1886 } 1887 1888 fn rollback(&self) -> Result<(), SqlError> { 1889 self.inner.rollback() 1890 } 1891 } 1892 1893 fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> GcsLocation { 1894 GcsLocation { 1895 lat, 1896 lng, 1897 geohash: geohash.to_string(), 1898 point: GeoJsonPoint { 1899 r#type: "Point".to_string(), 1900 coordinates: [lng, lat], 1901 }, 1902 polygon: GeoJsonPolygon { 1903 r#type: "Polygon".to_string(), 1904 coordinates: vec![vec![ 1905 [lng, lat], 1906 [lng, lat + 0.001], 1907 [lng - 0.001, lat + 0.001], 1908 [lng, lat], 1909 ]], 1910 }, 1911 accuracy: Some(1.0), 1912 altitude: Some(2.0), 1913 tag_0: Some("tag".to_string()), 1914 label: Some("label".to_string()), 1915 area: Some(3.0), 1916 elevation: Some(4), 1917 soil: Some("soil".to_string()), 1918 climate: Some("climate".to_string()), 1919 gc_id: Some("gc_id".to_string()), 1920 gc_name: Some("gc_name".to_string()), 1921 gc_admin1_id: Some("gc_admin1_id".to_string()), 1922 gc_admin1_name: Some("gc_admin1_name".to_string()), 1923 gc_country_id: Some("gc_country_id".to_string()), 1924 gc_country_name: Some("gc_country_name".to_string()), 1925 } 1926 } 1927 1928 fn profile_event( 1929 id: u64, 1930 author: &str, 1931 created_at: u32, 1932 profile_type: Option<ProfileType>, 1933 name: &str, 1934 ) -> EventEnvelope { 1935 let profile = serde_json::json!({ 1936 "name": name, 1937 "display_name": format!("{name}-display"), 1938 "nip05": format!("{name}@example.com"), 1939 "about": format!("{name}-about"), 1940 "website": "https://example.com", 1941 "picture": "https://example.com/p.png", 1942 "banner": "https://example.com/b.png", 1943 "lud06": "lud06", 1944 "lud16": "lud16" 1945 }); 1946 let mut tags = Vec::new(); 1947 if let Some(profile_type) = profile_type { 1948 tags.push(vec![ 1949 RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), 1950 radroots_profile_type_tag_value(profile_type).to_string(), 1951 ]); 1952 } 1953 test_event_envelope( 1954 id, 1955 author, 1956 u64::from(created_at), 1957 KIND_PROFILE, 1958 tags, 1959 profile.to_string(), 1960 ) 1961 } 1962 1963 fn test_keys_for_author(author: &str) -> Keys { 1964 if author == FIXTURE_ALICE_PUBLIC_KEY_HEX { 1965 return Keys::parse(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture signing key"); 1966 } 1967 (1_u8..=u8::MAX) 1968 .find_map(|seed| { 1969 let keys = Keys::parse(&format!("{seed:064x}")).ok()?; 1970 (keys.public_key().to_hex() == author).then_some(keys) 1971 }) 1972 .expect("test author must resolve to a fixture signing key") 1973 } 1974 1975 fn sign_test_event(event: &EventEnvelope) -> EventEnvelope { 1976 let keys = test_keys_for_author(&event.author().to_hex()); 1977 let tags = event 1978 .tags_as_vec() 1979 .into_iter() 1980 .map(|tag| Tag::parse(tag).expect("test event tag")) 1981 .collect::<Vec<_>>(); 1982 let event = EventBuilder::new( 1983 Kind::Custom(u16::try_from(event.kind_u32()).expect("test event kind")), 1984 event.content(), 1985 ) 1986 .tags(tags) 1987 .allow_self_tagging() 1988 .custom_created_at(Timestamp::from_secs(event.created_at_u64())) 1989 .sign_with_keys(&keys) 1990 .expect("signed test event"); 1991 from_nostr(&event).expect("test event adapter") 1992 } 1993 1994 fn ingest_test_profile( 1995 exec: &dyn SqlExecutor, 1996 event: &EventEnvelope, 1997 ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { 1998 let verified = 1999 verify_nip01_event(sign_test_event(event)).expect("verified profile fixture"); 2000 ingest_profile_event(exec, &verified) 2001 } 2002 2003 fn farm_event( 2004 id: u64, 2005 author: &str, 2006 created_at: u32, 2007 d_tag: &str, 2008 name: &str, 2009 location: Option<FarmPublicLocation>, 2010 tags: Option<Vec<String>>, 2011 ) -> EventEnvelope { 2012 let farm = Farm { 2013 d_tag: d_tag.to_string(), 2014 name: name.to_string(), 2015 about: Some("about".to_string()), 2016 website: Some("https://farm.example.com".to_string()), 2017 picture: Some("https://farm.example.com/p.png".to_string()), 2018 banner: Some("https://farm.example.com/b.png".to_string()), 2019 location, 2020 tags, 2021 }; 2022 let tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); 2023 test_event_envelope( 2024 id, 2025 author, 2026 u64::from(created_at), 2027 KIND_FARM, 2028 tags, 2029 serde_json::to_string(&farm).expect("farm json"), 2030 ) 2031 } 2032 2033 #[allow(clippy::too_many_arguments)] 2034 fn plot_event( 2035 id: u64, 2036 author: &str, 2037 created_at: u32, 2038 d_tag: &str, 2039 farm_ref: FarmRef, 2040 name: &str, 2041 location: Option<PlotLocation>, 2042 tags: Option<Vec<String>>, 2043 ) -> EventEnvelope { 2044 let plot = Plot { 2045 d_tag: d_tag.to_string(), 2046 farm: farm_ref, 2047 name: name.to_string(), 2048 about: Some("plot-about".to_string()), 2049 location, 2050 tags, 2051 }; 2052 let tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); 2053 test_event_envelope( 2054 id, 2055 author, 2056 u64::from(created_at), 2057 KIND_PLOT, 2058 tags, 2059 serde_json::to_string(&plot).expect("plot json"), 2060 ) 2061 } 2062 2063 fn list_set_event( 2064 id: u64, 2065 author: &str, 2066 created_at: u32, 2067 kind: u32, 2068 list_set: &ListSet, 2069 ) -> EventEnvelope { 2070 let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set"); 2071 test_event_envelope( 2072 id, 2073 author, 2074 u64::from(created_at), 2075 kind, 2076 parts.tags, 2077 parts.content, 2078 ) 2079 } 2080 2081 fn listing_event( 2082 author: &str, 2083 created_at: u32, 2084 d_tag: &str, 2085 status: &str, 2086 title: &str, 2087 ) -> EventEnvelope { 2088 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 2089 signed_listing_event( 2090 author, 2091 u64::from(created_at), 2092 vec![ 2093 vec!["d".to_string(), d_tag.to_string()], 2094 vec![ 2095 "a".to_string(), 2096 format!("{}:{}:{}", KIND_FARM, author, farm_d_tag), 2097 ], 2098 vec!["p".to_string(), author.to_string()], 2099 vec!["key".to_string(), "pasture-eggs".to_string()], 2100 vec!["title".to_string(), title.to_string()], 2101 vec!["category".to_string(), "eggs".to_string()], 2102 vec!["summary".to_string(), "Pasture-raised eggs".to_string()], 2103 vec!["process".to_string(), "washed".to_string()], 2104 vec!["lot".to_string(), "lot-a".to_string()], 2105 vec!["profile".to_string(), "dozen".to_string()], 2106 vec!["year".to_string(), "2026".to_string()], 2107 vec!["radroots:primary_bin".to_string(), "bin-a".to_string()], 2108 vec![ 2109 "radroots:bin".to_string(), 2110 "bin-a".to_string(), 2111 "12".to_string(), 2112 "each".to_string(), 2113 "12".to_string(), 2114 "each".to_string(), 2115 "dozen".to_string(), 2116 ], 2117 vec![ 2118 "radroots:price".to_string(), 2119 "bin-a".to_string(), 2120 "6".to_string(), 2121 "USD".to_string(), 2122 "1".to_string(), 2123 "each".to_string(), 2124 "6".to_string(), 2125 "each".to_string(), 2126 ], 2127 vec!["inventory".to_string(), "5".to_string()], 2128 vec!["status".to_string(), status.to_string()], 2129 ], 2130 format!("# {title}"), 2131 ) 2132 } 2133 2134 fn signed_listing_event( 2135 author: &str, 2136 created_at: u64, 2137 tags: Vec<Vec<String>>, 2138 content: String, 2139 ) -> EventEnvelope { 2140 assert_eq!( 2141 author, FIXTURE_ALICE_PUBLIC_KEY_HEX, 2142 "listing tests must use the approved fixture signer" 2143 ); 2144 let keys = Keys::parse(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture signing key"); 2145 let tags = tags 2146 .into_iter() 2147 .map(|tag| Tag::parse(tag).expect("listing tag")) 2148 .collect::<Vec<_>>(); 2149 let event = EventBuilder::new( 2150 Kind::Custom(u16::try_from(KIND_CLASSIFIED_LISTING).expect("classified listing kind")), 2151 content, 2152 ) 2153 .tags(tags) 2154 .allow_self_tagging() 2155 .custom_created_at(Timestamp::from_secs(created_at)) 2156 .sign_with_keys(&keys) 2157 .expect("signed listing event"); 2158 from_nostr(&event).expect("listing event adapter") 2159 } 2160 2161 fn focused_listing_event(author: &str, created_at: u64, d_tag: &str) -> EventEnvelope { 2162 focused_listing_event_with_content( 2163 author, 2164 created_at, 2165 d_tag, 2166 "Carrots available this week.", 2167 ) 2168 } 2169 2170 fn focused_listing_event_with_content( 2171 author: &str, 2172 created_at: u64, 2173 d_tag: &str, 2174 content: &str, 2175 ) -> EventEnvelope { 2176 signed_listing_event( 2177 author, 2178 created_at, 2179 vec![ 2180 vec!["d".to_string(), d_tag.to_string()], 2181 vec!["title".to_string(), "Nantes Carrots".to_string()], 2182 vec!["summary".to_string(), "Fresh bunches".to_string()], 2183 vec!["published_at".to_string(), "1".to_string()], 2184 vec!["location".to_string(), "Central Saanich, BC".to_string()], 2185 vec!["price".to_string(), "3".to_string(), "CAD".to_string()], 2186 vec!["radroots:price_unit".to_string(), "lb".to_string()], 2187 vec!["status".to_string(), "active".to_string()], 2188 ], 2189 content.to_string(), 2190 ) 2191 } 2192 2193 fn generic_listing_event(author: &str, created_at: u64, d_tag: &str) -> EventEnvelope { 2194 signed_listing_event( 2195 author, 2196 created_at, 2197 vec![ 2198 vec!["d".to_string(), d_tag.to_string()], 2199 vec!["title".to_string(), "Generic listing".to_string()], 2200 vec!["price".to_string(), "3".to_string(), "CAD".to_string()], 2201 ], 2202 "A standards-compatible marker-free NIP-99 listing.".to_string(), 2203 ) 2204 } 2205 2206 fn listing_decimal(raw: &str) -> Decimal { 2207 raw.parse().expect("decimal") 2208 } 2209 2210 fn listing_currency() -> Currency { 2211 "USD".parse().expect("currency") 2212 } 2213 2214 fn listing_model() -> OperationalListing { 2215 OperationalListing { 2216 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".parse().expect("d tag"), 2217 published_at: Some(1), 2218 farm: FarmRef { 2219 pubkey: fixture_public_key(12), 2220 d_tag: "AAAAAAAAAAAAAAAAAAAAAZ".to_string(), 2221 }, 2222 product: OperationalListingProduct { 2223 key: "pasture-eggs".to_string(), 2224 title: "Pasture Eggs".to_string(), 2225 category: "eggs".to_string(), 2226 summary: Some("Pasture-raised eggs".to_string()), 2227 process: None, 2228 lot: None, 2229 location: None, 2230 profile: None, 2231 year: Some("2026".to_string()), 2232 }, 2233 primary_bin_id: "bin-a".parse().expect("primary bin id"), 2234 bins: vec![OperationalListingBin { 2235 bin_id: "bin-a".parse().expect("bin id"), 2236 quantity: Quantity::try_new(listing_decimal("12"), Unit::Each) 2237 .unwrap() 2238 .with_label("unit label"), 2239 price_per_canonical_unit: QuantityPrice::try_new( 2240 Money::try_new(listing_decimal("6"), listing_currency()).unwrap(), 2241 Quantity::try_new(listing_decimal("1"), Unit::Each).unwrap(), 2242 ) 2243 .unwrap(), 2244 display_amount: None, 2245 display_unit: None, 2246 display_label: None, 2247 display_price: None, 2248 display_price_unit: None, 2249 }], 2250 resource_area: None, 2251 plot: None, 2252 discounts: Some(Vec::new()), 2253 inventory_available: Some(listing_decimal("5")), 2254 availability: Some(OperationalListingAvailability::Status { 2255 status: OperationalListingStatus::Active, 2256 }), 2257 delivery_method: None, 2258 location: None, 2259 images: None, 2260 } 2261 } 2262 2263 #[test] 2264 fn listing_field_helpers_cover_optional_label_and_error_paths() { 2265 let listing = listing_model(); 2266 let fields = 2267 trade_product_fields_from_listing(&listing, "30402:pubkey:listing").expect("fields"); 2268 assert_eq!(fields.qty_label.as_deref(), Some("unit label")); 2269 assert_eq!(fields.notes, None); 2270 2271 let mut missing_bin = listing.clone(); 2272 missing_bin.primary_bin_id = "bin-missing".parse().expect("missing bin id"); 2273 let err = trade_product_fields_from_listing(&missing_bin, "30402:pubkey:listing") 2274 .err() 2275 .expect("missing primary bin should fail"); 2276 assert!(err.to_string().contains("primary bin missing")); 2277 2278 let mut fractional_inventory = listing; 2279 fractional_inventory.inventory_available = Some(listing_decimal("1.5")); 2280 let err = trade_product_fields_from_listing(&fractional_inventory, "30402:pubkey:listing") 2281 .err() 2282 .expect("fractional inventory should fail"); 2283 assert!(err.to_string().contains("whole number")); 2284 2285 let err = decimal_to_i64(&listing_decimal("9223372036854775808"), "listing inventory") 2286 .expect_err("i64 overflow should fail"); 2287 assert!(err.to_string().contains("exceeds i64 range")); 2288 } 2289 2290 #[test] 2291 fn current_event_head_reports_invalid_stored_event_id() { 2292 let row = NostrEventHead { 2293 id: "head-1".to_string(), 2294 created_at: "2026-01-01T00:00:00Z".to_string(), 2295 updated_at: "2026-01-01T00:00:00Z".to_string(), 2296 key: "profile".to_string(), 2297 kind: KIND_PROFILE, 2298 pubkey: fixture_public_key(10), 2299 d_tag: String::new(), 2300 last_event_id: "not-an-event-id".to_string(), 2301 last_created_at: 1, 2302 content_hash: "hash".to_string(), 2303 }; 2304 let coordinate = EventHeadCoordinate::Replaceable { 2305 kind: KIND_PROFILE, 2306 pubkey: fixture_public_key(10).parse().expect("pubkey"), 2307 }; 2308 2309 let err = current_event_head_from_row(&row, &coordinate) 2310 .expect_err("invalid stored event id should fail"); 2311 assert!(err.to_string().contains("last_event_id invalid")); 2312 } 2313 2314 fn seed_rows(exec: &SqlxSqliteExecutor) -> (String, String, String, String) { 2315 migrations::run_all_up(exec).expect("migrations"); 2316 let farm_row = farm::create( 2317 exec, 2318 &IFarmFields { 2319 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 2320 pubkey: fixture_public_key(15), 2321 name: "farm".to_string(), 2322 about: None, 2323 website: None, 2324 picture: None, 2325 banner: None, 2326 location_primary: None, 2327 location_city: None, 2328 location_region: None, 2329 location_country: None, 2330 }, 2331 ) 2332 .expect("farm") 2333 .result; 2334 let plot_row = plot::create( 2335 exec, 2336 &IPlotFields { 2337 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 2338 farm_id: farm_row.id.clone(), 2339 name: "plot".to_string(), 2340 about: None, 2341 location_primary: None, 2342 location_city: None, 2343 location_region: None, 2344 location_country: None, 2345 }, 2346 ) 2347 .expect("plot") 2348 .result; 2349 let gcs_row = gcs_location::create( 2350 exec, 2351 &IGcsLocationFields { 2352 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 2353 lat: 1.0, 2354 lng: 2.0, 2355 geohash: "s0".to_string(), 2356 point: "{\"type\":\"Point\",\"coordinates\":[2.0,1.0]}".to_string(), 2357 polygon: 2358 "{\"type\":\"Polygon\",\"coordinates\":[[[2.0,1.0],[2.1,1.1],[1.9,1.1],[2.0,1.0]]]}".to_string(), 2359 accuracy: None, 2360 altitude: None, 2361 tag_0: None, 2362 label: None, 2363 area: None, 2364 elevation: None, 2365 soil: None, 2366 climate: None, 2367 gc_id: None, 2368 gc_name: None, 2369 gc_admin1_id: None, 2370 gc_admin1_name: None, 2371 gc_country_id: None, 2372 gc_country_name: None, 2373 }, 2374 ) 2375 .expect("gcs") 2376 .result; 2377 2378 let _ = farm_tag::create( 2379 exec, 2380 &IFarmTagFields { 2381 farm_id: farm_row.id.clone(), 2382 tag: "alpha".to_string(), 2383 }, 2384 ) 2385 .expect("farm tag"); 2386 let _ = plot_tag::create( 2387 exec, 2388 &IPlotTagFields { 2389 plot_id: plot_row.id.clone(), 2390 tag: "beta".to_string(), 2391 }, 2392 ) 2393 .expect("plot tag"); 2394 let _ = farm_gcs_location::create( 2395 exec, 2396 &IFarmGcsLocationFields { 2397 farm_id: farm_row.id.clone(), 2398 gcs_location_id: gcs_row.id.clone(), 2399 role: "primary".to_string(), 2400 }, 2401 ) 2402 .expect("farm gcs"); 2403 let _ = plot_gcs_location::create( 2404 exec, 2405 &IPlotGcsLocationFields { 2406 plot_id: plot_row.id.clone(), 2407 gcs_location_id: gcs_row.id.clone(), 2408 role: "primary".to_string(), 2409 }, 2410 ) 2411 .expect("plot gcs"); 2412 let _ = farm_member::create( 2413 exec, 2414 &IFarmMemberFields { 2415 farm_id: farm_row.id.clone(), 2416 member_pubkey: fixture_public_key(6), 2417 role: "member".to_string(), 2418 }, 2419 ) 2420 .expect("member"); 2421 let _ = farm_member_claim::create( 2422 exec, 2423 &IFarmMemberClaimFields { 2424 member_pubkey: fixture_public_key(6), 2425 farm_pubkey: farm_row.pubkey.clone(), 2426 }, 2427 ) 2428 .expect("claim"); 2429 ( 2430 farm_row.id, 2431 farm_row.pubkey, 2432 farm_row.d_tag, 2433 plot_row.d_tag.clone(), 2434 ) 2435 } 2436 2437 #[test] 2438 fn ingest_transaction_paths_are_covered() { 2439 let begin_executor = TxnExecutor { 2440 inner: None, 2441 begin_err: Some(SqlError::Internal), 2442 commit_err: None, 2443 rollback_count: Arc::new(AtomicUsize::new(0)), 2444 }; 2445 let event = test_event_envelope( 2446 1, 2447 &fixture_public_key(10), 2448 1, 2449 KIND_LIST_SET_FOLLOW, 2450 Vec::new(), 2451 String::new(), 2452 ); 2453 let begin_err = 2454 radroots_replica_ingest_event_with_factory(&begin_executor, &event, &FixedFactory) 2455 .expect_err("begin"); 2456 assert!(begin_err.to_string().contains("replica_sync.sql")); 2457 assert!(begin_executor.commit().is_ok()); 2458 assert_eq!( 2459 begin_executor 2460 .exec("select 1", "[]") 2461 .expect_err("exec") 2462 .code(), 2463 "ERR_UNSUPPORTED_PLATFORM" 2464 ); 2465 assert_eq!( 2466 begin_executor 2467 .query_raw("select 1", "[]") 2468 .expect_err("query") 2469 .code(), 2470 "ERR_UNSUPPORTED_PLATFORM" 2471 ); 2472 2473 let rollback_count = Arc::new(AtomicUsize::new(0)); 2474 let commit_executor = TxnExecutor { 2475 inner: None, 2476 begin_err: None, 2477 commit_err: Some(SqlError::Internal), 2478 rollback_count: rollback_count.clone(), 2479 }; 2480 let commit_err = 2481 radroots_replica_ingest_event_with_factory(&commit_executor, &event, &FixedFactory) 2482 .expect_err("commit"); 2483 assert!(commit_err.to_string().contains("replica_sync.sql")); 2484 assert_eq!(rollback_count.load(Ordering::SeqCst), 0); 2485 2486 let rollback_executor = TxnExecutor { 2487 inner: None, 2488 begin_err: None, 2489 commit_err: None, 2490 rollback_count: Arc::new(AtomicUsize::new(0)), 2491 }; 2492 let unsupported = 2493 test_event_envelope(2, &fixture_public_key(10), 2, 42, Vec::new(), String::new()); 2494 let err = radroots_replica_ingest_event_with_factory( 2495 &rollback_executor, 2496 &unsupported, 2497 &FixedFactory, 2498 ) 2499 .expect_err("rollback"); 2500 assert!(err.to_string().contains("unsupported kind")); 2501 assert_eq!(rollback_executor.rollback_count.load(Ordering::SeqCst), 1); 2502 } 2503 2504 #[test] 2505 fn classified_listing_signature_rejection_precedes_transaction_acquisition() { 2506 let executor = TxnExecutor { 2507 inner: None, 2508 begin_err: Some(SqlError::Internal), 2509 commit_err: None, 2510 rollback_count: Arc::new(AtomicUsize::new(0)), 2511 }; 2512 let signed = 2513 focused_listing_event(FIXTURE_ALICE_PUBLIC_KEY_HEX, 10, "pre-transaction-check"); 2514 let tampered = test_event_with_content(&signed, "tampered".to_string()); 2515 2516 assert!(matches!( 2517 radroots_replica_ingest_event_with_factory(&executor, &tampered, &FixedFactory), 2518 Err(RadrootsReplicaEventsError::Verification(_)) 2519 )); 2520 assert_eq!(executor.rollback_count.load(Ordering::SeqCst), 0); 2521 } 2522 2523 #[test] 2524 fn calendar_kind_uses_unsupported_transaction_path() { 2525 let rollback_count = Arc::new(AtomicUsize::new(0)); 2526 let executor = TxnExecutor { 2527 inner: None, 2528 begin_err: None, 2529 commit_err: None, 2530 rollback_count: rollback_count.clone(), 2531 }; 2532 let event = test_event_envelope( 2533 3, 2534 &fixture_public_key(10), 2535 3, 2536 KIND_CALENDAR, 2537 Vec::new(), 2538 String::new(), 2539 ); 2540 2541 let err = radroots_replica_ingest_event_with_factory(&executor, &event, &FixedFactory) 2542 .expect_err("calendar is not supported by the replica projection"); 2543 2544 assert!(matches!( 2545 err, 2546 RadrootsReplicaEventsError::InvalidData(ref message) 2547 if message == "unsupported kind 31924" 2548 )); 2549 assert_eq!(rollback_count.load(Ordering::SeqCst), 1); 2550 } 2551 2552 #[test] 2553 fn ingest_core_paths_cover_helpers_and_decisions() { 2554 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 2555 migrations::run_all_up(&exec).expect("migrations"); 2556 2557 let factory = RadrootsReplicaDefaultIdFactory; 2558 assert_eq!(factory.new_d_tag().len(), 22); 2559 2560 let profile_pubkey = fixture_public_key(9); 2561 let profile = profile_event( 2562 10, 2563 &profile_pubkey, 2564 1, 2565 Some(ProfileType::Individual), 2566 "alice", 2567 ); 2568 let profile_no_type = profile_event(9, &profile_pubkey, 0, None, "alice-none"); 2569 assert!(ingest_test_profile(&exec, &profile_no_type).is_err()); 2570 let signed_profile = sign_test_event(&profile); 2571 assert_eq!( 2572 radroots_replica_ingest_event(&exec, &signed_profile).expect("ingest wrapper"), 2573 RadrootsReplicaIngestOutcome::Applied 2574 ); 2575 let profile_update = profile_event( 2576 11, 2577 &profile_pubkey, 2578 2, 2579 Some(ProfileType::Individual), 2580 "alice-2", 2581 ); 2582 assert_eq!( 2583 ingest_test_profile(&exec, &profile_update).expect("profile update"), 2584 RadrootsReplicaIngestOutcome::Applied 2585 ); 2586 assert_eq!( 2587 ingest_test_profile(&exec, &profile_update).expect("profile skip"), 2588 RadrootsReplicaIngestOutcome::Skipped 2589 ); 2590 let profile_older = profile_event( 2591 8, 2592 &profile_pubkey, 2593 1, 2594 Some(ProfileType::Individual), 2595 "alice-old", 2596 ); 2597 let decision_old = event_head_decision(&exec, &profile_older).expect("decision old"); 2598 assert!(!decision_old.apply); 2599 let signed_profile_update = sign_test_event(&profile_update); 2600 let decision_same = 2601 event_head_decision(&exec, &signed_profile_update).expect("decision same"); 2602 assert!(!decision_same.apply); 2603 let profile_same_time_higher_id = 2604 test_event_with_id(&signed_profile_update, "f".repeat(64)); 2605 let decision = event_head_decision(&exec, &profile_same_time_higher_id).expect("decision"); 2606 assert!(!decision.apply); 2607 let profile_same_time_lower_id = test_event_with_id(&signed_profile_update, "0".repeat(64)); 2608 let decision = event_head_decision(&exec, &profile_same_time_lower_id).expect("decision"); 2609 assert!(decision.apply); 2610 2611 let farm_pubkey = fixture_public_key(15); 2612 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 2613 let farm = farm_event( 2614 20, 2615 &farm_pubkey, 2616 10, 2617 farm_d_tag, 2618 "farm-a", 2619 Some(FarmPublicLocation { 2620 primary: "primary".to_string(), 2621 city: Some("city".to_string()), 2622 region: Some("region".to_string()), 2623 country: Some("country".to_string()), 2624 geohash: "9q8yy".to_string(), 2625 }), 2626 Some(vec![ 2627 "coffee".to_string(), 2628 "coffee".to_string(), 2629 " ".to_string(), 2630 ]), 2631 ); 2632 assert_eq!( 2633 ingest_farm_event(&exec, &farm, &FixedFactory).expect("farm"), 2634 RadrootsReplicaIngestOutcome::Applied 2635 ); 2636 let farm_update = farm_event( 2637 21, 2638 &farm_pubkey, 2639 11, 2640 farm_d_tag, 2641 "farm-b", 2642 None, 2643 Some(vec!["market".to_string()]), 2644 ); 2645 assert_eq!( 2646 ingest_farm_event(&exec, &farm_update, &FixedFactory).expect("farm update"), 2647 RadrootsReplicaIngestOutcome::Applied 2648 ); 2649 assert_eq!( 2650 ingest_farm_event(&exec, &farm_update, &FixedFactory).expect("farm skip"), 2651 RadrootsReplicaIngestOutcome::Skipped 2652 ); 2653 2654 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 2655 let plot = plot_event( 2656 30, 2657 &farm_pubkey, 2658 20, 2659 plot_d_tag, 2660 FarmRef { 2661 pubkey: farm_pubkey.clone(), 2662 d_tag: farm_d_tag.to_string(), 2663 }, 2664 "plot-a", 2665 Some(PlotLocation { 2666 primary: Some("p".to_string()), 2667 city: Some("c".to_string()), 2668 region: Some("r".to_string()), 2669 country: Some("k".to_string()), 2670 gcs: sample_gcs(11.0, 21.0, "s1"), 2671 }), 2672 Some(vec!["tag".to_string()]), 2673 ); 2674 assert_eq!( 2675 ingest_plot_event(&exec, &plot, &FixedFactory).expect("plot"), 2676 RadrootsReplicaIngestOutcome::Applied 2677 ); 2678 let plot_update = plot_event( 2679 31, 2680 &farm_pubkey, 2681 21, 2682 plot_d_tag, 2683 FarmRef { 2684 pubkey: farm_pubkey.clone(), 2685 d_tag: farm_d_tag.to_string(), 2686 }, 2687 "plot-b", 2688 None, 2689 Some(vec!["tag2".to_string()]), 2690 ); 2691 assert_eq!( 2692 ingest_plot_event(&exec, &plot_update, &FixedFactory).expect("plot update"), 2693 RadrootsReplicaIngestOutcome::Applied 2694 ); 2695 assert_eq!( 2696 ingest_plot_event(&exec, &plot_update, &FixedFactory).expect("plot skip"), 2697 RadrootsReplicaIngestOutcome::Skipped 2698 ); 2699 2700 let members = 2701 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(6)]) 2702 .expect("members"); 2703 let owners = farm_list_sets::farm_owners_list_set(farm_d_tag, vec![fixture_public_key(8)]) 2704 .expect("owners"); 2705 let workers = 2706 farm_list_sets::farm_workers_list_set(farm_d_tag, vec![fixture_public_key(16)]) 2707 .expect("workers"); 2708 let plots = farm_list_sets::farm_plots_list_set( 2709 farm_d_tag, 2710 &farm_pubkey, 2711 vec![plot_d_tag.to_string()], 2712 ) 2713 .expect("plots"); 2714 let member_of = 2715 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 2716 2717 for (idx, list_set) in [members, owners, workers, plots, member_of] 2718 .iter() 2719 .enumerate() 2720 { 2721 let event = list_set_event( 2722 40 + idx as u64, 2723 if list_set.d_tag == "member_of.farms" { 2724 &profile_pubkey 2725 } else { 2726 &farm_pubkey 2727 }, 2728 30 + idx as u32, 2729 KIND_LIST_SET_GENERIC, 2730 list_set, 2731 ); 2732 assert_eq!( 2733 ingest_list_set_event(&exec, &event).expect("list set"), 2734 RadrootsReplicaIngestOutcome::Applied 2735 ); 2736 assert_eq!( 2737 ingest_list_set_event(&exec, &event).expect("list set skip"), 2738 RadrootsReplicaIngestOutcome::Skipped 2739 ); 2740 } 2741 2742 let bad_description = ListSet { 2743 d_tag: "member_of.farms".to_string(), 2744 content: String::new(), 2745 entries: vec![ListEntry { 2746 tag: "p".to_string(), 2747 values: vec![farm_pubkey.clone()], 2748 }], 2749 title: None, 2750 description: Some("bad".to_string()), 2751 image: None, 2752 }; 2753 let bad_description_event = list_set_event( 2754 90, 2755 &profile_pubkey, 2756 100, 2757 KIND_LIST_SET_GENERIC, 2758 &bad_description, 2759 ); 2760 assert!(ingest_list_set_event(&exec, &bad_description_event).is_err()); 2761 2762 let bad_image = ListSet { 2763 d_tag: "member_of.farms".to_string(), 2764 content: String::new(), 2765 entries: vec![ListEntry { 2766 tag: "p".to_string(), 2767 values: vec![farm_pubkey.clone()], 2768 }], 2769 title: None, 2770 description: None, 2771 image: Some("bad".to_string()), 2772 }; 2773 let bad_image_event = 2774 list_set_event(91, &profile_pubkey, 101, KIND_LIST_SET_GENERIC, &bad_image); 2775 assert!(ingest_list_set_event(&exec, &bad_image_event).is_err()); 2776 2777 let bad_title = ListSet { 2778 d_tag: "member_of.farms".to_string(), 2779 content: String::new(), 2780 entries: vec![ListEntry { 2781 tag: "p".to_string(), 2782 values: vec![farm_pubkey.clone()], 2783 }], 2784 title: Some("bad".to_string()), 2785 description: None, 2786 image: None, 2787 }; 2788 let bad_title_event = 2789 list_set_event(92, &profile_pubkey, 102, KIND_LIST_SET_GENERIC, &bad_title); 2790 assert!(ingest_list_set_event(&exec, &bad_title_event).is_err()); 2791 2792 let bad_content = ListSet { 2793 d_tag: "member_of.farms".to_string(), 2794 content: "bad".to_string(), 2795 entries: vec![ListEntry { 2796 tag: "p".to_string(), 2797 values: vec![farm_pubkey.clone()], 2798 }], 2799 title: None, 2800 description: None, 2801 image: None, 2802 }; 2803 let bad_content_event = list_set_event( 2804 93, 2805 &profile_pubkey, 2806 103, 2807 KIND_LIST_SET_GENERIC, 2808 &bad_content, 2809 ); 2810 assert!(ingest_list_set_event(&exec, &bad_content_event).is_err()); 2811 2812 let unknown_farm_list_set = ListSet { 2813 d_tag: format!("farm:{farm_d_tag}:unknown"), 2814 content: String::new(), 2815 entries: vec![ListEntry { 2816 tag: "p".to_string(), 2817 values: vec![profile_pubkey.clone()], 2818 }], 2819 title: None, 2820 description: None, 2821 image: None, 2822 }; 2823 let unknown_farm_list_event = list_set_event( 2824 94, 2825 &farm_pubkey, 2826 104, 2827 KIND_LIST_SET_GENERIC, 2828 &unknown_farm_list_set, 2829 ); 2830 assert!(ingest_list_set_event(&exec, &unknown_farm_list_event).is_err()); 2831 2832 assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:unknown").is_none()); 2833 assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:plots").is_some()); 2834 assert_eq!(to_value_opt(Some("x".to_string())), Some(Value::from("x"))); 2835 assert_eq!(to_value_opt(None), Some(Value::Null)); 2836 let location = FarmPublicLocation { 2837 primary: "p".to_string(), 2838 city: Some("c".to_string()), 2839 region: Some("r".to_string()), 2840 country: Some("k".to_string()), 2841 geohash: "9q8yy".to_string(), 2842 }; 2843 assert_eq!( 2844 unpack_farm_location_strings(Some(&location)).0, 2845 Some("p".to_string()) 2846 ); 2847 assert_eq!( 2848 unpack_plot_location_strings(Some(&PlotLocation { 2849 primary: Some("p".to_string()), 2850 city: None, 2851 region: None, 2852 country: None, 2853 gcs: sample_gcs(13.0, 23.0, "s3"), 2854 })) 2855 .0, 2856 Some("p".to_string()) 2857 ); 2858 assert!(ensure_list_set_entries_tag(&bad_image, "p", "x").is_ok()); 2859 assert!( 2860 ensure_list_set_entries_tag( 2861 &ListSet { 2862 d_tag: "x".to_string(), 2863 content: String::new(), 2864 entries: vec![ListEntry { 2865 tag: "a".to_string(), 2866 values: vec!["x".to_string()], 2867 }], 2868 title: None, 2869 description: None, 2870 image: None, 2871 }, 2872 "p", 2873 "x", 2874 ) 2875 .is_err() 2876 ); 2877 } 2878 2879 #[test] 2880 fn ingest_listing_projects_trade_product_and_removes_archived_replacements() { 2881 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 2882 migrations::run_all_up(&exec).expect("migrations"); 2883 2884 let seller_pubkey = FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned(); 2885 let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 2886 let listing_addr = format!( 2887 "{}:{}:{}", 2888 KIND_CLASSIFIED_LISTING, seller_pubkey, listing_d_tag 2889 ); 2890 2891 let mut active = listing_event(&seller_pubkey, 10, listing_d_tag, "active", "Pasture Eggs"); 2892 let mut active_tags = active.tags_as_vec(); 2893 active_tags.push(vec![ 2894 "radroots:discount".to_string(), 2895 serde_json::json!({ 2896 "scope": "bin", 2897 "threshold": { 2898 "kind": "bin_count", 2899 "amount": { "bin_id": "bin-a", "min": 1 } 2900 }, 2901 "value": { 2902 "kind": "percent", 2903 "amount": { "value": "10" } 2904 } 2905 }) 2906 .to_string(), 2907 ]); 2908 active = signed_listing_event( 2909 &seller_pubkey, 2910 active.created_at_u64(), 2911 active_tags, 2912 active.content().to_owned(), 2913 ); 2914 assert_eq!( 2915 radroots_replica_ingest_event(&exec, &active).expect("active ingest"), 2916 RadrootsReplicaIngestOutcome::Applied 2917 ); 2918 2919 let replica = ReplicaSql::new(&exec); 2920 let search_rows = replica 2921 .trade_product_search(&["eggs".to_string()]) 2922 .expect("search"); 2923 assert_eq!(search_rows.len(), 1); 2924 assert_eq!( 2925 search_rows[0].listing_addr.as_deref(), 2926 Some(listing_addr.as_str()) 2927 ); 2928 assert_eq!(search_rows[0].title, "Pasture Eggs"); 2929 assert_eq!(search_rows[0].primary_bin_id.as_deref(), Some("bin-a")); 2930 assert_eq!( 2931 search_rows[0].verified_primary_bin_id.as_deref(), 2932 Some("bin-a") 2933 ); 2934 assert_eq!(search_rows[0].qty_amt, 12.0); 2935 assert_eq!(search_rows[0].qty_amt_exact.as_deref(), Some("12")); 2936 assert_eq!(search_rows[0].qty_avail, Some(5)); 2937 assert_eq!(search_rows[0].price_amt, 6.0); 2938 assert_eq!(search_rows[0].price_amt_exact.as_deref(), Some("6")); 2939 assert_eq!(search_rows[0].price_currency, "USD"); 2940 assert_eq!(search_rows[0].price_qty_amt, 1.0); 2941 assert_eq!(search_rows[0].price_qty_amt_exact.as_deref(), Some("1")); 2942 assert!( 2943 search_rows[0] 2944 .notes 2945 .as_deref() 2946 .is_some_and(|notes| notes.contains("listing_discounts")) 2947 ); 2948 2949 let updated = listing_event(&seller_pubkey, 11, listing_d_tag, "active", "Market Eggs"); 2950 assert_eq!( 2951 radroots_replica_ingest_event(&exec, &updated).expect("listing update"), 2952 RadrootsReplicaIngestOutcome::Applied 2953 ); 2954 let product_rows = trade_product::find_many( 2955 &exec, 2956 &ITradeProductFindMany { 2957 filter: Some(trade_product_listing_addr_filter(&listing_addr)), 2958 }, 2959 ) 2960 .expect("product rows") 2961 .results; 2962 assert_eq!(product_rows.len(), 1); 2963 assert_eq!(product_rows[0].title, "Market Eggs"); 2964 assert_eq!(product_rows[0].primary_bin_id.as_deref(), Some("bin-a")); 2965 assert_eq!( 2966 product_rows[0].verified_primary_bin_id.as_deref(), 2967 Some("bin-a") 2968 ); 2969 2970 let archived = listing_event(&seller_pubkey, 12, listing_d_tag, "archived", "Market Eggs"); 2971 assert_eq!( 2972 radroots_replica_ingest_event(&exec, &archived).expect("archived ingest"), 2973 RadrootsReplicaIngestOutcome::Applied 2974 ); 2975 let search_rows = replica 2976 .trade_product_search(&["eggs".to_string()]) 2977 .expect("search archived"); 2978 assert!(search_rows.is_empty()); 2979 2980 let product_rows = trade_product::find_many( 2981 &exec, 2982 &ITradeProductFindMany { 2983 filter: Some(trade_product_listing_addr_filter(&listing_addr)), 2984 }, 2985 ) 2986 .expect("archived product rows") 2987 .results; 2988 assert!(product_rows.is_empty()); 2989 2990 let state = nostr_event_head::find_one( 2991 &exec, 2992 &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { 2993 on: NostrEventHeadQueryBindValues::Key { 2994 key: event_head_key(KIND_CLASSIFIED_LISTING, &seller_pubkey, listing_d_tag), 2995 }, 2996 }), 2997 ) 2998 .expect("event state") 2999 .result 3000 .expect("state row"); 3001 assert_eq!(state.last_event_id, archived.id_hex()); 3002 3003 let stale_active = listing_event(&seller_pubkey, 11, listing_d_tag, "active", "Stale Eggs"); 3004 assert_eq!( 3005 radroots_replica_ingest_event(&exec, &stale_active).expect("stale ingest"), 3006 RadrootsReplicaIngestOutcome::Skipped 3007 ); 3008 let product_rows = trade_product::find_many( 3009 &exec, 3010 &ITradeProductFindMany { 3011 filter: Some(trade_product_listing_addr_filter(&listing_addr)), 3012 }, 3013 ) 3014 .expect("stale product rows") 3015 .results; 3016 assert!(product_rows.is_empty()); 3017 } 3018 3019 #[test] 3020 fn ingest_listing_preserves_fractional_exact_economics() { 3021 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3022 migrations::run_all_up(&exec).expect("migrations"); 3023 3024 let seller_pubkey = FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned(); 3025 let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAg"; 3026 let listing_addr = format!( 3027 "{}:{}:{}", 3028 KIND_CLASSIFIED_LISTING, seller_pubkey, listing_d_tag 3029 ); 3030 3031 let mut active = listing_event( 3032 &seller_pubkey, 3033 10, 3034 listing_d_tag, 3035 "active", 3036 "Half Gram Greens", 3037 ); 3038 let mut active_tags = active.tags_as_vec(); 3039 for tag in &mut active_tags { 3040 if tag.first().is_some_and(|name| name == "radroots:bin") { 3041 tag[2] = "0.5".to_string(); 3042 tag[3] = "g".to_string(); 3043 tag[4] = "0.5".to_string(); 3044 tag[5] = "g".to_string(); 3045 tag[6] = "half gram".to_string(); 3046 } 3047 if tag.first().is_some_and(|name| name == "radroots:price") { 3048 tag[2] = "3.25".to_string(); 3049 tag[3] = "USD".to_string(); 3050 tag[4] = "1".to_string(); 3051 tag[5] = "g".to_string(); 3052 tag[6] = "3.25".to_string(); 3053 tag[7] = "g".to_string(); 3054 } 3055 } 3056 active = signed_listing_event( 3057 &seller_pubkey, 3058 active.created_at_u64(), 3059 active_tags, 3060 active.content().to_owned(), 3061 ); 3062 3063 assert_eq!( 3064 radroots_replica_ingest_event(&exec, &active).expect("fractional active ingest"), 3065 RadrootsReplicaIngestOutcome::Applied 3066 ); 3067 3068 let replica = ReplicaSql::new(&exec); 3069 let search_rows = replica 3070 .trade_product_search(&["greens".to_string()]) 3071 .expect("search"); 3072 assert_eq!(search_rows.len(), 1); 3073 assert_eq!( 3074 search_rows[0].listing_addr.as_deref(), 3075 Some(listing_addr.as_str()) 3076 ); 3077 assert_eq!(search_rows[0].qty_amt, 0.5); 3078 assert_eq!(search_rows[0].qty_amt_exact.as_deref(), Some("0.5")); 3079 assert_eq!(search_rows[0].price_amt, 3.25); 3080 assert_eq!(search_rows[0].price_amt_exact.as_deref(), Some("3.25")); 3081 assert_eq!(search_rows[0].price_qty_amt, 1.0); 3082 assert_eq!(search_rows[0].price_qty_amt_exact.as_deref(), Some("1")); 3083 } 3084 3085 #[test] 3086 fn listing_raw_head_partition_prevents_projection_fallback_and_tampering() { 3087 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3088 migrations::run_all_up(&exec).expect("migrations"); 3089 3090 let seller = FIXTURE_ALICE_PUBLIC_KEY_HEX; 3091 let d_tag = "AAAAAAAAAAAAAAAAAAAAAw"; 3092 let listing_addr = event_head_key(KIND_CLASSIFIED_LISTING, seller, d_tag); 3093 let product_count = || { 3094 trade_product::find_many( 3095 &exec, 3096 &ITradeProductFindMany { 3097 filter: Some(trade_product_listing_addr_filter(&listing_addr)), 3098 }, 3099 ) 3100 .expect("listing products") 3101 .results 3102 .len() 3103 }; 3104 3105 let active = listing_event(seller, 10, d_tag, "active", "Pasture Eggs"); 3106 assert_eq!( 3107 radroots_replica_ingest_event(&exec, &active).expect("operational ingest"), 3108 RadrootsReplicaIngestOutcome::Applied 3109 ); 3110 assert_eq!(product_count(), 1); 3111 3112 let focused = focused_listing_event(seller, 20, d_tag); 3113 assert_eq!( 3114 radroots_replica_ingest_event(&exec, &focused).expect("focused ingest"), 3115 RadrootsReplicaIngestOutcome::Excluded 3116 ); 3117 assert_eq!(product_count(), 0); 3118 3119 let invalid_focused = signed_listing_event( 3120 seller, 3121 25, 3122 vec![ 3123 vec!["d".to_string(), d_tag.to_string()], 3124 vec!["radroots:price_unit".to_string(), "lb".to_string()], 3125 ], 3126 "invalid focused listing".to_string(), 3127 ); 3128 assert_eq!( 3129 radroots_replica_ingest_event(&exec, &invalid_focused).expect("invalid focused ingest"), 3130 RadrootsReplicaIngestOutcome::Rejected 3131 ); 3132 assert_eq!(product_count(), 0); 3133 3134 let stale = listing_event(seller, 15, d_tag, "active", "Stale Eggs"); 3135 assert_eq!( 3136 radroots_replica_ingest_event(&exec, &stale).expect("stale ingest"), 3137 RadrootsReplicaIngestOutcome::Skipped 3138 ); 3139 assert_eq!(product_count(), 0); 3140 3141 let active = listing_event(seller, 30, d_tag, "active", "Market Eggs"); 3142 assert_eq!( 3143 radroots_replica_ingest_event(&exec, &active).expect("operational replacement"), 3144 RadrootsReplicaIngestOutcome::Applied 3145 ); 3146 assert_eq!(product_count(), 1); 3147 3148 let generic = generic_listing_event(seller, 40, d_tag); 3149 assert_eq!( 3150 radroots_replica_ingest_event(&exec, &generic).expect("generic ingest"), 3151 RadrootsReplicaIngestOutcome::Excluded 3152 ); 3153 assert_eq!(product_count(), 0); 3154 3155 let active = listing_event(seller, 50, d_tag, "active", "Market Eggs"); 3156 assert_eq!( 3157 radroots_replica_ingest_event(&exec, &active).expect("operational replacement"), 3158 RadrootsReplicaIngestOutcome::Applied 3159 ); 3160 let ambiguous = signed_listing_event( 3161 seller, 3162 60, 3163 vec![ 3164 vec!["d".to_string(), d_tag.to_string()], 3165 vec!["radroots:price_unit".to_string(), "lb".to_string()], 3166 vec!["radroots:primary_bin".to_string(), "bin-a".to_string()], 3167 ], 3168 "ambiguous".to_string(), 3169 ); 3170 assert_eq!( 3171 radroots_replica_ingest_event(&exec, &ambiguous).expect("ambiguous ingest"), 3172 RadrootsReplicaIngestOutcome::Rejected 3173 ); 3174 assert_eq!(product_count(), 0); 3175 3176 let active = listing_event(seller, 70, d_tag, "active", "Market Eggs"); 3177 assert_eq!( 3178 radroots_replica_ingest_event(&exec, &active).expect("operational replacement"), 3179 RadrootsReplicaIngestOutcome::Applied 3180 ); 3181 let malformed_operational = signed_listing_event( 3182 seller, 3183 80, 3184 vec![ 3185 vec!["d".to_string(), d_tag.to_string()], 3186 vec!["radroots:primary_bin".to_string()], 3187 ], 3188 "malformed".to_string(), 3189 ); 3190 assert_eq!( 3191 radroots_replica_ingest_event(&exec, &malformed_operational) 3192 .expect("malformed operational ingest"), 3193 RadrootsReplicaIngestOutcome::Rejected 3194 ); 3195 assert_eq!(product_count(), 0); 3196 3197 let active = listing_event(seller, 90, d_tag, "active", "Market Eggs"); 3198 assert_eq!( 3199 radroots_replica_ingest_event(&exec, &active).expect("operational replacement"), 3200 RadrootsReplicaIngestOutcome::Applied 3201 ); 3202 assert_eq!(product_count(), 1); 3203 3204 let signed_replacement = focused_listing_event(seller, 100, d_tag); 3205 assert!(matches!( 3206 radroots_replica_ingest_event_head(&exec, &signed_replacement), 3207 Err(RadrootsReplicaEventsError::InvalidData(ref message)) 3208 if message == "classified listing heads require profile-aware replica ingestion" 3209 )); 3210 assert_eq!(product_count(), 1); 3211 3212 let tampered = test_event_with_content(&signed_replacement, "tampered".to_string()); 3213 assert!(matches!( 3214 radroots_replica_ingest_event(&exec, &tampered), 3215 Err(RadrootsReplicaEventsError::Verification(_)) 3216 )); 3217 assert!(matches!( 3218 radroots_replica_ingest_event_head(&exec, &tampered), 3219 Err(RadrootsReplicaEventsError::Verification(_)) 3220 )); 3221 assert_eq!(product_count(), 1); 3222 3223 let state_before_replacement = nostr_event_head::find_one( 3224 &exec, 3225 &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { 3226 on: NostrEventHeadQueryBindValues::Key { 3227 key: listing_addr.clone(), 3228 }, 3229 }), 3230 ) 3231 .expect("event state") 3232 .result 3233 .expect("state row"); 3234 assert_eq!(state_before_replacement.last_event_id, active.id_hex()); 3235 3236 assert_eq!( 3237 radroots_replica_ingest_event(&exec, &signed_replacement) 3238 .expect("profile-aware focused replacement"), 3239 RadrootsReplicaIngestOutcome::Excluded 3240 ); 3241 assert_eq!(product_count(), 0); 3242 3243 let tied_operational = 3244 listing_event(seller, 110, d_tag, "active", "Equal-time Market Eggs"); 3245 assert_eq!( 3246 radroots_replica_ingest_event(&exec, &tied_operational) 3247 .expect("equal-time operational head"), 3248 RadrootsReplicaIngestOutcome::Applied 3249 ); 3250 assert_eq!(product_count(), 1); 3251 3252 let tied_focused = (0..256) 3253 .map(|nonce| { 3254 focused_listing_event_with_content( 3255 seller, 3256 110, 3257 d_tag, 3258 &format!("Equal-time focused replacement {nonce}"), 3259 ) 3260 }) 3261 .find(|candidate| candidate.id_hex() < tied_operational.id_hex()) 3262 .expect("deterministic lower-id focused candidate"); 3263 assert_eq!( 3264 radroots_replica_ingest_event(&exec, &tied_focused) 3265 .expect("lower-id focused replacement"), 3266 RadrootsReplicaIngestOutcome::Excluded 3267 ); 3268 assert_eq!(product_count(), 0); 3269 assert_eq!( 3270 radroots_replica_ingest_event(&exec, &tied_operational) 3271 .expect("higher-id equal-time operational replay"), 3272 RadrootsReplicaIngestOutcome::Skipped 3273 ); 3274 3275 let final_state = nostr_event_head::find_one( 3276 &exec, 3277 &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { 3278 on: NostrEventHeadQueryBindValues::Key { key: listing_addr }, 3279 }), 3280 ) 3281 .expect("final event state") 3282 .result 3283 .expect("final state row"); 3284 assert_eq!(final_state.last_event_id, tied_focused.id_hex()); 3285 } 3286 3287 #[test] 3288 fn upsert_location_none_paths_are_ok() { 3289 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3290 migrations::run_all_up(&exec).expect("migrations"); 3291 3292 let farm_row = farm::create( 3293 &exec, 3294 &IFarmFields { 3295 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 3296 pubkey: fixture_public_key(15), 3297 name: "farm-none".to_string(), 3298 about: None, 3299 website: None, 3300 picture: None, 3301 banner: None, 3302 location_primary: None, 3303 location_city: None, 3304 location_region: None, 3305 location_country: None, 3306 }, 3307 ) 3308 .expect("farm") 3309 .result; 3310 let plot_row = plot::create( 3311 &exec, 3312 &IPlotFields { 3313 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 3314 farm_id: farm_row.id.clone(), 3315 name: "plot-none".to_string(), 3316 about: None, 3317 location_primary: None, 3318 location_city: None, 3319 location_region: None, 3320 location_country: None, 3321 }, 3322 ) 3323 .expect("plot") 3324 .result; 3325 3326 upsert_plot_location(&exec, &plot_row.id, None, &FixedFactory).expect("plot none"); 3327 } 3328 3329 #[test] 3330 fn ingest_delete_error_paths_are_covered() { 3331 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3332 let (farm_id, _farm_pubkey, farm_d_tag, _plot_d_tag) = seed_rows(&exec); 3333 3334 let not_found_farm_tags = DeleteErrorExecutor { 3335 inner: &exec, 3336 table_name: "farm_tag", 3337 err: SqlError::NotFound("farm_tag".to_string()), 3338 }; 3339 assert!( 3340 upsert_farm_tags( 3341 ¬_found_farm_tags, 3342 &farm_id, 3343 Some(vec!["next".to_string()]) 3344 ) 3345 .is_ok() 3346 ); 3347 3348 let not_found_plot_tags = DeleteErrorExecutor { 3349 inner: &exec, 3350 table_name: "plot_tag", 3351 err: SqlError::NotFound("plot_tag".to_string()), 3352 }; 3353 let plot_id = plot::find_many(&exec, &IPlotFindMany { filter: None }) 3354 .expect("plots") 3355 .results[0] 3356 .id 3357 .clone(); 3358 assert!( 3359 upsert_plot_tags( 3360 ¬_found_plot_tags, 3361 &plot_id, 3362 Some(vec!["next".to_string()]) 3363 ) 3364 .is_ok() 3365 ); 3366 assert!( 3367 upsert_plot_tags( 3368 &exec, 3369 &plot_id, 3370 Some(vec!["next".to_string(), " ".to_string()]) 3371 ) 3372 .is_ok() 3373 ); 3374 3375 let not_found_plot_locations = DeleteErrorExecutor { 3376 inner: &exec, 3377 table_name: "plot_gcs_location", 3378 err: SqlError::NotFound("plot_gcs_location".to_string()), 3379 }; 3380 assert!( 3381 upsert_plot_location( 3382 ¬_found_plot_locations, 3383 &plot_id, 3384 Some(PlotLocation { 3385 primary: None, 3386 city: None, 3387 region: None, 3388 country: None, 3389 gcs: sample_gcs(1.1, 2.1, "s5"), 3390 }), 3391 &FixedFactory, 3392 ) 3393 .is_ok() 3394 ); 3395 3396 let members_list_set = 3397 farm_list_sets::farm_members_list_set(&farm_d_tag, vec![fixture_public_key(7)]) 3398 .expect("members"); 3399 assert!( 3400 upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &members_list_set).is_ok() 3401 ); 3402 let not_found_members = DeleteErrorExecutor { 3403 inner: &exec, 3404 table_name: "farm_member", 3405 err: SqlError::NotFound("farm_member".to_string()), 3406 }; 3407 let not_found_members_list_set = 3408 farm_list_sets::farm_members_list_set(&farm_d_tag, vec![fixture_public_key(10)]) 3409 .expect("not found members"); 3410 assert!( 3411 upsert_farm_members( 3412 ¬_found_members, 3413 &farm_id, 3414 ListSetRole::Members, 3415 ¬_found_members_list_set, 3416 ) 3417 .is_ok() 3418 ); 3419 assert!( 3420 upsert_farm_members( 3421 ¬_found_members, 3422 &farm_id, 3423 ListSetRole::Plots, 3424 ¬_found_members_list_set, 3425 ) 3426 .is_ok() 3427 ); 3428 3429 let member_claims = 3430 farm_list_sets::member_of_farms_list_set(vec![fixture_public_key(3)]).expect("claims"); 3431 assert!(upsert_member_claims(&exec, &fixture_public_key(6), &member_claims).is_ok()); 3432 let not_found_claims = DeleteErrorExecutor { 3433 inner: &exec, 3434 table_name: "farm_member_claim", 3435 err: SqlError::NotFound("farm_member_claim".to_string()), 3436 }; 3437 let not_found_member_claims = 3438 farm_list_sets::member_of_farms_list_set(vec![fixture_public_key(2)]) 3439 .expect("claims nf"); 3440 assert!( 3441 upsert_member_claims( 3442 ¬_found_claims, 3443 &fixture_public_key(6), 3444 ¬_found_member_claims 3445 ) 3446 .is_ok() 3447 ); 3448 assert!(not_found_claims.begin().is_ok()); 3449 assert!(not_found_claims.commit().is_ok()); 3450 let _ = not_found_claims.rollback(); 3451 assert!(not_found_claims.query_raw("SELECT 1", "[]").is_ok()); 3452 assert_eq!( 3453 not_found_claims 3454 .exec("DELETE FROM farm_member_claim WHERE id = 1", "[]") 3455 .expect_err("exec not found") 3456 .code(), 3457 "ERR_NOT_FOUND" 3458 ); 3459 let _ = not_found_claims.exec("DELETE FROM other_table WHERE id = 1", "[]"); 3460 3461 let internal_farm_tags = DeleteErrorExecutor { 3462 inner: &exec, 3463 table_name: "farm_tag", 3464 err: SqlError::Internal, 3465 }; 3466 assert!( 3467 upsert_farm_tags(&internal_farm_tags, &farm_id, Some(vec!["x".to_string()])).is_err() 3468 ); 3469 3470 let internal_plot_tags = DeleteErrorExecutor { 3471 inner: &exec, 3472 table_name: "plot_tag", 3473 err: SqlError::Internal, 3474 }; 3475 assert!( 3476 upsert_plot_tags(&internal_plot_tags, &plot_id, Some(vec!["x".to_string()])).is_err() 3477 ); 3478 3479 let internal_plot_locations = DeleteErrorExecutor { 3480 inner: &exec, 3481 table_name: "plot_gcs_location", 3482 err: SqlError::Internal, 3483 }; 3484 assert!( 3485 upsert_plot_location( 3486 &internal_plot_locations, 3487 &plot_id, 3488 Some(PlotLocation { 3489 primary: None, 3490 city: None, 3491 region: None, 3492 country: None, 3493 gcs: sample_gcs(2.1, 3.1, "s7"), 3494 }), 3495 &FixedFactory, 3496 ) 3497 .is_err() 3498 ); 3499 3500 let internal_members = DeleteErrorExecutor { 3501 inner: &exec, 3502 table_name: "farm_member", 3503 err: SqlError::Internal, 3504 }; 3505 assert!( 3506 upsert_farm_members( 3507 &internal_members, 3508 &farm_id, 3509 ListSetRole::Members, 3510 &members_list_set, 3511 ) 3512 .is_err() 3513 ); 3514 3515 let internal_claims = DeleteErrorExecutor { 3516 inner: &exec, 3517 table_name: "farm_member_claim", 3518 err: SqlError::Internal, 3519 }; 3520 assert!( 3521 upsert_member_claims(&internal_claims, &fixture_public_key(6), &member_claims).is_err() 3522 ); 3523 } 3524 3525 #[test] 3526 fn create_gcs_location_error_mapping_helpers_are_covered() { 3527 let point_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); 3528 let point_err = map_gcs_point_serialize_error(point_json_err); 3529 assert_eq!(point_err.to_string(), "replica_sync.data: gcs.point"); 3530 3531 let polygon_json_err = serde_json::from_str::<Value>("{").expect_err("invalid json"); 3532 let polygon_err = map_gcs_polygon_serialize_error(polygon_json_err); 3533 assert_eq!(polygon_err.to_string(), "replica_sync.data: gcs.polygon"); 3534 } 3535 3536 #[test] 3537 fn ingest_pass_executor_and_parse_edge_paths_are_covered() { 3538 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3539 migrations::run_all_up(&exec).expect("migrations"); 3540 let pass = PassExecutor { inner: &exec }; 3541 3542 let profile_pubkey = fixture_public_key(9); 3543 let farm_pubkey = fixture_public_key(15); 3544 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 3545 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 3546 3547 let profile = profile_event( 3548 500, 3549 &profile_pubkey, 3550 50, 3551 Some(ProfileType::Individual), 3552 "pass-profile", 3553 ); 3554 let signed_profile = sign_test_event(&profile); 3555 assert_eq!( 3556 radroots_replica_ingest_event_with_factory(&pass, &signed_profile, &FixedFactory) 3557 .expect("profile ingest"), 3558 RadrootsReplicaIngestOutcome::Applied 3559 ); 3560 assert_eq!( 3561 ingest_test_profile(&pass, &profile).expect("profile skip"), 3562 RadrootsReplicaIngestOutcome::Skipped 3563 ); 3564 3565 let farm = farm_event( 3566 501, 3567 &farm_pubkey, 3568 51, 3569 farm_d_tag, 3570 "pass-farm", 3571 Some(FarmPublicLocation { 3572 primary: "primary".to_string(), 3573 city: Some("city".to_string()), 3574 region: Some("region".to_string()), 3575 country: Some("country".to_string()), 3576 geohash: "9q8yy".to_string(), 3577 }), 3578 Some(vec!["coffee".to_string(), "coffee".to_string()]), 3579 ); 3580 assert_eq!( 3581 ingest_farm_event(&pass, &farm, &FixedFactory).expect("farm ingest"), 3582 RadrootsReplicaIngestOutcome::Applied 3583 ); 3584 3585 let plot = plot_event( 3586 502, 3587 &farm_pubkey, 3588 52, 3589 plot_d_tag, 3590 FarmRef { 3591 pubkey: farm_pubkey.clone(), 3592 d_tag: farm_d_tag.to_string(), 3593 }, 3594 "pass-plot", 3595 Some(PlotLocation { 3596 primary: Some("plot".to_string()), 3597 city: None, 3598 region: None, 3599 country: None, 3600 gcs: sample_gcs(11.0, 21.0, "s1"), 3601 }), 3602 Some(vec!["orchard".to_string()]), 3603 ); 3604 assert_eq!( 3605 ingest_plot_event(&pass, &plot, &FixedFactory).expect("plot ingest"), 3606 RadrootsReplicaIngestOutcome::Applied 3607 ); 3608 3609 let members = 3610 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(6)]) 3611 .expect("list"); 3612 let members_event = list_set_event(503, &farm_pubkey, 53, KIND_LIST_SET_GENERIC, &members); 3613 assert_eq!( 3614 ingest_list_set_event(&pass, &members_event).expect("members list set"), 3615 RadrootsReplicaIngestOutcome::Applied 3616 ); 3617 3618 let claims = farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]) 3619 .expect("claims list set"); 3620 let claims_event = list_set_event(504, &profile_pubkey, 54, KIND_LIST_SET_GENERIC, &claims); 3621 assert_eq!( 3622 ingest_list_set_event(&pass, &claims_event).expect("claims list set"), 3623 RadrootsReplicaIngestOutcome::Applied 3624 ); 3625 3626 let farm_row = find_farm_by_ref(&pass, &farm_pubkey, farm_d_tag).expect("farm row"); 3627 let mixed_member_entries = ListSet { 3628 d_tag: format!("farm:{farm_d_tag}:members"), 3629 content: String::new(), 3630 entries: vec![ 3631 ListEntry { 3632 tag: "p".to_string(), 3633 values: Vec::new(), 3634 }, 3635 ListEntry { 3636 tag: "p".to_string(), 3637 values: vec![fixture_public_key(6)], 3638 }, 3639 ], 3640 title: None, 3641 description: None, 3642 image: None, 3643 }; 3644 assert!( 3645 upsert_farm_members( 3646 &pass, 3647 &farm_row.id, 3648 ListSetRole::Members, 3649 &mixed_member_entries 3650 ) 3651 .is_ok() 3652 ); 3653 let mixed_claim_entries = ListSet { 3654 d_tag: "member_of.farms".to_string(), 3655 content: String::new(), 3656 entries: vec![ 3657 ListEntry { 3658 tag: "p".to_string(), 3659 values: Vec::new(), 3660 }, 3661 ListEntry { 3662 tag: "p".to_string(), 3663 values: vec![farm_pubkey.clone()], 3664 }, 3665 ], 3666 title: None, 3667 description: None, 3668 image: None, 3669 }; 3670 assert!(upsert_member_claims(&pass, &profile_pubkey, &mixed_claim_entries).is_ok()); 3671 assert!(pass.begin().is_ok()); 3672 assert!(pass.rollback().is_ok()); 3673 3674 assert!(parse_farm_list_set_d_tag("coop:AAAAAAAAAAAAAAAAAAAAAA:members").is_none()); 3675 assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA").is_none()); 3676 assert!(parse_farm_list_set_d_tag("farm:AAAAAAAAAAAAAAAAAAAAAA:members").is_some()); 3677 } 3678 3679 #[test] 3680 fn create_gcs_location_success_path_is_covered() { 3681 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3682 migrations::run_all_up(&exec).expect("migrations"); 3683 3684 let id = create_gcs_location(&exec, sample_gcs(1.0, 2.0, "s0"), &FixedFactory) 3685 .expect("create gcs"); 3686 assert!(!id.trim().is_empty()); 3687 } 3688 3689 #[test] 3690 fn ingest_default_factory_wrapper_paths_are_covered() { 3691 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3692 migrations::run_all_up(&exec).expect("migrations"); 3693 3694 let farm_pubkey = fixture_public_key(15); 3695 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 3696 let farm_create = farm_event( 3697 600, 3698 &farm_pubkey, 3699 60, 3700 farm_d_tag, 3701 "wrapper-farm", 3702 Some(FarmPublicLocation { 3703 primary: "primary".to_string(), 3704 city: Some("city".to_string()), 3705 region: None, 3706 country: None, 3707 geohash: "9q8yy".to_string(), 3708 }), 3709 Some(vec!["coffee".to_string()]), 3710 ); 3711 assert_eq!( 3712 radroots_replica_ingest_event(&exec, &farm_create).expect("farm create"), 3713 RadrootsReplicaIngestOutcome::Applied 3714 ); 3715 3716 let farm_update = farm_event( 3717 601, 3718 &farm_pubkey, 3719 61, 3720 farm_d_tag, 3721 "wrapper-farm-updated", 3722 None, 3723 Some(vec!["market".to_string()]), 3724 ); 3725 assert_eq!( 3726 radroots_replica_ingest_event(&exec, &farm_update).expect("farm update"), 3727 RadrootsReplicaIngestOutcome::Applied 3728 ); 3729 3730 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 3731 let plot_create = plot_event( 3732 602, 3733 &farm_pubkey, 3734 62, 3735 plot_d_tag, 3736 FarmRef { 3737 pubkey: farm_pubkey.clone(), 3738 d_tag: farm_d_tag.to_string(), 3739 }, 3740 "wrapper-plot", 3741 Some(PlotLocation { 3742 primary: Some("plot-primary".to_string()), 3743 city: None, 3744 region: None, 3745 country: None, 3746 gcs: sample_gcs(11.0, 21.0, "s1"), 3747 }), 3748 Some(vec!["orchard".to_string()]), 3749 ); 3750 assert_eq!( 3751 radroots_replica_ingest_event(&exec, &plot_create).expect("plot create"), 3752 RadrootsReplicaIngestOutcome::Applied 3753 ); 3754 3755 let plot_update = plot_event( 3756 603, 3757 &farm_pubkey, 3758 63, 3759 plot_d_tag, 3760 FarmRef { 3761 pubkey: farm_pubkey.clone(), 3762 d_tag: farm_d_tag.to_string(), 3763 }, 3764 "wrapper-plot-updated", 3765 None, 3766 Some(vec!["updated".to_string()]), 3767 ); 3768 assert_eq!( 3769 radroots_replica_ingest_event(&exec, &plot_update).expect("plot update"), 3770 RadrootsReplicaIngestOutcome::Applied 3771 ); 3772 } 3773 3774 #[test] 3775 fn ingest_txn_executor_instantiation_error_paths_are_covered() { 3776 let pass_db = SqlxSqliteExecutor::open_memory().expect("db"); 3777 migrations::run_all_up(&pass_db).expect("migrations"); 3778 let pass_txn = TxnExecutor { 3779 inner: Some(&pass_db), 3780 begin_err: None, 3781 commit_err: None, 3782 rollback_count: Arc::new(AtomicUsize::new(0)), 3783 }; 3784 3785 let profile_pubkey = fixture_public_key(9); 3786 let profile_event_row = profile_event( 3787 700, 3788 &profile_pubkey, 3789 70, 3790 Some(ProfileType::Individual), 3791 "txn-profile", 3792 ); 3793 assert_eq!( 3794 ingest_test_profile(&pass_txn, &profile_event_row).expect("txn profile"), 3795 RadrootsReplicaIngestOutcome::Applied 3796 ); 3797 let signed_profile_event_row = sign_test_event(&profile_event_row); 3798 let profile_decision = 3799 event_head_decision(&pass_txn, &signed_profile_event_row).expect("profile decision"); 3800 assert!(!profile_decision.apply); 3801 assert!(radroots_replica_ingest_event_head(&pass_txn, &signed_profile_event_row).is_ok()); 3802 assert_eq!( 3803 radroots_replica_ingest_event_with_factory( 3804 &pass_txn, 3805 &signed_profile_event_row, 3806 &FixedFactory 3807 ) 3808 .expect("txn wrapper"), 3809 RadrootsReplicaIngestOutcome::Skipped 3810 ); 3811 3812 let farm_pubkey = fixture_public_key(15); 3813 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 3814 let farm_event_row = farm_event( 3815 701, 3816 &farm_pubkey, 3817 71, 3818 farm_d_tag, 3819 "txn-farm", 3820 Some(FarmPublicLocation { 3821 primary: "primary".to_string(), 3822 city: Some("city".to_string()), 3823 region: None, 3824 country: None, 3825 geohash: "9q8yy".to_string(), 3826 }), 3827 Some(vec!["coffee".to_string()]), 3828 ); 3829 assert_eq!( 3830 ingest_farm_event(&pass_txn, &farm_event_row, &FixedFactory).expect("txn farm"), 3831 RadrootsReplicaIngestOutcome::Applied 3832 ); 3833 3834 let plot_event_row = plot_event( 3835 702, 3836 &farm_pubkey, 3837 72, 3838 "AAAAAAAAAAAAAAAAAAAAAQ", 3839 FarmRef { 3840 pubkey: farm_pubkey.clone(), 3841 d_tag: farm_d_tag.to_string(), 3842 }, 3843 "txn-plot", 3844 Some(PlotLocation { 3845 primary: Some("primary".to_string()), 3846 city: None, 3847 region: None, 3848 country: None, 3849 gcs: sample_gcs(13.0, 23.0, "s3"), 3850 }), 3851 Some(vec!["orchard".to_string()]), 3852 ); 3853 assert_eq!( 3854 ingest_plot_event(&pass_txn, &plot_event_row, &FixedFactory).expect("txn plot"), 3855 RadrootsReplicaIngestOutcome::Applied 3856 ); 3857 3858 let farm_row = find_farm_by_ref(&pass_txn, &farm_pubkey, farm_d_tag).expect("find farm"); 3859 assert!(upsert_farm_tags(&pass_txn, &farm_row.id, Some(vec!["x".to_string()])).is_ok()); 3860 let plot_id = plot::find_many(&pass_db, &IPlotFindMany { filter: None }) 3861 .expect("plots") 3862 .results[0] 3863 .id 3864 .clone(); 3865 assert!(upsert_plot_tags(&pass_txn, &plot_id, Some(vec!["y".to_string()])).is_ok()); 3866 assert!(clear_farm_locations(&pass_txn, &farm_row.id).is_ok()); 3867 assert!(clear_plot_locations(&pass_txn, &plot_id).is_ok()); 3868 assert!( 3869 create_gcs_location(&pass_txn, sample_gcs(14.0, 24.0, "s4"), &FixedFactory).is_ok() 3870 ); 3871 assert!( 3872 upsert_plot_location( 3873 &pass_txn, 3874 &plot_id, 3875 Some(PlotLocation { 3876 primary: Some("primary".to_string()), 3877 city: None, 3878 region: None, 3879 country: None, 3880 gcs: sample_gcs(16.0, 26.0, "s6"), 3881 }), 3882 &FixedFactory, 3883 ) 3884 .is_ok() 3885 ); 3886 let members_list = 3887 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(6)]) 3888 .expect("list"); 3889 assert!( 3890 upsert_farm_members(&pass_txn, &farm_row.id, ListSetRole::Members, &members_list) 3891 .is_ok() 3892 ); 3893 let member_of_list = 3894 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 3895 assert!(upsert_member_claims(&pass_txn, &fixture_public_key(6), &member_of_list).is_ok()); 3896 3897 let rollback_count = Arc::new(AtomicUsize::new(0)); 3898 let txn = TxnExecutor { 3899 inner: None, 3900 begin_err: None, 3901 commit_err: None, 3902 rollback_count, 3903 }; 3904 3905 assert!(ingest_test_profile(&txn, &profile_event_row).is_err()); 3906 assert!(event_head_decision(&txn, &profile_event_row).is_err()); 3907 assert!(radroots_replica_ingest_event_head(&txn, &profile_event_row).is_err()); 3908 let signed_profile_event_row = sign_test_event(&profile_event_row); 3909 assert!( 3910 radroots_replica_ingest_event_with_factory( 3911 &txn, 3912 &signed_profile_event_row, 3913 &FixedFactory 3914 ) 3915 .is_err() 3916 ); 3917 3918 assert!(ingest_farm_event(&txn, &farm_event_row, &FixedFactory).is_err()); 3919 3920 assert!(ingest_plot_event(&txn, &plot_event_row, &FixedFactory).is_err()); 3921 3922 assert!(find_farm_by_ref(&txn, &farm_pubkey, farm_d_tag).is_err()); 3923 assert!(upsert_farm_tags(&txn, "farm-id", Some(vec!["x".to_string()])).is_err()); 3924 assert!(upsert_plot_tags(&txn, "plot-id", Some(vec!["y".to_string()])).is_err()); 3925 assert!(clear_farm_locations(&txn, "farm-id").is_err()); 3926 assert!(clear_plot_locations(&txn, "plot-id").is_err()); 3927 assert!(create_gcs_location(&txn, sample_gcs(14.0, 24.0, "s4"), &FixedFactory).is_err()); 3928 assert!( 3929 upsert_plot_location( 3930 &txn, 3931 "plot-id", 3932 Some(PlotLocation { 3933 primary: Some("primary".to_string()), 3934 city: None, 3935 region: None, 3936 country: None, 3937 gcs: sample_gcs(16.0, 26.0, "s6"), 3938 }), 3939 &FixedFactory, 3940 ) 3941 .is_err() 3942 ); 3943 assert!(upsert_farm_members(&txn, "farm-id", ListSetRole::Members, &members_list).is_err()); 3944 assert!(upsert_member_claims(&txn, &fixture_public_key(6), &member_of_list).is_err()); 3945 } 3946 3947 #[test] 3948 fn ingest_sqlite_queryfail_and_parser_edges_are_covered() { 3949 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 3950 migrations::run_all_up(&exec).expect("migrations"); 3951 let pass_through = QueryFailExecutor { 3952 inner: &exec, 3953 needle: "__missing__", 3954 err: SqlError::Internal, 3955 }; 3956 assert!(pass_through.query_raw("select 1", "[]").is_ok()); 3957 assert!( 3958 pass_through 3959 .exec( 3960 "create table if not exists coverage_probe (id integer)", 3961 "[]" 3962 ) 3963 .is_ok() 3964 ); 3965 let _ = pass_through.begin(); 3966 let _ = pass_through.rollback(); 3967 let _ = pass_through.commit(); 3968 3969 let farm_pubkey = fixture_public_key(15); 3970 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 3971 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 3972 let profile_pubkey = fixture_public_key(9); 3973 3974 let profile = profile_event( 3975 800, 3976 &profile_pubkey, 3977 80, 3978 Some(ProfileType::Individual), 3979 "profile-base", 3980 ); 3981 let profile_bad_content = test_event_with_content(&profile, "{".to_string()); 3982 assert!(ingest_test_profile(&exec, &profile_bad_content).is_err()); 3983 3984 let profile_query_fail = QueryFailExecutor { 3985 inner: &exec, 3986 needle: "nostr_profile", 3987 err: SqlError::Internal, 3988 }; 3989 assert!(ingest_test_profile(&profile_query_fail, &profile).is_err()); 3990 3991 assert_eq!( 3992 ingest_test_profile(&exec, &profile).expect("profile seed"), 3993 RadrootsReplicaIngestOutcome::Applied 3994 ); 3995 let profile_update = profile_event( 3996 801, 3997 &profile_pubkey, 3998 81, 3999 Some(ProfileType::Individual), 4000 "profile-update", 4001 ); 4002 let profile_update_fail = QueryFailExecutor { 4003 inner: &exec, 4004 needle: "update nostr_profile", 4005 err: SqlError::Internal, 4006 }; 4007 assert!(ingest_test_profile(&profile_update_fail, &profile_update).is_err()); 4008 4009 let profile_create_fail = QueryFailExecutor { 4010 inner: &exec, 4011 needle: "insert into nostr_profile", 4012 err: SqlError::Internal, 4013 }; 4014 let profile_new = profile_event( 4015 802, 4016 &fixture_public_key(7), 4017 82, 4018 Some(ProfileType::Individual), 4019 "profile-new", 4020 ); 4021 assert!(ingest_test_profile(&profile_create_fail, &profile_new).is_err()); 4022 4023 let profile_state_fail = QueryFailExecutor { 4024 inner: &exec, 4025 needle: "nostr_event_head", 4026 err: SqlError::Internal, 4027 }; 4028 let profile_state_event = profile_event( 4029 803, 4030 &fixture_public_key(12), 4031 83, 4032 Some(ProfileType::Individual), 4033 "profile-state", 4034 ); 4035 assert!(ingest_test_profile(&profile_state_fail, &profile_state_event).is_err()); 4036 4037 let farm_seed = farm_event( 4038 810, 4039 &farm_pubkey, 4040 90, 4041 farm_d_tag, 4042 "farm-seed", 4043 Some(FarmPublicLocation { 4044 primary: "primary".to_string(), 4045 city: Some("city".to_string()), 4046 region: Some("region".to_string()), 4047 country: Some("country".to_string()), 4048 geohash: "9q8yy".to_string(), 4049 }), 4050 Some(vec!["seed".to_string()]), 4051 ); 4052 assert_eq!( 4053 ingest_farm_event(&exec, &farm_seed, &FixedFactory).expect("farm seed"), 4054 RadrootsReplicaIngestOutcome::Applied 4055 ); 4056 4057 let farm_bad_content = test_event_with_content(&farm_seed, "{".to_string()); 4058 assert!(ingest_farm_event(&exec, &farm_bad_content, &FixedFactory).is_err()); 4059 4060 let farm_query_fail = QueryFailExecutor { 4061 inner: &exec, 4062 needle: "from farm", 4063 err: SqlError::Internal, 4064 }; 4065 let farm_query_event = farm_event( 4066 811, 4067 &fixture_public_key(10), 4068 91, 4069 farm_d_tag, 4070 "farm-query", 4071 None, 4072 None, 4073 ); 4074 assert!(ingest_farm_event(&farm_query_fail, &farm_query_event, &FixedFactory).is_err()); 4075 4076 let farm_update_fail = QueryFailExecutor { 4077 inner: &exec, 4078 needle: "update farm", 4079 err: SqlError::Internal, 4080 }; 4081 let farm_update = farm_event( 4082 812, 4083 &farm_pubkey, 4084 92, 4085 farm_d_tag, 4086 "farm-update", 4087 None, 4088 Some(vec!["u".to_string()]), 4089 ); 4090 assert!(ingest_farm_event(&farm_update_fail, &farm_update, &FixedFactory).is_err()); 4091 4092 let farm_create_fail = QueryFailExecutor { 4093 inner: &exec, 4094 needle: "insert into farm", 4095 err: SqlError::Internal, 4096 }; 4097 let farm_create = farm_event( 4098 813, 4099 &fixture_public_key(12), 4100 93, 4101 farm_d_tag, 4102 "farm-create", 4103 None, 4104 None, 4105 ); 4106 assert!(ingest_farm_event(&farm_create_fail, &farm_create, &FixedFactory).is_err()); 4107 4108 let farm_tag_fail = QueryFailExecutor { 4109 inner: &exec, 4110 needle: "farm_tag", 4111 err: SqlError::Internal, 4112 }; 4113 let farm_tag_event = farm_event( 4114 814, 4115 &fixture_public_key(13), 4116 94, 4117 farm_d_tag, 4118 "farm-tag", 4119 None, 4120 Some(vec!["coffee".to_string()]), 4121 ); 4122 assert!(ingest_farm_event(&farm_tag_fail, &farm_tag_event, &FixedFactory).is_err()); 4123 4124 let farm_gcs_fail = QueryFailExecutor { 4125 inner: &exec, 4126 needle: "gcs_location", 4127 err: SqlError::Internal, 4128 }; 4129 let farm_gcs_event = farm_event( 4130 815, 4131 &fixture_public_key(16), 4132 95, 4133 farm_d_tag, 4134 "farm-gcs", 4135 Some(FarmPublicLocation { 4136 primary: "primary".to_string(), 4137 city: Some("city".to_string()), 4138 region: None, 4139 country: None, 4140 geohash: "9q8yy".to_string(), 4141 }), 4142 None, 4143 ); 4144 assert_eq!( 4145 ingest_farm_event(&farm_gcs_fail, &farm_gcs_event, &FixedFactory).expect("farm gcs"), 4146 RadrootsReplicaIngestOutcome::Applied 4147 ); 4148 4149 let farm_rel_fail = QueryFailExecutor { 4150 inner: &exec, 4151 needle: "farm_gcs_location", 4152 err: SqlError::Internal, 4153 }; 4154 let farm_rel_event = farm_event( 4155 816, 4156 &fixture_public_key(11), 4157 96, 4158 farm_d_tag, 4159 "farm-rel", 4160 Some(FarmPublicLocation { 4161 primary: "primary".to_string(), 4162 city: Some("city".to_string()), 4163 region: None, 4164 country: None, 4165 geohash: "9q8yy".to_string(), 4166 }), 4167 None, 4168 ); 4169 assert_eq!( 4170 ingest_farm_event(&farm_rel_fail, &farm_rel_event, &FixedFactory).expect("farm rel"), 4171 RadrootsReplicaIngestOutcome::Applied 4172 ); 4173 4174 let farm_state_fail = QueryFailExecutor { 4175 inner: &exec, 4176 needle: "nostr_event_head", 4177 err: SqlError::Internal, 4178 }; 4179 let farm_state_event = farm_event( 4180 817, 4181 &fixture_public_key(16), 4182 97, 4183 farm_d_tag, 4184 "farm-state", 4185 None, 4186 None, 4187 ); 4188 assert!(ingest_farm_event(&farm_state_fail, &farm_state_event, &FixedFactory).is_err()); 4189 4190 let farm_public_location = farm_event( 4191 818, 4192 &fixture_public_key(1), 4193 98, 4194 farm_d_tag, 4195 "farm-public-location", 4196 Some(FarmPublicLocation { 4197 primary: "primary".to_string(), 4198 city: Some("city".to_string()), 4199 region: None, 4200 country: None, 4201 geohash: "9q8yy".to_string(), 4202 }), 4203 None, 4204 ); 4205 assert_eq!( 4206 ingest_farm_event(&exec, &farm_public_location, &FixedFactory) 4207 .expect("farm public location"), 4208 RadrootsReplicaIngestOutcome::Applied 4209 ); 4210 4211 let farm_public_locality = farm_event( 4212 819, 4213 &fixture_public_key(2), 4214 99, 4215 farm_d_tag, 4216 "farm-public-locality", 4217 Some(FarmPublicLocation { 4218 primary: "primary".to_string(), 4219 city: Some("city".to_string()), 4220 region: None, 4221 country: None, 4222 geohash: "9q8yy".to_string(), 4223 }), 4224 None, 4225 ); 4226 assert_eq!( 4227 ingest_farm_event(&exec, &farm_public_locality, &FixedFactory) 4228 .expect("farm public locality"), 4229 RadrootsReplicaIngestOutcome::Applied 4230 ); 4231 4232 let plot_seed = plot_event( 4233 820, 4234 &farm_pubkey, 4235 100, 4236 plot_d_tag, 4237 FarmRef { 4238 pubkey: farm_pubkey.clone(), 4239 d_tag: farm_d_tag.to_string(), 4240 }, 4241 "plot-seed", 4242 Some(PlotLocation { 4243 primary: Some("primary".to_string()), 4244 city: None, 4245 region: None, 4246 country: None, 4247 gcs: sample_gcs(15.0, 25.0, "s5"), 4248 }), 4249 Some(vec!["orchard".to_string()]), 4250 ); 4251 assert_eq!( 4252 ingest_plot_event(&exec, &plot_seed, &FixedFactory).expect("plot seed"), 4253 RadrootsReplicaIngestOutcome::Applied 4254 ); 4255 4256 let plot_bad_content = test_event_with_content(&plot_seed, "{".to_string()); 4257 assert!(ingest_plot_event(&exec, &plot_bad_content, &FixedFactory).is_err()); 4258 4259 let plot_query_fail = QueryFailExecutor { 4260 inner: &exec, 4261 needle: "from plot", 4262 err: SqlError::Internal, 4263 }; 4264 let plot_query = plot_event( 4265 821, 4266 &farm_pubkey, 4267 101, 4268 plot_d_tag, 4269 FarmRef { 4270 pubkey: farm_pubkey.clone(), 4271 d_tag: farm_d_tag.to_string(), 4272 }, 4273 "plot-query", 4274 None, 4275 None, 4276 ); 4277 assert!(ingest_plot_event(&plot_query_fail, &plot_query, &FixedFactory).is_err()); 4278 4279 let plot_update_fail = QueryFailExecutor { 4280 inner: &exec, 4281 needle: "update plot", 4282 err: SqlError::Internal, 4283 }; 4284 let plot_update = plot_event( 4285 822, 4286 &farm_pubkey, 4287 102, 4288 plot_d_tag, 4289 FarmRef { 4290 pubkey: farm_pubkey.clone(), 4291 d_tag: farm_d_tag.to_string(), 4292 }, 4293 "plot-update", 4294 None, 4295 Some(vec!["u".to_string()]), 4296 ); 4297 assert!(ingest_plot_event(&plot_update_fail, &plot_update, &FixedFactory).is_err()); 4298 4299 let plot_create_fail = QueryFailExecutor { 4300 inner: &exec, 4301 needle: "insert into plot", 4302 err: SqlError::Internal, 4303 }; 4304 let plot_create = plot_event( 4305 823, 4306 &farm_pubkey, 4307 103, 4308 "AAAAAAAAAAAAAAAAAAAAAg", 4309 FarmRef { 4310 pubkey: farm_pubkey.clone(), 4311 d_tag: farm_d_tag.to_string(), 4312 }, 4313 "plot-create", 4314 None, 4315 None, 4316 ); 4317 assert!(ingest_plot_event(&plot_create_fail, &plot_create, &FixedFactory).is_err()); 4318 4319 let plot_tag_fail = QueryFailExecutor { 4320 inner: &exec, 4321 needle: "plot_tag", 4322 err: SqlError::Internal, 4323 }; 4324 let plot_tag_event = plot_event( 4325 824, 4326 &farm_pubkey, 4327 104, 4328 "AAAAAAAAAAAAAAAAAAAAAw", 4329 FarmRef { 4330 pubkey: farm_pubkey.clone(), 4331 d_tag: farm_d_tag.to_string(), 4332 }, 4333 "plot-tag", 4334 None, 4335 Some(vec!["tag".to_string()]), 4336 ); 4337 assert!(ingest_plot_event(&plot_tag_fail, &plot_tag_event, &FixedFactory).is_err()); 4338 4339 let plot_gcs_fail = QueryFailExecutor { 4340 inner: &exec, 4341 needle: "gcs_location", 4342 err: SqlError::Internal, 4343 }; 4344 let plot_gcs_event = plot_event( 4345 825, 4346 &farm_pubkey, 4347 105, 4348 "AAAAAAAAAAAAAAAAAAAAAw", 4349 FarmRef { 4350 pubkey: farm_pubkey.clone(), 4351 d_tag: farm_d_tag.to_string(), 4352 }, 4353 "plot-gcs", 4354 Some(PlotLocation { 4355 primary: Some("primary".to_string()), 4356 city: None, 4357 region: None, 4358 country: None, 4359 gcs: sample_gcs(16.0, 26.0, "s6"), 4360 }), 4361 None, 4362 ); 4363 assert!(ingest_plot_event(&plot_gcs_fail, &plot_gcs_event, &FixedFactory).is_err()); 4364 4365 let plot_rel_fail = QueryFailExecutor { 4366 inner: &exec, 4367 needle: "plot_gcs_location", 4368 err: SqlError::Internal, 4369 }; 4370 let plot_rel_event = plot_event( 4371 826, 4372 &farm_pubkey, 4373 106, 4374 "AAAAAAAAAAAAAAAAAAAAAw", 4375 FarmRef { 4376 pubkey: farm_pubkey.clone(), 4377 d_tag: farm_d_tag.to_string(), 4378 }, 4379 "plot-rel", 4380 Some(PlotLocation { 4381 primary: Some("primary".to_string()), 4382 city: None, 4383 region: None, 4384 country: None, 4385 gcs: sample_gcs(17.0, 27.0, "s7"), 4386 }), 4387 None, 4388 ); 4389 assert!(ingest_plot_event(&plot_rel_fail, &plot_rel_event, &FixedFactory).is_err()); 4390 4391 let plot_state_fail = QueryFailExecutor { 4392 inner: &exec, 4393 needle: "nostr_event_head", 4394 err: SqlError::Internal, 4395 }; 4396 let plot_state_event = plot_event( 4397 827, 4398 &farm_pubkey, 4399 107, 4400 "AAAAAAAAAAAAAAAAAAAAAw", 4401 FarmRef { 4402 pubkey: farm_pubkey.clone(), 4403 d_tag: farm_d_tag.to_string(), 4404 }, 4405 "plot-state", 4406 None, 4407 None, 4408 ); 4409 assert!(ingest_plot_event(&plot_state_fail, &plot_state_event, &FixedFactory).is_err()); 4410 4411 let list_decode_fail = 4412 profile_event(830, &farm_pubkey, 108, Some(ProfileType::Farm), "unused"); 4413 let list_decode_fail = test_event_with_parts( 4414 &list_decode_fail, 4415 KIND_LIST_SET_GENERIC, 4416 Vec::new(), 4417 "{".to_string(), 4418 ); 4419 assert!(ingest_list_set_event(&exec, &list_decode_fail).is_err()); 4420 4421 let members_list = 4422 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(6)]) 4423 .expect("members list"); 4424 let member_event = 4425 list_set_event(831, &farm_pubkey, 109, KIND_LIST_SET_GENERIC, &members_list); 4426 let list_decision_fail = QueryFailExecutor { 4427 inner: &exec, 4428 needle: "nostr_event_head", 4429 err: SqlError::Internal, 4430 }; 4431 assert!(ingest_list_set_event(&list_decision_fail, &member_event).is_err()); 4432 4433 let member_of = 4434 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member-of"); 4435 let member_of_event = list_set_event( 4436 832, 4437 &fixture_public_key(6), 4438 110, 4439 KIND_LIST_SET_GENERIC, 4440 &member_of, 4441 ); 4442 let claims_fail = QueryFailExecutor { 4443 inner: &exec, 4444 needle: "farm_member_claim", 4445 err: SqlError::Internal, 4446 }; 4447 assert!(ingest_list_set_event(&claims_fail, &member_of_event).is_err()); 4448 4449 let claims_state_fail = QueryFailExecutor { 4450 inner: &exec, 4451 needle: "nostr_event_head", 4452 err: SqlError::Internal, 4453 }; 4454 assert!(ingest_list_set_event(&claims_state_fail, &member_of_event).is_err()); 4455 4456 let plots_list = farm_list_sets::farm_plots_list_set( 4457 farm_d_tag, 4458 &farm_pubkey, 4459 vec![plot_d_tag.to_string()], 4460 ) 4461 .expect("plots list"); 4462 let plots_event = 4463 list_set_event(833, &farm_pubkey, 111, KIND_LIST_SET_GENERIC, &plots_list); 4464 let plots_state_fail = QueryFailExecutor { 4465 inner: &exec, 4466 needle: "nostr_event_head", 4467 err: SqlError::Internal, 4468 }; 4469 assert!(ingest_list_set_event(&plots_state_fail, &plots_event).is_err()); 4470 4471 let missing_farm_members = 4472 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(7)]) 4473 .expect("list"); 4474 let missing_farm_event = list_set_event( 4475 834, 4476 &fixture_public_key(3), 4477 112, 4478 KIND_LIST_SET_GENERIC, 4479 &missing_farm_members, 4480 ); 4481 assert!(ingest_list_set_event(&exec, &missing_farm_event).is_err()); 4482 4483 let members_create_fail = QueryFailExecutor { 4484 inner: &exec, 4485 needle: "farm_member", 4486 err: SqlError::Internal, 4487 }; 4488 assert!(ingest_list_set_event(&members_create_fail, &member_event).is_err()); 4489 4490 let members_state_fail = QueryFailExecutor { 4491 inner: &exec, 4492 needle: "nostr_event_head", 4493 err: SqlError::Internal, 4494 }; 4495 assert!(ingest_list_set_event(&members_state_fail, &member_event).is_err()); 4496 4497 assert!(parse_farm_list_set_d_tag("").is_none()); 4498 assert!(parse_farm_list_set_d_tag("farm").is_none()); 4499 4500 let state_create_fail = QueryFailExecutor { 4501 inner: &exec, 4502 needle: "nostr_event_head", 4503 err: SqlError::Internal, 4504 }; 4505 assert!(radroots_replica_ingest_event_head(&state_create_fail, &profile).is_err()); 4506 4507 radroots_replica_ingest_event_head(&exec, &profile).expect("seed state"); 4508 let state_update_fail = QueryFailExecutor { 4509 inner: &exec, 4510 needle: "update nostr_event_head", 4511 err: SqlError::Internal, 4512 }; 4513 let profile_update = profile_event( 4514 808, 4515 &fixture_public_key(9), 4516 101, 4517 Some(ProfileType::Individual), 4518 "profile-update-error", 4519 ); 4520 assert!(radroots_replica_ingest_event_head(&state_update_fail, &profile_update).is_err()); 4521 } 4522 4523 #[test] 4524 fn ingest_insert_and_state_error_branches_are_covered() { 4525 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 4526 let (farm_id, farm_pubkey, farm_d_tag, plot_d_tag) = seed_rows(&exec); 4527 4528 let profile = profile_event( 4529 900, 4530 &fixture_public_key(14), 4531 120, 4532 Some(ProfileType::Individual), 4533 "profile-state-insert", 4534 ); 4535 let state_insert_fail = QueryFailExecutor { 4536 inner: &exec, 4537 needle: "insert into nostr_event_head", 4538 err: SqlError::Internal, 4539 }; 4540 assert!(ingest_test_profile(&state_insert_fail, &profile).is_err()); 4541 4542 let farm_state = farm_event( 4543 901, 4544 &fixture_public_key(10), 4545 121, 4546 "AAAAAAAAAAAAAAAAAAAAAQ", 4547 "farm-state-insert", 4548 None, 4549 None, 4550 ); 4551 assert!(ingest_farm_event(&state_insert_fail, &farm_state, &FixedFactory).is_err()); 4552 4553 let plot_state = plot_event( 4554 902, 4555 &farm_pubkey, 4556 122, 4557 "AAAAAAAAAAAAAAAAAAAAAg", 4558 FarmRef { 4559 pubkey: farm_pubkey.clone(), 4560 d_tag: farm_d_tag.clone(), 4561 }, 4562 "plot-state-insert", 4563 None, 4564 None, 4565 ); 4566 assert!(ingest_plot_event(&state_insert_fail, &plot_state, &FixedFactory).is_err()); 4567 4568 let members_set = 4569 farm_list_sets::farm_members_list_set(&farm_d_tag, vec![fixture_public_key(7)]) 4570 .expect("members"); 4571 let members_event = 4572 list_set_event(903, &farm_pubkey, 123, KIND_LIST_SET_GENERIC, &members_set); 4573 assert!(ingest_list_set_event(&state_insert_fail, &members_event).is_err()); 4574 4575 let plots_set = farm_list_sets::farm_plots_list_set( 4576 &farm_d_tag, 4577 &farm_pubkey, 4578 vec![plot_d_tag.clone()], 4579 ) 4580 .expect("plots"); 4581 let plots_event = list_set_event(904, &farm_pubkey, 124, KIND_LIST_SET_GENERIC, &plots_set); 4582 assert!(ingest_list_set_event(&state_insert_fail, &plots_event).is_err()); 4583 4584 let member_of_set = 4585 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 4586 let member_of_event = list_set_event( 4587 905, 4588 &fixture_public_key(7), 4589 125, 4590 KIND_LIST_SET_GENERIC, 4591 &member_of_set, 4592 ); 4593 assert!(ingest_list_set_event(&state_insert_fail, &member_of_event).is_err()); 4594 4595 let state_insert_only_fail = QueryFailExecutor { 4596 inner: &exec, 4597 needle: "insert into nostr_event_head", 4598 err: SqlError::Internal, 4599 }; 4600 assert!(radroots_replica_ingest_event_head(&state_insert_only_fail, &profile).is_err()); 4601 4602 crate::event_head::event_content_hash_fail_next(); 4603 assert!(event_head_decision(&exec, &profile).is_err()); 4604 4605 let farm_tag_insert_fail = QueryFailExecutor { 4606 inner: &exec, 4607 needle: "insert into farm_tag", 4608 err: SqlError::Internal, 4609 }; 4610 assert!( 4611 upsert_farm_tags( 4612 &farm_tag_insert_fail, 4613 &farm_id, 4614 Some(vec!["delta".to_string()]) 4615 ) 4616 .is_err() 4617 ); 4618 4619 let plot_id = plot::find_many(&exec, &IPlotFindMany { filter: None }) 4620 .expect("plots") 4621 .results[0] 4622 .id 4623 .clone(); 4624 let plot_tag_insert_fail = QueryFailExecutor { 4625 inner: &exec, 4626 needle: "insert into plot_tag", 4627 err: SqlError::Internal, 4628 }; 4629 assert!( 4630 upsert_plot_tags( 4631 &plot_tag_insert_fail, 4632 &plot_id, 4633 Some(vec!["epsilon".to_string()]) 4634 ) 4635 .is_err() 4636 ); 4637 4638 let plot_gcs_insert_fail = QueryFailExecutor { 4639 inner: &exec, 4640 needle: "insert into gcs_location", 4641 err: SqlError::Internal, 4642 }; 4643 assert!( 4644 upsert_plot_location( 4645 &plot_gcs_insert_fail, 4646 &plot_id, 4647 Some(PlotLocation { 4648 primary: Some("primary".to_string()), 4649 city: None, 4650 region: None, 4651 country: None, 4652 gcs: sample_gcs(33.0, 43.0, "sa"), 4653 }), 4654 &FixedFactory, 4655 ) 4656 .is_err() 4657 ); 4658 4659 let plot_rel_insert_fail = QueryFailExecutor { 4660 inner: &exec, 4661 needle: "insert into plot_gcs_location", 4662 err: SqlError::Internal, 4663 }; 4664 assert!( 4665 upsert_plot_location( 4666 &plot_rel_insert_fail, 4667 &plot_id, 4668 Some(PlotLocation { 4669 primary: Some("primary".to_string()), 4670 city: None, 4671 region: None, 4672 country: None, 4673 gcs: sample_gcs(34.0, 44.0, "sb"), 4674 }), 4675 &FixedFactory, 4676 ) 4677 .is_err() 4678 ); 4679 4680 let member_insert_fail = QueryFailExecutor { 4681 inner: &exec, 4682 needle: "insert into farm_member", 4683 err: SqlError::Internal, 4684 }; 4685 assert!( 4686 upsert_farm_members( 4687 &member_insert_fail, 4688 &farm_id, 4689 ListSetRole::Members, 4690 &members_set 4691 ) 4692 .is_err() 4693 ); 4694 4695 let claims_insert_fail = QueryFailExecutor { 4696 inner: &exec, 4697 needle: "insert into farm_member_claim", 4698 err: SqlError::Internal, 4699 }; 4700 assert!( 4701 upsert_member_claims(&claims_insert_fail, &fixture_public_key(7), &member_of_set) 4702 .is_err() 4703 ); 4704 4705 super::failpoints::set_gcs_point_serialize_error(); 4706 assert!(create_gcs_location(&exec, sample_gcs(35.0, 45.0, "sc"), &FixedFactory).is_err()); 4707 4708 super::failpoints::set_gcs_polygon_serialize_error(); 4709 assert!(create_gcs_location(&exec, sample_gcs(36.0, 46.0, "sd"), &FixedFactory).is_err()); 4710 4711 assert!(parse_farm_list_set_d_tag("coop:AAAAAAAAAAAAAAAAAAAAAA:members").is_none()); 4712 } 4713 4714 #[test] 4715 fn upsert_member_helpers_ignore_empty_entry_values() { 4716 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 4717 let (farm_id, farm_pubkey, _, _) = seed_rows(&exec); 4718 4719 let member_pubkey = fixture_public_key(6); 4720 let member_list_set = ListSet { 4721 d_tag: "farm:AAAAAAAAAAAAAAAAAAAAAQ:members".to_string(), 4722 content: String::new(), 4723 entries: vec![ 4724 ListEntry { 4725 tag: "p".to_string(), 4726 values: Vec::new(), 4727 }, 4728 ListEntry { 4729 tag: "p".to_string(), 4730 values: vec![member_pubkey.clone(), "ignored".to_string()], 4731 }, 4732 ListEntry { 4733 tag: "p".to_string(), 4734 values: vec![member_pubkey.clone()], 4735 }, 4736 ], 4737 title: None, 4738 description: None, 4739 image: None, 4740 }; 4741 upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &member_list_set) 4742 .expect("members"); 4743 let members = farm_member::find_many( 4744 &exec, 4745 &IFarmMemberFindMany { 4746 filter: Some(IFarmMemberFieldsFilter { 4747 id: None, 4748 created_at: None, 4749 updated_at: None, 4750 farm_id: Some(farm_id.clone()), 4751 member_pubkey: None, 4752 role: Some(ROLE_MEMBER.to_string()), 4753 }), 4754 }, 4755 ) 4756 .expect("member rows") 4757 .results; 4758 assert_eq!(members.len(), 1); 4759 assert_eq!(members[0].member_pubkey, member_pubkey); 4760 4761 upsert_farm_members(&exec, &farm_id, ListSetRole::Plots, &member_list_set) 4762 .expect("plots is no-op"); 4763 4764 let claimant_pubkey = fixture_public_key(7); 4765 let claims_list_set = ListSet { 4766 d_tag: "member_of.farms".to_string(), 4767 content: String::new(), 4768 entries: vec![ 4769 ListEntry { 4770 tag: "p".to_string(), 4771 values: Vec::new(), 4772 }, 4773 ListEntry { 4774 tag: "p".to_string(), 4775 values: vec![farm_pubkey.clone(), "ignored".to_string()], 4776 }, 4777 ListEntry { 4778 tag: "p".to_string(), 4779 values: vec![farm_pubkey.clone()], 4780 }, 4781 ], 4782 title: None, 4783 description: None, 4784 image: None, 4785 }; 4786 upsert_member_claims(&exec, &claimant_pubkey, &claims_list_set).expect("claims"); 4787 let claims = farm_member_claim::find_many( 4788 &exec, 4789 &IFarmMemberClaimFindMany { 4790 filter: Some(IFarmMemberClaimFieldsFilter { 4791 id: None, 4792 created_at: None, 4793 updated_at: None, 4794 member_pubkey: Some(claimant_pubkey), 4795 farm_pubkey: None, 4796 }), 4797 }, 4798 ) 4799 .expect("claim rows") 4800 .results; 4801 assert_eq!(claims.len(), 1); 4802 assert_eq!(claims[0].farm_pubkey, farm_pubkey); 4803 } 4804 4805 #[test] 4806 fn ingest_error_paths_cover_missing_farm_and_bad_list_set_tags() { 4807 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 4808 let (_, farm_pubkey, farm_d_tag, plot_d_tag) = seed_rows(&exec); 4809 4810 let missing_farm_plot = plot_event( 4811 950, 4812 &farm_pubkey, 4813 220, 4814 &plot_d_tag, 4815 FarmRef { 4816 pubkey: fixture_public_key(1), 4817 d_tag: farm_d_tag.clone(), 4818 }, 4819 "plot-missing-farm", 4820 None, 4821 None, 4822 ); 4823 assert!(ingest_plot_event(&exec, &missing_farm_plot, &FixedFactory).is_err()); 4824 4825 let mut bad_member_of = 4826 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 4827 bad_member_of.entries[0].tag = "x".to_string(); 4828 let bad_member_of_event = list_set_event( 4829 951, 4830 &fixture_public_key(7), 4831 221, 4832 KIND_LIST_SET_GENERIC, 4833 &bad_member_of, 4834 ); 4835 assert!(ingest_list_set_event(&exec, &bad_member_of_event).is_err()); 4836 4837 let mut bad_plots = farm_list_sets::farm_plots_list_set( 4838 &farm_d_tag, 4839 &farm_pubkey, 4840 vec![plot_d_tag.clone()], 4841 ) 4842 .expect("plots"); 4843 bad_plots.entries[0].tag = "p".to_string(); 4844 let bad_plots_event = 4845 list_set_event(952, &farm_pubkey, 222, KIND_LIST_SET_GENERIC, &bad_plots); 4846 assert!(ingest_list_set_event(&exec, &bad_plots_event).is_err()); 4847 4848 let mut bad_members = 4849 farm_list_sets::farm_members_list_set(&farm_d_tag, vec![fixture_public_key(6)]) 4850 .expect("members"); 4851 bad_members.entries[0].tag = "a".to_string(); 4852 let bad_members_event = 4853 list_set_event(953, &farm_pubkey, 223, KIND_LIST_SET_GENERIC, &bad_members); 4854 assert!(ingest_list_set_event(&exec, &bad_members_event).is_err()); 4855 } 4856 }