emit.rs (84053B)
1 #[cfg(not(feature = "std"))] 2 use alloc::format; 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 collections::BTreeMap, 6 string::{String, ToString}, 7 vec::Vec, 8 }; 9 #[cfg(feature = "std")] 10 use std::collections::BTreeMap; 11 12 use radroots_events::farm::{ 13 RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, 14 RadrootsGeoJsonPolygon, 15 }; 16 use radroots_events::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT}; 17 use radroots_events::plot::RadrootsPlot; 18 use radroots_events::profile::{ 19 RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, 20 radroots_profile_type_from_tag_value, radroots_profile_type_tag_value, 21 }; 22 use radroots_events_codec::farm::encode as farm_encode; 23 use radroots_events_codec::farm::list_sets as farm_list_sets; 24 use radroots_events_codec::list_set::encode as list_set_encode; 25 use radroots_events_codec::plot::encode as plot_encode; 26 use radroots_events_codec::wire::WireEventParts; 27 use radroots_replica_db::{ 28 farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_profile, 29 plot, plot_gcs_location, plot_tag, 30 }; 31 use radroots_replica_db_schema::farm::{ 32 Farm, IFarmFieldsFilter, IFarmFindMany, IFarmFindOne, IFarmFindOneArgs, 33 }; 34 use radroots_replica_db_schema::farm_gcs_location::{ 35 FarmGcsLocation, IFarmGcsLocationFieldsFilter, IFarmGcsLocationFindMany, 36 }; 37 use radroots_replica_db_schema::farm_member::{ 38 FarmMember, IFarmMemberFieldsFilter, IFarmMemberFindMany, 39 }; 40 use radroots_replica_db_schema::farm_member_claim::{ 41 FarmMemberClaim, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, 42 }; 43 use radroots_replica_db_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; 44 use radroots_replica_db_schema::gcs_location::{ 45 GcsLocation, GcsLocationQueryBindValues, IGcsLocationFindOne, IGcsLocationFindOneArgs, 46 }; 47 use radroots_replica_db_schema::nostr_profile::{ 48 INostrProfileFindOne, INostrProfileFindOneArgs, NostrProfileQueryBindValues, 49 }; 50 use radroots_replica_db_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; 51 use radroots_replica_db_schema::plot_gcs_location::{ 52 IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, PlotGcsLocation, 53 }; 54 use radroots_replica_db_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; 55 use radroots_sql_core::SqlExecutor; 56 use serde_json::Value; 57 58 use crate::canonical::canonical_json_string; 59 use crate::error::RadrootsReplicaEventsError; 60 use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84}; 61 use crate::types::{ 62 RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, 63 RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, 64 }; 65 66 const ROLE_PRIMARY: &str = "primary"; 67 const ROLE_MEMBER: &str = "member"; 68 const ROLE_OWNER: &str = "owner"; 69 const ROLE_WORKER: &str = "worker"; 70 71 #[cfg(test)] 72 pub(crate) mod failpoints { 73 use std::cell::Cell; 74 75 thread_local! { 76 static FORCE_LIST_SET_TO_WIRE_ERROR: Cell<bool> = const { Cell::new(false) }; 77 static FORCE_GCS_LOCATION_TO_EVENT_ERROR: Cell<bool> = const { Cell::new(false) }; 78 } 79 80 pub(crate) fn set_list_set_to_wire_error() { 81 FORCE_LIST_SET_TO_WIRE_ERROR.with(|flag| flag.set(true)); 82 } 83 84 pub(crate) fn take_list_set_to_wire_error() -> bool { 85 FORCE_LIST_SET_TO_WIRE_ERROR.with(|flag| { 86 let value = flag.get(); 87 flag.set(false); 88 value 89 }) 90 } 91 92 pub(crate) fn set_gcs_location_to_event_error() { 93 FORCE_GCS_LOCATION_TO_EVENT_ERROR.with(|flag| flag.set(true)); 94 } 95 96 pub(crate) fn take_gcs_location_to_event_error() -> bool { 97 FORCE_GCS_LOCATION_TO_EVENT_ERROR.with(|flag| { 98 let value = flag.get(); 99 flag.set(false); 100 value 101 }) 102 } 103 } 104 105 pub fn radroots_replica_sync_all( 106 exec: &dyn SqlExecutor, 107 request: &RadrootsReplicaSyncRequest, 108 ) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> { 109 radroots_replica_sync_all_with_options(exec, &request.farm, request.options.as_ref()) 110 } 111 112 pub fn radroots_replica_sync_all_with_options( 113 exec: &dyn SqlExecutor, 114 farm_selector: &RadrootsReplicaFarmSelector, 115 options: Option<&RadrootsReplicaSyncOptions>, 116 ) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> { 117 let farm = resolve_farm(exec, farm_selector)?; 118 let include_profiles = options.and_then(|opt| opt.include_profiles).unwrap_or(true); 119 let include_list_sets = options 120 .and_then(|opt| opt.include_list_sets) 121 .unwrap_or(true); 122 let include_claims = options 123 .and_then(|opt| opt.include_membership_claims) 124 .unwrap_or(true); 125 126 let mut events = Vec::new(); 127 128 if include_profiles { 129 let profiles = radroots_replica_profile_events(exec, &farm)?; 130 events.extend(profiles); 131 } 132 133 events.push(radroots_replica_farm_event(exec, &farm)?); 134 135 let plots = radroots_replica_plot_events(exec, &farm)?; 136 events.extend(plots); 137 138 if include_list_sets { 139 let list_sets = radroots_replica_list_set_events(exec, &farm)?; 140 events.extend(list_sets); 141 } 142 143 if include_claims { 144 let claims = radroots_replica_membership_claim_events(exec, &farm.pubkey)?; 145 events.extend(claims); 146 } 147 148 Ok(RadrootsReplicaSyncBundle { 149 version: RADROOTS_REPLICA_TRANSFER_VERSION, 150 events, 151 }) 152 } 153 154 pub fn radroots_replica_profile_events( 155 exec: &dyn SqlExecutor, 156 farm: &Farm, 157 ) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { 158 let mut pubkeys = collect_profile_pubkeys(exec, farm)?; 159 pubkeys.sort(); 160 pubkeys.dedup(); 161 162 let mut events = Vec::new(); 163 for pubkey in pubkeys { 164 if let Some(profile) = load_profile(exec, &pubkey)? { 165 events.push(profile_event(&pubkey, profile)?); 166 } 167 } 168 Ok(events) 169 } 170 171 pub fn radroots_replica_farm_event( 172 exec: &dyn SqlExecutor, 173 farm: &Farm, 174 ) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { 175 let tags = collect_farm_tags(exec, &farm.id)?; 176 let location = load_farm_location(exec, farm)?; 177 let farm_event = RadrootsFarm { 178 d_tag: farm.d_tag.clone(), 179 name: farm.name.clone(), 180 about: farm.about.clone(), 181 website: farm.website.clone(), 182 picture: farm.picture.clone(), 183 banner: farm.banner.clone(), 184 location, 185 tags: if tags.is_empty() { None } else { Some(tags) }, 186 }; 187 let tags = farm_encode::farm_build_tags(&farm_event)?; 188 let content = canonical_json_string(&farm_event)?; 189 let parts = WireEventParts { 190 kind: KIND_FARM, 191 content, 192 tags, 193 }; 194 Ok(parts_to_draft(&farm.pubkey, parts)) 195 } 196 197 pub fn radroots_replica_plot_events( 198 exec: &dyn SqlExecutor, 199 farm: &Farm, 200 ) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { 201 let plots = load_plots(exec, &farm.id)?; 202 let mut events = Vec::new(); 203 for plot_row in plots { 204 let tags = collect_plot_tags(exec, &plot_row.id)?; 205 let location = load_plot_location(exec, &plot_row)?; 206 let plot_event = RadrootsPlot { 207 d_tag: plot_row.d_tag.clone(), 208 farm: RadrootsFarmRef { 209 pubkey: farm.pubkey.clone(), 210 d_tag: farm.d_tag.clone(), 211 }, 212 name: plot_row.name.clone(), 213 about: plot_row.about.clone(), 214 location, 215 tags: if tags.is_empty() { None } else { Some(tags) }, 216 }; 217 let tags = plot_encode::plot_build_tags(&plot_event)?; 218 let content = canonical_json_string(&plot_event)?; 219 let parts = WireEventParts { 220 kind: KIND_PLOT, 221 content, 222 tags, 223 }; 224 events.push(parts_to_draft(&farm.pubkey, parts)); 225 } 226 Ok(events) 227 } 228 229 pub fn radroots_replica_list_set_events( 230 exec: &dyn SqlExecutor, 231 farm: &Farm, 232 ) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { 233 let members = load_farm_members(exec, &farm.id)?; 234 let plots = load_plots(exec, &farm.id)?; 235 236 let members_list = 237 farm_list_sets::farm_members_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_MEMBER))?; 238 let owners_list = 239 farm_list_sets::farm_owners_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_OWNER))?; 240 let workers_list = 241 farm_list_sets::farm_workers_list_set(&farm.d_tag, role_pubkeys(&members, ROLE_WORKER))?; 242 243 let plot_ids = sorted_plot_ids(&plots); 244 let plots_list = farm_list_sets::farm_plots_list_set(&farm.d_tag, &farm.pubkey, plot_ids)?; 245 246 let list_sets = [members_list, owners_list, workers_list, plots_list]; 247 let mut events = Vec::new(); 248 for list_set in list_sets { 249 let parts = list_set_to_wire_parts(&list_set)?; 250 events.push(parts_to_draft(&farm.pubkey, parts)); 251 } 252 Ok(events) 253 } 254 255 pub fn radroots_replica_membership_claim_events( 256 exec: &dyn SqlExecutor, 257 farm_pubkey: &str, 258 ) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { 259 let claims = load_member_claims(exec, farm_pubkey)?; 260 let mut by_member: BTreeMap<String, Vec<String>> = BTreeMap::new(); 261 for claim in claims { 262 by_member 263 .entry(claim.member_pubkey.clone()) 264 .or_default() 265 .push(claim.farm_pubkey.clone()); 266 } 267 268 let mut events = Vec::new(); 269 for (member_pubkey, _) in by_member.iter() { 270 let all_claims = load_member_claims_for_member(exec, member_pubkey)?; 271 let mut farm_pubkeys = all_claims 272 .into_iter() 273 .map(|claim| claim.farm_pubkey) 274 .collect::<Vec<String>>(); 275 farm_pubkeys.sort(); 276 farm_pubkeys.dedup(); 277 let list_set = farm_list_sets::member_of_farms_list_set(farm_pubkeys)?; 278 let parts = list_set_to_wire_parts(&list_set)?; 279 events.push(parts_to_draft(member_pubkey, parts)); 280 } 281 282 Ok(events) 283 } 284 285 fn resolve_farm( 286 exec: &dyn SqlExecutor, 287 selector: &RadrootsReplicaFarmSelector, 288 ) -> Result<Farm, RadrootsReplicaEventsError> { 289 if let Some(id) = selector.id.as_ref().filter(|v| !v.trim().is_empty()) { 290 let result_query = farm::find_one( 291 exec, 292 &IFarmFindOne::On(IFarmFindOneArgs { 293 on: radroots_replica_db_schema::farm::FarmQueryBindValues::Id { id: id.clone() }, 294 }), 295 ); 296 let result = result_query?; 297 return result.result.ok_or_else(|| { 298 RadrootsReplicaEventsError::InvalidSelector(format!("farm not found: {id}")) 299 }); 300 } 301 302 let d_tag = selector 303 .d_tag 304 .as_ref() 305 .map(|v| v.trim()) 306 .filter(|v| !v.is_empty()); 307 let pubkey = selector 308 .pubkey 309 .as_ref() 310 .map(|v| v.trim()) 311 .filter(|v| !v.is_empty()); 312 313 let (d_tag, pubkey) = match (d_tag, pubkey) { 314 (Some(d_tag), Some(pubkey)) => (d_tag, pubkey), 315 _ => { 316 return Err(RadrootsReplicaEventsError::InvalidSelector( 317 "farm selector requires id or (d_tag + pubkey)".to_string(), 318 )); 319 } 320 }; 321 322 let filter = IFarmFieldsFilter { 323 id: None, 324 created_at: None, 325 updated_at: None, 326 d_tag: Some(d_tag.to_string()), 327 pubkey: Some(pubkey.to_string()), 328 name: None, 329 about: None, 330 website: None, 331 picture: None, 332 banner: None, 333 location_primary: None, 334 location_city: None, 335 location_region: None, 336 location_country: None, 337 }; 338 let result_query = farm::find_many( 339 exec, 340 &IFarmFindMany { 341 filter: Some(filter), 342 }, 343 ); 344 let result = result_query?; 345 if result.results.len() == 1 { 346 return Ok(result.results.into_iter().next().expect("farm result")); 347 } 348 Err(RadrootsReplicaEventsError::InvalidSelector( 349 "farm selector did not resolve to a single farm".to_string(), 350 )) 351 } 352 353 fn collect_farm_tags( 354 exec: &dyn SqlExecutor, 355 farm_id: &str, 356 ) -> Result<Vec<String>, RadrootsReplicaEventsError> { 357 let filter = IFarmTagFieldsFilter { 358 id: None, 359 created_at: None, 360 updated_at: None, 361 farm_id: Some(farm_id.to_string()), 362 tag: None, 363 }; 364 let result_query = farm_tag::find_many( 365 exec, 366 &IFarmTagFindMany { 367 filter: Some(filter), 368 }, 369 ); 370 let result = result_query?; 371 let mut tags = result 372 .results 373 .into_iter() 374 .map(|row| row.tag) 375 .collect::<Vec<_>>(); 376 tags.sort(); 377 tags.dedup(); 378 Ok(tags) 379 } 380 381 fn collect_plot_tags( 382 exec: &dyn SqlExecutor, 383 plot_id: &str, 384 ) -> Result<Vec<String>, RadrootsReplicaEventsError> { 385 let filter = IPlotTagFieldsFilter { 386 id: None, 387 created_at: None, 388 updated_at: None, 389 plot_id: Some(plot_id.to_string()), 390 tag: None, 391 }; 392 let result_query = plot_tag::find_many( 393 exec, 394 &IPlotTagFindMany { 395 filter: Some(filter), 396 }, 397 ); 398 let result = result_query?; 399 let mut tags = result 400 .results 401 .into_iter() 402 .map(|row| row.tag) 403 .collect::<Vec<_>>(); 404 tags.sort(); 405 tags.dedup(); 406 Ok(tags) 407 } 408 409 fn load_farm_members( 410 exec: &dyn SqlExecutor, 411 farm_id: &str, 412 ) -> Result<Vec<FarmMember>, RadrootsReplicaEventsError> { 413 let filter = IFarmMemberFieldsFilter { 414 id: None, 415 created_at: None, 416 updated_at: None, 417 farm_id: Some(farm_id.to_string()), 418 member_pubkey: None, 419 role: None, 420 }; 421 let result_query = farm_member::find_many( 422 exec, 423 &IFarmMemberFindMany { 424 filter: Some(filter), 425 }, 426 ); 427 let result = result_query?; 428 Ok(result.results) 429 } 430 431 fn role_pubkeys(members: &[FarmMember], role: &str) -> Vec<String> { 432 let mut values = members 433 .iter() 434 .filter(|member| member.role == role) 435 .map(|member| member.member_pubkey.clone()) 436 .collect::<Vec<_>>(); 437 values.sort(); 438 values.dedup(); 439 values 440 } 441 442 fn sorted_plot_ids(plots: &[Plot]) -> Vec<String> { 443 let mut ids = plots 444 .iter() 445 .map(|plot| plot.d_tag.clone()) 446 .collect::<Vec<_>>(); 447 ids.sort(); 448 ids.dedup(); 449 ids 450 } 451 452 fn load_plots( 453 exec: &dyn SqlExecutor, 454 farm_id: &str, 455 ) -> Result<Vec<Plot>, RadrootsReplicaEventsError> { 456 let filter = IPlotFieldsFilter { 457 id: None, 458 created_at: None, 459 updated_at: None, 460 d_tag: None, 461 farm_id: Some(farm_id.to_string()), 462 name: None, 463 about: None, 464 location_primary: None, 465 location_city: None, 466 location_region: None, 467 location_country: None, 468 }; 469 let result_query = plot::find_many( 470 exec, 471 &IPlotFindMany { 472 filter: Some(filter), 473 }, 474 ); 475 let result = result_query?; 476 let mut plots = result.results; 477 plots.sort_by(|a, b| a.d_tag.cmp(&b.d_tag)); 478 Ok(plots) 479 } 480 481 fn load_farm_location( 482 exec: &dyn SqlExecutor, 483 farm: &Farm, 484 ) -> Result<Option<RadrootsFarmLocation>, RadrootsReplicaEventsError> { 485 let gcs = load_gcs_location_for_farm(exec, &farm.id)?; 486 let has_strings = [ 487 farm.location_primary.as_deref(), 488 farm.location_city.as_deref(), 489 farm.location_region.as_deref(), 490 farm.location_country.as_deref(), 491 ] 492 .into_iter() 493 .flatten() 494 .any(|value| !value.trim().is_empty()); 495 if !has_strings && gcs.is_none() { 496 return Ok(None); 497 } 498 Ok(Some(RadrootsFarmLocation { 499 primary: farm.location_primary.clone(), 500 city: farm.location_city.clone(), 501 region: farm.location_region.clone(), 502 country: farm.location_country.clone(), 503 gcs, 504 })) 505 } 506 507 fn load_plot_location( 508 exec: &dyn SqlExecutor, 509 plot: &Plot, 510 ) -> Result<Option<radroots_events::plot::RadrootsPlotLocation>, RadrootsReplicaEventsError> { 511 let location = load_gcs_location_for_plot(exec, &plot.id)?; 512 Ok( 513 location.map(|gcs| radroots_events::plot::RadrootsPlotLocation { 514 primary: plot.location_primary.clone(), 515 city: plot.location_city.clone(), 516 region: plot.location_region.clone(), 517 country: plot.location_country.clone(), 518 gcs, 519 }), 520 ) 521 } 522 523 fn load_gcs_location_for_farm( 524 exec: &dyn SqlExecutor, 525 farm_id: &str, 526 ) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { 527 let primary = load_relation_by_role(exec, farm_id, ROLE_PRIMARY, RelationType::Farm)?; 528 match primary { 529 Some(gcs) => Ok(Some(gcs)), 530 None => load_relation_by_role(exec, farm_id, "", RelationType::Farm), 531 } 532 } 533 534 fn load_gcs_location_for_plot( 535 exec: &dyn SqlExecutor, 536 plot_id: &str, 537 ) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { 538 let primary = load_relation_by_role(exec, plot_id, ROLE_PRIMARY, RelationType::Plot)?; 539 match primary { 540 Some(gcs) => Ok(Some(gcs)), 541 None => load_relation_by_role(exec, plot_id, "", RelationType::Plot), 542 } 543 } 544 545 enum RelationType { 546 Farm, 547 Plot, 548 } 549 550 fn load_relation_by_role( 551 exec: &dyn SqlExecutor, 552 id: &str, 553 role: &str, 554 relation: RelationType, 555 ) -> Result<Option<RadrootsGcsLocation>, RadrootsReplicaEventsError> { 556 let mut rels = match relation { 557 RelationType::Farm => { 558 let filter = IFarmGcsLocationFieldsFilter { 559 id: None, 560 created_at: None, 561 updated_at: None, 562 farm_id: Some(id.to_string()), 563 gcs_location_id: None, 564 role: if role.is_empty() { 565 None 566 } else { 567 Some(role.to_string()) 568 }, 569 }; 570 let result_query = farm_gcs_location::find_many( 571 exec, 572 &IFarmGcsLocationFindMany { 573 filter: Some(filter), 574 }, 575 ); 576 let result = result_query?; 577 result 578 .results 579 .into_iter() 580 .map(RelationRow::Farm) 581 .collect::<Vec<_>>() 582 } 583 RelationType::Plot => { 584 let filter = IPlotGcsLocationFieldsFilter { 585 id: None, 586 created_at: None, 587 updated_at: None, 588 plot_id: Some(id.to_string()), 589 gcs_location_id: None, 590 role: if role.is_empty() { 591 None 592 } else { 593 Some(role.to_string()) 594 }, 595 }; 596 let result_query = plot_gcs_location::find_many( 597 exec, 598 &IPlotGcsLocationFindMany { 599 filter: Some(filter), 600 }, 601 ); 602 let result = result_query?; 603 result 604 .results 605 .into_iter() 606 .map(RelationRow::Plot) 607 .collect::<Vec<_>>() 608 } 609 }; 610 611 if rels.is_empty() { 612 return Ok(None); 613 } 614 615 rels.sort_by(compare_relation_rows); 616 let gcs_id = rels[0].gcs_location_id().to_string(); 617 let gcs_result = gcs_location::find_one( 618 exec, 619 &IGcsLocationFindOne::On(IGcsLocationFindOneArgs { 620 on: GcsLocationQueryBindValues::Id { id: gcs_id }, 621 }), 622 ); 623 let gcs = match gcs_result?.result { 624 Some(gcs) => gcs, 625 None => { 626 return Err(RadrootsReplicaEventsError::InvalidData( 627 "gcs_location not found".to_string(), 628 )); 629 } 630 }; 631 Ok(Some(gcs_location_to_event(&gcs)?)) 632 } 633 634 fn compare_relation_rows(a: &RelationRow, b: &RelationRow) -> core::cmp::Ordering { 635 let rank = location_role_rank(a.role()).cmp(&location_role_rank(b.role())); 636 rank.then_with(|| a.gcs_location_id().cmp(b.gcs_location_id())) 637 } 638 639 fn list_set_to_wire_parts( 640 list_set: &radroots_events::list_set::RadrootsListSet, 641 ) -> Result<WireEventParts, RadrootsReplicaEventsError> { 642 #[cfg(test)] 643 if failpoints::take_list_set_to_wire_error() { 644 return Err(RadrootsReplicaEventsError::InvalidData( 645 "list_set_to_wire".to_string(), 646 )); 647 } 648 Ok(list_set_encode::to_wire_parts_with_kind( 649 list_set, 650 KIND_LIST_SET_GENERIC, 651 )?) 652 } 653 654 enum RelationRow { 655 Farm(FarmGcsLocation), 656 Plot(PlotGcsLocation), 657 } 658 659 impl RelationRow { 660 fn gcs_location_id(&self) -> &str { 661 match self { 662 Self::Farm(row) => row.gcs_location_id.as_str(), 663 Self::Plot(row) => row.gcs_location_id.as_str(), 664 } 665 } 666 667 fn role(&self) -> &str { 668 match self { 669 Self::Farm(row) => row.role.as_str(), 670 Self::Plot(row) => row.role.as_str(), 671 } 672 } 673 } 674 675 fn location_role_rank(role: &str) -> u8 { 676 u8::from(role != ROLE_PRIMARY) 677 } 678 679 fn gcs_location_to_event( 680 gcs: &GcsLocation, 681 ) -> Result<RadrootsGcsLocation, RadrootsReplicaEventsError> { 682 #[cfg(test)] 683 if failpoints::take_gcs_location_to_event_error() { 684 return Err(RadrootsReplicaEventsError::InvalidData( 685 "gcs_location_to_event".to_string(), 686 )); 687 } 688 let point = parse_point(&gcs.point, gcs.lat, gcs.lng); 689 let polygon = parse_polygon(&gcs.polygon, gcs.lat, gcs.lng); 690 Ok(RadrootsGcsLocation { 691 lat: gcs.lat, 692 lng: gcs.lng, 693 geohash: gcs.geohash.clone(), 694 point, 695 polygon, 696 accuracy: gcs.accuracy, 697 altitude: gcs.altitude, 698 tag_0: gcs.tag_0.clone(), 699 label: gcs.label.clone(), 700 area: gcs.area, 701 elevation: gcs.elevation, 702 soil: gcs.soil.clone(), 703 climate: gcs.climate.clone(), 704 gc_id: gcs.gc_id.clone(), 705 gc_name: gcs.gc_name.clone(), 706 gc_admin1_id: gcs.gc_admin1_id.clone(), 707 gc_admin1_name: gcs.gc_admin1_name.clone(), 708 gc_country_id: gcs.gc_country_id.clone(), 709 gc_country_name: gcs.gc_country_name.clone(), 710 }) 711 } 712 713 fn parse_point(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPoint { 714 if !value.trim().is_empty() 715 && let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPoint>(value) 716 { 717 return parsed; 718 } 719 geojson_point_from_lat_lng(lat, lng) 720 } 721 722 fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { 723 if !value.trim().is_empty() 724 && let Ok(parsed) = serde_json::from_str::<RadrootsGeoJsonPolygon>(value) 725 && !parsed.coordinates.is_empty() 726 && !parsed.coordinates[0].is_empty() 727 { 728 return parsed; 729 } 730 geojson_polygon_circle_wgs84(lat, lng, 100.0, 64) 731 } 732 733 fn load_profile( 734 exec: &dyn SqlExecutor, 735 pubkey: &str, 736 ) -> Result< 737 Option<radroots_replica_db_schema::nostr_profile::NostrProfile>, 738 RadrootsReplicaEventsError, 739 > { 740 let result_query = nostr_profile::find_one( 741 exec, 742 &INostrProfileFindOne::On(INostrProfileFindOneArgs { 743 on: NostrProfileQueryBindValues::PublicKey { 744 public_key: pubkey.to_string(), 745 }, 746 }), 747 ); 748 let result = result_query?; 749 Ok(result.result) 750 } 751 752 fn profile_event( 753 pubkey: &str, 754 profile: radroots_replica_db_schema::nostr_profile::NostrProfile, 755 ) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { 756 let profile_type = match profile.profile_type.as_str() { 757 "individual" | "farmer" => Some(RadrootsProfileType::Individual), 758 "farm" => Some(RadrootsProfileType::Farm), 759 "coop" => Some(RadrootsProfileType::Coop), 760 "any" => Some(RadrootsProfileType::Any), 761 other => radroots_profile_type_from_tag_value(other), 762 }; 763 let profile_event = RadrootsProfile { 764 name: profile.name, 765 display_name: profile.display_name, 766 nip05: profile.nip05, 767 about: profile.about, 768 website: profile.website, 769 picture: profile.picture, 770 banner: profile.banner, 771 lud06: profile.lud06, 772 lud16: profile.lud16, 773 bot: None, 774 }; 775 let content = serialize_profile_content(&profile_event)?; 776 let mut tags = Vec::new(); 777 if let Some(profile_type) = profile_type { 778 let tag = vec![ 779 RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), 780 radroots_profile_type_tag_value(profile_type).to_string(), 781 ]; 782 tags.push(tag); 783 } 784 Ok(RadrootsReplicaEventDraft { 785 kind: radroots_events::kinds::KIND_PROFILE, 786 author: pubkey.to_string(), 787 content, 788 tags, 789 }) 790 } 791 792 fn serialize_profile_content( 793 profile: &RadrootsProfile, 794 ) -> Result<String, RadrootsReplicaEventsError> { 795 let mut obj = serde_json::Map::new(); 796 obj.insert("name".to_string(), Value::from(profile.name.clone())); 797 if let Some(value) = profile.display_name.as_ref() { 798 obj.insert("display_name".to_string(), Value::from(value.clone())); 799 } 800 if let Some(value) = profile.nip05.as_ref() { 801 obj.insert("nip05".to_string(), Value::from(value.clone())); 802 } 803 if let Some(value) = profile.about.as_ref() { 804 obj.insert("about".to_string(), Value::from(value.clone())); 805 } 806 if let Some(value) = profile.website.as_ref() { 807 obj.insert("website".to_string(), Value::from(value.clone())); 808 } 809 if let Some(value) = profile.picture.as_ref() { 810 obj.insert("picture".to_string(), Value::from(value.clone())); 811 } 812 if let Some(value) = profile.banner.as_ref() { 813 obj.insert("banner".to_string(), Value::from(value.clone())); 814 } 815 if let Some(value) = profile.lud06.as_ref() { 816 obj.insert("lud06".to_string(), Value::from(value.clone())); 817 } 818 if let Some(value) = profile.lud16.as_ref() { 819 obj.insert("lud16".to_string(), Value::from(value.clone())); 820 } 821 canonical_json_string(&Value::Object(obj)) 822 } 823 824 fn collect_member_pubkeys( 825 exec: &dyn SqlExecutor, 826 farm_id: &str, 827 ) -> Result<Vec<String>, RadrootsReplicaEventsError> { 828 let members = load_farm_members(exec, farm_id)?; 829 let mut pubkeys = members 830 .into_iter() 831 .map(|row| row.member_pubkey) 832 .collect::<Vec<_>>(); 833 pubkeys.sort(); 834 pubkeys.dedup(); 835 Ok(pubkeys) 836 } 837 838 fn collect_profile_pubkeys( 839 exec: &dyn SqlExecutor, 840 farm: &Farm, 841 ) -> Result<Vec<String>, RadrootsReplicaEventsError> { 842 let mut pubkeys = collect_member_pubkeys(exec, &farm.id)?; 843 let claims = load_member_claims(exec, &farm.pubkey)?; 844 pubkeys.extend(claims.into_iter().map(|claim| claim.member_pubkey)); 845 pubkeys.push(farm.pubkey.clone()); 846 Ok(pubkeys) 847 } 848 849 fn load_member_claims( 850 exec: &dyn SqlExecutor, 851 farm_pubkey: &str, 852 ) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> { 853 let filter = IFarmMemberClaimFieldsFilter { 854 id: None, 855 created_at: None, 856 updated_at: None, 857 member_pubkey: None, 858 farm_pubkey: Some(farm_pubkey.to_string()), 859 }; 860 let result_query = farm_member_claim::find_many( 861 exec, 862 &IFarmMemberClaimFindMany { 863 filter: Some(filter), 864 }, 865 ); 866 let result = result_query?; 867 Ok(result.results) 868 } 869 870 fn load_member_claims_for_member( 871 exec: &dyn SqlExecutor, 872 member_pubkey: &str, 873 ) -> Result<Vec<FarmMemberClaim>, RadrootsReplicaEventsError> { 874 let filter = IFarmMemberClaimFieldsFilter { 875 id: None, 876 created_at: None, 877 updated_at: None, 878 member_pubkey: Some(member_pubkey.to_string()), 879 farm_pubkey: None, 880 }; 881 let result_query = farm_member_claim::find_many( 882 exec, 883 &IFarmMemberClaimFindMany { 884 filter: Some(filter), 885 }, 886 ); 887 let result = result_query?; 888 Ok(result.results) 889 } 890 891 fn parts_to_draft(author: &str, parts: WireEventParts) -> RadrootsReplicaEventDraft { 892 RadrootsReplicaEventDraft { 893 kind: parts.kind, 894 author: author.to_string(), 895 content: parts.content, 896 tags: parts.tags, 897 } 898 } 899 900 #[cfg(test)] 901 mod tests { 902 use super::*; 903 use radroots_replica_db::{ 904 farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, 905 migrations, nostr_profile, plot, plot_gcs_location, plot_tag, 906 }; 907 use radroots_replica_db_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; 908 use radroots_replica_db_schema::farm_gcs_location::{ 909 IFarmGcsLocationFields, IFarmGcsLocationFindMany, 910 }; 911 use radroots_replica_db_schema::farm_member::IFarmMemberFields; 912 use radroots_replica_db_schema::farm_member_claim::IFarmMemberClaimFields; 913 use radroots_replica_db_schema::farm_tag::IFarmTagFields; 914 use radroots_replica_db_schema::gcs_location::IGcsLocationFields; 915 use radroots_replica_db_schema::nostr_profile::INostrProfileFields; 916 use radroots_replica_db_schema::plot::{IPlotFields, IPlotFindMany}; 917 use radroots_replica_db_schema::plot_gcs_location::{ 918 IPlotGcsLocationFields, IPlotGcsLocationFindMany, 919 }; 920 use radroots_replica_db_schema::plot_tag::IPlotTagFields; 921 use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor, SqliteExecutor}; 922 923 struct ErrorExecutor; 924 925 impl SqlExecutor for ErrorExecutor { 926 fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { 927 Err(SqlError::Internal) 928 } 929 930 fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { 931 Err(SqlError::Internal) 932 } 933 934 fn begin(&self) -> Result<(), SqlError> { 935 Ok(()) 936 } 937 938 fn commit(&self) -> Result<(), SqlError> { 939 Ok(()) 940 } 941 942 fn rollback(&self) -> Result<(), SqlError> { 943 Ok(()) 944 } 945 } 946 947 struct QueryFailExecutor<'a> { 948 inner: &'a SqliteExecutor, 949 needle: &'static str, 950 err: SqlError, 951 } 952 953 impl SqlExecutor for QueryFailExecutor<'_> { 954 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 955 self.inner.exec(sql, params_json) 956 } 957 958 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 959 if sql.to_ascii_lowercase().contains(self.needle) { 960 return Err(self.err.clone()); 961 } 962 self.inner.query_raw(sql, params_json) 963 } 964 965 fn begin(&self) -> Result<(), SqlError> { 966 self.inner.begin() 967 } 968 969 fn commit(&self) -> Result<(), SqlError> { 970 self.inner.commit() 971 } 972 973 fn rollback(&self) -> Result<(), SqlError> { 974 self.inner.rollback() 975 } 976 } 977 978 struct DuplicateFarmSelectorExecutor<'a> { 979 inner: &'a SqliteExecutor, 980 duplicated_rows_json: String, 981 } 982 983 impl SqlExecutor for DuplicateFarmSelectorExecutor<'_> { 984 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 985 self.inner.exec(sql, params_json) 986 } 987 988 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 989 let _ = (sql, params_json); 990 Ok(self.duplicated_rows_json.clone()) 991 } 992 993 fn begin(&self) -> Result<(), SqlError> { 994 self.inner.begin() 995 } 996 997 fn commit(&self) -> Result<(), SqlError> { 998 self.inner.commit() 999 } 1000 1001 fn rollback(&self) -> Result<(), SqlError> { 1002 self.inner.rollback() 1003 } 1004 } 1005 1006 fn seed(exec: &SqliteExecutor) -> (Farm, Plot, Plot) { 1007 migrations::run_all_up(exec).expect("migrations"); 1008 let farm = farm::create( 1009 exec, 1010 &IFarmFields { 1011 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 1012 pubkey: "f".repeat(64), 1013 name: "farm".to_string(), 1014 about: Some("about".to_string()), 1015 website: Some("https://farm.example.com".to_string()), 1016 picture: Some("https://farm.example.com/p.png".to_string()), 1017 banner: Some("https://farm.example.com/b.png".to_string()), 1018 location_primary: Some("primary".to_string()), 1019 location_city: Some("city".to_string()), 1020 location_region: Some("region".to_string()), 1021 location_country: Some("country".to_string()), 1022 }, 1023 ) 1024 .expect("farm") 1025 .result; 1026 1027 let gcs_primary = gcs_location::create( 1028 exec, 1029 &IGcsLocationFields { 1030 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 1031 lat: 10.0, 1032 lng: 20.0, 1033 geohash: "s0".to_string(), 1034 point: "{\"type\":\"Point\",\"coordinates\":[20.0,10.0]}".to_string(), 1035 polygon: 1036 "{\"type\":\"Polygon\",\"coordinates\":[[[20.0,10.0],[20.1,10.1],[19.9,10.1],[20.0,10.0]]]}".to_string(), 1037 accuracy: None, 1038 altitude: None, 1039 tag_0: None, 1040 label: None, 1041 area: None, 1042 elevation: None, 1043 soil: None, 1044 climate: None, 1045 gc_id: None, 1046 gc_name: None, 1047 gc_admin1_id: None, 1048 gc_admin1_name: None, 1049 gc_country_id: None, 1050 gc_country_name: None, 1051 }, 1052 ) 1053 .expect("gcs primary") 1054 .result; 1055 let gcs_secondary = gcs_location::create( 1056 exec, 1057 &IGcsLocationFields { 1058 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 1059 lat: 11.0, 1060 lng: 21.0, 1061 geohash: "s1".to_string(), 1062 point: "{".to_string(), 1063 polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), 1064 accuracy: None, 1065 altitude: None, 1066 tag_0: None, 1067 label: None, 1068 area: None, 1069 elevation: None, 1070 soil: None, 1071 climate: None, 1072 gc_id: None, 1073 gc_name: None, 1074 gc_admin1_id: None, 1075 gc_admin1_name: None, 1076 gc_country_id: None, 1077 gc_country_name: None, 1078 }, 1079 ) 1080 .expect("gcs secondary") 1081 .result; 1082 1083 let _ = farm_gcs_location::create( 1084 exec, 1085 &IFarmGcsLocationFields { 1086 farm_id: farm.id.clone(), 1087 gcs_location_id: gcs_secondary.id.clone(), 1088 role: "".to_string(), 1089 }, 1090 ) 1091 .expect("farm gcs secondary"); 1092 let _ = farm_gcs_location::create( 1093 exec, 1094 &IFarmGcsLocationFields { 1095 farm_id: farm.id.clone(), 1096 gcs_location_id: gcs_primary.id.clone(), 1097 role: "primary".to_string(), 1098 }, 1099 ) 1100 .expect("farm gcs primary"); 1101 1102 let plot_primary = plot::create( 1103 exec, 1104 &IPlotFields { 1105 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 1106 farm_id: farm.id.clone(), 1107 name: "plot-primary".to_string(), 1108 about: Some("plot about".to_string()), 1109 location_primary: Some("plot primary".to_string()), 1110 location_city: Some("plot city".to_string()), 1111 location_region: Some("plot region".to_string()), 1112 location_country: Some("plot country".to_string()), 1113 }, 1114 ) 1115 .expect("plot primary") 1116 .result; 1117 let plot_secondary = plot::create( 1118 exec, 1119 &IPlotFields { 1120 d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), 1121 farm_id: farm.id.clone(), 1122 name: "plot-secondary".to_string(), 1123 about: Some("plot secondary about".to_string()), 1124 location_primary: Some("plot secondary primary".to_string()), 1125 location_city: None, 1126 location_region: None, 1127 location_country: None, 1128 }, 1129 ) 1130 .expect("plot secondary") 1131 .result; 1132 1133 let _ = plot_gcs_location::create( 1134 exec, 1135 &IPlotGcsLocationFields { 1136 plot_id: plot_primary.id.clone(), 1137 gcs_location_id: gcs_secondary.id.clone(), 1138 role: "secondary".to_string(), 1139 }, 1140 ) 1141 .expect("plot primary secondary relation"); 1142 let _ = plot_gcs_location::create( 1143 exec, 1144 &IPlotGcsLocationFields { 1145 plot_id: plot_primary.id.clone(), 1146 gcs_location_id: gcs_primary.id.clone(), 1147 role: "primary".to_string(), 1148 }, 1149 ) 1150 .expect("plot primary relation"); 1151 let _ = plot_gcs_location::create( 1152 exec, 1153 &IPlotGcsLocationFields { 1154 plot_id: plot_secondary.id.clone(), 1155 gcs_location_id: gcs_secondary.id.clone(), 1156 role: "secondary".to_string(), 1157 }, 1158 ) 1159 .expect("plot secondary relation"); 1160 1161 let _ = farm_tag::create( 1162 exec, 1163 &IFarmTagFields { 1164 farm_id: farm.id.clone(), 1165 tag: "coffee".to_string(), 1166 }, 1167 ) 1168 .expect("farm tag"); 1169 let _ = plot_tag::create( 1170 exec, 1171 &IPlotTagFields { 1172 plot_id: plot_primary.id.clone(), 1173 tag: "orchard".to_string(), 1174 }, 1175 ) 1176 .expect("plot tag"); 1177 1178 let _ = farm_member::create( 1179 exec, 1180 &IFarmMemberFields { 1181 farm_id: farm.id.clone(), 1182 member_pubkey: "6".repeat(64), 1183 role: "member".to_string(), 1184 }, 1185 ) 1186 .expect("member"); 1187 let _ = farm_member::create( 1188 exec, 1189 &IFarmMemberFields { 1190 farm_id: farm.id.clone(), 1191 member_pubkey: "8".repeat(64), 1192 role: "owner".to_string(), 1193 }, 1194 ) 1195 .expect("owner"); 1196 let _ = farm_member::create( 1197 exec, 1198 &IFarmMemberFields { 1199 farm_id: farm.id.clone(), 1200 member_pubkey: "e".repeat(64), 1201 role: "worker".to_string(), 1202 }, 1203 ) 1204 .expect("worker"); 1205 let _ = farm_member::create( 1206 exec, 1207 &IFarmMemberFields { 1208 farm_id: farm.id.clone(), 1209 member_pubkey: "1".repeat(64), 1210 role: "member".to_string(), 1211 }, 1212 ) 1213 .expect("member no profile"); 1214 1215 let _ = farm_member_claim::create( 1216 exec, 1217 &IFarmMemberClaimFields { 1218 member_pubkey: "6".repeat(64), 1219 farm_pubkey: farm.pubkey.clone(), 1220 }, 1221 ) 1222 .expect("claim member"); 1223 let _ = farm_member_claim::create( 1224 exec, 1225 &IFarmMemberClaimFields { 1226 member_pubkey: "1".repeat(64), 1227 farm_pubkey: farm.pubkey.clone(), 1228 }, 1229 ) 1230 .expect("claim member no profile"); 1231 1232 let _ = nostr_profile::create( 1233 exec, 1234 &INostrProfileFields { 1235 public_key: farm.pubkey.clone(), 1236 profile_type: "farm".to_string(), 1237 name: "farm profile".to_string(), 1238 display_name: None, 1239 about: None, 1240 website: None, 1241 picture: None, 1242 banner: None, 1243 nip05: None, 1244 lud06: None, 1245 lud16: None, 1246 }, 1247 ) 1248 .expect("farm profile"); 1249 let _ = nostr_profile::create( 1250 exec, 1251 &INostrProfileFields { 1252 public_key: "6".repeat(64), 1253 profile_type: "legacy".to_string(), 1254 name: "member profile".to_string(), 1255 display_name: Some("member".to_string()), 1256 about: Some("about".to_string()), 1257 website: Some("https://member.example.com".to_string()), 1258 picture: Some("https://member.example.com/p.png".to_string()), 1259 banner: Some("https://member.example.com/b.png".to_string()), 1260 nip05: Some("member@example.com".to_string()), 1261 lud06: Some("lud06".to_string()), 1262 lud16: Some("lud16".to_string()), 1263 }, 1264 ) 1265 .expect("member profile"); 1266 1267 (farm, plot_primary, plot_secondary) 1268 } 1269 1270 fn create_farm_record(exec: &SqliteExecutor, d_tag: &str, pubkey: &str, name: &str) -> Farm { 1271 farm::create( 1272 exec, 1273 &IFarmFields { 1274 d_tag: d_tag.to_string(), 1275 pubkey: pubkey.to_string(), 1276 name: name.to_string(), 1277 about: None, 1278 website: None, 1279 picture: None, 1280 banner: None, 1281 location_primary: None, 1282 location_city: None, 1283 location_region: None, 1284 location_country: None, 1285 }, 1286 ) 1287 .expect("farm") 1288 .result 1289 } 1290 1291 fn create_plot_record(exec: &SqliteExecutor, farm_id: &str, d_tag: &str, name: &str) { 1292 let _ = plot::create( 1293 exec, 1294 &IPlotFields { 1295 d_tag: d_tag.to_string(), 1296 farm_id: farm_id.to_string(), 1297 name: name.to_string(), 1298 about: None, 1299 location_primary: None, 1300 location_city: None, 1301 location_region: None, 1302 location_country: None, 1303 }, 1304 ) 1305 .expect("plot"); 1306 } 1307 1308 fn add_member_record(exec: &SqliteExecutor, farm_id: &str, member_pubkey: &str, role: &str) { 1309 let _ = farm_member::create( 1310 exec, 1311 &IFarmMemberFields { 1312 farm_id: farm_id.to_string(), 1313 member_pubkey: member_pubkey.to_string(), 1314 role: role.to_string(), 1315 }, 1316 ) 1317 .expect("member"); 1318 } 1319 1320 #[test] 1321 fn emit_paths_cover_private_and_public_helpers() { 1322 let exec = SqliteExecutor::open_memory().expect("db"); 1323 let (farm_row, plot_primary, plot_secondary) = seed(&exec); 1324 1325 let by_id = resolve_farm( 1326 &exec, 1327 &RadrootsReplicaFarmSelector { 1328 id: Some(farm_row.id.clone()), 1329 d_tag: None, 1330 pubkey: None, 1331 }, 1332 ) 1333 .expect("resolve by id"); 1334 assert_eq!(by_id.id, farm_row.id); 1335 1336 assert!( 1337 resolve_farm( 1338 &exec, 1339 &RadrootsReplicaFarmSelector { 1340 id: Some("00000000-0000-0000-0000-000000000000".to_string()), 1341 d_tag: None, 1342 pubkey: None, 1343 }, 1344 ) 1345 .is_err() 1346 ); 1347 assert!( 1348 radroots_replica_sync_all_with_options( 1349 &exec, 1350 &RadrootsReplicaFarmSelector { 1351 id: Some("00000000-0000-0000-0000-000000000000".to_string()), 1352 d_tag: None, 1353 pubkey: None, 1354 }, 1355 None, 1356 ) 1357 .is_err() 1358 ); 1359 assert!( 1360 resolve_farm( 1361 &exec, 1362 &RadrootsReplicaFarmSelector { 1363 id: None, 1364 d_tag: None, 1365 pubkey: None, 1366 }, 1367 ) 1368 .is_err() 1369 ); 1370 1371 assert!( 1372 farm::create( 1373 &exec, 1374 &IFarmFields { 1375 d_tag: farm_row.d_tag.clone(), 1376 pubkey: farm_row.pubkey.clone(), 1377 name: "duplicate".to_string(), 1378 about: None, 1379 website: None, 1380 picture: None, 1381 banner: None, 1382 location_primary: None, 1383 location_city: None, 1384 location_region: None, 1385 location_country: None, 1386 }, 1387 ) 1388 .is_err() 1389 ); 1390 let by_identity = resolve_farm( 1391 &exec, 1392 &RadrootsReplicaFarmSelector { 1393 id: None, 1394 d_tag: Some(farm_row.d_tag.clone()), 1395 pubkey: Some(farm_row.pubkey.clone()), 1396 }, 1397 ) 1398 .expect("resolve unique farm"); 1399 assert_eq!(by_identity.id, farm_row.id); 1400 1401 let tags = collect_farm_tags(&exec, &farm_row.id).expect("farm tags"); 1402 assert_eq!(tags, vec!["coffee".to_string()]); 1403 let plot_tags = collect_plot_tags(&exec, &plot_primary.id).expect("plot tags"); 1404 assert_eq!(plot_tags, vec!["orchard".to_string()]); 1405 1406 let members = load_farm_members(&exec, &farm_row.id).expect("members"); 1407 assert_eq!(role_pubkeys(&members, ROLE_MEMBER).len(), 2); 1408 assert_eq!(role_pubkeys(&members, ROLE_OWNER).len(), 1); 1409 assert_eq!(role_pubkeys(&members, ROLE_WORKER).len(), 1); 1410 let plots = load_plots(&exec, &farm_row.id).expect("plots"); 1411 assert_eq!(sorted_plot_ids(&plots).len(), 2); 1412 1413 let farm_location = load_farm_location(&exec, &farm_row).expect("farm location"); 1414 assert!(farm_location.is_some()); 1415 let plot_location_primary = load_plot_location(&exec, &plot_primary).expect("plot primary"); 1416 assert!(plot_location_primary.is_some()); 1417 let plot_location_secondary = 1418 load_plot_location(&exec, &plot_secondary).expect("plot secondary"); 1419 assert!(plot_location_secondary.is_some()); 1420 1421 assert!( 1422 load_relation_by_role(&exec, &farm_row.id, "primary", RelationType::Farm) 1423 .expect("farm primary") 1424 .is_some() 1425 ); 1426 assert!( 1427 load_relation_by_role(&exec, &farm_row.id, "", RelationType::Farm) 1428 .expect("farm fallback") 1429 .is_some() 1430 ); 1431 assert!( 1432 load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot) 1433 .expect("plot fallback") 1434 .is_some() 1435 ); 1436 1437 let mut farm_rel = 1438 farm_gcs_location::find_many(&exec, &IFarmGcsLocationFindMany { filter: None }) 1439 .expect("farm rels") 1440 .results; 1441 let mut plot_rel = 1442 plot_gcs_location::find_many(&exec, &IPlotGcsLocationFindMany { filter: None }) 1443 .expect("plot rels") 1444 .results; 1445 let farm_row_role = RelationRow::Farm(farm_rel.remove(0)).role().to_string(); 1446 let plot_row_role = RelationRow::Plot(plot_rel.remove(0)).role().to_string(); 1447 let _ = farm_row_role; 1448 let _ = plot_row_role; 1449 assert_eq!(location_role_rank(ROLE_PRIMARY), 0); 1450 assert_eq!(location_role_rank("secondary"), 1); 1451 1452 let point_valid = parse_point("{\"type\":\"Point\",\"coordinates\":[1.0,2.0]}", 3.0, 4.0); 1453 assert_eq!(point_valid.coordinates, [1.0, 2.0]); 1454 let point_invalid = parse_point("{", 3.0, 4.0); 1455 assert_eq!(point_invalid.coordinates, [4.0, 3.0]); 1456 let point_empty = parse_point("", 3.0, 4.0); 1457 assert_eq!(point_empty.coordinates, [4.0, 3.0]); 1458 1459 let polygon_valid = parse_polygon( 1460 "{\"type\":\"Polygon\",\"coordinates\":[[[1.0,2.0],[1.1,2.1],[1.0,2.0]]]}", 1461 3.0, 1462 4.0, 1463 ); 1464 assert!(!polygon_valid.coordinates[0].is_empty()); 1465 let polygon_empty_outer = 1466 parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[]}", 3.0, 4.0); 1467 assert!(!polygon_empty_outer.coordinates[0].is_empty()); 1468 let polygon_empty_inner = 1469 parse_polygon("{\"type\":\"Polygon\",\"coordinates\":[[]]}", 3.0, 4.0); 1470 assert!(!polygon_empty_inner.coordinates[0].is_empty()); 1471 let polygon_invalid = parse_polygon("{", 3.0, 4.0); 1472 assert!(!polygon_invalid.coordinates[0].is_empty()); 1473 let polygon_blank = parse_polygon("", 3.0, 4.0); 1474 assert!(!polygon_blank.coordinates[0].is_empty()); 1475 1476 assert!( 1477 load_profile(&exec, &farm_row.pubkey) 1478 .expect("farm profile") 1479 .is_some() 1480 ); 1481 assert!( 1482 load_profile(&exec, &"3".repeat(64)) 1483 .expect("missing profile") 1484 .is_none() 1485 ); 1486 1487 let profile_event_farm = profile_event( 1488 &farm_row.pubkey, 1489 radroots_replica_db_schema::nostr_profile::NostrProfile { 1490 id: "00000000-0000-0000-0000-000000000001".to_string(), 1491 created_at: "2024-01-01T00:00:00.000Z".to_string(), 1492 updated_at: "2024-01-01T00:00:00.000Z".to_string(), 1493 public_key: farm_row.pubkey.clone(), 1494 profile_type: "farm".to_string(), 1495 name: "farm".to_string(), 1496 display_name: None, 1497 about: None, 1498 website: None, 1499 picture: None, 1500 banner: None, 1501 nip05: None, 1502 lud06: None, 1503 lud16: None, 1504 }, 1505 ) 1506 .expect("profile farm"); 1507 assert!(!profile_event_farm.tags.is_empty()); 1508 let profile_event_unknown = profile_event( 1509 &"6".repeat(64), 1510 radroots_replica_db_schema::nostr_profile::NostrProfile { 1511 id: "00000000-0000-0000-0000-000000000002".to_string(), 1512 created_at: "2024-01-01T00:00:00.000Z".to_string(), 1513 updated_at: "2024-01-01T00:00:00.000Z".to_string(), 1514 public_key: "6".repeat(64), 1515 profile_type: "legacy".to_string(), 1516 name: "legacy".to_string(), 1517 display_name: None, 1518 about: None, 1519 website: None, 1520 picture: None, 1521 banner: None, 1522 nip05: None, 1523 lud06: None, 1524 lud16: None, 1525 }, 1526 ) 1527 .expect("profile legacy"); 1528 assert!(profile_event_unknown.tags.is_empty()); 1529 1530 let profile_content = serialize_profile_content(&RadrootsProfile { 1531 name: "name".to_string(), 1532 display_name: Some("display".to_string()), 1533 nip05: Some("nip05".to_string()), 1534 about: Some("about".to_string()), 1535 website: Some("website".to_string()), 1536 picture: Some("picture".to_string()), 1537 banner: Some("banner".to_string()), 1538 lud06: Some("lud06".to_string()), 1539 lud16: Some("lud16".to_string()), 1540 bot: None, 1541 }) 1542 .expect("serialize profile"); 1543 assert!(profile_content.contains("\"name\":\"name\"")); 1544 1545 let member_pubkeys = collect_member_pubkeys(&exec, &farm_row.id).expect("member pubkeys"); 1546 assert!(!member_pubkeys.is_empty()); 1547 let profile_pubkeys = collect_profile_pubkeys(&exec, &farm_row).expect("profile pubkeys"); 1548 assert!(!profile_pubkeys.is_empty()); 1549 let claims = load_member_claims(&exec, &farm_row.pubkey).expect("claims"); 1550 assert!(!claims.is_empty()); 1551 let member_claims = 1552 load_member_claims_for_member(&exec, &"6".repeat(64)).expect("claims by member"); 1553 assert!(!member_claims.is_empty()); 1554 1555 let profile_events = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); 1556 assert!(!profile_events.is_empty()); 1557 let farm_event = radroots_replica_farm_event(&exec, &farm_row).expect("farm event"); 1558 assert_eq!(farm_event.kind, KIND_FARM); 1559 let plot_events = radroots_replica_plot_events(&exec, &farm_row).expect("plot events"); 1560 assert_eq!(plot_events.len(), 2); 1561 let list_sets = radroots_replica_list_set_events(&exec, &farm_row).expect("list sets"); 1562 assert_eq!(list_sets.len(), 4); 1563 let membership_claims = 1564 radroots_replica_membership_claim_events(&exec, &farm_row.pubkey).expect("membership"); 1565 assert!(!membership_claims.is_empty()); 1566 let bundle = radroots_replica_sync_all_with_options( 1567 &exec, 1568 &RadrootsReplicaFarmSelector { 1569 id: Some(farm_row.id.clone()), 1570 d_tag: None, 1571 pubkey: None, 1572 }, 1573 Some(&RadrootsReplicaSyncOptions { 1574 include_profiles: Some(true), 1575 include_list_sets: Some(true), 1576 include_membership_claims: Some(true), 1577 }), 1578 ) 1579 .expect("sync all"); 1580 assert!(!bundle.events.is_empty()); 1581 1582 let _ = exec.exec("PRAGMA foreign_keys = OFF", "[]"); 1583 let _ = plot_gcs_location::create( 1584 &exec, 1585 &IPlotGcsLocationFields { 1586 plot_id: plot_secondary.id.clone(), 1587 gcs_location_id: "00000000-0000-0000-0000-000000000000".to_string(), 1588 role: "".to_string(), 1589 }, 1590 ); 1591 assert!(load_relation_by_role(&exec, &plot_secondary.id, "", RelationType::Plot).is_err()); 1592 1593 let by_pair = farm::find_many( 1594 &exec, 1595 &IFarmFindMany { 1596 filter: Some(IFarmFieldsFilter { 1597 id: None, 1598 created_at: None, 1599 updated_at: None, 1600 d_tag: Some("AAAAAAAAAAAAAAAAAAAAAA".to_string()), 1601 pubkey: Some("f".repeat(64)), 1602 name: None, 1603 about: None, 1604 website: None, 1605 picture: None, 1606 banner: None, 1607 location_primary: None, 1608 location_city: None, 1609 location_region: None, 1610 location_country: None, 1611 }), 1612 }, 1613 ) 1614 .expect("by pair"); 1615 assert!(!by_pair.results.is_empty()); 1616 1617 let plots_lookup = plot::find_many( 1618 &exec, 1619 &IPlotFindMany { 1620 filter: Some(IPlotFieldsFilter { 1621 id: None, 1622 created_at: None, 1623 updated_at: None, 1624 d_tag: None, 1625 farm_id: Some(farm_row.id), 1626 name: None, 1627 about: None, 1628 location_primary: None, 1629 location_city: None, 1630 location_region: None, 1631 location_country: None, 1632 }), 1633 }, 1634 ) 1635 .expect("plots lookup"); 1636 assert_eq!(plots_lookup.results.len(), 2); 1637 } 1638 1639 #[test] 1640 fn emit_option_toggles_and_empty_rows_cover_branches() { 1641 let exec = SqliteExecutor::open_memory().expect("db"); 1642 migrations::run_all_up(&exec).expect("migrations"); 1643 1644 let farm = farm::create( 1645 &exec, 1646 &IFarmFields { 1647 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 1648 pubkey: "b".repeat(64), 1649 name: "farm-empty".to_string(), 1650 about: None, 1651 website: None, 1652 picture: None, 1653 banner: None, 1654 location_primary: None, 1655 location_city: None, 1656 location_region: None, 1657 location_country: None, 1658 }, 1659 ) 1660 .expect("farm") 1661 .result; 1662 1663 let _plot = plot::create( 1664 &exec, 1665 &IPlotFields { 1666 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 1667 farm_id: farm.id.clone(), 1668 name: "plot-empty".to_string(), 1669 about: None, 1670 location_primary: None, 1671 location_city: None, 1672 location_region: None, 1673 location_country: None, 1674 }, 1675 ) 1676 .expect("plot"); 1677 1678 let selector = RadrootsReplicaFarmSelector { 1679 id: Some(farm.id.clone()), 1680 d_tag: None, 1681 pubkey: None, 1682 }; 1683 let bundle = radroots_replica_sync_all_with_options( 1684 &exec, 1685 &selector, 1686 Some(&RadrootsReplicaSyncOptions { 1687 include_profiles: Some(false), 1688 include_list_sets: Some(false), 1689 include_membership_claims: Some(false), 1690 }), 1691 ) 1692 .expect("sync"); 1693 assert_eq!(bundle.events.len(), 2); 1694 1695 let farm_event = radroots_replica_farm_event(&exec, &farm).expect("farm event"); 1696 assert_eq!(farm_event.kind, KIND_FARM); 1697 let plot_events = radroots_replica_plot_events(&exec, &farm).expect("plot events"); 1698 assert_eq!(plot_events.len(), 1); 1699 1700 let claims = 1701 radroots_replica_membership_claim_events(&exec, &"3".repeat(64)).expect("empty claims"); 1702 assert!(claims.is_empty()); 1703 1704 let by_pair = resolve_farm( 1705 &exec, 1706 &RadrootsReplicaFarmSelector { 1707 id: None, 1708 d_tag: Some(farm.d_tag.clone()), 1709 pubkey: Some(farm.pubkey.clone()), 1710 }, 1711 ) 1712 .expect("resolve by pair"); 1713 assert_eq!(by_pair.id, farm.id); 1714 } 1715 1716 #[test] 1717 fn emit_profile_variants_and_missing_profiles_are_handled() { 1718 let exec = SqliteExecutor::open_memory().expect("db"); 1719 let (farm_row, _, _) = seed(&exec); 1720 1721 let _ = farm_member::create( 1722 &exec, 1723 &IFarmMemberFields { 1724 farm_id: farm_row.id.clone(), 1725 member_pubkey: "3".repeat(64), 1726 role: ROLE_MEMBER.to_string(), 1727 }, 1728 ) 1729 .expect("member"); 1730 1731 let profiles = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); 1732 assert!(!profiles.is_empty()); 1733 1734 let profile_coop = profile_event( 1735 &"c".repeat(64), 1736 radroots_replica_db_schema::nostr_profile::NostrProfile { 1737 id: "00000000-0000-0000-0000-0000000000c0".to_string(), 1738 created_at: "2024-01-01T00:00:00.000Z".to_string(), 1739 updated_at: "2024-01-01T00:00:00.000Z".to_string(), 1740 public_key: "c".repeat(64), 1741 profile_type: "coop".to_string(), 1742 name: "coop".to_string(), 1743 display_name: None, 1744 about: None, 1745 website: None, 1746 picture: None, 1747 banner: None, 1748 nip05: None, 1749 lud06: None, 1750 lud16: None, 1751 }, 1752 ) 1753 .expect("profile coop"); 1754 assert!(!profile_coop.tags.is_empty()); 1755 1756 let profile_any = profile_event( 1757 &"a".repeat(64), 1758 radroots_replica_db_schema::nostr_profile::NostrProfile { 1759 id: "00000000-0000-0000-0000-0000000000a0".to_string(), 1760 created_at: "2024-01-01T00:00:00.000Z".to_string(), 1761 updated_at: "2024-01-01T00:00:00.000Z".to_string(), 1762 public_key: "a".repeat(64), 1763 profile_type: "any".to_string(), 1764 name: "any".to_string(), 1765 display_name: None, 1766 about: None, 1767 website: None, 1768 picture: None, 1769 banner: None, 1770 nip05: None, 1771 lud06: None, 1772 lud16: None, 1773 }, 1774 ) 1775 .expect("profile any"); 1776 assert!(!profile_any.tags.is_empty()); 1777 1778 let profile_sparse = serialize_profile_content(&RadrootsProfile { 1779 name: "sparse".to_string(), 1780 display_name: None, 1781 nip05: None, 1782 about: None, 1783 website: None, 1784 picture: None, 1785 banner: None, 1786 lud06: None, 1787 lud16: None, 1788 bot: None, 1789 }) 1790 .expect("serialize"); 1791 assert!(profile_sparse.contains("\"name\"")); 1792 } 1793 1794 #[test] 1795 fn emit_query_error_paths_are_reported() { 1796 let exec = SqliteExecutor::open_memory().expect("db"); 1797 migrations::run_all_up(&exec).expect("migrations"); 1798 1799 let farm = Farm { 1800 id: "farm".to_string(), 1801 created_at: "now".to_string(), 1802 updated_at: "now".to_string(), 1803 d_tag: "d".to_string(), 1804 pubkey: "9".repeat(64), 1805 name: "farm".to_string(), 1806 about: None, 1807 website: None, 1808 picture: None, 1809 banner: None, 1810 location_primary: None, 1811 location_city: None, 1812 location_region: None, 1813 location_country: None, 1814 }; 1815 let plot = Plot { 1816 id: "plot".to_string(), 1817 created_at: "now".to_string(), 1818 updated_at: "now".to_string(), 1819 d_tag: "plot".to_string(), 1820 farm_id: farm.id.clone(), 1821 name: "plot".to_string(), 1822 about: None, 1823 location_primary: None, 1824 location_city: None, 1825 location_region: None, 1826 location_country: None, 1827 }; 1828 1829 let tags_fail = QueryFailExecutor { 1830 inner: &exec, 1831 needle: "farm_tag", 1832 err: SqlError::Internal, 1833 }; 1834 assert!(collect_farm_tags(&tags_fail, "id").is_err()); 1835 1836 let plot_tags_fail = QueryFailExecutor { 1837 inner: &exec, 1838 needle: "plot_tag", 1839 err: SqlError::Internal, 1840 }; 1841 assert!(collect_plot_tags(&plot_tags_fail, "id").is_err()); 1842 1843 let members_fail = QueryFailExecutor { 1844 inner: &exec, 1845 needle: "farm_member", 1846 err: SqlError::Internal, 1847 }; 1848 assert!(load_farm_members(&members_fail, "id").is_err()); 1849 1850 let plots_fail = QueryFailExecutor { 1851 inner: &exec, 1852 needle: "from plot", 1853 err: SqlError::Internal, 1854 }; 1855 assert!(load_plots(&plots_fail, "id").is_err()); 1856 1857 let farm_location_fail = QueryFailExecutor { 1858 inner: &exec, 1859 needle: "farm_gcs_location", 1860 err: SqlError::Internal, 1861 }; 1862 assert!(load_farm_location(&farm_location_fail, &farm).is_err()); 1863 1864 let plot_location_fail = QueryFailExecutor { 1865 inner: &exec, 1866 needle: "plot_gcs_location", 1867 err: SqlError::Internal, 1868 }; 1869 assert!(load_plot_location(&plot_location_fail, &plot).is_err()); 1870 1871 let profile_fail = QueryFailExecutor { 1872 inner: &exec, 1873 needle: "nostr_profile", 1874 err: SqlError::Internal, 1875 }; 1876 assert!(load_profile(&profile_fail, "p").is_err()); 1877 1878 let claims_fail = QueryFailExecutor { 1879 inner: &exec, 1880 needle: "farm_member_claim", 1881 err: SqlError::Internal, 1882 }; 1883 assert!(load_member_claims(&claims_fail, "p").is_err()); 1884 assert!(load_member_claims_for_member(&claims_fail, "p").is_err()); 1885 assert!(collect_profile_pubkeys(&claims_fail, &farm).is_err()); 1886 } 1887 1888 #[test] 1889 fn load_farm_location_preserves_string_only_locations() { 1890 let exec = SqliteExecutor::open_memory().expect("db"); 1891 migrations::run_all_up(&exec).expect("migrations"); 1892 let farm_row = farm::create( 1893 &exec, 1894 &IFarmFields { 1895 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 1896 pubkey: "f".repeat(64), 1897 name: "string-only farm".to_string(), 1898 about: None, 1899 website: None, 1900 picture: None, 1901 banner: None, 1902 location_primary: Some("San Francisco, CA".to_string()), 1903 location_city: Some("San Francisco".to_string()), 1904 location_region: Some("CA".to_string()), 1905 location_country: Some("US".to_string()), 1906 }, 1907 ) 1908 .expect("farm") 1909 .result; 1910 1911 let location = load_farm_location(&exec, &farm_row) 1912 .expect("location query") 1913 .expect("string-only location"); 1914 assert_eq!(location.primary.as_deref(), Some("San Francisco, CA")); 1915 assert_eq!(location.city.as_deref(), Some("San Francisco")); 1916 assert_eq!(location.region.as_deref(), Some("CA")); 1917 assert_eq!(location.country.as_deref(), Some("US")); 1918 assert!(location.gcs.is_none()); 1919 } 1920 1921 #[test] 1922 fn emit_propagates_queryfail_and_builder_errors() { 1923 let exec = SqliteExecutor::open_memory().expect("db"); 1924 let (farm_row, _, _) = seed(&exec); 1925 let selector = RadrootsReplicaFarmSelector { 1926 id: Some(farm_row.id.clone()), 1927 d_tag: None, 1928 pubkey: None, 1929 }; 1930 1931 let profile_query_fail = QueryFailExecutor { 1932 inner: &exec, 1933 needle: "farm_member_claim", 1934 err: SqlError::Internal, 1935 }; 1936 assert!(radroots_replica_profile_events(&profile_query_fail, &farm_row).is_err()); 1937 let profile_load_fail = QueryFailExecutor { 1938 inner: &exec, 1939 needle: "nostr_profile", 1940 err: SqlError::Internal, 1941 }; 1942 assert!(radroots_replica_profile_events(&profile_load_fail, &farm_row).is_err()); 1943 1944 let farm_tag_fail = QueryFailExecutor { 1945 inner: &exec, 1946 needle: "farm_tag", 1947 err: SqlError::Internal, 1948 }; 1949 assert!(radroots_replica_farm_event(&farm_tag_fail, &farm_row).is_err()); 1950 1951 let plot_tag_fail = QueryFailExecutor { 1952 inner: &exec, 1953 needle: "plot_tag", 1954 err: SqlError::Internal, 1955 }; 1956 assert!(radroots_replica_plot_events(&plot_tag_fail, &farm_row).is_err()); 1957 1958 let list_set_fail = QueryFailExecutor { 1959 inner: &exec, 1960 needle: "farm_member", 1961 err: SqlError::Internal, 1962 }; 1963 assert!(radroots_replica_list_set_events(&list_set_fail, &farm_row).is_err()); 1964 1965 let claims_fail = QueryFailExecutor { 1966 inner: &exec, 1967 needle: "farm_member_claim", 1968 err: SqlError::Internal, 1969 }; 1970 assert!(radroots_replica_membership_claim_events(&claims_fail, &farm_row.pubkey).is_err()); 1971 1972 let sync_profiles_fail = QueryFailExecutor { 1973 inner: &exec, 1974 needle: "nostr_profile", 1975 err: SqlError::Internal, 1976 }; 1977 assert!( 1978 radroots_replica_sync_all_with_options( 1979 &sync_profiles_fail, 1980 &selector, 1981 Some(&RadrootsReplicaSyncOptions { 1982 include_profiles: Some(true), 1983 include_list_sets: Some(false), 1984 include_membership_claims: Some(false), 1985 }), 1986 ) 1987 .is_err() 1988 ); 1989 let sync_farm_fail = QueryFailExecutor { 1990 inner: &exec, 1991 needle: "farm_tag", 1992 err: SqlError::Internal, 1993 }; 1994 assert!( 1995 radroots_replica_sync_all_with_options( 1996 &sync_farm_fail, 1997 &selector, 1998 Some(&RadrootsReplicaSyncOptions { 1999 include_profiles: Some(false), 2000 include_list_sets: Some(false), 2001 include_membership_claims: Some(false), 2002 }), 2003 ) 2004 .is_err() 2005 ); 2006 let sync_plot_fail = QueryFailExecutor { 2007 inner: &exec, 2008 needle: "plot_tag", 2009 err: SqlError::Internal, 2010 }; 2011 assert!( 2012 radroots_replica_sync_all_with_options( 2013 &sync_plot_fail, 2014 &selector, 2015 Some(&RadrootsReplicaSyncOptions { 2016 include_profiles: Some(false), 2017 include_list_sets: Some(false), 2018 include_membership_claims: Some(false), 2019 }), 2020 ) 2021 .is_err() 2022 ); 2023 let sync_list_set_fail = QueryFailExecutor { 2024 inner: &exec, 2025 needle: "farm_member", 2026 err: SqlError::Internal, 2027 }; 2028 assert!( 2029 radroots_replica_sync_all_with_options( 2030 &sync_list_set_fail, 2031 &selector, 2032 Some(&RadrootsReplicaSyncOptions { 2033 include_profiles: Some(false), 2034 include_list_sets: Some(true), 2035 include_membership_claims: Some(false), 2036 }), 2037 ) 2038 .is_err() 2039 ); 2040 let sync_claims_fail = QueryFailExecutor { 2041 inner: &exec, 2042 needle: "farm_member_claim", 2043 err: SqlError::Internal, 2044 }; 2045 assert!( 2046 radroots_replica_sync_all_with_options( 2047 &sync_claims_fail, 2048 &selector, 2049 Some(&RadrootsReplicaSyncOptions { 2050 include_profiles: Some(false), 2051 include_list_sets: Some(false), 2052 include_membership_claims: Some(true), 2053 }), 2054 ) 2055 .is_err() 2056 ); 2057 2058 assert!(radroots_replica_farm_event(&exec, &farm_row).is_ok()); 2059 assert!(radroots_replica_plot_events(&exec, &farm_row).is_ok()); 2060 } 2061 2062 #[test] 2063 fn emit_additional_error_branches_are_reported() { 2064 let exec = SqliteExecutor::open_memory().expect("db"); 2065 let (farm_row, _, _) = seed(&exec); 2066 2067 crate::canonical::failpoints::set_error(); 2068 assert!(radroots_replica_profile_events(&exec, &farm_row).is_err()); 2069 let farm_profile = load_profile(&exec, &farm_row.pubkey) 2070 .expect("load profile") 2071 .expect("farm profile"); 2072 crate::canonical::failpoints::set_error(); 2073 assert!(profile_event(&farm_row.pubkey, farm_profile).is_err()); 2074 2075 let farm_location_fail = QueryFailExecutor { 2076 inner: &exec, 2077 needle: "farm_gcs_location", 2078 err: SqlError::Internal, 2079 }; 2080 assert!(radroots_replica_farm_event(&farm_location_fail, &farm_row).is_err()); 2081 let invalid_farm = Farm { 2082 id: farm_row.id.clone(), 2083 created_at: farm_row.created_at.clone(), 2084 updated_at: farm_row.updated_at.clone(), 2085 d_tag: "invalid".to_string(), 2086 pubkey: farm_row.pubkey.clone(), 2087 name: farm_row.name.clone(), 2088 about: farm_row.about.clone(), 2089 website: farm_row.website.clone(), 2090 picture: farm_row.picture.clone(), 2091 banner: farm_row.banner.clone(), 2092 location_primary: farm_row.location_primary.clone(), 2093 location_city: farm_row.location_city.clone(), 2094 location_region: farm_row.location_region.clone(), 2095 location_country: farm_row.location_country.clone(), 2096 }; 2097 assert!(radroots_replica_farm_event(&exec, &invalid_farm).is_err()); 2098 crate::canonical::failpoints::set_error(); 2099 assert!(radroots_replica_farm_event(&exec, &farm_row).is_err()); 2100 2101 let plots_fail = QueryFailExecutor { 2102 inner: &exec, 2103 needle: "from plot", 2104 err: SqlError::Internal, 2105 }; 2106 assert!(radroots_replica_plot_events(&plots_fail, &farm_row).is_err()); 2107 let plot_location_fail = QueryFailExecutor { 2108 inner: &exec, 2109 needle: "plot_gcs_location", 2110 err: SqlError::Internal, 2111 }; 2112 assert!(radroots_replica_plot_events(&plot_location_fail, &farm_row).is_err()); 2113 crate::canonical::failpoints::set_error(); 2114 assert!(radroots_replica_plot_events(&exec, &farm_row).is_err()); 2115 create_plot_record(&exec, &farm_row.id, "invalid", "plot-invalid"); 2116 assert!(radroots_replica_plot_events(&exec, &farm_row).is_err()); 2117 2118 let resolve_id_fail = QueryFailExecutor { 2119 inner: &exec, 2120 needle: "from farm", 2121 err: SqlError::Internal, 2122 }; 2123 assert!( 2124 resolve_farm( 2125 &resolve_id_fail, 2126 &RadrootsReplicaFarmSelector { 2127 id: Some(farm_row.id.clone()), 2128 d_tag: None, 2129 pubkey: None, 2130 } 2131 ) 2132 .is_err() 2133 ); 2134 let resolve_pair_fail = QueryFailExecutor { 2135 inner: &exec, 2136 needle: "from farm", 2137 err: SqlError::Internal, 2138 }; 2139 assert!( 2140 resolve_farm( 2141 &resolve_pair_fail, 2142 &RadrootsReplicaFarmSelector { 2143 id: None, 2144 d_tag: Some(farm_row.d_tag.clone()), 2145 pubkey: Some(farm_row.pubkey.clone()), 2146 } 2147 ) 2148 .is_err() 2149 ); 2150 2151 let gcs_query_fail = QueryFailExecutor { 2152 inner: &exec, 2153 needle: "from gcs_location", 2154 err: SqlError::Internal, 2155 }; 2156 assert!( 2157 load_relation_by_role( 2158 &gcs_query_fail, 2159 &farm_row.id, 2160 ROLE_PRIMARY, 2161 RelationType::Farm 2162 ) 2163 .is_err() 2164 ); 2165 super::failpoints::set_gcs_location_to_event_error(); 2166 assert!( 2167 load_relation_by_role(&exec, &farm_row.id, ROLE_PRIMARY, RelationType::Farm).is_err() 2168 ); 2169 2170 let member_fail = QueryFailExecutor { 2171 inner: &exec, 2172 needle: "farm_member", 2173 err: SqlError::Internal, 2174 }; 2175 assert!(collect_member_pubkeys(&member_fail, &farm_row.id).is_err()); 2176 assert!(collect_profile_pubkeys(&member_fail, &farm_row).is_err()); 2177 2178 let list_plot_fail = QueryFailExecutor { 2179 inner: &exec, 2180 needle: "from plot", 2181 err: SqlError::Internal, 2182 }; 2183 assert!(radroots_replica_list_set_events(&list_plot_fail, &farm_row).is_err()); 2184 2185 let list_member_error_farm = create_farm_record( 2186 &exec, 2187 "AAAAAAAAAAAAAAAAAAAAAA", 2188 &"1".repeat(64), 2189 "list-member-error", 2190 ); 2191 add_member_record( 2192 &exec, 2193 &list_member_error_farm.id, 2194 &" ".repeat(64), 2195 ROLE_MEMBER, 2196 ); 2197 create_plot_record( 2198 &exec, 2199 &list_member_error_farm.id, 2200 "AAAAAAAAAAAAAAAAAAAAAQ", 2201 "plot-member-error", 2202 ); 2203 assert!(radroots_replica_list_set_events(&exec, &list_member_error_farm).is_err()); 2204 2205 let list_owner_error_farm = create_farm_record( 2206 &exec, 2207 "AAAAAAAAAAAAAAAAAAAAAA", 2208 &"2".repeat(64), 2209 "list-owner-error", 2210 ); 2211 add_member_record( 2212 &exec, 2213 &list_owner_error_farm.id, 2214 &"a".repeat(64), 2215 ROLE_MEMBER, 2216 ); 2217 add_member_record( 2218 &exec, 2219 &list_owner_error_farm.id, 2220 &" ".repeat(64), 2221 ROLE_OWNER, 2222 ); 2223 add_member_record( 2224 &exec, 2225 &list_owner_error_farm.id, 2226 &"b".repeat(64), 2227 ROLE_WORKER, 2228 ); 2229 create_plot_record( 2230 &exec, 2231 &list_owner_error_farm.id, 2232 "AAAAAAAAAAAAAAAAAAAAAQ", 2233 "plot-owner-error", 2234 ); 2235 assert!(radroots_replica_list_set_events(&exec, &list_owner_error_farm).is_err()); 2236 2237 let list_worker_error_farm = create_farm_record( 2238 &exec, 2239 "AAAAAAAAAAAAAAAAAAAAAA", 2240 &"3".repeat(64), 2241 "list-worker-error", 2242 ); 2243 add_member_record( 2244 &exec, 2245 &list_worker_error_farm.id, 2246 &"c".repeat(64), 2247 ROLE_MEMBER, 2248 ); 2249 add_member_record( 2250 &exec, 2251 &list_worker_error_farm.id, 2252 &"d".repeat(64), 2253 ROLE_OWNER, 2254 ); 2255 add_member_record( 2256 &exec, 2257 &list_worker_error_farm.id, 2258 &" ".repeat(64), 2259 ROLE_WORKER, 2260 ); 2261 create_plot_record( 2262 &exec, 2263 &list_worker_error_farm.id, 2264 "AAAAAAAAAAAAAAAAAAAAAQ", 2265 "plot-worker-error", 2266 ); 2267 assert!(radroots_replica_list_set_events(&exec, &list_worker_error_farm).is_err()); 2268 2269 let list_plot_error_farm = create_farm_record( 2270 &exec, 2271 "AAAAAAAAAAAAAAAAAAAAAA", 2272 &"4".repeat(64), 2273 "list-plot-error", 2274 ); 2275 add_member_record( 2276 &exec, 2277 &list_plot_error_farm.id, 2278 &"e".repeat(64), 2279 ROLE_MEMBER, 2280 ); 2281 add_member_record(&exec, &list_plot_error_farm.id, &"f".repeat(64), ROLE_OWNER); 2282 add_member_record( 2283 &exec, 2284 &list_plot_error_farm.id, 2285 &"7".repeat(64), 2286 ROLE_WORKER, 2287 ); 2288 create_plot_record(&exec, &list_plot_error_farm.id, "", "plot-list-error"); 2289 assert!(radroots_replica_list_set_events(&exec, &list_plot_error_farm).is_err()); 2290 2291 let clean_exec = SqliteExecutor::open_memory().expect("db clean"); 2292 let (clean_farm, _, _) = seed(&clean_exec); 2293 super::failpoints::set_list_set_to_wire_error(); 2294 assert!(radroots_replica_list_set_events(&clean_exec, &clean_farm).is_err()); 2295 2296 let invalid_list_set = radroots_events::list_set::RadrootsListSet { 2297 d_tag: String::new(), 2298 content: String::new(), 2299 entries: Vec::new(), 2300 title: None, 2301 description: None, 2302 image: None, 2303 }; 2304 assert!(list_set_to_wire_parts(&invalid_list_set).is_err()); 2305 2306 let claims_member_query_fail = QueryFailExecutor { 2307 inner: &exec, 2308 needle: "where member_pubkey", 2309 err: SqlError::Internal, 2310 }; 2311 assert!( 2312 radroots_replica_membership_claim_events(&claims_member_query_fail, &farm_row.pubkey) 2313 .is_err() 2314 ); 2315 2316 let _ = farm_member_claim::create( 2317 &exec, 2318 &IFarmMemberClaimFields { 2319 member_pubkey: "a".repeat(64), 2320 farm_pubkey: " ".repeat(64), 2321 }, 2322 ) 2323 .expect("empty-farm-pubkey claim"); 2324 assert!(radroots_replica_membership_claim_events(&exec, &" ".repeat(64)).is_err()); 2325 2326 super::failpoints::set_list_set_to_wire_error(); 2327 assert!(radroots_replica_membership_claim_events(&clean_exec, &clean_farm.pubkey).is_err()); 2328 } 2329 2330 #[test] 2331 fn emit_list_set_wire_error_paths_are_reported() { 2332 let exec = SqliteExecutor::open_memory().expect("db"); 2333 let (farm_row, _, _) = seed(&exec); 2334 2335 super::failpoints::set_list_set_to_wire_error(); 2336 assert!(radroots_replica_list_set_events(&exec, &farm_row).is_err()); 2337 2338 let invalid_list_set = radroots_events::list_set::RadrootsListSet { 2339 d_tag: String::new(), 2340 content: String::new(), 2341 entries: Vec::new(), 2342 title: None, 2343 description: None, 2344 image: None, 2345 }; 2346 assert!(list_set_to_wire_parts(&invalid_list_set).is_err()); 2347 } 2348 2349 #[test] 2350 fn emit_pass_through_executor_instantiation_paths_are_covered() { 2351 let exec = SqliteExecutor::open_memory().expect("db"); 2352 let (farm_row, _, plot_secondary) = seed(&exec); 2353 2354 let pass = QueryFailExecutor { 2355 inner: &exec, 2356 needle: "__never_match__", 2357 err: SqlError::Internal, 2358 }; 2359 2360 let selector = RadrootsReplicaFarmSelector { 2361 id: Some(farm_row.id.clone()), 2362 d_tag: None, 2363 pubkey: None, 2364 }; 2365 let request = RadrootsReplicaSyncRequest { 2366 farm: selector.clone(), 2367 options: None, 2368 }; 2369 let bundle = radroots_replica_sync_all(&pass, &request).expect("sync via pass executor"); 2370 assert!(!bundle.events.is_empty()); 2371 2372 let resolved_by_id = resolve_farm(&pass, &selector).expect("resolve by id"); 2373 assert_eq!(resolved_by_id.id, farm_row.id); 2374 let resolved_by_pair = resolve_farm( 2375 &pass, 2376 &RadrootsReplicaFarmSelector { 2377 id: None, 2378 d_tag: Some(farm_row.d_tag.clone()), 2379 pubkey: Some(farm_row.pubkey.clone()), 2380 }, 2381 ) 2382 .expect("resolve by pair"); 2383 assert_eq!(resolved_by_pair.id, farm_row.id); 2384 let duplicate_pair = DuplicateFarmSelectorExecutor { 2385 inner: &exec, 2386 duplicated_rows_json: { 2387 let farm_json = serde_json::to_string(&farm_row).expect("farm json"); 2388 format!("[{farm_json},{farm_json}]") 2389 }, 2390 }; 2391 duplicate_pair.begin().expect("duplicate begin"); 2392 duplicate_pair.rollback().expect("duplicate rollback"); 2393 duplicate_pair.begin().expect("duplicate begin"); 2394 duplicate_pair.commit().expect("duplicate commit"); 2395 duplicate_pair 2396 .exec("CREATE TABLE duplicate_probe (id INTEGER)", "[]") 2397 .expect("duplicate exec"); 2398 let duplicate_err = resolve_farm( 2399 &duplicate_pair, 2400 &RadrootsReplicaFarmSelector { 2401 id: None, 2402 d_tag: Some(farm_row.d_tag.clone()), 2403 pubkey: Some(farm_row.pubkey.clone()), 2404 }, 2405 ) 2406 .map(|_| ()) 2407 .unwrap_err(); 2408 assert!( 2409 duplicate_err 2410 .to_string() 2411 .contains("did not resolve to a single farm") 2412 ); 2413 assert!( 2414 resolve_farm( 2415 &pass, 2416 &RadrootsReplicaFarmSelector { 2417 id: Some("00000000-0000-0000-0000-000000000000".to_string()), 2418 d_tag: None, 2419 pubkey: None, 2420 }, 2421 ) 2422 .is_err() 2423 ); 2424 2425 let member_pubkeys = collect_member_pubkeys(&pass, &farm_row.id).expect("member pubkeys"); 2426 assert!(!member_pubkeys.is_empty()); 2427 let profile_pubkeys = collect_profile_pubkeys(&pass, &farm_row).expect("profile pubkeys"); 2428 assert!(!profile_pubkeys.is_empty()); 2429 2430 assert!( 2431 load_relation_by_role(&pass, &farm_row.id, ROLE_PRIMARY, RelationType::Farm) 2432 .expect("farm primary relation") 2433 .is_some() 2434 ); 2435 assert!( 2436 load_relation_by_role(&pass, &farm_row.id, "", RelationType::Farm) 2437 .expect("farm fallback relation") 2438 .is_some() 2439 ); 2440 assert!( 2441 load_relation_by_role(&pass, &plot_secondary.id, "", RelationType::Plot) 2442 .expect("plot fallback relation") 2443 .is_some() 2444 ); 2445 } 2446 2447 #[test] 2448 fn emit_executor_trait_method_paths_are_covered() { 2449 let sqlite = SqliteExecutor::open_memory().expect("db"); 2450 migrations::run_all_up(&sqlite).expect("migrations"); 2451 2452 let err_exec = ErrorExecutor; 2453 assert!(err_exec.exec("SELECT 1", "[]").is_err()); 2454 assert!(err_exec.query_raw("SELECT 1", "[]").is_err()); 2455 assert!(err_exec.begin().is_ok()); 2456 assert!(err_exec.commit().is_ok()); 2457 assert!(err_exec.rollback().is_ok()); 2458 2459 let pass = QueryFailExecutor { 2460 inner: &sqlite, 2461 needle: "__never_match__", 2462 err: SqlError::Internal, 2463 }; 2464 assert!(pass.exec("PRAGMA foreign_keys = ON", "[]").is_ok()); 2465 assert!(pass.query_raw("SELECT 1", "[]").is_ok()); 2466 assert!(pass.begin().is_ok()); 2467 assert!(pass.commit().is_ok()); 2468 2469 let pass_rollback = QueryFailExecutor { 2470 inner: &sqlite, 2471 needle: "__never_match__", 2472 err: SqlError::Internal, 2473 }; 2474 assert!(pass_rollback.begin().is_ok()); 2475 assert!(pass_rollback.rollback().is_ok()); 2476 2477 let fail_query = QueryFailExecutor { 2478 inner: &sqlite, 2479 needle: "select 1", 2480 err: SqlError::Internal, 2481 }; 2482 assert!(fail_query.query_raw("SELECT 1", "[]").is_err()); 2483 } 2484 }