lib

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

ingest_roundtrip.rs (68239B)


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