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 }