field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

runtime_delegation.rs (32771B)


      1 use std::os::fd::AsRawFd;
      2 use std::sync::Arc;
      3 
      4 use tera_ffi::{
      5     FfiAddCommandType, FfiAddDraftInput, FfiBlossomAuthorityPreference,
      6     FfiBlossomEndpointAuthority, FfiBlossomHostKind, FfiBlossomPreferencesRecord,
      7     FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiEventTimingKind,
      8     FfiIdentityCommandKind, FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord,
      9     FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMediaOperation,
     10     FfiMobileNetworkEnvironment, FfiNativeUploadCompletionInput, FfiOutboxState,
     11     FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord,
     12     FfiRelayAccessPreference, FfiRelayAccessRecord, FfiRelayPreferenceRecord,
     13     FfiRelayPreferencesRecord, FfiRelaySatisfaction, FfiReplaceSettingsRecord,
     14     FfiRetractionDraftInput, FfiRevisionInputRecord, FfiRevisionPhase, FfiTodayCardType,
     15     FfiTodayProjectionUpdate, MOBILE_FFI_SCHEMA_VERSION, TeraAppError,
     16 };
     17 
     18 #[path = "support/retraction.rs"]
     19 mod retraction;
     20 mod support;
     21 
     22 #[tokio::test]
     23 async fn native_boundary_delegates_the_complete_core_surface() {
     24     let (_root, runtime) = support::runtime().await;
     25     assert!(runtime.uptime_millis() >= 0);
     26     assert!(runtime.info_json().contains("sdk"));
     27     runtime.set_app_info_platform(
     28         Some("ios".to_owned()),
     29         Some("org.radroots.app".to_owned()),
     30         Some("0.1.0-alpha".to_owned()),
     31         Some("1".to_owned()),
     32         Some("0123456789abcdef0123456789abcdef01234567".to_owned()),
     33     );
     34     assert_eq!(
     35         runtime.info().app.platform.expect("platform").platform,
     36         Some("ios".to_owned())
     37     );
     38     assert!(!runtime.sdk_capabilities().is_empty());
     39     assert_eq!(
     40         runtime.sdk_storage_status().await.expect("storage").backend,
     41         "sqlite"
     42     );
     43 
     44     let public = runtime
     45         .sdk_relay_status()
     46         .expect("relay status")
     47         .expect("default public profile");
     48     assert_eq!(public.profile, "public");
     49     assert_eq!(public.state, "configured");
     50     assert_eq!(public.read_availability, "unavailable");
     51     assert_eq!(public.write_availability, "unavailable");
     52     assert_eq!(public.relays.len(), 1);
     53     assert_eq!(public.relays[0].access, FfiRelayAccessRecord::ReadWrite);
     54     assert_eq!(public.relays[0].read_state, "unobserved");
     55     assert_eq!(public.relays[0].write_state, "unobserved");
     56 
     57     runtime
     58         .configure_public_relays(vec!["wss://write.example".to_owned()])
     59         .await
     60         .expect("public relays");
     61     let public = runtime
     62         .sdk_relay_status()
     63         .expect("relay status")
     64         .expect("public profile");
     65     assert_eq!(public.relays.len(), 1);
     66     assert_eq!(public.relays[0].relay_url, "wss://write.example");
     67     assert_eq!(public.relays[0].access, FfiRelayAccessRecord::ReadWrite);
     68     assert!(
     69         runtime
     70             .configure_public_relays(vec!["ws://127.0.0.1:7447".to_owned()])
     71             .await
     72             .is_err()
     73     );
     74 
     75     let settings = runtime.phase1_settings().await.expect("settings");
     76     runtime
     77         .phase1_replace_settings(FfiReplaceSettingsRecord {
     78             schema_version: MOBILE_FFI_SCHEMA_VERSION,
     79             expected_revision: settings.revision,
     80             relays: FfiRelayPreferencesRecord {
     81                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
     82                 environment: FfiMobileNetworkEnvironment::Public,
     83                 endpoints: vec![
     84                     FfiRelayPreferenceRecord {
     85                         schema_version: MOBILE_FFI_SCHEMA_VERSION,
     86                         url: "wss://read.example".to_owned(),
     87                         access: FfiRelayAccessPreference::ReadOnly,
     88                     },
     89                     FfiRelayPreferenceRecord {
     90                         schema_version: MOBILE_FFI_SCHEMA_VERSION,
     91                         url: "wss://write.example".to_owned(),
     92                         access: FfiRelayAccessPreference::ReadWrite,
     93                     },
     94                 ],
     95             },
     96             blossom: FfiBlossomPreferencesRecord {
     97                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
     98                 environment: FfiMobileNetworkEnvironment::Public,
     99                 authority: FfiBlossomAuthorityPreference::PublicWebPki,
    100                 primary_origin: "https://media.example".to_owned(),
    101                 fallback_origins: vec![],
    102             },
    103             media_network: FfiMediaNetworkPolicyRecord {
    104                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    105                 allow_cellular_downloads: true,
    106                 allow_cellular_uploads: true,
    107                 allow_background_transfers: true,
    108             },
    109             local_storage: FfiLocalStoragePolicyRecord {
    110                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    111                 media_cache_bytes: 256 * 1024 * 1024,
    112                 media_cache_artifacts: 1024,
    113             },
    114         })
    115         .await
    116         .expect("replace settings");
    117     runtime
    118         .phase1_apply_settings_to_runtime()
    119         .await
    120         .expect("apply settings");
    121     let applied = runtime
    122         .sdk_relay_status()
    123         .expect("relay status")
    124         .expect("applied settings profile");
    125     assert_eq!(applied.relays.len(), 2);
    126     assert_eq!(applied.relays[0].access, FfiRelayAccessRecord::ReadOnly);
    127     assert_eq!(applied.relays[1].access, FfiRelayAccessRecord::ReadWrite);
    128 
    129     runtime
    130         .configure_simulator_relays(vec!["ws://127.0.0.1:7447".to_owned()])
    131         .await
    132         .expect("simulator relays");
    133     let simulator = runtime
    134         .sdk_relay_status()
    135         .expect("relay status")
    136         .expect("simulator profile");
    137     assert_eq!(simulator.profile, "simulator_local");
    138     assert_eq!(simulator.relays.len(), 1);
    139     assert_eq!(simulator.relays[0].access, FfiRelayAccessRecord::ReadWrite);
    140     assert!(
    141         runtime
    142             .phase1_local_network(FfiLocalNetworkRecord {
    143                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    144                 id: "simulator".to_owned(),
    145                 label: "Simulator".to_owned(),
    146                 relay_urls: vec!["ws://127.0.0.1:7447".to_owned()],
    147                 locality: None,
    148                 followed_authors: vec![],
    149                 generation: 1,
    150             })
    151             .is_ok()
    152     );
    153     assert!(
    154         runtime
    155             .configure_simulator_relays(vec!["wss://relay.example".to_owned()])
    156             .await
    157             .is_err()
    158     );
    159 
    160     runtime
    161         .configure_device_relays(vec!["wss://10.0.0.5:7447".to_owned()])
    162         .await
    163         .expect("device relays");
    164     let device = runtime
    165         .sdk_relay_status()
    166         .expect("relay status")
    167         .expect("device profile");
    168     assert_eq!(device.profile, "device_development");
    169     assert_eq!(device.relays.len(), 1);
    170     assert_eq!(device.relays[0].relay_url, "wss://10.0.0.5:7447");
    171     assert_eq!(device.relays[0].access, FfiRelayAccessRecord::ReadWrite);
    172     assert!(
    173         runtime
    174             .configure_device_relays(vec!["wss://127.0.0.1:7447".to_owned()])
    175             .await
    176             .is_err()
    177     );
    178 
    179     runtime
    180         .configure_blossom(
    181             FfiBlossomHostKind::PhysicalDevice,
    182             FfiBlossomEndpointAuthority::PublicWebPki,
    183             "https://media.example".to_owned(),
    184             vec!["https://fallback.example".to_owned()],
    185         )
    186         .await
    187         .expect("public Blossom");
    188     let blossom = runtime
    189         .sdk_blossom_configuration()
    190         .expect("Blossom configuration")
    191         .expect("configured Blossom");
    192     assert_eq!(blossom.host_kind, "physical_device");
    193     assert_eq!(blossom.endpoint_authority, "public_webpki");
    194     assert_eq!(blossom.primary_origin, "https://media.example");
    195     assert_eq!(blossom.fallback_origins, ["https://fallback.example"]);
    196     assert_eq!(blossom.config_fingerprint.len(), 64);
    197     let evidence = runtime
    198         .sdk_blossom_evidence()
    199         .expect("Blossom evidence")
    200         .expect("configured evidence");
    201     assert_eq!(evidence.schema_version, 2);
    202     assert_eq!(evidence.origin, "https://media.example");
    203     assert_eq!(evidence.config_fingerprint, blossom.config_fingerprint);
    204     assert_eq!(evidence.state, "configured_unobserved");
    205     assert_eq!(evidence.transport_security, "public_webpki");
    206     assert!(evidence.observed_at_unix_ms.is_none());
    207     assert!(evidence.error_code.is_none());
    208     assert!(evidence.server_error_code.is_none());
    209     runtime
    210         .configure_blossom(
    211             FfiBlossomHostKind::Simulator,
    212             FfiBlossomEndpointAuthority::LoopbackDevelopment,
    213             "http://127.0.0.1:3100".to_owned(),
    214             vec![],
    215         )
    216         .await
    217         .expect("simulator Blossom");
    218     runtime
    219         .configure_blossom(
    220             FfiBlossomHostKind::Simulator,
    221             FfiBlossomEndpointAuthority::LoopbackDevelopment,
    222             "http://127.0.0.1:1".to_owned(),
    223             vec![],
    224         )
    225         .await
    226         .expect("unavailable simulator Blossom");
    227     assert!(runtime.probe_blossom().await.is_err());
    228     runtime
    229         .configure_blossom(
    230             FfiBlossomHostKind::PhysicalDevice,
    231             FfiBlossomEndpointAuthority::PrivateNetworkDevelopment,
    232             "https://10.0.0.5:3100".to_owned(),
    233             vec![],
    234         )
    235         .await
    236         .expect("device Blossom");
    237 
    238     assert_eq!(
    239         runtime
    240             .phase1_card_add_parity()
    241             .into_iter()
    242             .map(|item| item.card_type)
    243             .collect::<Vec<_>>(),
    244         vec![
    245             FfiTodayCardType::Update,
    246             FfiTodayCardType::PhotoUpdate,
    247             FfiTodayCardType::Ask,
    248             FfiTodayCardType::Event,
    249             FfiTodayCardType::FoodAvailability,
    250         ]
    251     );
    252     assert_eq!(
    253         runtime
    254             .phase1_add_schemas()
    255             .into_iter()
    256             .map(|schema| schema.command_type)
    257             .collect::<Vec<_>>(),
    258         vec![
    259             FfiAddCommandType::CreateUpdate,
    260             FfiAddCommandType::CreatePhotoUpdate,
    261             FfiAddCommandType::CreateAsk,
    262             FfiAddCommandType::CreateEvent,
    263             FfiAddCommandType::CreateFoodAvailability,
    264         ]
    265     );
    266     let parity = runtime.phase1_card_add_parity();
    267     let schemas = runtime.phase1_add_schemas();
    268     assert_eq!(parity.len(), 5);
    269     for (index, item) in parity.iter().enumerate() {
    270         assert_eq!(item.command_type, schemas[index].command_type);
    271     }
    272     assert_eq!(
    273         schemas[1]
    274             .fields
    275             .iter()
    276             .find(|field| field.id == "media")
    277             .and_then(|field| field.max_items),
    278         Some(20)
    279     );
    280     assert_eq!(
    281         schemas[3]
    282             .fields
    283             .iter()
    284             .find(|field| field.id == "media")
    285             .and_then(|field| field.max_items),
    286         Some(1)
    287     );
    288     let local_network = runtime
    289         .phase1_local_network(FfiLocalNetworkRecord {
    290             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    291             id: "nearby".to_owned(),
    292             label: "Near me".to_owned(),
    293             relay_urls: vec!["ws://192.168.1.7:7447".to_owned()],
    294             locality: Some("u10h".to_owned()),
    295             followed_authors: vec!["a".repeat(64)],
    296             generation: 1,
    297         })
    298         .expect("valid local network");
    299     assert_eq!(local_network.id, "nearby");
    300     assert!(
    301         runtime
    302             .phase1_local_network(FfiLocalNetworkRecord {
    303                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    304                 id: "public-host".to_owned(),
    305                 label: "Public host".to_owned(),
    306                 relay_urls: vec!["wss://relay.example".to_owned()],
    307                 locality: None,
    308                 followed_authors: vec![],
    309                 generation: 1,
    310             })
    311             .is_err()
    312     );
    313     let refresh = runtime
    314         .phase1_refresh_today(
    315             local_network.clone(),
    316             1_800_000_001,
    317             FfiTodayProjectionUpdate::Incremental,
    318         )
    319         .await
    320         .expect("empty Today refresh");
    321     assert_eq!(refresh.update, FfiTodayProjectionUpdate::Incremental);
    322     assert_eq!(refresh.source_events, 0);
    323     assert_eq!(refresh.visible_cards, 0);
    324     assert!(refresh.content_generation > 0);
    325     let today = runtime
    326         .phase1_today_page(
    327             local_network.clone(),
    328             20,
    329             Some(1_800_000_001),
    330             None,
    331             Some("UTC".to_owned()),
    332         )
    333         .await
    334         .expect("empty Today page");
    335     assert!(today.items.is_empty());
    336     assert!(today.next_cursor.is_none());
    337     assert!(
    338         runtime
    339             .phase1_today_page(local_network.clone(), 20, None, None, None)
    340             .await
    341             .is_err()
    342     );
    343     assert!(
    344         runtime
    345             .phase1_today_page(
    346                 local_network.clone(),
    347                 20,
    348                 Some(1_800_000_001),
    349                 Some("opaque".to_owned()),
    350                 Some("UTC".to_owned())
    351             )
    352             .await
    353             .is_err()
    354     );
    355     assert!(
    356         runtime
    357             .phase1_search(
    358                 local_network.clone(),
    359                 "carrots".to_owned(),
    360                 20,
    361                 1_800_000_001,
    362                 "UTC".to_owned()
    363             )
    364             .await
    365             .expect("empty search")
    366             .is_empty()
    367     );
    368     let me = runtime
    369         .phase1_me(local_network.clone(), 1_800_000_001, "UTC".to_owned())
    370         .await
    371         .expect("Me snapshot");
    372     assert_eq!(me.public_key, support::PUBLIC_KEY);
    373 
    374     let add = FfiAddDraftInput {
    375         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    376         command_type: FfiAddCommandType::CreateUpdate,
    377         content: "Farm stand opens at noon".to_owned(),
    378         identifier: None,
    379         title: None,
    380         summary: None,
    381         location: None,
    382         event_timing: None,
    383         event_start_date: None,
    384         event_end_date: None,
    385         event_start_unix_s: None,
    386         event_end_unix_s: None,
    387         event_timezone: None,
    388         price_amount: None,
    389         currency: None,
    390         unit: None,
    391         quantity: None,
    392         food_published_at_unix_s: None,
    393         food_status: None,
    394         media: Vec::new(),
    395     };
    396     runtime
    397         .phase1_validate_add_draft(add.clone(), 1_800_000_001)
    398         .expect("valid draft");
    399     let saved = runtime
    400         .phase1_save_add_intent(add.clone(), None, None)
    401         .await
    402         .expect("saved draft");
    403     let draft_id = saved.draft_id.clone();
    404     assert_eq!(draft_id.len(), 32);
    405     assert_eq!(saved.state, FfiOutboxState::Draft);
    406     assert_eq!(saved.kind, FfiDraftKind::Add);
    407     assert_eq!(
    408         saved.form.as_ref().map(|form| form.content.as_str()),
    409         Some("Farm stand opens at noon")
    410     );
    411     assert_eq!(
    412         runtime
    413             .phase1_draft_status(draft_id.clone())
    414             .await
    415             .expect("draft status")
    416             .revision,
    417         saved.revision
    418     );
    419     assert_eq!(
    420         runtime
    421             .phase1_draft_heads(10)
    422             .await
    423             .expect("draft heads")
    424             .len(),
    425         1
    426     );
    427     let queued = runtime
    428         .phase1_queue_add_intent(draft_id.clone(), saved.revision)
    429         .await
    430         .expect("queued draft");
    431     assert_eq!(queued.state, FfiOutboxState::Queued);
    432     let recovered = runtime
    433         .phase1_recover_add_intent(draft_id.clone())
    434         .await
    435         .expect("recovered queue");
    436     assert_eq!(recovered.revision, queued.revision);
    437     let cancelled = runtime
    438         .phase1_cancel_add_intent(draft_id.clone(), recovered.revision)
    439         .await
    440         .expect("cancelled draft");
    441     assert_eq!(cancelled.state, FfiOutboxState::Cancelled);
    442     assert!(
    443         runtime
    444             .phase1_save_add_intent(
    445                 FfiAddDraftInput {
    446                     schema_version: MOBILE_FFI_SCHEMA_VERSION,
    447                     command_type: FfiAddCommandType::CreateEvent,
    448                     content: "Market opens Saturday".to_owned(),
    449                     identifier: None,
    450                     title: Some("Saturday market".to_owned()),
    451                     summary: Some("Weekly market".to_owned()),
    452                     location: Some("Town square".to_owned()),
    453                     event_timing: Some(FfiEventTimingKind::AllDay),
    454                     event_start_date: Some("2027-01-02".to_owned()),
    455                     event_end_date: None,
    456                     event_start_unix_s: None,
    457                     event_end_unix_s: None,
    458                     event_timezone: None,
    459                     price_amount: None,
    460                     currency: None,
    461                     unit: None,
    462                     quantity: None,
    463                     food_published_at_unix_s: None,
    464                     food_status: None,
    465                     media: Vec::new(),
    466                 },
    467                 None,
    468                 Some(1),
    469             )
    470             .await
    471             .is_err()
    472     );
    473     assert!(
    474         runtime
    475             .phase1_advance_draft(draft_id.clone(), cancelled.revision)
    476             .await
    477             .is_err()
    478     );
    479 
    480     runtime.shutdown().await.unwrap();
    481     let (source_card, source_event) = retraction::seed(_root.path(), add).await;
    482     let runtime = tera_ffi::TeraRuntime::new(
    483         _root.path().to_string_lossy().into_owned(),
    484         support::PUBLIC_KEY.into(),
    485         support::GENERATION.into(),
    486         1_800_000_000_000,
    487         tera_ffi::ProtectedDataAvailability::Available,
    488     )
    489     .await
    490     .unwrap();
    491     runtime
    492         .configure_device_relays(vec!["wss://10.0.0.5:7447".to_owned()])
    493         .await
    494         .unwrap();
    495     runtime
    496         .configure_blossom(
    497             FfiBlossomHostKind::PhysicalDevice,
    498             FfiBlossomEndpointAuthority::PrivateNetworkDevelopment,
    499             "https://10.0.0.5:3100".to_owned(),
    500             vec![],
    501         )
    502         .await
    503         .unwrap();
    504     let retraction_id = "0a".repeat(16);
    505     let retraction_input = FfiRetractionDraftInput {
    506         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    507         command_type: FfiAddCommandType::CreateUpdate,
    508         target_card_id: source_card.clone(),
    509         target_event_id: source_event.clone(),
    510         target_kind: 1,
    511         target_address: None,
    512         reason: "Replaced with a corrected copy".to_owned(),
    513     };
    514     let mut unsupported_retraction = retraction_input.clone();
    515     unsupported_retraction.schema_version += 1;
    516     assert_eq!(
    517         runtime
    518             .phase1_save_retraction_draft(
    519                 retraction_id.clone(),
    520                 unsupported_retraction,
    521                 1_800_000_005,
    522                 1_800_000_005_000,
    523             )
    524             .await
    525             .expect_err("unsupported retraction schema")
    526             .report()
    527             .code,
    528         "unsupported_schema_version"
    529     );
    530     let retraction = runtime
    531         .phase1_save_retraction_draft(
    532             retraction_id.clone(),
    533             retraction_input,
    534             1_800_000_005,
    535             1_800_000_005_000,
    536         )
    537         .await
    538         .expect("saved retraction");
    539     assert_eq!(retraction.kind, FfiDraftKind::Retraction);
    540     assert_eq!(retraction.card_id, source_card);
    541     assert!(retraction.form.is_none());
    542     let queued_retraction = runtime
    543         .phase1_queue_draft(
    544             retraction_id.clone(),
    545             retraction.revision,
    546             FfiQueuePolicyRecord {
    547                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    548                 relay_urls: vec!["wss://write.example".to_owned()],
    549                 satisfaction: FfiRelaySatisfaction::AllAccepted,
    550                 delivery_deadline_unix_ms: 1_800_100_000_000,
    551                 cancellation: FfiCancellationPolicy::LocalCooperative,
    552             },
    553             1_800_000_006_000,
    554         )
    555         .await
    556         .expect("queued retraction");
    557     let cancelled_retraction = runtime
    558         .phase1_cancel_draft(retraction_id, queued_retraction.revision, 1_800_000_007_000)
    559         .await
    560         .expect("cancelled retraction");
    561     assert_eq!(cancelled_retraction.state, FfiOutboxState::Cancelled);
    562 
    563     let upload_file = tempfile::tempfile().expect("media file");
    564     upload_file.set_len(1).expect("bounded media");
    565     let upload = FfiBlossomUploadIntent {
    566         schema_version: MOBILE_FFI_SCHEMA_VERSION + 1,
    567         draft_id,
    568         expected_revision: cancelled.revision,
    569         media: FfiPreparedMediaInput {
    570             schema_version: tera_ffi::PREPARED_MEDIA_FFI_SCHEMA_VERSION,
    571             opaque_reference: "media:unused".to_owned(),
    572             file: Arc::new(
    573                 tera_ffi::FfiMediaFile::new(upload_file.as_raw_fd() as u64, 1)
    574                     .expect("admitted media"),
    575             ),
    576             sha256: "00".repeat(32),
    577             media_type: "image/png".to_owned(),
    578             byte_size: 1,
    579             width: 1,
    580             height: 1,
    581             alt: "unused".to_owned(),
    582             prepared_at_unix_s: 1_800_000_000,
    583         },
    584     };
    585     let upload_error = runtime
    586         .phase1_upload_add_media_intent(upload.clone())
    587         .await
    588         .expect_err("unsupported upload schema");
    589     assert_eq!(upload_error.report().code, "unsupported_schema_version");
    590     assert_eq!(
    591         runtime
    592             .phase1_prepare_add_media_background(upload.clone())
    593             .await
    594             .expect_err("unsupported background upload schema")
    595             .report()
    596             .code,
    597         "unsupported_schema_version"
    598     );
    599     assert_eq!(
    600         runtime
    601             .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
    602                 schema_version: MOBILE_FFI_SCHEMA_VERSION + 1,
    603                 draft_id: upload.draft_id.clone(),
    604                 expected_revision: upload.expected_revision,
    605                 media: upload.media.clone(),
    606                 status_code: 200,
    607                 response_media_type: Some("application/json".to_owned()),
    608                 response_content_encoding: None,
    609                 response_body: Vec::new(),
    610             })
    611             .await
    612             .expect_err("unsupported completion schema")
    613             .report()
    614             .code,
    615         "invalid_native_upload_completion"
    616     );
    617     assert_eq!(
    618         runtime
    619             .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
    620                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    621                 draft_id: upload.draft_id.clone(),
    622                 expected_revision: upload.expected_revision,
    623                 media: upload.media.clone(),
    624                 status_code: 200,
    625                 response_media_type: Some("application/json".to_owned()),
    626                 response_content_encoding: None,
    627                 response_body: vec![0; 16_385],
    628             })
    629             .await
    630             .expect_err("oversized completion body")
    631             .report()
    632             .code,
    633         "invalid_native_upload_completion"
    634     );
    635     let mut invalid_id_upload = upload;
    636     invalid_id_upload.schema_version = MOBILE_FFI_SCHEMA_VERSION;
    637     invalid_id_upload.draft_id = "not-a-draft-id".to_owned();
    638     assert_eq!(
    639         runtime
    640             .phase1_upload_add_media_intent(invalid_id_upload.clone())
    641             .await
    642             .expect_err("invalid draft id")
    643             .report()
    644             .code,
    645         "invalid_draft_id"
    646     );
    647     assert_eq!(
    648         runtime
    649             .phase1_prepare_add_media_background(invalid_id_upload.clone())
    650             .await
    651             .expect_err("invalid background draft id")
    652             .report()
    653             .code,
    654         "invalid_draft_id"
    655     );
    656     assert_eq!(
    657         runtime
    658             .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
    659                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    660                 draft_id: invalid_id_upload.draft_id,
    661                 expected_revision: invalid_id_upload.expected_revision,
    662                 media: invalid_id_upload.media,
    663                 status_code: 200,
    664                 response_media_type: Some("application/json".to_owned()),
    665                 response_content_encoding: None,
    666                 response_body: Vec::new(),
    667             })
    668             .await
    669             .expect_err("invalid completion draft id")
    670             .report()
    671             .code,
    672         "invalid_draft_id"
    673     );
    674     assert!(
    675         runtime
    676             .phase1_local_network(FfiLocalNetworkRecord {
    677                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    678                 id: "nearby".to_owned(),
    679                 label: "Near me".to_owned(),
    680                 relay_urls: Vec::new(),
    681                 locality: None,
    682                 followed_authors: Vec::new(),
    683                 generation: 1,
    684             })
    685             .is_err()
    686     );
    687 
    688     let initial_settings = runtime.phase1_settings().await.expect("settings");
    689     let begun = runtime
    690         .phase1_apply_identity_command(
    691             initial_settings.revision,
    692             FfiIdentityCommandRecord {
    693                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    694                 kind: FfiIdentityCommandKind::BeginImport,
    695                 operation_id: Some("import-ffi-1".to_owned()),
    696                 identity_id: None,
    697                 public_key: None,
    698             },
    699         )
    700         .await
    701         .expect("begin identity import");
    702     let completed = runtime
    703         .phase1_apply_identity_command(
    704             begun.settings.revision,
    705             FfiIdentityCommandRecord {
    706                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    707                 kind: FfiIdentityCommandKind::CompleteImport,
    708                 operation_id: Some("import-ffi-1".to_owned()),
    709                 identity_id: Some("primary".to_owned()),
    710                 public_key: Some(support::PUBLIC_KEY.to_owned()),
    711             },
    712         )
    713         .await
    714         .expect("complete identity import");
    715     let unlocked = runtime
    716         .phase1_apply_identity_command(
    717             completed.settings.revision,
    718             FfiIdentityCommandRecord {
    719                 schema_version: MOBILE_FFI_SCHEMA_VERSION,
    720                 kind: FfiIdentityCommandKind::Unlock,
    721                 operation_id: None,
    722                 identity_id: None,
    723                 public_key: None,
    724             },
    725         )
    726         .await
    727         .expect("record process-local unlock");
    728     assert_eq!(
    729         unlocked.settings.identity.lock_state,
    730         FfiIdentityLockState::Unlocked
    731     );
    732     assert_eq!(unlocked.settings.revision, completed.settings.revision);
    733 
    734     let source_event_id = source_event;
    735     let source = tera_core::runtime::product_surface::CardSourceIdentity::Event(
    736         radroots_event::EventId::parse(&source_event_id).expect("source event id"),
    737     );
    738     let card_id = tera_core::runtime::product_surface::CardId::derive(
    739         tera_core::runtime::product_surface::TodayCardType::Update,
    740         &source,
    741     )
    742     .to_hex();
    743     let revision_input = FfiRevisionInputRecord {
    744         request_id: "ac".repeat(16),
    745         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    746         card_id,
    747         source_event_id,
    748         source_address: None,
    749         author_public_key: support::PUBLIC_KEY.to_owned(),
    750         replacement: FfiAddDraftInput {
    751             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    752             command_type: FfiAddCommandType::CreateUpdate,
    753             content: "Corrected farm stand hours".to_owned(),
    754             identifier: None,
    755             title: None,
    756             summary: None,
    757             location: None,
    758             event_timing: None,
    759             event_start_date: None,
    760             event_end_date: None,
    761             event_start_unix_s: None,
    762             event_end_unix_s: None,
    763             event_timezone: None,
    764             price_amount: None,
    765             currency: None,
    766             unit: None,
    767             quantity: None,
    768             food_published_at_unix_s: None,
    769             food_status: None,
    770             media: Vec::new(),
    771         },
    772     };
    773     let revision = runtime
    774         .phase1_save_revision_intent(revision_input.clone())
    775         .await
    776         .expect("save lossless revision intent");
    777     assert_eq!(
    778         runtime
    779             .phase1_save_revision_intent(revision_input.clone())
    780             .await
    781             .unwrap(),
    782         revision
    783     );
    784     let mut changed = revision_input;
    785     changed.replacement.content = "Conflicting correction".into();
    786     assert!(runtime.phase1_save_revision_intent(changed).await.is_err());
    787     assert_eq!(revision.phase, FfiRevisionPhase::ReplacementPending);
    788     assert_eq!(revision.operation_id, revision.replacement.draft_id);
    789     assert!(revision.replacement.is_revision);
    790     assert_eq!(
    791         runtime
    792             .phase1_revision_status(revision.operation_id.clone())
    793             .await
    794             .expect("revision status")
    795             .phase,
    796         FfiRevisionPhase::ReplacementPending
    797     );
    798     assert!(
    799         runtime
    800             .phase1_advance_revision(revision.operation_id.clone())
    801             .await
    802             .is_err()
    803     );
    804     let cancelled_revision = runtime
    805         .phase1_cancel_revision(revision.operation_id.clone())
    806         .await
    807         .expect("cancel revision intent");
    808     assert_eq!(cancelled_revision.operation_id, revision.operation_id);
    809     assert_eq!(cancelled_revision.phase, FfiRevisionPhase::Cancelled);
    810 
    811     let profile = runtime
    812         .phase1_save_profile_metadata(FfiProfileMetadataInputRecord {
    813             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    814             name: "grower".to_owned(),
    815             display_name: Some("Local Grower".to_owned()),
    816             about: Some("Seasonal produce".to_owned()),
    817             picture: None,
    818             banner: None,
    819             nip05: Some("grower@farm.example".to_owned()),
    820             bot: Some(false),
    821         })
    822         .await
    823         .expect("save profile intent");
    824     assert_eq!(profile.state, FfiOutboxState::Draft);
    825     assert_eq!(profile.operation_id.len(), 32);
    826     assert_eq!(
    827         runtime
    828             .phase1_profile_status(profile.operation_id.clone())
    829             .await
    830             .expect("profile status")
    831             .revision,
    832         profile.revision
    833     );
    834     let cancelled_profile = runtime
    835         .phase1_cancel_profile(profile.operation_id.clone(), profile.revision)
    836         .await
    837         .expect("cancel profile intent");
    838     assert_eq!(cancelled_profile.operation_id, profile.operation_id);
    839     assert_eq!(cancelled_profile.state, FfiOutboxState::Cancelled);
    840     assert_eq!(
    841         runtime
    842             .phase1_advance_profile(profile.operation_id.clone())
    843             .await
    844             .expect("advance cancelled profile")
    845             .state,
    846         FfiOutboxState::Cancelled
    847     );
    848 
    849     // The seeded source changed history after the earlier empty projection.
    850     runtime
    851         .phase1_refresh_today(
    852             local_network.clone(),
    853             1_800_000_010,
    854             FfiTodayProjectionUpdate::Incremental,
    855         )
    856         .await
    857         .unwrap();
    858     let cache = runtime
    859         .phase1_media_cache_status(local_network.clone())
    860         .await
    861         .expect("empty media cache status");
    862     assert_eq!(cache.artifact_count, 0);
    863     assert_eq!(cache.total_bytes, 0);
    864     assert!(cache.configuration_fingerprint.is_none());
    865     assert!(
    866         runtime
    867             .phase1_verified_media_artifact(local_network.clone(), "11".repeat(32))
    868             .await
    869             .expect("empty verified media lookup")
    870             .is_none()
    871     );
    872     assert!(
    873         !runtime
    874             .phase1_invalidate_media_artifact(local_network.clone(), "11".repeat(32))
    875             .await
    876             .expect("missing media invalidation")
    877     );
    878     assert!(
    879         runtime
    880             .phase1_invalidate_media_configuration(local_network.clone(), "22".repeat(32))
    881             .await
    882             .expect("empty configuration invalidation")
    883             .is_empty()
    884     );
    885     let media_operation = Arc::new(FfiMediaOperation::new().expect("media operation"));
    886     let media_error = runtime
    887         .phase1_retrieve_media(
    888             local_network.clone(),
    889             "invalid-reference".to_owned(),
    890             Arc::clone(&media_operation),
    891         )
    892         .await
    893         .expect_err("invalid media reference");
    894     assert_eq!(
    895         media_error.report().code,
    896         "invalid_media_reference_fingerprint"
    897     );
    898     assert_eq!(
    899         runtime
    900             .phase1_retrieve_media(local_network, "00".repeat(32), Arc::clone(&media_operation),)
    901             .await
    902             .expect_err("single-use media operation")
    903             .report()
    904             .code,
    905         "media_operation_already_used"
    906     );
    907 
    908     runtime.shutdown().await.expect("shutdown");
    909     assert!(matches!(
    910         runtime.sdk_storage_status().await,
    911         Err(TeraAppError::Failure { .. })
    912     ));
    913     assert!(matches!(
    914         runtime.sdk_relay_status(),
    915         Err(TeraAppError::Failure { .. })
    916     ));
    917     assert!(matches!(
    918         runtime.configure_public_relays(Vec::new()).await,
    919         Err(TeraAppError::Failure { .. })
    920     ));
    921 }