field_ios

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

uniffi_contract.rs (9505B)


      1 use secp256k1::{Keypair, Message, Secp256k1, SecretKey};
      2 use std::sync::{Arc, Mutex};
      3 use tera_ffi::{
      4     FfiAddCommandType, FfiAddDraftInput, FfiCancellationPolicy, FfiMediaOperation,
      5     FfiQueuePolicyRecord, FfiRelaySatisfaction, FfiTradeEvidenceCoverage, FfiTradeEvidenceOutcome,
      6     HostSigningOutcome, HostSigningRequest, HostSigningResult, MOBILE_FFI_SCHEMA_VERSION,
      7     ProtectedDataAvailability, SignerAvailabilityRecord, SignerStatusRecord, TeraAppError,
      8     TeraHostSigner, TeraRuntime,
      9 };
     10 
     11 mod support;
     12 
     13 struct TestHostSigner(Arc<Mutex<HostSigningOutcome>>);
     14 
     15 #[async_trait::async_trait]
     16 impl TeraHostSigner for TestHostSigner {
     17     async fn signer_status(&self) -> SignerStatusRecord {
     18         SignerStatusRecord {
     19             schema_version: MOBILE_FFI_SCHEMA_VERSION,
     20             availability: SignerAvailabilityRecord::Ready,
     21         }
     22     }
     23 
     24     async fn sign(&self, request: HostSigningRequest) -> HostSigningResult {
     25         let outcome = *self
     26             .0
     27             .lock()
     28             .unwrap_or_else(std::sync::PoisonError::into_inner);
     29         let signature_hex = (outcome == HostSigningOutcome::Signed).then(|| {
     30             let mut secret_bytes = [0; 32];
     31             secret_bytes[31] = 1;
     32             let secret = SecretKey::from_slice(&secret_bytes).expect("fixture secret key");
     33             let keypair = Keypair::from_secret_key(&Secp256k1::new(), &secret);
     34             let digest: [u8; 32] = request
     35                 .event_id_digest
     36                 .clone()
     37                 .try_into()
     38                 .expect("32-byte event ID digest");
     39             assert_eq!(hex::encode(digest), request.expected_event_id);
     40             assert_eq!(
     41                 keypair.x_only_public_key().0.to_string(),
     42                 request.public_key
     43             );
     44             let message = Message::from_digest(digest);
     45             let signature = Secp256k1::new().sign_schnorr_no_aux_rand(&message, &keypair);
     46             Secp256k1::new()
     47                 .verify_schnorr(&signature, &message, &keypair.x_only_public_key().0)
     48                 .expect("fixture host signature verifies");
     49             signature.to_string()
     50         });
     51         let completed_at_unix_ms = std::time::SystemTime::now()
     52             .duration_since(std::time::UNIX_EPOCH)
     53             .expect("system clock after epoch")
     54             .as_millis()
     55             .try_into()
     56             .expect("current time fits u64");
     57         assert!(completed_at_unix_ms < request.deadline_unix_ms);
     58         HostSigningResult {
     59             schema_version: MOBILE_FFI_SCHEMA_VERSION,
     60             outcome,
     61             operation_id: request.operation_id,
     62             signer_request_id: request.signer_request_id,
     63             public_key: request.public_key,
     64             purpose: request.purpose,
     65             signature_hex,
     66             completed_at_unix_ms,
     67         }
     68     }
     69 }
     70 
     71 #[test]
     72 fn swift_module_names_bind_the_tera_producer() {
     73     let config = include_str!("../uniffi.toml");
     74     assert_eq!(
     75         config,
     76         "[bindings.swift]\nmodule_name = \"TeraKitBindings\"\nffi_module_name = \"TeraFFI\"\n"
     77     );
     78 }
     79 
     80 #[test]
     81 fn final_evidence_vocabularies_are_exact_at_the_mobile_boundary() {
     82     assert_eq!(
     83         [
     84             FfiTradeEvidenceCoverage::Missing,
     85             FfiTradeEvidenceCoverage::Partial,
     86             FfiTradeEvidenceCoverage::ScopeSatisfied,
     87             FfiTradeEvidenceCoverage::Unsupported,
     88         ]
     89         .len(),
     90         4
     91     );
     92     assert_eq!(
     93         [
     94             FfiTradeEvidenceOutcome::Valid,
     95             FfiTradeEvidenceOutcome::Invalid,
     96             FfiTradeEvidenceOutcome::Indeterminate,
     97         ]
     98         .len(),
     99         3
    100     );
    101 }
    102 
    103 #[test]
    104 fn media_cancellation_handle_owns_one_stable_opaque_operation_identity() {
    105     let operation = FfiMediaOperation::new().expect("media operation");
    106     let operation_id = operation.operation_id();
    107     assert_eq!(operation_id.len(), 32);
    108     assert!(!operation.is_cancelled());
    109     operation.cancel();
    110     assert!(operation.is_cancelled());
    111     assert_eq!(operation.operation_id(), operation_id);
    112 }
    113 
    114 #[tokio::test]
    115 async fn protected_data_failure_is_typed_and_opens_no_store() {
    116     let root = tempfile::tempdir().expect("tempdir");
    117     support::prepare(root.path());
    118     let result = TeraRuntime::new(
    119         root.path().to_string_lossy().into_owned(),
    120         support::PUBLIC_KEY.to_owned(),
    121         support::GENERATION.to_owned(),
    122         1_800_000_000_000,
    123         ProtectedDataAvailability::Unavailable,
    124     )
    125     .await;
    126     let Err(TeraAppError::Failure { report }) = result else {
    127         panic!("protected data failure must remain typed across UniFFI");
    128     };
    129     assert_eq!(report.code, "protected_data_unavailable");
    130     assert!(report.retryable);
    131     assert!(
    132         !root
    133             .path()
    134             .join("radroots/users")
    135             .join(support::PUBLIC_KEY)
    136             .join("runtime.sqlite")
    137             .exists()
    138     );
    139 }
    140 
    141 #[tokio::test]
    142 async fn final_mobile_abi_uses_async_sdk_dtos_and_versioned_errors() {
    143     let (_root, runtime) = support::runtime().await;
    144     let storage = runtime.sdk_storage_status().await.expect("storage status");
    145     assert_eq!(storage.backend, "sqlite");
    146 
    147     runtime.shutdown().await.expect("shutdown");
    148     let error = runtime
    149         .sdk_storage_status()
    150         .await
    151         .expect_err("closed client must reject operations");
    152     let TeraAppError::Failure { report } = error;
    153     assert_eq!(report.schema_version, 1);
    154     assert_eq!(report.code, "client_closed");
    155     assert_eq!(report.category, "runtime");
    156     assert!(!report.retryable);
    157     assert_eq!(report.safe_message, "SDK client is closed");
    158 }
    159 
    160 #[tokio::test]
    161 async fn host_signer_constructor_exposes_only_an_opaque_configured_boundary() {
    162     let root = tempfile::tempdir().expect("tempdir");
    163     support::prepare(root.path());
    164     let outcome = Arc::new(Mutex::new(HostSigningOutcome::Rejected));
    165     let runtime = TeraRuntime::with_host_signer(
    166         root.path().to_string_lossy().into_owned(),
    167         support::PUBLIC_KEY.to_owned(),
    168         support::GENERATION.to_owned(),
    169         1_800_000_000_000,
    170         ProtectedDataAvailability::Available,
    171         Box::new(TestHostSigner(Arc::clone(&outcome))),
    172     )
    173     .await
    174     .expect("runtime with host signer");
    175 
    176     let identity = runtime.identity_status().expect("identity status");
    177     assert_eq!(identity.public_key, support::PUBLIC_KEY);
    178     assert!(identity.host_signer_configured);
    179 
    180     for (index, host_outcome) in [
    181         HostSigningOutcome::Signed,
    182         HostSigningOutcome::Locked,
    183         HostSigningOutcome::Cancelled,
    184         HostSigningOutcome::Rejected,
    185         HostSigningOutcome::TimedOut,
    186         HostSigningOutcome::Unavailable,
    187         HostSigningOutcome::Invalidated,
    188         HostSigningOutcome::Failed,
    189     ]
    190     .into_iter()
    191     .enumerate()
    192     {
    193         *outcome
    194             .lock()
    195             .unwrap_or_else(std::sync::PoisonError::into_inner) = host_outcome;
    196         let draft_id = format!("{:02x}", index + 17).repeat(16);
    197         let saved = runtime
    198             .phase1_save_draft(
    199                 draft_id.clone(),
    200                 FfiAddDraftInput {
    201                     schema_version: MOBILE_FFI_SCHEMA_VERSION,
    202                     command_type: FfiAddCommandType::CreateUpdate,
    203                     content: format!("Signer boundary test {index}"),
    204                     identifier: None,
    205                     title: None,
    206                     summary: None,
    207                     location: None,
    208                     event_timing: None,
    209                     event_start_date: None,
    210                     event_end_date: None,
    211                     event_start_unix_s: None,
    212                     event_end_unix_s: None,
    213                     event_timezone: None,
    214                     price_amount: None,
    215                     currency: None,
    216                     unit: None,
    217                     quantity: None,
    218                     food_published_at_unix_s: None,
    219                     food_status: None,
    220                     media: Vec::new(),
    221                 },
    222                 1_800_000_000 + index as u64,
    223                 None,
    224                 1_800_000_000_000 + index as u64,
    225             )
    226             .await
    227             .expect("saved draft");
    228         let queued = runtime
    229             .phase1_queue_draft(
    230                 draft_id.clone(),
    231                 saved.revision,
    232                 FfiQueuePolicyRecord {
    233                     schema_version: MOBILE_FFI_SCHEMA_VERSION,
    234                     relay_urls: vec!["wss://relay.example".to_owned()],
    235                     satisfaction: FfiRelaySatisfaction::AnyAccepted,
    236                     delivery_deadline_unix_ms: u64::MAX,
    237                     cancellation: FfiCancellationPolicy::PreservePublishedRequest,
    238                 },
    239                 1_800_000_001_000 + index as u64,
    240             )
    241             .await
    242             .expect("queued draft");
    243         let signing_result = runtime
    244             .phase1_sign_queued_draft(draft_id, queued.revision)
    245             .await;
    246         if host_outcome == HostSigningOutcome::Signed {
    247             assert_eq!(
    248                 signing_result.expect("valid host signature").state,
    249                 tera_ffi::FfiOutboxState::Signed
    250             );
    251         } else {
    252             let signing_error = signing_result.expect_err("host failure remains typed");
    253             assert_eq!(signing_error.report().category, "authoring");
    254             assert!(!signing_error.report().safe_message.contains("signature"));
    255         }
    256     }
    257     runtime.shutdown().await.expect("shutdown");
    258 }