lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }