commit c263f7787b217e4168a56374e782e133b5c4c6bf
parent be78712eb08288f7811974c4f9f8e96aaaac8429
Author: triesap <tyson@radroots.org>
Date: Thu, 13 Aug 2026 18:50:19 +0000
test: close mobile ffi coverage gate
- exercise stable error and identity boundary vocabularies
- verify public media cache behavior against real local I/O
- cover profile, revision, and native upload admission paths
- clear the frozen per-crate coverage policy without exclusions
Diffstat:
4 files changed, 640 insertions(+), 28 deletions(-)
diff --git a/core/crates/tera_ffi/src/error.rs b/core/crates/tera_ffi/src/error.rs
@@ -374,10 +374,110 @@ mod tests {
Phase1DraftError::Storage,
Phase1DraftError::Overlay,
Phase1DraftError::Corrupt,
+ Phase1DraftError::ClockUnavailable,
+ Phase1DraftError::DeadlineOverflow,
+ Phase1DraftError::NoWritableRelay,
+ Phase1DraftError::InvalidRevision,
] {
let ffi = RadrootsAppError::from(error);
assert_eq!(ffi.report().category, "authoring");
assert!(!ffi.report().safe_message.is_empty());
}
}
+
+ #[test]
+ fn cursor_media_settings_and_profile_failures_cover_every_stable_class() {
+ use radroots_mobile_core::runtime::product_surface::{
+ CursorError, IdentitySettingsError, Phase1InboundMediaError,
+ };
+
+ for cursor in [
+ CursorError::InvalidContext,
+ CursorError::Malformed,
+ CursorError::Integrity,
+ CursorError::Version,
+ CursorError::ContextMismatch,
+ CursorError::SnapshotMismatch,
+ CursorError::Stale,
+ CursorError::InvalidPosition,
+ ] {
+ let error = RadrootsAppError::from(TodayError::Cursor(cursor));
+ assert_eq!(error.report().code, "today_cursor_invalid");
+ }
+ for media in [
+ Phase1InboundMediaError::InvalidReference,
+ Phase1InboundMediaError::InvalidDigest,
+ Phase1InboundMediaError::MissingDigest,
+ Phase1InboundMediaError::InvalidMediaType,
+ Phase1InboundMediaError::InvalidDimensions,
+ Phase1InboundMediaError::InvalidByteSize,
+ Phase1InboundMediaError::InvalidAlt,
+ Phase1InboundMediaError::MetadataMismatch,
+ Phase1InboundMediaError::InvalidOperation,
+ Phase1InboundMediaError::OperationMismatch,
+ Phase1InboundMediaError::InvalidFailure,
+ Phase1InboundMediaError::InvalidConfiguration,
+ Phase1InboundMediaError::ConfigurationMismatch,
+ Phase1InboundMediaError::InvalidVerificationTime,
+ Phase1InboundMediaError::InvalidCachePolicy,
+ Phase1InboundMediaError::InvalidCacheObservation,
+ Phase1InboundMediaError::CacheQuotaExceeded,
+ Phase1InboundMediaError::ArtifactCollision,
+ Phase1InboundMediaError::CorruptReceipt,
+ Phase1InboundMediaError::CorruptState,
+ Phase1InboundMediaError::UnsupportedSchema,
+ Phase1InboundMediaError::CacheUnavailable,
+ Phase1InboundMediaError::CacheIo,
+ Phase1InboundMediaError::CorruptArtifact,
+ ] {
+ let error = RadrootsAppError::from(TodayError::InboundMedia(media));
+ assert_eq!(error.report().code, "today_media_invalid");
+ }
+ for settings in [
+ SettingsError::UnknownRelayAccess,
+ SettingsError::InvalidRelayEndpoint,
+ SettingsError::InvalidRelayEndpointCount,
+ SettingsError::DuplicateRelayEndpoint,
+ SettingsError::InvalidBlossomEndpoint,
+ SettingsError::InvalidBlossomEndpointCount,
+ SettingsError::NetworkEnvironmentMismatch,
+ SettingsError::InvalidMediaCacheBytes,
+ SettingsError::InvalidMediaCacheArtifacts,
+ SettingsError::RevisionConflict,
+ SettingsError::RevisionExhausted,
+ SettingsError::UnsupportedSchema,
+ SettingsError::CorruptDocument,
+ SettingsError::Storage,
+ SettingsError::Identity(IdentitySettingsError::InvalidIdentityId),
+ ] {
+ let expected_retryable = matches!(
+ settings,
+ SettingsError::RevisionConflict
+ | SettingsError::RevisionExhausted
+ | SettingsError::Storage
+ );
+ let error = RadrootsAppError::from(settings);
+ assert_eq!(error.report().retryable, expected_retryable);
+ }
+ for profile in [
+ ProfileMetadataError::InvalidName,
+ ProfileMetadataError::InvalidDisplayName,
+ ProfileMetadataError::InvalidAbout,
+ ProfileMetadataError::InvalidNip05,
+ ] {
+ assert_eq!(RadrootsAppError::from(profile).report().category, "profile");
+ }
+ assert_eq!(
+ RadrootsAppError::initialization("private").report().code,
+ "initialization_failed"
+ );
+ assert_eq!(
+ RadrootsAppError::invalid_argument("bad")
+ .with_operation_id("operation".to_owned())
+ .report()
+ .operation_id
+ .as_deref(),
+ Some("operation")
+ );
+ }
}
diff --git a/core/crates/tera_ffi/src/operations.rs b/core/crates/tera_ffi/src/operations.rs
@@ -730,6 +730,9 @@ pub(crate) fn decode_reference_fingerprint(value: &str) -> Result<[u8; 32], Radr
#[cfg(test)]
mod tests {
use super::*;
+ use radroots_mobile_core::runtime::product_surface::Phase1MediaConfigurationFingerprint;
+
+ const PUBLIC_KEY: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
#[test]
fn media_operation_can_be_claimed_exactly_once() {
@@ -769,4 +772,237 @@ mod tests {
.unwrap_err();
assert_eq!(error.report().code, "invalid_identity_command");
}
+
+ #[test]
+ fn boundary_enums_and_identity_commands_cover_the_closed_vocabularies() {
+ for (ffi, core) in [
+ (
+ crate::FfiAddCommandType::CreateUpdate,
+ AddCommandType::CreateUpdate,
+ ),
+ (
+ crate::FfiAddCommandType::CreatePhotoUpdate,
+ AddCommandType::CreatePhotoUpdate,
+ ),
+ (
+ crate::FfiAddCommandType::CreateAsk,
+ AddCommandType::CreateAsk,
+ ),
+ (
+ crate::FfiAddCommandType::CreateEvent,
+ AddCommandType::CreateEvent,
+ ),
+ (
+ crate::FfiAddCommandType::CreateFoodAvailability,
+ AddCommandType::CreateFoodAvailability,
+ ),
+ ] {
+ assert_eq!(AddCommandType::from(ffi), core);
+ }
+ for environment in [
+ FfiMobileNetworkEnvironment::Public,
+ FfiMobileNetworkEnvironment::Simulator,
+ FfiMobileNetworkEnvironment::PhysicalDevice,
+ ] {
+ assert_eq!(
+ FfiMobileNetworkEnvironment::from(MobileNetworkEnvironment::from(environment)),
+ environment
+ );
+ }
+ for access in [
+ FfiRelayAccessPreference::ReadOnly,
+ FfiRelayAccessPreference::ReadWrite,
+ ] {
+ assert_eq!(
+ FfiRelayAccessPreference::from(RelayAccessPreference::from(access)),
+ access
+ );
+ }
+ for authority in [
+ FfiBlossomAuthorityPreference::PublicWebPki,
+ FfiBlossomAuthorityPreference::LoopbackDevelopment,
+ FfiBlossomAuthorityPreference::PrivateNetworkDevelopment,
+ ] {
+ assert_eq!(
+ FfiBlossomAuthorityPreference::from(BlossomEndpointAuthorityPreference::from(
+ authority
+ )),
+ authority
+ );
+ }
+
+ let commands = [
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::BeginImport,
+ operation_id: Some("operation".to_owned()),
+ identity_id: None,
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::CompleteImport,
+ operation_id: Some("operation".to_owned()),
+ identity_id: Some("primary".to_owned()),
+ public_key: Some(PUBLIC_KEY.to_owned()),
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::CancelImport,
+ operation_id: Some("operation".to_owned()),
+ identity_id: None,
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Select,
+ operation_id: None,
+ identity_id: Some("primary".to_owned()),
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Lock,
+ operation_id: None,
+ identity_id: None,
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Unlock,
+ operation_id: None,
+ identity_id: None,
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Recover,
+ operation_id: None,
+ identity_id: None,
+ public_key: None,
+ },
+ ];
+ for command in commands {
+ assert!(IdentityCommand::try_from(command).is_ok());
+ }
+ for command in [
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::BeginImport,
+ operation_id: Some("operation".to_owned()),
+ identity_id: Some("unexpected".to_owned()),
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::CancelImport,
+ operation_id: Some("operation".to_owned()),
+ identity_id: None,
+ public_key: Some(PUBLIC_KEY.to_owned()),
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Select,
+ operation_id: Some("unexpected".to_owned()),
+ identity_id: Some("primary".to_owned()),
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Select,
+ operation_id: None,
+ identity_id: Some("primary".to_owned()),
+ public_key: Some(PUBLIC_KEY.to_owned()),
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Lock,
+ operation_id: Some("unexpected".to_owned()),
+ identity_id: None,
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Unlock,
+ operation_id: None,
+ identity_id: Some("unexpected".to_owned()),
+ public_key: None,
+ },
+ FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ kind: FfiIdentityCommandKind::Recover,
+ operation_id: None,
+ identity_id: None,
+ public_key: Some(PUBLIC_KEY.to_owned()),
+ },
+ ] {
+ assert!(IdentityCommand::try_from(command).is_err());
+ }
+ assert!(
+ IdentityCommand::try_from(FfiIdentityCommandRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION + 1,
+ kind: FfiIdentityCommandKind::Lock,
+ operation_id: None,
+ identity_id: None,
+ public_key: None,
+ })
+ .is_err()
+ );
+
+ let identity = IdentityRecord::new("primary", PUBLIC_KEY).unwrap();
+ for lock_state in [IdentityLockState::Locked, IdentityLockState::Unlocked] {
+ let state = IdentityState::new(
+ vec![identity.clone()],
+ Some("primary".to_owned()),
+ lock_state,
+ None,
+ )
+ .unwrap();
+ let record = FfiIdentityStateRecord::from(&state);
+ assert_eq!(record.identities.len(), 1);
+ assert_eq!(record.lock_state, lock_state.into());
+ }
+ }
+
+ #[test]
+ fn media_decoders_status_and_private_operation_accessors_are_exhaustive() {
+ assert!(require_schema(MOBILE_FFI_SCHEMA_VERSION).is_ok());
+ assert!(require_schema(MOBILE_FFI_SCHEMA_VERSION + 1).is_err());
+ for invalid in ["", "not-hex", "00"] {
+ assert!(decode_artifact_id(invalid).is_err());
+ assert!(decode_configuration(invalid).is_err());
+ assert!(decode_reference_fingerprint(invalid).is_err());
+ }
+ let digest = "11".repeat(32);
+ assert!(decode_artifact_id(&digest).is_ok());
+ assert!(decode_configuration(&digest).is_ok());
+ assert_eq!(decode_reference_fingerprint(&digest).unwrap(), [0x11; 32]);
+
+ let configuration = Phase1MediaConfigurationFingerprint::new([7; 32]).unwrap();
+ let status = FfiMediaCacheStatusRecord::from(Phase1MediaCacheStatus {
+ artifacts: 2,
+ bytes: 9,
+ configuration: Some(configuration),
+ });
+ assert_eq!(status.artifact_count, 2);
+ assert_eq!(status.total_bytes, 9);
+ assert_eq!(status.configuration_fingerprint, Some("07".repeat(32)));
+ assert!(
+ FfiMediaCacheStatusRecord::from(Phase1MediaCacheStatus {
+ artifacts: 0,
+ bytes: 0,
+ configuration: None,
+ })
+ .configuration_fingerprint
+ .is_none()
+ );
+
+ let operation = FfiMediaOperation::new().unwrap();
+ assert_eq!(operation.id(), operation.operation_id);
+ assert!(!operation.cancellation().is_cancelled());
+ assert!(!operation.is_cancelled());
+ operation.cancel();
+ assert!(operation.is_cancelled());
+ assert!(operation.cancellation().is_cancelled());
+ }
}
diff --git a/core/crates/tera_ffi/tests/local_mvp_real_io.rs b/core/crates/tera_ffi/tests/local_mvp_real_io.rs
@@ -1,5 +1,6 @@
use std::io::Write;
use std::os::fd::AsRawFd;
+use std::sync::Arc;
use std::time::Duration;
use nostr::{EventBuilder, Keys, Kind, Metadata, Tag, Timestamp};
@@ -9,7 +10,7 @@ use radroots_blossom::Sha256;
use radroots_mobile_ffi::{
FfiAddCommandType, FfiAddDraftInput, FfiBlossomEndpointAuthority, FfiBlossomHostKind,
FfiBlossomUploadInput, FfiCancellationPolicy, FfiEventTimingKind, FfiLocalNetworkRecord,
- FfiMediaStage, FfiOutboxState, FfiPreparedMediaInput, FfiQueuePolicyRecord,
+ FfiMediaOperation, FfiMediaStage, FfiOutboxState, FfiPreparedMediaInput, FfiQueuePolicyRecord,
FfiRelaySatisfaction, FfiRetractionDraftInput, FfiTodayCardType, FfiTodayProjectionUpdate,
FfiTodayRelaySyncState, HostSigningOutcome, HostSigningRequest, HostSigningResult,
MOBILE_FFI_SCHEMA_VERSION, ProtectedDataAvailability, RadrootsHostSigner, RadrootsRuntime,
@@ -75,6 +76,14 @@ struct BlossomServer {
impl BlossomServer {
async fn spawn(bytes: Vec<u8>, corrupt_retrieval: bool) -> Self {
+ Self::spawn_with_retrievals(bytes, corrupt_retrieval, 1).await
+ }
+
+ async fn spawn_with_retrievals(
+ bytes: Vec<u8>,
+ corrupt_retrieval: bool,
+ retrievals: usize,
+ ) -> Self {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind Blossom server");
@@ -106,17 +115,19 @@ impl BlossomServer {
);
write_http_response(&mut upload, "application/json", &descriptor).await;
- let (mut retrieval, _) = listener.accept().await.expect("accept Blossom retrieval");
- let request = read_http_request(&mut retrieval).await;
- assert!(
- String::from_utf8_lossy(&request)
- .starts_with(format!("GET /{hash}.png HTTP/1.1\r\n").as_str())
- );
- let mut response_bytes = bytes;
- if corrupt_retrieval {
- response_bytes[0] ^= 1;
+ for _ in 0..retrievals {
+ let (mut retrieval, _) = listener.accept().await.expect("accept Blossom retrieval");
+ let request = read_http_request(&mut retrieval).await;
+ assert!(
+ String::from_utf8_lossy(&request)
+ .starts_with(format!("GET /{hash}.png HTTP/1.1\r\n").as_str())
+ );
+ let mut response_bytes = bytes.clone();
+ if corrupt_retrieval {
+ response_bytes[0] ^= 1;
+ }
+ write_http_response(&mut retrieval, "image/png", &response_bytes).await;
}
- write_http_response(&mut retrieval, "image/png", &response_bytes).await;
});
Self { origin, task }
}
@@ -131,7 +142,7 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services()
let relay = MockRelay::run().await.expect("local NIP-01 relay");
let relay_url = relay.url().await.to_string();
let image_bytes = png(2, 3);
- let blossom = BlossomServer::spawn(image_bytes.clone(), false).await;
+ let blossom = BlossomServer::spawn_with_retrievals(image_bytes.clone(), false, 2).await;
let mut image_file = tempfile::tempfile().expect("media file");
image_file.write_all(&image_bytes).expect("write media");
let media = prepared_media(&blossom.origin, &image_bytes, &image_file, "Harvest photo");
@@ -256,8 +267,6 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services()
.await;
advance_complete(&publisher, &id, queued.revision).await;
}
- blossom.finish().await;
-
let reader_root = tempfile::tempdir().expect("fresh reader root");
support::prepare(reader_root.path());
let reader = RadrootsRuntime::new(
@@ -308,6 +317,55 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services()
photo.media[0].sha256.as_deref(),
Some(media.sha256.as_str())
);
+ reader
+ .configure_blossom(
+ FfiBlossomHostKind::Simulator,
+ FfiBlossomEndpointAuthority::LoopbackDevelopment,
+ blossom.origin.clone(),
+ vec![],
+ )
+ .expect("reader Blossom profile");
+ let artifact = reader
+ .phase1_retrieve_media(
+ context.clone(),
+ photo.media[0].reference_fingerprint.clone(),
+ Arc::new(FfiMediaOperation::new().expect("media operation")),
+ )
+ .await
+ .expect("retrieve projected media");
+ assert_eq!(artifact.bytes, image_bytes);
+ assert_eq!(
+ artifact.byte_size,
+ u64::try_from(image_bytes.len()).unwrap()
+ );
+ assert!(
+ reader
+ .phase1_verified_media_artifact(context.clone(), artifact.artifact_id.clone())
+ .await
+ .expect("verify cached media")
+ .is_some()
+ );
+ let media_cache = reader
+ .phase1_media_cache_status(context.clone())
+ .await
+ .expect("media cache status");
+ assert_eq!(media_cache.artifact_count, 1);
+ assert_eq!(media_cache.total_bytes, artifact.byte_size);
+ assert!(media_cache.configuration_fingerprint.is_some());
+ assert!(
+ reader
+ .phase1_invalidate_media_artifact(context.clone(), artifact.artifact_id.clone())
+ .await
+ .expect("invalidate cached media")
+ );
+ assert!(
+ reader
+ .phase1_verified_media_artifact(context.clone(), artifact.artifact_id)
+ .await
+ .expect("verify invalidated media")
+ .is_none()
+ );
+ blossom.finish().await;
let update = cards
.iter()
.find(|card| card.card_type == FfiTodayCardType::Update)
diff --git a/core/crates/tera_ffi/tests/runtime_delegation.rs b/core/crates/tera_ffi/tests/runtime_delegation.rs
@@ -1,10 +1,13 @@
+use std::sync::Arc;
+
use radroots_mobile_ffi::{
FfiAddCommandType, FfiAddDraftInput, FfiBlossomAuthorityPreference,
FfiBlossomEndpointAuthority, FfiBlossomHostKind, FfiBlossomPreferencesRecord,
- FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiIdentityCommandKind,
- FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord,
- FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMobileNetworkEnvironment,
- FfiOutboxState, FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord,
+ FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiEventTimingKind,
+ FfiIdentityCommandKind, FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord,
+ FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMediaOperation,
+ FfiMobileNetworkEnvironment, FfiNativeUploadCompletionInput, FfiOutboxState,
+ FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord,
FfiRelayAccessPreference, FfiRelayAccessRecord, FfiRelayPreferenceRecord,
FfiRelayPreferencesRecord, FfiRelaySatisfaction, FfiReplaceSettingsRecord,
FfiRetractionDraftInput, FfiRevisionInputRecord, FfiRevisionPhase, FfiTodayCardType,
@@ -200,6 +203,15 @@ async fn native_boundary_delegates_the_complete_core_surface() {
.expect("simulator Blossom");
runtime
.configure_blossom(
+ FfiBlossomHostKind::Simulator,
+ FfiBlossomEndpointAuthority::LoopbackDevelopment,
+ "http://127.0.0.1:1".to_owned(),
+ vec![],
+ )
+ .expect("unavailable simulator Blossom");
+ assert!(runtime.probe_blossom().await.is_err());
+ runtime
+ .configure_blossom(
FfiBlossomHostKind::PhysicalDevice,
FfiBlossomEndpointAuthority::PrivateNetworkDevelopment,
"https://10.0.0.5:3100".to_owned(),
@@ -409,20 +421,74 @@ async fn native_boundary_delegates_the_complete_core_surface() {
.await
.expect("cancelled draft");
assert_eq!(cancelled.state, FfiOutboxState::Cancelled);
+ assert!(
+ runtime
+ .phase1_save_add_intent(
+ FfiAddDraftInput {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ command_type: FfiAddCommandType::CreateEvent,
+ content: "Market opens Saturday".to_owned(),
+ identifier: None,
+ title: Some("Saturday market".to_owned()),
+ summary: Some("Weekly market".to_owned()),
+ location: Some("Town square".to_owned()),
+ event_timing: Some(FfiEventTimingKind::AllDay),
+ event_start_date: Some("2027-01-02".to_owned()),
+ event_end_date: None,
+ event_start_unix_s: None,
+ event_end_unix_s: None,
+ event_timezone: None,
+ price_amount: None,
+ currency: None,
+ unit: None,
+ quantity: None,
+ food_published_at_unix_s: None,
+ food_status: None,
+ media: Vec::new(),
+ },
+ None,
+ Some(1),
+ )
+ .await
+ .is_err()
+ );
+ assert!(
+ runtime
+ .phase1_advance_draft(draft_id.clone(), cancelled.revision)
+ .await
+ .is_err()
+ );
let retraction_id = "0a".repeat(16);
+ let retraction_input = FfiRetractionDraftInput {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ command_type: FfiAddCommandType::CreateUpdate,
+ target_card_id: "c".repeat(64),
+ target_event_id: "a".repeat(64),
+ target_kind: 1,
+ target_address: None,
+ reason: "Replaced with a corrected copy".to_owned(),
+ };
+ let mut unsupported_retraction = retraction_input.clone();
+ unsupported_retraction.schema_version += 1;
+ assert_eq!(
+ runtime
+ .phase1_save_retraction_draft(
+ retraction_id.clone(),
+ unsupported_retraction,
+ 1_800_000_005,
+ 1_800_000_005_000,
+ )
+ .await
+ .expect_err("unsupported retraction schema")
+ .report()
+ .code,
+ "unsupported_schema_version"
+ );
let retraction = runtime
.phase1_save_retraction_draft(
retraction_id.clone(),
- FfiRetractionDraftInput {
- schema_version: MOBILE_FFI_SCHEMA_VERSION,
- command_type: FfiAddCommandType::CreateUpdate,
- target_card_id: "c".repeat(64),
- target_event_id: "a".repeat(64),
- target_kind: 1,
- target_address: None,
- reason: "Replaced with a corrected copy".to_owned(),
- },
+ retraction_input,
1_800_000_005,
1_800_000_005_000,
)
@@ -474,18 +540,90 @@ async fn native_boundary_delegates_the_complete_core_surface() {
.await
.expect_err("unsupported upload schema");
assert_eq!(upload_error.report().code, "unsupported_schema_version");
+ assert_eq!(
+ runtime
+ .phase1_prepare_add_media_background(upload.clone())
+ .await
+ .expect_err("unsupported background upload schema")
+ .report()
+ .code,
+ "unsupported_schema_version"
+ );
+ assert_eq!(
+ runtime
+ .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION + 1,
+ draft_id: upload.draft_id.clone(),
+ expected_revision: upload.expected_revision,
+ media: upload.media.clone(),
+ status_code: 200,
+ response_media_type: Some("application/json".to_owned()),
+ response_content_encoding: None,
+ response_body: Vec::new(),
+ })
+ .await
+ .expect_err("unsupported completion schema")
+ .report()
+ .code,
+ "invalid_native_upload_completion"
+ );
+ assert_eq!(
+ runtime
+ .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ draft_id: upload.draft_id.clone(),
+ expected_revision: upload.expected_revision,
+ media: upload.media.clone(),
+ status_code: 200,
+ response_media_type: Some("application/json".to_owned()),
+ response_content_encoding: None,
+ response_body: vec![0; 16_385],
+ })
+ .await
+ .expect_err("oversized completion body")
+ .report()
+ .code,
+ "invalid_native_upload_completion"
+ );
let mut invalid_id_upload = upload;
invalid_id_upload.schema_version = MOBILE_FFI_SCHEMA_VERSION;
invalid_id_upload.draft_id = "not-a-draft-id".to_owned();
assert_eq!(
runtime
- .phase1_upload_add_media_intent(invalid_id_upload)
+ .phase1_upload_add_media_intent(invalid_id_upload.clone())
.await
.expect_err("invalid draft id")
.report()
.code,
"invalid_draft_id"
);
+ assert_eq!(
+ runtime
+ .phase1_prepare_add_media_background(invalid_id_upload.clone())
+ .await
+ .expect_err("invalid background draft id")
+ .report()
+ .code,
+ "invalid_draft_id"
+ );
+ assert_eq!(
+ runtime
+ .phase1_complete_add_media_background(FfiNativeUploadCompletionInput {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ draft_id: invalid_id_upload.draft_id,
+ expected_revision: invalid_id_upload.expected_revision,
+ media: invalid_id_upload.media,
+ status_code: 200,
+ response_media_type: Some("application/json".to_owned()),
+ response_content_encoding: None,
+ response_body: Vec::new(),
+ })
+ .await
+ .expect_err("invalid completion draft id")
+ .report()
+ .code,
+ "invalid_draft_id"
+ );
assert!(
runtime
.phase1_local_network(FfiLocalNetworkRecord {
@@ -590,6 +728,20 @@ async fn native_boundary_delegates_the_complete_core_surface() {
assert_eq!(revision.phase, FfiRevisionPhase::ReplacementPending);
assert_eq!(revision.operation_id, revision.replacement.draft_id);
assert!(revision.replacement.is_revision);
+ assert_eq!(
+ runtime
+ .phase1_revision_status(revision.operation_id.clone())
+ .await
+ .expect("revision status")
+ .phase,
+ FfiRevisionPhase::ReplacementPending
+ );
+ assert!(
+ runtime
+ .phase1_advance_revision(revision.operation_id.clone())
+ .await
+ .is_err()
+ );
let cancelled_revision = runtime
.phase1_cancel_revision(revision.operation_id.clone())
.await
@@ -612,12 +764,78 @@ async fn native_boundary_delegates_the_complete_core_surface() {
.expect("save profile intent");
assert_eq!(profile.state, FfiOutboxState::Draft);
assert_eq!(profile.operation_id.len(), 32);
+ assert_eq!(
+ runtime
+ .phase1_profile_status(profile.operation_id.clone())
+ .await
+ .expect("profile status")
+ .revision,
+ profile.revision
+ );
let cancelled_profile = runtime
.phase1_cancel_profile(profile.operation_id.clone(), profile.revision)
.await
.expect("cancel profile intent");
assert_eq!(cancelled_profile.operation_id, profile.operation_id);
assert_eq!(cancelled_profile.state, FfiOutboxState::Cancelled);
+ assert_eq!(
+ runtime
+ .phase1_advance_profile(profile.operation_id.clone())
+ .await
+ .expect("advance cancelled profile")
+ .state,
+ FfiOutboxState::Cancelled
+ );
+
+ let cache = runtime
+ .phase1_media_cache_status(local_network.clone())
+ .await
+ .expect("empty media cache status");
+ assert_eq!(cache.artifact_count, 0);
+ assert_eq!(cache.total_bytes, 0);
+ assert!(cache.configuration_fingerprint.is_none());
+ assert!(
+ runtime
+ .phase1_verified_media_artifact(local_network.clone(), "11".repeat(32))
+ .await
+ .expect("empty verified media lookup")
+ .is_none()
+ );
+ assert!(
+ !runtime
+ .phase1_invalidate_media_artifact(local_network.clone(), "11".repeat(32))
+ .await
+ .expect("missing media invalidation")
+ );
+ assert!(
+ runtime
+ .phase1_invalidate_media_configuration(local_network.clone(), "22".repeat(32))
+ .await
+ .expect("empty configuration invalidation")
+ .is_empty()
+ );
+ let media_operation = Arc::new(FfiMediaOperation::new().expect("media operation"));
+ let media_error = runtime
+ .phase1_retrieve_media(
+ local_network.clone(),
+ "invalid-reference".to_owned(),
+ Arc::clone(&media_operation),
+ )
+ .await
+ .expect_err("invalid media reference");
+ assert_eq!(
+ media_error.report().code,
+ "invalid_media_reference_fingerprint"
+ );
+ assert_eq!(
+ runtime
+ .phase1_retrieve_media(local_network, "00".repeat(32), Arc::clone(&media_operation),)
+ .await
+ .expect_err("single-use media operation")
+ .report()
+ .code,
+ "media_operation_already_used"
+ );
runtime.shutdown().await.expect("shutdown");
assert!(matches!(