ingest_roundtrip.rs (68239B)
1 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp}; 2 use radroots_event::envelope::kind::{ 3 KIND_FARM, KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE, 4 }; 5 use radroots_event::farm::change_set::{GcsLocation, GeoJsonPoint, GeoJsonPolygon}; 6 use radroots_event::farm::plot::{Plot, PlotLocation}; 7 use radroots_event::farm::{Farm, FarmPublicLocation, FarmRef}; 8 use radroots_event::profile::{ 9 ProfileType, RADROOTS_PROFILE_TYPE_TAG_KEY, radroots_profile_type_tag_value, 10 }; 11 use radroots_event::social::list::ListEntry; 12 use radroots_event::social::list_set::ListSet; 13 use radroots_event::{envelope::EventEnvelope, envelope::EventEnvelopeParts}; 14 use radroots_event_codec::encode::farm as farm_encode; 15 use radroots_event_codec::encode::farm as farm_list_sets; 16 use radroots_event_codec::encode::list_set as list_set_encode; 17 use radroots_event_codec::encode::plot as plot_encode; 18 use radroots_event_codec::{decode::EventParseError, encode::EventEncodeError}; 19 use radroots_nostr::event::from_nostr; 20 use radroots_replica_schema::ReplicaSchemaError; 21 use radroots_replica_schema::farm::{IFarmFields, IFarmFieldsFilter, IFarmFindMany}; 22 use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; 23 use radroots_replica_schema::farm_member::{ 24 IFarmMemberFields, IFarmMemberFieldsFilter, IFarmMemberFindMany, 25 }; 26 use radroots_replica_schema::farm_member_claim::{ 27 IFarmMemberClaimFields, IFarmMemberClaimFieldsFilter, IFarmMemberClaimFindMany, 28 }; 29 use radroots_replica_schema::farm_tag::{IFarmTagFields, IFarmTagFieldsFilter, IFarmTagFindMany}; 30 use radroots_replica_schema::gcs_location::IGcsLocationFields; 31 use radroots_replica_schema::nostr_profile::INostrProfileFields; 32 use radroots_replica_schema::plot::IPlotFields; 33 use radroots_replica_schema::plot_gcs_location::IPlotGcsLocationFields; 34 use radroots_replica_schema::plot_tag::{IPlotTagFields, IPlotTagFieldsFilter, IPlotTagFindMany}; 35 use radroots_replica_store::{ 36 farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, migrations, 37 nostr_profile, plot, plot_gcs_location, plot_tag, 38 }; 39 use radroots_replica_sync::{ 40 RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaEventsError, 41 RadrootsReplicaFarmSelector, RadrootsReplicaIngestOutcome, RadrootsReplicaSyncOptions, 42 RadrootsReplicaSyncRequest, radroots_replica_ingest_event, radroots_replica_sync_all, 43 radroots_replica_sync_status, 44 }; 45 use radroots_sql_core::SqlxSqliteExecutor; 46 use radroots_sql_core::error::SqlError; 47 use radroots_sql_core::{ExecOutcome, SqlExecutor}; 48 use std::panic; 49 50 fn fixture_public_key(seed: u8) -> String { 51 Keys::parse(&format!("{seed:064x}")) 52 .expect("fixture secret key") 53 .public_key() 54 .to_hex() 55 } 56 57 fn unwrap_sql<T>(result: Result<T, ReplicaSchemaError<SqlError>>, label: &str) -> T { 58 match result { 59 Ok(value) => value, 60 Err(err) => panic!("{label}: {}", err.error), 61 } 62 } 63 64 struct BeginFailExecutor<'a> { 65 inner: &'a SqlxSqliteExecutor, 66 } 67 68 impl SqlExecutor for BeginFailExecutor<'_> { 69 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 70 self.inner.exec(sql, params_json) 71 } 72 73 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 74 self.inner.query_raw(sql, params_json) 75 } 76 77 fn begin(&self) -> Result<(), SqlError> { 78 Err(SqlError::Internal) 79 } 80 81 fn commit(&self) -> Result<(), SqlError> { 82 self.inner.commit() 83 } 84 85 fn rollback(&self) -> Result<(), SqlError> { 86 self.inner.rollback() 87 } 88 } 89 90 struct CommitFailExecutor<'a> { 91 inner: &'a SqlxSqliteExecutor, 92 } 93 94 impl SqlExecutor for CommitFailExecutor<'_> { 95 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 96 self.inner.exec(sql, params_json) 97 } 98 99 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 100 self.inner.query_raw(sql, params_json) 101 } 102 103 fn begin(&self) -> Result<(), SqlError> { 104 self.inner.begin() 105 } 106 107 fn commit(&self) -> Result<(), SqlError> { 108 Err(SqlError::Internal) 109 } 110 111 fn rollback(&self) -> Result<(), SqlError> { 112 self.inner.rollback() 113 } 114 } 115 116 struct DeleteFailExecutor<'a> { 117 inner: &'a SqlxSqliteExecutor, 118 table_name: &'static str, 119 err: SqlError, 120 } 121 122 impl SqlExecutor for DeleteFailExecutor<'_> { 123 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 124 if sql.contains("DELETE") && sql.contains(self.table_name) { 125 return Err(self.err.clone()); 126 } 127 self.inner.exec(sql, params_json) 128 } 129 130 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 131 self.inner.query_raw(sql, params_json) 132 } 133 134 fn begin(&self) -> Result<(), SqlError> { 135 self.inner.begin() 136 } 137 138 fn commit(&self) -> Result<(), SqlError> { 139 self.inner.commit() 140 } 141 142 fn rollback(&self) -> Result<(), SqlError> { 143 self.inner.rollback() 144 } 145 } 146 147 struct QueryFailExecutor<'a> { 148 inner: &'a SqlxSqliteExecutor, 149 needle: &'static str, 150 err: SqlError, 151 } 152 153 impl SqlExecutor for QueryFailExecutor<'_> { 154 fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { 155 if sql.to_ascii_lowercase().contains(self.needle) { 156 return Err(self.err.clone()); 157 } 158 self.inner.exec(sql, params_json) 159 } 160 161 fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { 162 if sql.to_ascii_lowercase().contains(self.needle) { 163 return Err(self.err.clone()); 164 } 165 self.inner.query_raw(sql, params_json) 166 } 167 168 fn begin(&self) -> Result<(), SqlError> { 169 self.inner.begin() 170 } 171 172 fn commit(&self) -> Result<(), SqlError> { 173 self.inner.commit() 174 } 175 176 fn rollback(&self) -> Result<(), SqlError> { 177 self.inner.rollback() 178 } 179 } 180 181 #[test] 182 fn unwrap_sql_panics_on_error() { 183 let result = panic::catch_unwind(|| { 184 let err = ReplicaSchemaError::from(SqlError::InvalidArgument("bad".to_string())); 185 unwrap_sql::<()>(Err(err), "unwrap"); 186 }); 187 assert!(result.is_err()); 188 } 189 190 fn draft_to_event(draft: &RadrootsReplicaEventDraft, index: u32) -> EventEnvelope { 191 event_with_parts( 192 u64::from(index) + 1, 193 draft.author.as_str(), 194 1_720_000_000 + index, 195 draft.kind, 196 draft.content.clone(), 197 draft.tags.clone(), 198 ) 199 } 200 201 fn seed_source( 202 exec: &SqlxSqliteExecutor, 203 ) -> ( 204 RadrootsReplicaSyncRequest, 205 String, 206 String, 207 Vec<RadrootsReplicaEventDraft>, 208 ) { 209 migrations::run_all_up(exec).expect("migrations"); 210 211 let farm_pubkey = fixture_public_key(15); 212 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); 213 let farm_fields = IFarmFields { 214 d_tag: farm_d_tag.clone(), 215 pubkey: farm_pubkey.clone(), 216 name: "Green Farm".to_string(), 217 about: Some("About".to_string()), 218 website: None, 219 picture: None, 220 banner: None, 221 location_primary: None, 222 location_city: None, 223 location_region: None, 224 location_country: None, 225 }; 226 let farm_row = unwrap_sql(farm::create(exec, &farm_fields), "farm").result; 227 228 let point = radroots_event::farm::change_set::GeoJsonPoint { 229 r#type: "Point".to_string(), 230 coordinates: [-122.4, 37.7], 231 }; 232 let polygon = radroots_event::farm::change_set::GeoJsonPolygon { 233 r#type: "Polygon".to_string(), 234 coordinates: vec![vec![ 235 [-122.4, 37.7], 236 [-122.4, 37.701], 237 [-122.401, 37.701], 238 [-122.4, 37.7], 239 ]], 240 }; 241 let gcs_fields = IGcsLocationFields { 242 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 243 lat: 37.7, 244 lng: -122.4, 245 geohash: "9q8yy".to_string(), 246 point: serde_json::to_string(&point).expect("point"), 247 polygon: serde_json::to_string(&polygon).expect("polygon"), 248 accuracy: None, 249 altitude: None, 250 tag_0: None, 251 label: None, 252 area: None, 253 elevation: None, 254 soil: None, 255 climate: None, 256 gc_id: None, 257 gc_name: None, 258 gc_admin1_id: None, 259 gc_admin1_name: None, 260 gc_country_id: None, 261 gc_country_name: None, 262 }; 263 let gcs_row = unwrap_sql(gcs_location::create(exec, &gcs_fields), "gcs").result; 264 let gcs_secondary_fields = IGcsLocationFields { 265 d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), 266 lat: 37.71, 267 lng: -122.41, 268 geohash: "9q8yz".to_string(), 269 point: "{".to_string(), 270 polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), 271 accuracy: None, 272 altitude: None, 273 tag_0: None, 274 label: None, 275 area: None, 276 elevation: None, 277 soil: None, 278 climate: None, 279 gc_id: None, 280 gc_name: None, 281 gc_admin1_id: None, 282 gc_admin1_name: None, 283 gc_country_id: None, 284 gc_country_name: None, 285 }; 286 let gcs_secondary_row = unwrap_sql( 287 gcs_location::create(exec, &gcs_secondary_fields), 288 "gcs secondary", 289 ) 290 .result; 291 292 let _ = unwrap_sql( 293 farm_gcs_location::create( 294 exec, 295 &IFarmGcsLocationFields { 296 farm_id: farm_row.id.clone(), 297 gcs_location_id: gcs_row.id.clone(), 298 role: "primary".to_string(), 299 }, 300 ), 301 "farm_gcs", 302 ); 303 304 let plot_row = unwrap_sql( 305 plot::create( 306 exec, 307 &IPlotFields { 308 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 309 farm_id: farm_row.id.clone(), 310 name: "Plot A".to_string(), 311 about: None, 312 location_primary: None, 313 location_city: None, 314 location_region: None, 315 location_country: None, 316 }, 317 ), 318 "plot", 319 ) 320 .result; 321 322 let _ = unwrap_sql( 323 plot_gcs_location::create( 324 exec, 325 &IPlotGcsLocationFields { 326 plot_id: plot_row.id.clone(), 327 gcs_location_id: gcs_secondary_row.id.clone(), 328 role: "primary".to_string(), 329 }, 330 ), 331 "plot_gcs secondary primary", 332 ); 333 let _ = unwrap_sql( 334 plot_gcs_location::create( 335 exec, 336 &IPlotGcsLocationFields { 337 plot_id: plot_row.id.clone(), 338 gcs_location_id: gcs_row.id.clone(), 339 role: "primary".to_string(), 340 }, 341 ), 342 "plot_gcs", 343 ); 344 let plot_row_secondary = unwrap_sql( 345 plot::create( 346 exec, 347 &IPlotFields { 348 d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), 349 farm_id: farm_row.id.clone(), 350 name: "Plot B".to_string(), 351 about: None, 352 location_primary: None, 353 location_city: None, 354 location_region: None, 355 location_country: None, 356 }, 357 ), 358 "plot secondary", 359 ) 360 .result; 361 let _ = unwrap_sql( 362 plot_gcs_location::create( 363 exec, 364 &IPlotGcsLocationFields { 365 plot_id: plot_row_secondary.id.clone(), 366 gcs_location_id: gcs_row.id.clone(), 367 role: "secondary".to_string(), 368 }, 369 ), 370 "plot_secondary_gcs", 371 ); 372 373 let _ = unwrap_sql( 374 farm_tag::create( 375 exec, 376 &IFarmTagFields { 377 farm_id: farm_row.id.clone(), 378 tag: "coffee".to_string(), 379 }, 380 ), 381 "farm_tag", 382 ); 383 384 let _ = unwrap_sql( 385 plot_tag::create( 386 exec, 387 &IPlotTagFields { 388 plot_id: plot_row.id.clone(), 389 tag: "orchard".to_string(), 390 }, 391 ), 392 "plot_tag", 393 ); 394 395 let owner_pubkey = fixture_public_key(8); 396 let _ = unwrap_sql( 397 farm_member::create( 398 exec, 399 &IFarmMemberFields { 400 farm_id: farm_row.id.clone(), 401 member_pubkey: owner_pubkey.clone(), 402 role: "owner".to_string(), 403 }, 404 ), 405 "farm_member", 406 ); 407 let _ = unwrap_sql( 408 farm_member_claim::create( 409 exec, 410 &IFarmMemberClaimFields { 411 member_pubkey: owner_pubkey.clone(), 412 farm_pubkey: farm_pubkey.clone(), 413 }, 414 ), 415 "farm_member_claim", 416 ); 417 418 let _ = unwrap_sql( 419 nostr_profile::create( 420 exec, 421 &INostrProfileFields { 422 public_key: farm_pubkey.clone(), 423 profile_type: "farm".to_string(), 424 name: "Farm Profile".to_string(), 425 display_name: None, 426 about: None, 427 website: None, 428 picture: None, 429 banner: None, 430 nip05: None, 431 lud06: None, 432 lud16: None, 433 }, 434 ), 435 "farm_profile", 436 ); 437 let _ = unwrap_sql( 438 nostr_profile::create( 439 exec, 440 &INostrProfileFields { 441 public_key: owner_pubkey.clone(), 442 profile_type: "individual".to_string(), 443 name: "Owner".to_string(), 444 display_name: None, 445 about: None, 446 website: None, 447 picture: None, 448 banner: None, 449 nip05: None, 450 lud06: None, 451 lud16: None, 452 }, 453 ), 454 "owner_profile", 455 ); 456 457 let request = RadrootsReplicaSyncRequest { 458 farm: RadrootsReplicaFarmSelector { 459 id: Some(farm_row.id), 460 d_tag: None, 461 pubkey: None, 462 }, 463 options: None, 464 }; 465 let bundle = radroots_replica_sync_all(exec, &request).expect("sync"); 466 (request, farm_d_tag, farm_pubkey, bundle.events) 467 } 468 469 #[test] 470 fn ingest_roundtrip_yields_zero_pending_sync() { 471 let source = SqlxSqliteExecutor::open_memory().expect("source db"); 472 let (_source_request, farm_d_tag, farm_pubkey, drafts) = seed_source(&source); 473 assert_eq!(drafts.len(), 8); 474 475 let target = SqlxSqliteExecutor::open_memory().expect("target db"); 476 migrations::run_all_up(&target).expect("target migrations"); 477 478 let mut skipped = 0usize; 479 for (index, draft) in drafts.iter().enumerate() { 480 let event = draft_to_event(draft, index as u32); 481 let first = radroots_replica_ingest_event(&target, &event).expect("first ingest"); 482 assert_eq!(first, RadrootsReplicaIngestOutcome::Applied); 483 let second = radroots_replica_ingest_event(&target, &event).expect("second ingest"); 484 if second == RadrootsReplicaIngestOutcome::Skipped { 485 skipped += 1; 486 } 487 } 488 assert!(skipped > 0); 489 490 let status = radroots_replica_sync_status(&target).expect("sync status"); 491 assert_eq!(status.expected_count, drafts.len()); 492 assert_eq!(status.pending_count, 0); 493 494 let replay = radroots_replica_sync_all( 495 &target, 496 &RadrootsReplicaSyncRequest { 497 farm: RadrootsReplicaFarmSelector { 498 id: None, 499 d_tag: Some(farm_d_tag), 500 pubkey: Some(farm_pubkey), 501 }, 502 options: None, 503 }, 504 ) 505 .expect("replay sync"); 506 assert_eq!(replay.version, RADROOTS_REPLICA_TRANSFER_VERSION); 507 assert_eq!(replay.events.len(), drafts.len()); 508 } 509 510 #[test] 511 fn sync_status_empty_db_is_zero() { 512 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 513 migrations::run_all_up(&exec).expect("migrations"); 514 let status = radroots_replica_sync_status(&exec).expect("status"); 515 assert_eq!(status.expected_count, 0); 516 assert_eq!(status.pending_count, 0); 517 } 518 519 #[test] 520 fn sync_all_selector_and_options_paths_are_supported() { 521 let source = SqlxSqliteExecutor::open_memory().expect("source db"); 522 let (request, farm_d_tag, farm_pubkey, full_events) = seed_source(&source); 523 524 let by_pair = radroots_replica_sync_all( 525 &source, 526 &RadrootsReplicaSyncRequest { 527 farm: RadrootsReplicaFarmSelector { 528 id: None, 529 d_tag: Some(farm_d_tag.clone()), 530 pubkey: Some(farm_pubkey.clone()), 531 }, 532 options: None, 533 }, 534 ) 535 .expect("selector by d_tag + pubkey"); 536 assert_eq!(by_pair.events.len(), full_events.len()); 537 538 let reduced = radroots_replica_sync_all( 539 &source, 540 &RadrootsReplicaSyncRequest { 541 farm: request.farm, 542 options: Some(RadrootsReplicaSyncOptions { 543 include_list_sets: Some(false), 544 include_membership_claims: Some(false), 545 }), 546 }, 547 ) 548 .expect("reduced sync"); 549 assert_eq!(reduced.events.len(), 3); 550 } 551 552 #[test] 553 fn ingest_rejects_unsupported_kind() { 554 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 555 migrations::run_all_up(&exec).expect("migrations"); 556 let event = event_with_parts( 557 1, 558 &fixture_public_key(10), 559 1_720_000_001, 560 42, 561 String::new(), 562 Vec::new(), 563 ); 564 let err = radroots_replica_ingest_event(&exec, &event).expect_err("unsupported kind"); 565 assert!(err.to_string().contains("unsupported kind")); 566 } 567 568 #[test] 569 fn ingest_reports_transaction_boundary_errors() { 570 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 571 migrations::run_all_up(&exec).expect("migrations"); 572 let author = fixture_public_key(10); 573 let profile = profile_event( 574 9_001, 575 &author, 576 10, 577 Some(ProfileType::Individual), 578 "tx-errors", 579 ); 580 581 let begin_fail = BeginFailExecutor { inner: &exec }; 582 assert!(radroots_replica_ingest_event(&begin_fail, &profile).is_err()); 583 584 let commit_fail = CommitFailExecutor { inner: &exec }; 585 assert!(radroots_replica_ingest_event(&commit_fail, &profile).is_err()); 586 } 587 588 #[test] 589 fn ingest_reports_delete_internal_errors() { 590 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 591 migrations::run_all_up(&exec).expect("migrations"); 592 let farm_pubkey = fixture_public_key(15); 593 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 594 595 let create_event = farm_event( 596 9_101, 597 &farm_pubkey, 598 10, 599 farm_d_tag, 600 "delete-error-farm", 601 None, 602 Some(vec!["seed".to_string()]), 603 ); 604 assert_eq!( 605 radroots_replica_ingest_event(&exec, &create_event).expect("seed farm"), 606 RadrootsReplicaIngestOutcome::Applied 607 ); 608 609 let update_event = farm_event( 610 9_102, 611 &farm_pubkey, 612 11, 613 farm_d_tag, 614 "delete-error-farm", 615 None, 616 Some(vec!["next".to_string()]), 617 ); 618 let delete_fail = DeleteFailExecutor { 619 inner: &exec, 620 table_name: "farm_tag", 621 err: SqlError::Internal, 622 }; 623 assert!(radroots_replica_ingest_event(&delete_fail, &update_event).is_err()); 624 } 625 626 #[test] 627 fn ingest_reports_parse_and_state_error_paths_for_all_kinds() { 628 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 629 migrations::run_all_up(&exec).expect("migrations"); 630 631 let profile_pubkey = fixture_public_key(10); 632 let profile_ok = profile_event( 633 9_201, 634 &profile_pubkey, 635 10, 636 Some(ProfileType::Individual), 637 "profile-ok", 638 ); 639 let profile_parse_error = event_with_parts( 640 9_202, 641 &profile_pubkey, 642 11, 643 KIND_PROFILE, 644 "{".to_string(), 645 profile_ok.tags_as_vec(), 646 ); 647 assert!(radroots_replica_ingest_event(&exec, &profile_parse_error).is_err()); 648 649 let farm_pubkey = fixture_public_key(11); 650 let farm_seed_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 651 let farm_seed = farm_event( 652 9_203, 653 &farm_pubkey, 654 12, 655 farm_seed_d_tag, 656 "farm-seed", 657 None, 658 None, 659 ); 660 assert_eq!( 661 radroots_replica_ingest_event(&exec, &farm_seed).expect("seed farm"), 662 RadrootsReplicaIngestOutcome::Applied 663 ); 664 665 let farm_parse_error = event_with_parts( 666 9_204, 667 &farm_pubkey, 668 13, 669 KIND_FARM, 670 "{".to_string(), 671 farm_seed.tags_as_vec(), 672 ); 673 assert!(radroots_replica_ingest_event(&exec, &farm_parse_error).is_err()); 674 675 let plot_ok = plot_event( 676 9_205, 677 &farm_pubkey, 678 14, 679 "AAAAAAAAAAAAAAAAAAAAAQ", 680 FarmRef { 681 pubkey: farm_pubkey.clone(), 682 d_tag: farm_seed_d_tag.to_string(), 683 }, 684 "plot-ok", 685 None, 686 None, 687 ); 688 let plot_parse_error = event_with_parts( 689 9_206, 690 &farm_pubkey, 691 15, 692 KIND_PLOT, 693 "{".to_string(), 694 plot_ok.tags_as_vec(), 695 ); 696 assert!(radroots_replica_ingest_event(&exec, &plot_parse_error).is_err()); 697 698 let list_parse_error = event_with_parts( 699 9_207, 700 &profile_pubkey, 701 16, 702 KIND_LIST_SET_GENERIC, 703 String::new(), 704 Vec::new(), 705 ); 706 assert!(radroots_replica_ingest_event(&exec, &list_parse_error).is_err()); 707 708 let state_query_fail = QueryFailExecutor { 709 inner: &exec, 710 needle: "nostr_event_head", 711 err: SqlError::Internal, 712 }; 713 assert!(radroots_replica_ingest_event(&state_query_fail, &profile_ok).is_err()); 714 assert!(radroots_replica_ingest_event(&state_query_fail, &farm_seed).is_err()); 715 assert!(radroots_replica_ingest_event(&state_query_fail, &plot_ok).is_err()); 716 717 let claims_set = 718 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 719 let claims_event = list_set_event( 720 9_208, 721 &profile_pubkey, 722 17, 723 KIND_LIST_SET_GENERIC, 724 &claims_set, 725 ); 726 assert!(radroots_replica_ingest_event(&state_query_fail, &claims_event).is_err()); 727 728 let state_insert_fail = QueryFailExecutor { 729 inner: &exec, 730 needle: "insert into nostr_event_head", 731 err: SqlError::Internal, 732 }; 733 let profile_insert_state_error = profile_event( 734 9_209, 735 &fixture_public_key(12), 736 18, 737 Some(ProfileType::Individual), 738 "profile-state-insert", 739 ); 740 assert!( 741 radroots_replica_ingest_event(&state_insert_fail, &profile_insert_state_error).is_err() 742 ); 743 744 let farm_insert_state_error = farm_event( 745 9_210, 746 &farm_pubkey, 747 19, 748 "AAAAAAAAAAAAAAAAAAAAAw", 749 "farm-state-insert", 750 None, 751 None, 752 ); 753 assert!(radroots_replica_ingest_event(&state_insert_fail, &farm_insert_state_error).is_err()); 754 755 let plot_insert_state_error = plot_event( 756 9_211, 757 &farm_pubkey, 758 20, 759 "AAAAAAAAAAAAAAAAAAAAAg", 760 FarmRef { 761 pubkey: farm_pubkey.clone(), 762 d_tag: farm_seed_d_tag.to_string(), 763 }, 764 "plot-state-insert", 765 None, 766 None, 767 ); 768 assert!(radroots_replica_ingest_event(&state_insert_fail, &plot_insert_state_error).is_err()); 769 assert!(radroots_replica_ingest_event(&state_insert_fail, &claims_event).is_err()); 770 } 771 772 #[test] 773 fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() { 774 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 775 migrations::run_all_up(&exec).expect("migrations"); 776 777 let assert_query_fail = |needle: &'static str, event: &EventEnvelope| { 778 let fail = QueryFailExecutor { 779 inner: &exec, 780 needle, 781 err: SqlError::Internal, 782 }; 783 assert!( 784 radroots_replica_ingest_event(&fail, event).is_err(), 785 "needle {needle} should fail" 786 ); 787 }; 788 789 let profile_pubkey = fixture_public_key(13); 790 let profile_create = profile_event( 791 9_301, 792 &profile_pubkey, 793 10, 794 Some(ProfileType::Individual), 795 "profile-query", 796 ); 797 assert_query_fail("select * from nostr_profile", &profile_create); 798 assert_query_fail("insert into nostr_profile", &profile_create); 799 assert_eq!( 800 radroots_replica_ingest_event(&exec, &profile_create).expect("seed profile"), 801 RadrootsReplicaIngestOutcome::Applied 802 ); 803 let profile_update = profile_event( 804 9_302, 805 &profile_pubkey, 806 11, 807 Some(ProfileType::Individual), 808 "profile-query-updated", 809 ); 810 assert_query_fail("update nostr_profile", &profile_update); 811 812 let farm_pubkey = fixture_public_key(14); 813 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 814 let farm_create = farm_event( 815 9_303, 816 &farm_pubkey, 817 12, 818 farm_d_tag, 819 "farm-query", 820 Some(FarmPublicLocation { 821 primary: "farm".to_string(), 822 city: Some("city".to_string()), 823 region: None, 824 country: None, 825 geohash: "9q8yy".to_string(), 826 }), 827 Some(vec!["coffee".to_string()]), 828 ); 829 assert_query_fail("select * from farm where", &farm_create); 830 assert_query_fail("insert into farm", &farm_create); 831 assert_query_fail("insert into farm_tag", &farm_create); 832 assert_eq!( 833 radroots_replica_ingest_event(&exec, &farm_create).expect("seed farm"), 834 RadrootsReplicaIngestOutcome::Applied 835 ); 836 let farm_update = farm_event( 837 9_304, 838 &farm_pubkey, 839 13, 840 farm_d_tag, 841 "farm-query-updated", 842 None, 843 Some(vec!["grain".to_string()]), 844 ); 845 assert_query_fail("update farm", &farm_update); 846 847 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 848 let plot_create = plot_event( 849 9_305, 850 &farm_pubkey, 851 14, 852 plot_d_tag, 853 FarmRef { 854 pubkey: farm_pubkey.clone(), 855 d_tag: farm_d_tag.to_string(), 856 }, 857 "plot-query", 858 Some(PlotLocation { 859 primary: Some("plot".to_string()), 860 city: None, 861 region: None, 862 country: None, 863 gcs: sample_gcs(37.8, -122.5, "9q8yz"), 864 }), 865 Some(vec!["orchard".to_string()]), 866 ); 867 assert_query_fail("select * from plot where", &plot_create); 868 assert_query_fail("insert into plot", &plot_create); 869 assert_query_fail("insert into plot_tag", &plot_create); 870 assert_query_fail("insert into plot_gcs_location", &plot_create); 871 assert_eq!( 872 radroots_replica_ingest_event(&exec, &plot_create).expect("seed plot"), 873 RadrootsReplicaIngestOutcome::Applied 874 ); 875 let plot_update = plot_event( 876 9_306, 877 &farm_pubkey, 878 15, 879 plot_d_tag, 880 FarmRef { 881 pubkey: farm_pubkey.clone(), 882 d_tag: farm_d_tag.to_string(), 883 }, 884 "plot-query-updated", 885 None, 886 Some(vec!["updated".to_string()]), 887 ); 888 assert_query_fail("update plot", &plot_update); 889 890 let member_of_set = 891 farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); 892 let member_of_event = list_set_event( 893 9_307, 894 &profile_pubkey, 895 16, 896 KIND_LIST_SET_GENERIC, 897 &member_of_set, 898 ); 899 assert_query_fail("insert into farm_member_claim", &member_of_event); 900 901 let members_set = 902 farm_list_sets::farm_members_list_set(farm_d_tag, vec![fixture_public_key(6)]) 903 .expect("members"); 904 let members_event = 905 list_set_event(9_308, &farm_pubkey, 17, KIND_LIST_SET_GENERIC, &members_set); 906 assert_query_fail("insert into farm_member", &members_event); 907 assert_query_fail("select * from farm where", &members_event); 908 909 assert_query_fail("select * from nostr_event_head", &members_event); 910 assert_query_fail("insert into nostr_event_head", &members_event); 911 assert_eq!( 912 radroots_replica_ingest_event(&exec, &members_event).expect("seed members"), 913 RadrootsReplicaIngestOutcome::Applied 914 ); 915 let members_update = 916 list_set_event(9_309, &farm_pubkey, 18, KIND_LIST_SET_GENERIC, &members_set); 917 assert_query_fail("update nostr_event_head", &members_update); 918 } 919 920 fn event_with_parts( 921 id: u64, 922 author: &str, 923 created_at: u32, 924 kind: u32, 925 content: String, 926 tags: Vec<Vec<String>>, 927 ) -> EventEnvelope { 928 EventEnvelope::new(EventEnvelopeParts { 929 id: format!("{id:064x}"), 930 author: author.to_string(), 931 created_at: u64::from(created_at), 932 kind, 933 tags, 934 content, 935 sig: "f".repeat(128), 936 }) 937 .expect("test event envelope") 938 } 939 940 fn sample_point(lat: f64, lng: f64) -> GeoJsonPoint { 941 GeoJsonPoint { 942 r#type: "Point".to_string(), 943 coordinates: [lng, lat], 944 } 945 } 946 947 fn sample_polygon(lat: f64, lng: f64) -> GeoJsonPolygon { 948 GeoJsonPolygon { 949 r#type: "Polygon".to_string(), 950 coordinates: vec![vec![ 951 [lng, lat], 952 [lng, lat + 0.001], 953 [lng - 0.001, lat + 0.001], 954 [lng, lat], 955 ]], 956 } 957 } 958 959 fn sample_gcs(lat: f64, lng: f64, geohash: &str) -> GcsLocation { 960 GcsLocation { 961 lat, 962 lng, 963 geohash: geohash.to_string(), 964 point: sample_point(lat, lng), 965 polygon: sample_polygon(lat, lng), 966 accuracy: Some(2.0), 967 altitude: Some(10.0), 968 tag_0: Some("soil".to_string()), 969 label: Some("north".to_string()), 970 area: Some(1_000.0), 971 elevation: Some(5), 972 soil: Some("loam".to_string()), 973 climate: Some("temperate".to_string()), 974 gc_id: Some("gc".to_string()), 975 gc_name: Some("name".to_string()), 976 gc_admin1_id: Some("admin1".to_string()), 977 gc_admin1_name: Some("admin1_name".to_string()), 978 gc_country_id: Some("country".to_string()), 979 gc_country_name: Some("country_name".to_string()), 980 } 981 } 982 983 fn profile_event( 984 _id: u64, 985 author: &str, 986 created_at: u32, 987 profile_type: Option<ProfileType>, 988 name: &str, 989 ) -> EventEnvelope { 990 let profile = serde_json::json!({ 991 "name": name, 992 "display_name": format!("{name}_display"), 993 "nip05": format!("{name}@example.com"), 994 "about": format!("{name} about"), 995 "website": "https://example.com", 996 "picture": "https://example.com/p.png", 997 "banner": "https://example.com/b.png", 998 "lud06": "lud06", 999 "lud16": "lud16" 1000 }); 1001 let mut tags = Vec::new(); 1002 if let Some(kind) = profile_type { 1003 tags.push(vec![ 1004 RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), 1005 radroots_profile_type_tag_value(kind).to_string(), 1006 ]); 1007 } 1008 let keys = (1_u8..=u8::MAX) 1009 .find_map(|seed| { 1010 let keys = Keys::parse(&format!("{seed:064x}")).ok()?; 1011 (keys.public_key().to_hex() == author).then_some(keys) 1012 }) 1013 .expect("profile author must resolve to a fixture signing key"); 1014 let tags = tags 1015 .into_iter() 1016 .map(|tag| Tag::parse(tag).expect("profile tag")) 1017 .collect::<Vec<_>>(); 1018 let event = EventBuilder::new(Kind::Metadata, profile.to_string()) 1019 .tags(tags) 1020 .custom_created_at(Timestamp::from_secs(u64::from(created_at))) 1021 .sign_with_keys(&keys) 1022 .expect("signed profile event"); 1023 from_nostr(&event).expect("profile event adapter") 1024 } 1025 1026 fn farm_event( 1027 id: u64, 1028 author: &str, 1029 created_at: u32, 1030 d_tag: &str, 1031 name: &str, 1032 location: Option<FarmPublicLocation>, 1033 tags: Option<Vec<String>>, 1034 ) -> EventEnvelope { 1035 let farm = Farm { 1036 d_tag: d_tag.to_string(), 1037 name: name.to_string(), 1038 about: Some(format!("{name} about")), 1039 website: Some("https://farm.example.com".to_string()), 1040 picture: Some("https://farm.example.com/p.png".to_string()), 1041 banner: Some("https://farm.example.com/b.png".to_string()), 1042 location, 1043 tags, 1044 }; 1045 let event_tags = farm_encode::farm_build_tags(&farm).expect("farm tags"); 1046 event_with_parts( 1047 id, 1048 author, 1049 created_at, 1050 KIND_FARM, 1051 serde_json::to_string(&farm).expect("farm json"), 1052 event_tags, 1053 ) 1054 } 1055 1056 #[allow(clippy::too_many_arguments)] 1057 fn plot_event( 1058 id: u64, 1059 author: &str, 1060 created_at: u32, 1061 d_tag: &str, 1062 farm_ref: FarmRef, 1063 name: &str, 1064 location: Option<PlotLocation>, 1065 tags: Option<Vec<String>>, 1066 ) -> EventEnvelope { 1067 let plot = Plot { 1068 d_tag: d_tag.to_string(), 1069 farm: farm_ref, 1070 name: name.to_string(), 1071 about: Some(format!("{name} about")), 1072 location, 1073 tags, 1074 }; 1075 let event_tags = plot_encode::plot_build_tags(&plot).expect("plot tags"); 1076 event_with_parts( 1077 id, 1078 author, 1079 created_at, 1080 KIND_PLOT, 1081 serde_json::to_string(&plot).expect("plot json"), 1082 event_tags, 1083 ) 1084 } 1085 1086 fn list_set_event( 1087 id: u64, 1088 author: &str, 1089 created_at: u32, 1090 kind: u32, 1091 list_set: &ListSet, 1092 ) -> EventEnvelope { 1093 let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set parts"); 1094 event_with_parts(id, author, created_at, kind, parts.content, parts.tags) 1095 } 1096 1097 #[test] 1098 fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { 1099 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 1100 migrations::run_all_up(&exec).expect("migrations"); 1101 1102 let profile_pubkey = fixture_public_key(9); 1103 let profile_create = profile_event( 1104 101, 1105 &profile_pubkey, 1106 10, 1107 Some(ProfileType::Individual), 1108 "alice", 1109 ); 1110 assert_eq!( 1111 radroots_replica_ingest_event(&exec, &profile_create).expect("profile create"), 1112 RadrootsReplicaIngestOutcome::Applied 1113 ); 1114 assert_eq!( 1115 radroots_replica_ingest_event(&exec, &profile_create).expect("profile skip same"), 1116 RadrootsReplicaIngestOutcome::Skipped 1117 ); 1118 let profile_older = profile_event( 1119 102, 1120 &profile_pubkey, 1121 9, 1122 Some(ProfileType::Individual), 1123 "alice-older", 1124 ); 1125 assert_eq!( 1126 radroots_replica_ingest_event(&exec, &profile_older).expect("profile skip older"), 1127 RadrootsReplicaIngestOutcome::Skipped 1128 ); 1129 let profile_same_time_higher_id = (0_u32..1_024) 1130 .map(|index| { 1131 profile_event( 1132 u64::from(index), 1133 &profile_pubkey, 1134 10, 1135 Some(ProfileType::Individual), 1136 &format!("alice-higher-{index}"), 1137 ) 1138 }) 1139 .find(|event| event.id_hex() > profile_create.id_hex()) 1140 .expect("same-time fixture with a higher event id"); 1141 assert_eq!( 1142 radroots_replica_ingest_event(&exec, &profile_same_time_higher_id) 1143 .expect("profile skip same timestamp higher id"), 1144 RadrootsReplicaIngestOutcome::Skipped 1145 ); 1146 let profile_same_time_lower_id = (0_u32..1_024) 1147 .map(|index| { 1148 profile_event( 1149 u64::from(index), 1150 &profile_pubkey, 1151 10, 1152 Some(ProfileType::Individual), 1153 &format!("alice-lower-{index}"), 1154 ) 1155 }) 1156 .find(|event| event.id_hex() < profile_create.id_hex()) 1157 .expect("same-time fixture with a lower event id"); 1158 assert_eq!( 1159 radroots_replica_ingest_event(&exec, &profile_same_time_lower_id) 1160 .expect("profile apply same timestamp lower id"), 1161 RadrootsReplicaIngestOutcome::Applied 1162 ); 1163 let profile_missing_type = profile_event(104, &profile_pubkey, 11, None, "missing-type"); 1164 let err = radroots_replica_ingest_event(&exec, &profile_missing_type) 1165 .expect_err("profile type is required"); 1166 assert!(err.to_string().contains("profile_type required")); 1167 1168 let profile_types = [ 1169 (ProfileType::Farm, fixture_public_key(15), "farm-profile"), 1170 (ProfileType::Coop, fixture_public_key(12), "coop-profile"), 1171 (ProfileType::Any, fixture_public_key(10), "any-profile"), 1172 ( 1173 ProfileType::Radrootsd, 1174 fixture_public_key(13), 1175 "radrootsd-profile", 1176 ), 1177 ]; 1178 for (index, (profile_type, pubkey, name)) in profile_types.iter().enumerate() { 1179 let event = profile_event( 1180 110 + index as u64, 1181 pubkey, 1182 20 + index as u32, 1183 Some(*profile_type), 1184 name, 1185 ); 1186 assert_eq!( 1187 radroots_replica_ingest_event(&exec, &event).expect("profile variant"), 1188 RadrootsReplicaIngestOutcome::Applied 1189 ); 1190 } 1191 1192 let farm_pubkey = fixture_public_key(14); 1193 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; 1194 let farm_location = FarmPublicLocation { 1195 primary: "farm-primary".to_string(), 1196 city: Some("city".to_string()), 1197 region: Some("region".to_string()), 1198 country: Some("country".to_string()), 1199 geohash: "9q8yy".to_string(), 1200 }; 1201 let farm_create = farm_event( 1202 200, 1203 &farm_pubkey, 1204 100, 1205 farm_d_tag, 1206 "farm-a", 1207 Some(farm_location.clone()), 1208 Some(vec![ 1209 "coffee".to_string(), 1210 " ".to_string(), 1211 "coffee".to_string(), 1212 "grain".to_string(), 1213 ]), 1214 ); 1215 assert_eq!( 1216 radroots_replica_ingest_event(&exec, &farm_create).expect("farm create"), 1217 RadrootsReplicaIngestOutcome::Applied 1218 ); 1219 assert_eq!( 1220 radroots_replica_ingest_event(&exec, &farm_create).expect("farm skip same"), 1221 RadrootsReplicaIngestOutcome::Skipped 1222 ); 1223 let farm_older = farm_event( 1224 201, 1225 &farm_pubkey, 1226 99, 1227 farm_d_tag, 1228 "farm-older", 1229 Some(farm_location.clone()), 1230 None, 1231 ); 1232 assert_eq!( 1233 radroots_replica_ingest_event(&exec, &farm_older).expect("farm skip older"), 1234 RadrootsReplicaIngestOutcome::Skipped 1235 ); 1236 let farm_update_same_time_higher_id = farm_event( 1237 202, 1238 &farm_pubkey, 1239 100, 1240 farm_d_tag, 1241 "farm-a-updated", 1242 None, 1243 Some(vec!["market".to_string()]), 1244 ); 1245 assert_eq!( 1246 radroots_replica_ingest_event(&exec, &farm_update_same_time_higher_id) 1247 .expect("farm skip same timestamp higher id"), 1248 RadrootsReplicaIngestOutcome::Skipped 1249 ); 1250 let farm_update_same_time_lower_id = farm_event( 1251 199, 1252 &farm_pubkey, 1253 100, 1254 farm_d_tag, 1255 "farm-a-updated", 1256 None, 1257 Some(vec!["market".to_string()]), 1258 ); 1259 assert_eq!( 1260 radroots_replica_ingest_event(&exec, &farm_update_same_time_lower_id) 1261 .expect("farm update same timestamp lower id"), 1262 RadrootsReplicaIngestOutcome::Applied 1263 ); 1264 1265 let farm_rows = unwrap_sql( 1266 farm::find_many( 1267 &exec, 1268 &IFarmFindMany { 1269 filter: Some(IFarmFieldsFilter { 1270 id: None, 1271 created_at: None, 1272 updated_at: None, 1273 d_tag: Some(farm_d_tag.to_string()), 1274 pubkey: Some(farm_pubkey.clone()), 1275 name: None, 1276 about: None, 1277 website: None, 1278 picture: None, 1279 banner: None, 1280 location_primary: None, 1281 location_city: None, 1282 location_region: None, 1283 location_country: None, 1284 }), 1285 }, 1286 ), 1287 "farm find_many", 1288 ) 1289 .results; 1290 assert_eq!(farm_rows.len(), 1); 1291 let farm_id = farm_rows[0].id.clone(); 1292 1293 let farm_tags = unwrap_sql( 1294 farm_tag::find_many( 1295 &exec, 1296 &IFarmTagFindMany { 1297 filter: Some(IFarmTagFieldsFilter { 1298 id: None, 1299 created_at: None, 1300 updated_at: None, 1301 farm_id: Some(farm_id.clone()), 1302 tag: None, 1303 }), 1304 }, 1305 ), 1306 "farm tags", 1307 ) 1308 .results; 1309 assert_eq!(farm_tags.len(), 1); 1310 assert_eq!(farm_tags[0].tag, "market"); 1311 1312 let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; 1313 let plot_location = PlotLocation { 1314 primary: Some("plot-primary".to_string()), 1315 city: Some("plot-city".to_string()), 1316 region: Some("plot-region".to_string()), 1317 country: Some("plot-country".to_string()), 1318 gcs: sample_gcs(37.8, -122.5, "9q8yz"), 1319 }; 1320 let plot_create = plot_event( 1321 300, 1322 &farm_pubkey, 1323 200, 1324 plot_d_tag, 1325 FarmRef { 1326 pubkey: farm_pubkey.clone(), 1327 d_tag: farm_d_tag.to_string(), 1328 }, 1329 "plot-a", 1330 Some(plot_location.clone()), 1331 Some(vec![ 1332 "orchard".to_string(), 1333 " ".to_string(), 1334 "orchard".to_string(), 1335 "shade".to_string(), 1336 ]), 1337 ); 1338 assert_eq!( 1339 radroots_replica_ingest_event(&exec, &plot_create).expect("plot create"), 1340 RadrootsReplicaIngestOutcome::Applied 1341 ); 1342 assert_eq!( 1343 radroots_replica_ingest_event(&exec, &plot_create).expect("plot skip same"), 1344 RadrootsReplicaIngestOutcome::Skipped 1345 ); 1346 let plot_older = plot_event( 1347 301, 1348 &farm_pubkey, 1349 199, 1350 plot_d_tag, 1351 FarmRef { 1352 pubkey: farm_pubkey.clone(), 1353 d_tag: farm_d_tag.to_string(), 1354 }, 1355 "plot-older", 1356 Some(plot_location.clone()), 1357 None, 1358 ); 1359 assert_eq!( 1360 radroots_replica_ingest_event(&exec, &plot_older).expect("plot skip older"), 1361 RadrootsReplicaIngestOutcome::Skipped 1362 ); 1363 let plot_update_higher_id = plot_event( 1364 302, 1365 &farm_pubkey, 1366 200, 1367 plot_d_tag, 1368 FarmRef { 1369 pubkey: farm_pubkey.clone(), 1370 d_tag: farm_d_tag.to_string(), 1371 }, 1372 "plot-a-updated", 1373 None, 1374 Some(vec!["updated".to_string()]), 1375 ); 1376 assert_eq!( 1377 radroots_replica_ingest_event(&exec, &plot_update_higher_id) 1378 .expect("plot skip same timestamp higher id"), 1379 RadrootsReplicaIngestOutcome::Skipped 1380 ); 1381 let plot_update_lower_id = plot_event( 1382 299, 1383 &farm_pubkey, 1384 200, 1385 plot_d_tag, 1386 FarmRef { 1387 pubkey: farm_pubkey.clone(), 1388 d_tag: farm_d_tag.to_string(), 1389 }, 1390 "plot-a-updated", 1391 None, 1392 Some(vec!["updated".to_string()]), 1393 ); 1394 assert_eq!( 1395 radroots_replica_ingest_event(&exec, &plot_update_lower_id) 1396 .expect("plot update same timestamp lower id"), 1397 RadrootsReplicaIngestOutcome::Applied 1398 ); 1399 let plot_missing_farm = plot_event( 1400 303, 1401 &farm_pubkey, 1402 201, 1403 "AAAAAAAAAAAAAAAAAAAAAg", 1404 FarmRef { 1405 pubkey: fixture_public_key(3), 1406 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 1407 }, 1408 "plot-missing-farm", 1409 None, 1410 None, 1411 ); 1412 let missing_farm_err = radroots_replica_ingest_event(&exec, &plot_missing_farm) 1413 .expect_err("plot requires existing farm"); 1414 assert!(missing_farm_err.to_string().contains("farm not found")); 1415 1416 let plot_rows = unwrap_sql( 1417 plot::find_many( 1418 &exec, 1419 &radroots_replica_schema::plot::IPlotFindMany { filter: None }, 1420 ), 1421 "plot rows", 1422 ) 1423 .results; 1424 assert_eq!(plot_rows.len(), 1); 1425 let plot_id = plot_rows[0].id.clone(); 1426 let plot_tags = unwrap_sql( 1427 plot_tag::find_many( 1428 &exec, 1429 &IPlotTagFindMany { 1430 filter: Some(IPlotTagFieldsFilter { 1431 id: None, 1432 created_at: None, 1433 updated_at: None, 1434 plot_id: Some(plot_id), 1435 tag: None, 1436 }), 1437 }, 1438 ), 1439 "plot tags", 1440 ) 1441 .results; 1442 assert_eq!(plot_tags.len(), 1); 1443 assert_eq!(plot_tags[0].tag, "updated"); 1444 1445 let non_generic_list_set = ListSet { 1446 d_tag: "member_of.farms".to_string(), 1447 content: String::new(), 1448 entries: vec![ListEntry { 1449 tag: "p".to_string(), 1450 values: vec![farm_pubkey.clone()], 1451 }], 1452 title: None, 1453 description: None, 1454 image: None, 1455 }; 1456 let non_generic_event = list_set_event( 1457 400, 1458 &profile_pubkey, 1459 300, 1460 KIND_LIST_SET_FOLLOW, 1461 &non_generic_list_set, 1462 ); 1463 assert_eq!( 1464 radroots_replica_ingest_event(&exec, &non_generic_event).expect("non-generic list set"), 1465 RadrootsReplicaIngestOutcome::Skipped 1466 ); 1467 1468 let metadata_list_set = ListSet { 1469 d_tag: "member_of.farms".to_string(), 1470 content: String::new(), 1471 entries: vec![ListEntry { 1472 tag: "p".to_string(), 1473 values: vec![farm_pubkey.clone()], 1474 }], 1475 title: Some("title".to_string()), 1476 description: None, 1477 image: None, 1478 }; 1479 let metadata_event = list_set_event( 1480 401, 1481 &profile_pubkey, 1482 301, 1483 KIND_LIST_SET_GENERIC, 1484 &metadata_list_set, 1485 ); 1486 let metadata_err = radroots_replica_ingest_event(&exec, &metadata_event) 1487 .expect_err("metadata must be rejected"); 1488 assert!(metadata_err.to_string().contains("must omit metadata")); 1489 1490 let description_list_set = ListSet { 1491 d_tag: "member_of.farms".to_string(), 1492 content: String::new(), 1493 entries: vec![ListEntry { 1494 tag: "p".to_string(), 1495 values: vec![farm_pubkey.clone()], 1496 }], 1497 title: None, 1498 description: Some("desc".to_string()), 1499 image: None, 1500 }; 1501 let description_event = list_set_event( 1502 4011, 1503 &profile_pubkey, 1504 3011, 1505 KIND_LIST_SET_GENERIC, 1506 &description_list_set, 1507 ); 1508 let description_err = radroots_replica_ingest_event(&exec, &description_event) 1509 .expect_err("description metadata must be rejected"); 1510 assert!(description_err.to_string().contains("must omit metadata")); 1511 1512 let image_list_set = ListSet { 1513 d_tag: "member_of.farms".to_string(), 1514 content: String::new(), 1515 entries: vec![ListEntry { 1516 tag: "p".to_string(), 1517 values: vec![farm_pubkey.clone()], 1518 }], 1519 title: None, 1520 description: None, 1521 image: Some("image".to_string()), 1522 }; 1523 let image_event = list_set_event( 1524 4012, 1525 &profile_pubkey, 1526 3012, 1527 KIND_LIST_SET_GENERIC, 1528 &image_list_set, 1529 ); 1530 let image_err = radroots_replica_ingest_event(&exec, &image_event) 1531 .expect_err("image metadata must be rejected"); 1532 assert!(image_err.to_string().contains("must omit metadata")); 1533 1534 let content_list_set = ListSet { 1535 d_tag: "member_of.farms".to_string(), 1536 content: "not-empty".to_string(), 1537 entries: vec![ListEntry { 1538 tag: "p".to_string(), 1539 values: vec![farm_pubkey.clone()], 1540 }], 1541 title: None, 1542 description: None, 1543 image: None, 1544 }; 1545 let content_event = list_set_event( 1546 402, 1547 &profile_pubkey, 1548 302, 1549 KIND_LIST_SET_GENERIC, 1550 &content_list_set, 1551 ); 1552 let content_err = 1553 radroots_replica_ingest_event(&exec, &content_event).expect_err("content must be rejected"); 1554 assert!(content_err.to_string().contains("must not include content")); 1555 1556 let invalid_member_of = ListSet { 1557 d_tag: "member_of.farms".to_string(), 1558 content: String::new(), 1559 entries: vec![ListEntry { 1560 tag: "a".to_string(), 1561 values: vec![farm_pubkey.clone()], 1562 }], 1563 title: None, 1564 description: None, 1565 image: None, 1566 }; 1567 let invalid_member_of_event = list_set_event( 1568 403, 1569 &profile_pubkey, 1570 303, 1571 KIND_LIST_SET_GENERIC, 1572 &invalid_member_of, 1573 ); 1574 let invalid_member_of_err = radroots_replica_ingest_event(&exec, &invalid_member_of_event) 1575 .expect_err("member_of requires p tags"); 1576 assert!( 1577 invalid_member_of_err 1578 .to_string() 1579 .contains("must only include p tags") 1580 ); 1581 1582 let member_of_valid = ListSet { 1583 d_tag: "member_of.farms".to_string(), 1584 content: String::new(), 1585 entries: vec![ 1586 ListEntry { 1587 tag: "p".to_string(), 1588 values: vec![farm_pubkey.clone()], 1589 }, 1590 ListEntry { 1591 tag: "p".to_string(), 1592 values: vec![farm_pubkey.clone()], 1593 }, 1594 ], 1595 title: None, 1596 description: None, 1597 image: None, 1598 }; 1599 let member_of_event = list_set_event( 1600 404, 1601 &profile_pubkey, 1602 304, 1603 KIND_LIST_SET_GENERIC, 1604 &member_of_valid, 1605 ); 1606 assert_eq!( 1607 radroots_replica_ingest_event(&exec, &member_of_event).expect("member_of apply"), 1608 RadrootsReplicaIngestOutcome::Applied 1609 ); 1610 assert_eq!( 1611 radroots_replica_ingest_event(&exec, &member_of_event).expect("member_of skip"), 1612 RadrootsReplicaIngestOutcome::Skipped 1613 ); 1614 let mut member_of_with_empty_parts = 1615 list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC) 1616 .expect("member_of parts"); 1617 member_of_with_empty_parts 1618 .tags 1619 .insert(0, vec!["p".to_string()]); 1620 let member_of_with_empty_event = event_with_parts( 1621 4041, 1622 &profile_pubkey, 1623 305, 1624 KIND_LIST_SET_GENERIC, 1625 member_of_with_empty_parts.content, 1626 member_of_with_empty_parts.tags, 1627 ); 1628 assert_eq!( 1629 radroots_replica_ingest_event(&exec, &member_of_with_empty_event) 1630 .expect("member_of with empty entry"), 1631 RadrootsReplicaIngestOutcome::Applied 1632 ); 1633 1634 let claims = unwrap_sql( 1635 farm_member_claim::find_many( 1636 &exec, 1637 &IFarmMemberClaimFindMany { 1638 filter: Some(IFarmMemberClaimFieldsFilter { 1639 id: None, 1640 created_at: None, 1641 updated_at: None, 1642 member_pubkey: Some(profile_pubkey.clone()), 1643 farm_pubkey: None, 1644 }), 1645 }, 1646 ), 1647 "claims", 1648 ) 1649 .results; 1650 assert_eq!(claims.len(), 1); 1651 assert_eq!(claims[0].farm_pubkey, farm_pubkey); 1652 1653 let invalid_members = ListSet { 1654 d_tag: format!("farm:{farm_d_tag}:members"), 1655 content: String::new(), 1656 entries: vec![ListEntry { 1657 tag: "a".to_string(), 1658 values: vec!["x".to_string()], 1659 }], 1660 title: None, 1661 description: None, 1662 image: None, 1663 }; 1664 let invalid_members_event = list_set_event( 1665 405, 1666 &farm_pubkey, 1667 305, 1668 KIND_LIST_SET_GENERIC, 1669 &invalid_members, 1670 ); 1671 let invalid_members_err = radroots_replica_ingest_event(&exec, &invalid_members_event) 1672 .expect_err("members list requires p entries"); 1673 assert!( 1674 invalid_members_err 1675 .to_string() 1676 .contains("must only include p tags") 1677 ); 1678 1679 let members_valid = farm_list_sets::farm_members_list_set( 1680 farm_d_tag, 1681 vec![fixture_public_key(6), fixture_public_key(6)], 1682 ) 1683 .expect("members list"); 1684 let members_event = list_set_event( 1685 406, 1686 &farm_pubkey, 1687 306, 1688 KIND_LIST_SET_GENERIC, 1689 &members_valid, 1690 ); 1691 assert_eq!( 1692 radroots_replica_ingest_event(&exec, &members_event).expect("members apply"), 1693 RadrootsReplicaIngestOutcome::Applied 1694 ); 1695 let mut members_with_empty_parts = 1696 list_set_encode::to_wire_parts_with_kind(&members_valid, KIND_LIST_SET_GENERIC) 1697 .expect("members parts"); 1698 members_with_empty_parts 1699 .tags 1700 .insert(0, vec!["p".to_string()]); 1701 let members_with_empty_event = event_with_parts( 1702 4061, 1703 &farm_pubkey, 1704 307, 1705 KIND_LIST_SET_GENERIC, 1706 members_with_empty_parts.content, 1707 members_with_empty_parts.tags, 1708 ); 1709 assert_eq!( 1710 radroots_replica_ingest_event(&exec, &members_with_empty_event) 1711 .expect("members with empty entry"), 1712 RadrootsReplicaIngestOutcome::Applied 1713 ); 1714 let owners_valid = 1715 farm_list_sets::farm_owners_list_set(farm_d_tag, vec![fixture_public_key(8)]) 1716 .expect("owners"); 1717 let owners_event = list_set_event(407, &farm_pubkey, 307, KIND_LIST_SET_GENERIC, &owners_valid); 1718 assert_eq!( 1719 radroots_replica_ingest_event(&exec, &owners_event).expect("owners apply"), 1720 RadrootsReplicaIngestOutcome::Applied 1721 ); 1722 let workers_valid = 1723 farm_list_sets::farm_workers_list_set(farm_d_tag, vec![fixture_public_key(16)]) 1724 .expect("workers"); 1725 let workers_event = list_set_event( 1726 408, 1727 &farm_pubkey, 1728 308, 1729 KIND_LIST_SET_GENERIC, 1730 &workers_valid, 1731 ); 1732 assert_eq!( 1733 radroots_replica_ingest_event(&exec, &workers_event).expect("workers apply"), 1734 RadrootsReplicaIngestOutcome::Applied 1735 ); 1736 1737 let members = unwrap_sql( 1738 farm_member::find_many( 1739 &exec, 1740 &IFarmMemberFindMany { 1741 filter: Some(IFarmMemberFieldsFilter { 1742 id: None, 1743 created_at: None, 1744 updated_at: None, 1745 farm_id: Some(farm_id), 1746 member_pubkey: None, 1747 role: None, 1748 }), 1749 }, 1750 ), 1751 "members", 1752 ) 1753 .results; 1754 assert_eq!(members.len(), 3); 1755 1756 let invalid_plots = ListSet { 1757 d_tag: format!("farm:{farm_d_tag}:plots"), 1758 content: String::new(), 1759 entries: vec![ListEntry { 1760 tag: "p".to_string(), 1761 values: vec!["x".to_string()], 1762 }], 1763 title: None, 1764 description: None, 1765 image: None, 1766 }; 1767 let invalid_plots_event = list_set_event( 1768 409, 1769 &farm_pubkey, 1770 309, 1771 KIND_LIST_SET_GENERIC, 1772 &invalid_plots, 1773 ); 1774 let invalid_plots_err = radroots_replica_ingest_event(&exec, &invalid_plots_event) 1775 .expect_err("plots list requires a entries"); 1776 assert!( 1777 invalid_plots_err 1778 .to_string() 1779 .contains("must only include a tags") 1780 ); 1781 1782 let plot_address = plot_encode::plot_address(&farm_pubkey, plot_d_tag).expect("plot address"); 1783 let plots_valid = ListSet { 1784 d_tag: format!("farm:{farm_d_tag}:plots"), 1785 content: String::new(), 1786 entries: vec![ListEntry { 1787 tag: "a".to_string(), 1788 values: vec![plot_address], 1789 }], 1790 title: None, 1791 description: None, 1792 image: None, 1793 }; 1794 let plots_event = list_set_event(410, &farm_pubkey, 310, KIND_LIST_SET_GENERIC, &plots_valid); 1795 assert_eq!( 1796 radroots_replica_ingest_event(&exec, &plots_event).expect("plots apply"), 1797 RadrootsReplicaIngestOutcome::Applied 1798 ); 1799 1800 let unsupported_list_set = ListSet { 1801 d_tag: "unsupported.list".to_string(), 1802 content: String::new(), 1803 entries: vec![ListEntry { 1804 tag: "p".to_string(), 1805 values: vec![farm_pubkey.clone()], 1806 }], 1807 title: None, 1808 description: None, 1809 image: None, 1810 }; 1811 let unsupported_event = list_set_event( 1812 411, 1813 &profile_pubkey, 1814 311, 1815 KIND_LIST_SET_GENERIC, 1816 &unsupported_list_set, 1817 ); 1818 let unsupported_err = radroots_replica_ingest_event(&exec, &unsupported_event) 1819 .expect_err("unsupported list set d_tag"); 1820 assert!( 1821 unsupported_err 1822 .to_string() 1823 .contains("unsupported list set d_tag") 1824 ); 1825 1826 let mut malformed_farm_list_missing_farm_parts = 1827 list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC) 1828 .expect("malformed missing farm parts"); 1829 for tag in &mut malformed_farm_list_missing_farm_parts.tags { 1830 if tag.first().map(String::as_str) == Some("d") && tag.len() > 1 { 1831 tag[1] = "farm".to_string(); 1832 } 1833 } 1834 let malformed_farm_list_missing_farm_event = event_with_parts( 1835 412, 1836 &farm_pubkey, 1837 312, 1838 KIND_LIST_SET_GENERIC, 1839 malformed_farm_list_missing_farm_parts.content, 1840 malformed_farm_list_missing_farm_parts.tags, 1841 ); 1842 assert!(radroots_replica_ingest_event(&exec, &malformed_farm_list_missing_farm_event).is_err()); 1843 1844 let mut malformed_farm_list_missing_suffix_parts = 1845 list_set_encode::to_wire_parts_with_kind(&member_of_valid, KIND_LIST_SET_GENERIC) 1846 .expect("malformed missing suffix parts"); 1847 for tag in &mut malformed_farm_list_missing_suffix_parts.tags { 1848 if tag.first().map(String::as_str) == Some("d") && tag.len() > 1 { 1849 tag[1] = format!("farm:{farm_d_tag}"); 1850 } 1851 } 1852 let malformed_farm_list_missing_suffix_event = event_with_parts( 1853 413, 1854 &farm_pubkey, 1855 313, 1856 KIND_LIST_SET_GENERIC, 1857 malformed_farm_list_missing_suffix_parts.content, 1858 malformed_farm_list_missing_suffix_parts.tags, 1859 ); 1860 assert!( 1861 radroots_replica_ingest_event(&exec, &malformed_farm_list_missing_suffix_event).is_err() 1862 ); 1863 } 1864 1865 #[test] 1866 fn sync_status_reports_pending_when_not_all_events_are_ingested() { 1867 let source = SqlxSqliteExecutor::open_memory().expect("source"); 1868 let (_request, _farm_d_tag, _farm_pubkey, drafts) = seed_source(&source); 1869 let target = SqlxSqliteExecutor::open_memory().expect("target"); 1870 migrations::run_all_up(&target).expect("migrations"); 1871 1872 for (index, draft) in drafts.iter().enumerate() { 1873 let event = draft_to_event(draft, index as u32); 1874 let _ = radroots_replica_ingest_event(&target, &event).expect("ingest"); 1875 } 1876 target 1877 .exec( 1878 "UPDATE nostr_event_head SET content_hash = ? WHERE id = (SELECT id FROM nostr_event_head LIMIT 1)", 1879 "[\"invalid_hash\"]", 1880 ) 1881 .expect("mutate state hash"); 1882 1883 let status = radroots_replica_sync_status(&target).expect("status pending"); 1884 assert_eq!(status.expected_count, drafts.len()); 1885 assert!(status.pending_count > 0); 1886 } 1887 1888 #[test] 1889 fn sync_all_rejects_invalid_selectors_and_resolves_unique_pair() { 1890 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 1891 migrations::run_all_up(&exec).expect("migrations"); 1892 1893 let missing_selector_err = radroots_replica_sync_all( 1894 &exec, 1895 &RadrootsReplicaSyncRequest { 1896 farm: RadrootsReplicaFarmSelector { 1897 id: None, 1898 d_tag: None, 1899 pubkey: None, 1900 }, 1901 options: None, 1902 }, 1903 ) 1904 .expect_err("selector validation"); 1905 assert!( 1906 missing_selector_err 1907 .to_string() 1908 .contains("requires id or (d_tag + pubkey)") 1909 ); 1910 1911 let missing_id_err = radroots_replica_sync_all( 1912 &exec, 1913 &RadrootsReplicaSyncRequest { 1914 farm: RadrootsReplicaFarmSelector { 1915 id: Some("00000000-0000-0000-0000-000000000000".to_string()), 1916 d_tag: None, 1917 pubkey: None, 1918 }, 1919 options: None, 1920 }, 1921 ) 1922 .expect_err("missing farm id"); 1923 assert!(missing_id_err.to_string().contains("farm not found")); 1924 1925 let duplicate_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); 1926 let duplicate_pubkey = fixture_public_key(14); 1927 let fields = IFarmFields { 1928 d_tag: duplicate_d_tag.clone(), 1929 pubkey: duplicate_pubkey.clone(), 1930 name: "one".to_string(), 1931 about: None, 1932 website: None, 1933 picture: None, 1934 banner: None, 1935 location_primary: None, 1936 location_city: None, 1937 location_region: None, 1938 location_country: None, 1939 }; 1940 let _ = unwrap_sql(farm::create(&exec, &fields), "farm one"); 1941 assert!(farm::create(&exec, &fields).is_err()); 1942 1943 let bundle = radroots_replica_sync_all( 1944 &exec, 1945 &RadrootsReplicaSyncRequest { 1946 farm: RadrootsReplicaFarmSelector { 1947 id: None, 1948 d_tag: Some(duplicate_d_tag), 1949 pubkey: Some(duplicate_pubkey), 1950 }, 1951 options: None, 1952 }, 1953 ) 1954 .expect("unique pair should resolve"); 1955 assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); 1956 } 1957 1958 #[test] 1959 fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { 1960 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 1961 migrations::run_all_up(&exec).expect("migrations"); 1962 1963 let farm_pubkey = fixture_public_key(16); 1964 let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); 1965 let farm_row = unwrap_sql( 1966 farm::create( 1967 &exec, 1968 &IFarmFields { 1969 d_tag: farm_d_tag.clone(), 1970 pubkey: farm_pubkey.clone(), 1971 name: "farm".to_string(), 1972 about: Some("about".to_string()), 1973 website: None, 1974 picture: None, 1975 banner: None, 1976 location_primary: Some("primary".to_string()), 1977 location_city: Some("city".to_string()), 1978 location_region: Some("region".to_string()), 1979 location_country: Some("country".to_string()), 1980 }, 1981 ), 1982 "farm", 1983 ) 1984 .result; 1985 1986 let bad_gcs = unwrap_sql( 1987 gcs_location::create( 1988 &exec, 1989 &IGcsLocationFields { 1990 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), 1991 lat: 10.0, 1992 lng: 20.0, 1993 geohash: "s0".to_string(), 1994 point: "{".to_string(), 1995 polygon: "{\"type\":\"Polygon\",\"coordinates\":[[]]}".to_string(), 1996 accuracy: None, 1997 altitude: None, 1998 tag_0: None, 1999 label: None, 2000 area: None, 2001 elevation: None, 2002 soil: None, 2003 climate: None, 2004 gc_id: None, 2005 gc_name: None, 2006 gc_admin1_id: None, 2007 gc_admin1_name: None, 2008 gc_country_id: None, 2009 gc_country_name: None, 2010 }, 2011 ), 2012 "bad gcs", 2013 ) 2014 .result; 2015 let _ = unwrap_sql( 2016 farm_gcs_location::create( 2017 &exec, 2018 &IFarmGcsLocationFields { 2019 farm_id: farm_row.id.clone(), 2020 gcs_location_id: bad_gcs.id.clone(), 2021 role: "".to_string(), 2022 }, 2023 ), 2024 "farm gcs", 2025 ); 2026 2027 let plot_row = unwrap_sql( 2028 plot::create( 2029 &exec, 2030 &IPlotFields { 2031 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 2032 farm_id: farm_row.id.clone(), 2033 name: "plot".to_string(), 2034 about: Some("plot about".to_string()), 2035 location_primary: Some("plot primary".to_string()), 2036 location_city: None, 2037 location_region: None, 2038 location_country: None, 2039 }, 2040 ), 2041 "plot", 2042 ) 2043 .result; 2044 let _ = unwrap_sql( 2045 plot_gcs_location::create( 2046 &exec, 2047 &IPlotGcsLocationFields { 2048 plot_id: plot_row.id.clone(), 2049 gcs_location_id: bad_gcs.id, 2050 role: "primary".to_string(), 2051 }, 2052 ), 2053 "plot gcs", 2054 ); 2055 2056 let member_pubkey = fixture_public_key(6); 2057 let _ = unwrap_sql( 2058 farm_member::create( 2059 &exec, 2060 &IFarmMemberFields { 2061 farm_id: farm_row.id.clone(), 2062 member_pubkey: member_pubkey.clone(), 2063 role: "owner".to_string(), 2064 }, 2065 ), 2066 "member", 2067 ); 2068 let _ = unwrap_sql( 2069 farm_member_claim::create( 2070 &exec, 2071 &IFarmMemberClaimFields { 2072 member_pubkey: member_pubkey.clone(), 2073 farm_pubkey: farm_pubkey.clone(), 2074 }, 2075 ), 2076 "claim", 2077 ); 2078 let _ = unwrap_sql( 2079 nostr_profile::create( 2080 &exec, 2081 &INostrProfileFields { 2082 public_key: farm_pubkey.clone(), 2083 profile_type: "farm".to_string(), 2084 name: "farm profile".to_string(), 2085 display_name: None, 2086 about: None, 2087 website: None, 2088 picture: None, 2089 banner: None, 2090 nip05: None, 2091 lud06: None, 2092 lud16: None, 2093 }, 2094 ), 2095 "farm profile", 2096 ); 2097 let _ = unwrap_sql( 2098 nostr_profile::create( 2099 &exec, 2100 &INostrProfileFields { 2101 public_key: member_pubkey.clone(), 2102 profile_type: "legacy".to_string(), 2103 name: "legacy profile".to_string(), 2104 display_name: Some("legacy".to_string()), 2105 about: Some("about".to_string()), 2106 website: Some("https://example.com".to_string()), 2107 picture: Some("https://example.com/p.png".to_string()), 2108 banner: Some("https://example.com/b.png".to_string()), 2109 nip05: Some("legacy@example.com".to_string()), 2110 lud06: Some("lud06".to_string()), 2111 lud16: Some("lud16".to_string()), 2112 }, 2113 ), 2114 "legacy profile", 2115 ); 2116 2117 let bundle = radroots_replica_sync_all( 2118 &exec, 2119 &RadrootsReplicaSyncRequest { 2120 farm: RadrootsReplicaFarmSelector { 2121 id: Some(farm_row.id), 2122 d_tag: None, 2123 pubkey: None, 2124 }, 2125 options: None, 2126 }, 2127 ) 2128 .expect("sync"); 2129 assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); 2130 assert!(bundle.events.iter().any(|event| event.kind == KIND_FARM)); 2131 assert!(bundle.events.iter().any(|event| event.kind == KIND_PLOT)); 2132 let mut list_set_seen = false; 2133 let mut list_set_missed = false; 2134 for event in &bundle.events { 2135 if event.kind == KIND_LIST_SET_GENERIC { 2136 list_set_seen = true; 2137 } else { 2138 list_set_missed = true; 2139 } 2140 } 2141 assert!(list_set_seen); 2142 assert!(list_set_missed); 2143 assert!(bundle.events.iter().all(|event| event.kind != KIND_PROFILE)); 2144 } 2145 2146 #[test] 2147 fn sync_emit_reports_encode_error_for_invalid_farm_record() { 2148 let exec = SqlxSqliteExecutor::open_memory().expect("db"); 2149 migrations::run_all_up(&exec).expect("migrations"); 2150 2151 let farm_row = unwrap_sql( 2152 farm::create( 2153 &exec, 2154 &IFarmFields { 2155 d_tag: String::new(), 2156 pubkey: fixture_public_key(15), 2157 name: "invalid farm".to_string(), 2158 about: None, 2159 website: None, 2160 picture: None, 2161 banner: None, 2162 location_primary: None, 2163 location_city: None, 2164 location_region: None, 2165 location_country: None, 2166 }, 2167 ), 2168 "farm", 2169 ) 2170 .result; 2171 2172 let err = radroots_replica_sync_all( 2173 &exec, 2174 &RadrootsReplicaSyncRequest { 2175 farm: RadrootsReplicaFarmSelector { 2176 id: Some(farm_row.id), 2177 d_tag: None, 2178 pubkey: None, 2179 }, 2180 options: None, 2181 }, 2182 ) 2183 .expect_err("encode error"); 2184 assert!(err.to_string().contains("replica_sync.encode")); 2185 } 2186 2187 #[test] 2188 fn error_conversion_paths_are_exercised() { 2189 let sql: RadrootsReplicaEventsError = ReplicaSchemaError::from(SqlError::Internal).into(); 2190 assert!(sql.to_string().contains("replica_sync.sql")); 2191 2192 let encode: RadrootsReplicaEventsError = EventEncodeError::Json.into(); 2193 assert!(encode.to_string().contains("replica_sync.encode")); 2194 2195 let parse_number_err = "x".parse::<u32>().expect_err("parse should fail"); 2196 let parse: RadrootsReplicaEventsError = 2197 EventParseError::InvalidNumber("k", parse_number_err).into(); 2198 assert!(parse.to_string().contains("replica_sync.parse")); 2199 }