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 }