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