app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

lib.rs (21652B)


      1 #![doc = "Integration-only native test bridge for HarvestCircle."]
      2 
      3 use std::fmt::{self, Display, Formatter};
      4 use std::fs;
      5 use std::num::NonZeroUsize;
      6 use std::path::{Path, PathBuf};
      7 use std::sync::atomic::{AtomicBool, Ordering};
      8 use std::sync::{Arc, Mutex};
      9 use std::time::Duration;
     10 
     11 use harvestcircle_application::{
     12     AppLifecycle, AppSnapshot, Clock, DurableRequestId, GeneratedKeyRecoveryHandle,
     13     InMemorySecretStore, RelayAccess, RelayConfiguration, RelayEndpoint, RelayUrlPolicy,
     14     RemovalConfirmationToken, SecretStore, SessionState, SnapshotRevision,
     15 };
     16 use harvestcircle_domain::{PublicKey, SafeError, SecretKeyInput, UnixTimestamp};
     17 use harvestcircle_nostr::SdkNostrClient;
     18 use harvestcircle_runtime::{
     19     InstallationIdentity, InstallationIdentitySource, RuntimeActorHandle,
     20     RuntimeChangeSubscription, RuntimeDependencies,
     21 };
     22 use nostr::{EventBuilder, Keys, Metadata};
     23 use nostr_relay_builder::MockRelay;
     24 use nostr_sdk::Client;
     25 use radroots_runtime_paths::{
     26     InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
     27     RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
     28 };
     29 use radroots_service_sqlite::MigrationBuildIdentity;
     30 use tokio::runtime::{Builder, Runtime};
     31 
     32 const ACTOR_CAPACITY: usize = 16;
     33 const OBSERVER_CAPACITY: usize = 16;
     34 const DEFAULT_TIMEOUT_MILLIS: u64 = 2_000;
     35 const FIXED_TIME_SECONDS: i64 = 1_700_000_000;
     36 const FIXED_INSTALLATION_ID: &str = "0123456789abcdef0123456789abcdef";
     37 
     38 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     39 pub struct TestIdentity {
     40     pub public_key_hex: String,
     41     pub npub: String,
     42     pub display_label: String,
     43 }
     44 
     45 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     46 pub enum TestLifecycle {
     47     Booting,
     48     Ready,
     49     Degraded,
     50     Fatal,
     51     Closed,
     52 }
     53 
     54 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     55 pub enum TestSession {
     56     SignedOut,
     57     Activating,
     58     Active,
     59     SigningOut,
     60     Failed,
     61 }
     62 
     63 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     64 pub struct TestSnapshot {
     65     pub revision: u64,
     66     pub lifecycle: TestLifecycle,
     67     pub identities: Vec<TestIdentity>,
     68     pub selected_public_key_hex: Option<String>,
     69     pub session: TestSession,
     70     pub profile_display_name: Option<String>,
     71 }
     72 
     73 #[derive(uniffi::Object)]
     74 pub struct TestGeneratedRecoveryRequest {
     75     handle: GeneratedKeyRecoveryHandle,
     76     resolved: AtomicBool,
     77 }
     78 
     79 #[derive(uniffi::Object)]
     80 pub struct TestRemovalRequest {
     81     token: Mutex<Option<RemovalConfirmationToken>>,
     82     public_key_hex: String,
     83     deletes_local_credential: bool,
     84     signs_out: bool,
     85     expires_at_seconds: i64,
     86 }
     87 
     88 #[uniffi::export]
     89 impl TestRemovalRequest {
     90     pub fn public_key_hex(&self) -> String {
     91         self.public_key_hex.clone()
     92     }
     93 
     94     pub fn deletes_local_credential(&self) -> bool {
     95         self.deletes_local_credential
     96     }
     97 
     98     pub fn signs_out(&self) -> bool {
     99         self.signs_out
    100     }
    101 
    102     pub fn expires_at_seconds(&self) -> i64 {
    103         self.expires_at_seconds
    104     }
    105 }
    106 
    107 #[uniffi::export]
    108 impl TestGeneratedRecoveryRequest {
    109     pub fn identity(&self) -> TestIdentity {
    110         to_identity(self.handle.view().identity())
    111     }
    112 
    113     pub fn expires_at_seconds(&self) -> i64 {
    114         self.handle.view().expires_at().as_seconds()
    115     }
    116 
    117     pub fn take_recovery_nsec(&self) -> Result<String, TestBridgeError> {
    118         self.handle
    119             .take_recovery_nsec()
    120             .map(|nsec| nsec.with_exposed_secret(ToOwned::to_owned))
    121             .map_err(TestBridgeError::from)
    122     }
    123 }
    124 
    125 #[derive(Debug, uniffi::Error)]
    126 pub enum TestBridgeError {
    127     Failure { safe_message: String },
    128 }
    129 
    130 impl Display for TestBridgeError {
    131     fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    132         match self {
    133             Self::Failure { safe_message } => formatter.write_str(safe_message),
    134         }
    135     }
    136 }
    137 
    138 impl std::error::Error for TestBridgeError {}
    139 
    140 impl From<SafeError> for TestBridgeError {
    141     fn from(error: SafeError) -> Self {
    142         Self::Failure {
    143             safe_message: error.message().as_str().to_owned(),
    144         }
    145     }
    146 }
    147 
    148 impl From<std::io::Error> for TestBridgeError {
    149     fn from(_error: std::io::Error) -> Self {
    150         Self::Failure {
    151             safe_message: "The integration test data directory is unavailable.".to_owned(),
    152         }
    153     }
    154 }
    155 
    156 #[derive(Default)]
    157 struct FixedClock;
    158 
    159 impl Clock for FixedClock {
    160     fn now(&self) -> UnixTimestamp {
    161         UnixTimestamp::from_seconds(FIXED_TIME_SECONDS).expect("fixed test timestamp")
    162     }
    163 }
    164 
    165 struct FixedInstallationIdentity;
    166 
    167 impl InstallationIdentitySource for FixedInstallationIdentity {
    168     fn generate(&self) -> Result<InstallationIdentity, SafeError> {
    169         InstallationIdentity::parse(FIXED_INSTALLATION_ID)
    170     }
    171 }
    172 
    173 #[derive(uniffi::Object)]
    174 pub struct HarvestCircleTestBridge {
    175     runtime: Runtime,
    176     actor: Mutex<Option<RuntimeActorHandle>>,
    177     observer: Mutex<Option<RuntimeChangeSubscription>>,
    178     secrets: Arc<InMemorySecretStore>,
    179     clock: Arc<FixedClock>,
    180     relay: Mutex<Option<MockRelay>>,
    181     relay_url: String,
    182     context: RuntimeContext,
    183     network_degraded: AtomicBool,
    184 }
    185 
    186 #[uniffi::export]
    187 impl HarvestCircleTestBridge {
    188     #[uniffi::constructor]
    189     pub fn open(data_directory: String) -> Result<Arc<Self>, TestBridgeError> {
    190         let runtime = Builder::new_multi_thread()
    191             .enable_all()
    192             .build()
    193             .map_err(|_| TestBridgeError::Failure {
    194                 safe_message: "The integration test runtime could not start.".to_owned(),
    195             })?;
    196         let data_root = prepare_data_root(Path::new(&data_directory))?;
    197         let context = runtime_context(&data_root)?;
    198         let relay = runtime
    199             .block_on(MockRelay::run())
    200             .map_err(|_| TestBridgeError::Failure {
    201                 safe_message: "The local integration relay could not start.".to_owned(),
    202             })?;
    203         let relay_url = runtime.block_on(relay.url()).to_string();
    204         let secrets = Arc::new(InMemorySecretStore::default());
    205         let clock = Arc::new(FixedClock);
    206         let actor = runtime.block_on(open_actor(
    207             &context,
    208             &relay_url,
    209             Arc::clone(&secrets),
    210             Arc::clone(&clock),
    211             runtime.handle(),
    212         ))?;
    213         Ok(Arc::new(Self {
    214             runtime,
    215             actor: Mutex::new(Some(actor)),
    216             observer: Mutex::new(None),
    217             secrets,
    218             clock,
    219             relay: Mutex::new(Some(relay)),
    220             relay_url,
    221             context,
    222             network_degraded: AtomicBool::new(false),
    223         }))
    224     }
    225 
    226     pub fn bootstrap(&self) -> Result<TestSnapshot, TestBridgeError> {
    227         let actor = self.actor()?;
    228         Ok(self.to_test_snapshot(self.runtime.block_on(actor.bootstrap())?))
    229     }
    230 
    231     pub fn snapshot(&self) -> Result<TestSnapshot, TestBridgeError> {
    232         Ok(self.to_test_snapshot(self.actor()?.snapshot()))
    233     }
    234 
    235     pub fn begin_generated_identity(
    236         &self,
    237     ) -> Result<Arc<TestGeneratedRecoveryRequest>, TestBridgeError> {
    238         let actor = self.actor()?;
    239         let handle = self.runtime.block_on(actor.begin_generated_key_stage())?;
    240         Ok(Arc::new(TestGeneratedRecoveryRequest {
    241             handle,
    242             resolved: AtomicBool::new(false),
    243         }))
    244     }
    245 
    246     pub fn acknowledge_generated_identity(
    247         &self,
    248         request_id: String,
    249         expected_revision: u64,
    250         timeout_millis: u64,
    251         request: Arc<TestGeneratedRecoveryRequest>,
    252     ) -> Result<TestSnapshot, TestBridgeError> {
    253         if request.resolved.swap(true, Ordering::AcqRel) {
    254             return Err(request_unavailable());
    255         }
    256         let actor = self.actor()?;
    257         let snapshot = self
    258             .runtime
    259             .block_on(actor.acknowledge_generated_key_stage(
    260                 request.handle.id(),
    261                 DurableRequestId::parse(request_id)?,
    262                 SnapshotRevision::from_value(expected_revision),
    263                 Duration::from_millis(timeout_millis),
    264             ))?;
    265         Ok(self.to_test_snapshot(snapshot))
    266     }
    267 
    268     pub fn cancel_generated_identity(
    269         &self,
    270         request: Arc<TestGeneratedRecoveryRequest>,
    271     ) -> Result<bool, TestBridgeError> {
    272         if request.resolved.swap(true, Ordering::AcqRel) {
    273             return Ok(false);
    274         }
    275         let actor = self.actor()?;
    276         Ok(self.runtime.block_on(actor.cancel_generated_key_stage())?)
    277     }
    278 
    279     pub fn import_identity(
    280         &self,
    281         request_id: String,
    282         expected_revision: u64,
    283         mut secret: Vec<u8>,
    284         timeout_millis: u64,
    285     ) -> Result<TestSnapshot, TestBridgeError> {
    286         let input = SecretKeyInput::parse_bytes(std::mem::take(&mut secret))?;
    287         secret.fill(0);
    288         let actor = self.actor()?;
    289         let receipt = self.runtime.block_on(actor.import_secret_key(
    290             DurableRequestId::parse(request_id)?,
    291             SnapshotRevision::from_value(expected_revision),
    292             input,
    293             Duration::from_millis(timeout_millis),
    294         ))?;
    295         let _ = receipt.identity();
    296         Ok(self.to_test_snapshot(actor.snapshot()))
    297     }
    298 
    299     pub fn select_identity(&self, public_key_hex: String) -> Result<TestSnapshot, TestBridgeError> {
    300         let actor = self.actor()?;
    301         Ok(self.to_test_snapshot(
    302             self.runtime
    303                 .block_on(actor.select_identity(PublicKey::from_hex(&public_key_hex)?))?,
    304         ))
    305     }
    306 
    307     pub fn activate_identity(
    308         &self,
    309         public_key_hex: String,
    310     ) -> Result<TestSnapshot, TestBridgeError> {
    311         let actor = self.actor()?;
    312         Ok(self.to_test_snapshot(
    313             self.runtime
    314                 .block_on(actor.activate_identity(PublicKey::from_hex(&public_key_hex)?))?,
    315         ))
    316     }
    317 
    318     pub fn sign_out(&self) -> Result<TestSnapshot, TestBridgeError> {
    319         let actor = self.actor()?;
    320         Ok(self.to_test_snapshot(self.runtime.block_on(actor.sign_out())?))
    321     }
    322 
    323     pub fn seed_selected_profile(&self, display_name: String) -> Result<(), TestBridgeError> {
    324         let selected = self
    325             .actor()?
    326             .snapshot()
    327             .selected_identity()
    328             .ok_or_else(request_unavailable)?;
    329         self.runtime.block_on(async {
    330             let secret = self.secrets.load(selected).await?;
    331             let keys = secret
    332                 .with_exposed_secret(Keys::parse)
    333                 .map_err(|_| invalid_secret())?;
    334             let publisher = Client::new(keys);
    335             publisher
    336                 .add_relay(&self.relay_url)
    337                 .await
    338                 .map_err(|_| relay_failed())?;
    339             publisher.connect().await;
    340             publisher
    341                 .wait_for_connection(Duration::from_millis(DEFAULT_TIMEOUT_MILLIS))
    342                 .await;
    343             publisher
    344                 .send_event_builder(EventBuilder::metadata(
    345                     &Metadata::new().display_name(display_name),
    346                 ))
    347                 .await
    348                 .map_err(|_| relay_failed())?;
    349             publisher.shutdown().await;
    350             Ok(())
    351         })
    352     }
    353 
    354     pub fn refresh_active_profile(&self) -> Result<TestSnapshot, TestBridgeError> {
    355         let actor = self.actor()?;
    356         Ok(self.to_test_snapshot(self.runtime.block_on(actor.refresh_active_profile())?))
    357     }
    358 
    359     pub fn request_identity_removal(
    360         &self,
    361         public_key_hex: String,
    362     ) -> Result<Arc<TestRemovalRequest>, TestBridgeError> {
    363         let token = self.runtime.block_on(
    364             self.actor()?
    365                 .request_identity_removal(PublicKey::from_hex(&public_key_hex)?),
    366         )?;
    367         let impact = token.impact();
    368         let expires_at_seconds = token.expires_at().as_seconds();
    369         Ok(Arc::new(TestRemovalRequest {
    370             token: Mutex::new(Some(token)),
    371             public_key_hex,
    372             deletes_local_credential: impact.deletes_local_credential(),
    373             signs_out: impact.signs_out(),
    374             expires_at_seconds,
    375         }))
    376     }
    377 
    378     pub fn cancel_identity_removal(&self, request: Arc<TestRemovalRequest>) -> bool {
    379         request
    380             .token
    381             .lock()
    382             .unwrap_or_else(std::sync::PoisonError::into_inner)
    383             .take()
    384             .is_some()
    385     }
    386 
    387     pub fn confirm_identity_removal(
    388         &self,
    389         request_id: String,
    390         expected_revision: u64,
    391         timeout_millis: u64,
    392         request: Arc<TestRemovalRequest>,
    393     ) -> Result<TestSnapshot, TestBridgeError> {
    394         let token = request
    395             .token
    396             .lock()
    397             .unwrap_or_else(std::sync::PoisonError::into_inner)
    398             .take()
    399             .ok_or_else(request_unavailable)?;
    400         let snapshot = self
    401             .runtime
    402             .block_on(self.actor()?.confirm_identity_removal(
    403                 token,
    404                 DurableRequestId::parse(request_id)?,
    405                 SnapshotRevision::from_value(expected_revision),
    406                 Duration::from_millis(timeout_millis),
    407             ))?;
    408         Ok(self.to_test_snapshot(snapshot))
    409     }
    410 
    411     pub fn set_network_degraded(&self, degraded: bool) {
    412         self.network_degraded.store(degraded, Ordering::Release);
    413     }
    414 
    415     pub fn start_observer(&self) -> Result<(), TestBridgeError> {
    416         let actor = self.actor()?;
    417         let subscription =
    418             self.runtime.block_on(actor.subscribe_changes(
    419                 NonZeroUsize::new(OBSERVER_CAPACITY).expect("observer capacity"),
    420             ))?;
    421         *self
    422             .observer
    423             .lock()
    424             .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(subscription);
    425         Ok(())
    426     }
    427 
    428     pub fn next_observed_snapshot(
    429         &self,
    430         timeout_millis: u64,
    431     ) -> Result<Option<TestSnapshot>, TestBridgeError> {
    432         let mut observer = self
    433             .observer
    434             .lock()
    435             .unwrap_or_else(std::sync::PoisonError::into_inner);
    436         let subscription = observer.as_mut().ok_or_else(request_unavailable)?;
    437         let change = self.runtime.block_on(async {
    438             tokio::time::timeout(
    439                 Duration::from_millis(timeout_millis),
    440                 subscription.receive(),
    441             )
    442             .await
    443         });
    444         match change {
    445             Ok(Some(change)) => Ok(Some(self.to_test_snapshot(change.snapshot().clone()))),
    446             Ok(None) => Ok(None),
    447             Err(_) => Ok(None),
    448         }
    449     }
    450 
    451     pub fn stop_observer(&self) -> Result<bool, TestBridgeError> {
    452         let subscription = self
    453             .observer
    454             .lock()
    455             .unwrap_or_else(std::sync::PoisonError::into_inner)
    456             .take();
    457         let Some(subscription) = subscription else {
    458             return Ok(false);
    459         };
    460         let actor = self.actor()?;
    461         Ok(self
    462             .runtime
    463             .block_on(actor.unsubscribe_changes(subscription.id()))?)
    464     }
    465 
    466     pub fn restart(&self) -> Result<TestSnapshot, TestBridgeError> {
    467         let _ = self.stop_observer();
    468         self.close_actor()?;
    469         let actor = self.runtime.block_on(open_actor(
    470             &self.context,
    471             &self.relay_url,
    472             Arc::clone(&self.secrets),
    473             Arc::clone(&self.clock),
    474             self.runtime.handle(),
    475         ))?;
    476         let snapshot = actor.snapshot();
    477         *self
    478             .actor
    479             .lock()
    480             .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(actor);
    481         Ok(self.to_test_snapshot(snapshot))
    482     }
    483 
    484     pub fn shutdown(&self) -> Result<TestSnapshot, TestBridgeError> {
    485         let snapshot = self.snapshot()?;
    486         let _ = self.stop_observer();
    487         self.close_actor()?;
    488         Ok(TestSnapshot {
    489             lifecycle: TestLifecycle::Closed,
    490             ..snapshot
    491         })
    492     }
    493 }
    494 
    495 impl Drop for HarvestCircleTestBridge {
    496     fn drop(&mut self) {
    497         if let Some(relay) = self
    498             .relay
    499             .get_mut()
    500             .unwrap_or_else(std::sync::PoisonError::into_inner)
    501             .take()
    502         {
    503             relay.shutdown();
    504         }
    505     }
    506 }
    507 
    508 impl HarvestCircleTestBridge {
    509     fn to_test_snapshot(&self, snapshot: AppSnapshot) -> TestSnapshot {
    510         let mut snapshot = to_snapshot(snapshot);
    511         if self.network_degraded.load(Ordering::Acquire)
    512             && !matches!(
    513                 snapshot.lifecycle,
    514                 TestLifecycle::Closed | TestLifecycle::Fatal
    515             )
    516         {
    517             snapshot.lifecycle = TestLifecycle::Degraded;
    518         }
    519         snapshot
    520     }
    521 
    522     fn actor(&self) -> Result<RuntimeActorHandle, TestBridgeError> {
    523         self.actor
    524             .lock()
    525             .unwrap_or_else(std::sync::PoisonError::into_inner)
    526             .clone()
    527             .ok_or_else(request_unavailable)
    528     }
    529 
    530     fn close_actor(&self) -> Result<(), TestBridgeError> {
    531         let actor = self
    532             .actor
    533             .lock()
    534             .unwrap_or_else(std::sync::PoisonError::into_inner)
    535             .take();
    536         if let Some(actor) = actor {
    537             self.runtime.block_on(
    538                 actor.close_with_timeout(Duration::from_millis(DEFAULT_TIMEOUT_MILLIS)),
    539             )?;
    540         }
    541         Ok(())
    542     }
    543 }
    544 
    545 async fn open_actor(
    546     context: &RuntimeContext,
    547     relay_url: &str,
    548     secrets: Arc<InMemorySecretStore>,
    549     clock: Arc<FixedClock>,
    550     runtime: &tokio::runtime::Handle,
    551 ) -> Result<RuntimeActorHandle, TestBridgeError> {
    552     let relay = RelayEndpoint::new(relay_url, RelayUrlPolicy::Local, RelayAccess::ReadWrite)
    553         .map_err(|_| TestBridgeError::Failure {
    554             safe_message: "The local relay configuration is invalid.".to_owned(),
    555         })?;
    556     let dependencies = RuntimeDependencies::new(
    557         secrets,
    558         clock,
    559         Arc::new(SdkNostrClient::new(Duration::from_millis(
    560             DEFAULT_TIMEOUT_MILLIS,
    561         ))),
    562         Arc::new(FixedInstallationIdentity),
    563     );
    564     let build = migration_build_identity()?;
    565     Ok(RuntimeActorHandle::open(
    566         context,
    567         RelayConfiguration::new(vec![relay])?,
    568         dependencies,
    569         &build,
    570         NonZeroUsize::new(ACTOR_CAPACITY).expect("actor capacity"),
    571         runtime,
    572     )
    573     .await?)
    574 }
    575 
    576 fn prepare_data_root(path: &Path) -> Result<PathBuf, TestBridgeError> {
    577     fs::create_dir_all(path)?;
    578     Ok(path.canonicalize()?)
    579 }
    580 
    581 fn runtime_context(root: &Path) -> Result<RuntimeContext, TestBridgeError> {
    582     let resolver = RadrootsPathResolver::new(
    583         RadrootsPlatform::current(),
    584         RadrootsHostEnvironment::default(),
    585     );
    586     let bootstrap = RuntimeContextBootstrap::new(
    587         RadrootsPathProfile::RepoLocal,
    588         Some(root.to_path_buf()),
    589         RuntimeContextSource::BootstrapCli,
    590         RuntimeContextSource::SafeDefault,
    591     )
    592     .map_err(|_| invalid_runtime_evidence())?;
    593     let context = RuntimeContext::resolve(
    594         &resolver,
    595         bootstrap,
    596         ServiceId::new("harvestcircle").map_err(|_| invalid_runtime_evidence())?,
    597         InstanceId::new("desktop").map_err(|_| invalid_runtime_evidence())?,
    598     )
    599     .map_err(|_| invalid_runtime_evidence())?;
    600     fs::create_dir_all(root.join("data"))?;
    601     Ok(context)
    602 }
    603 
    604 fn migration_build_identity() -> Result<MigrationBuildIdentity, TestBridgeError> {
    605     MigrationBuildIdentity::new(
    606         "0.1.0-alpha",
    607         "1111111111111111111111111111111111111111",
    608         "2222222222222222222222222222222222222222",
    609         "1.97.1",
    610         "test",
    611         "test",
    612         1,
    613         1,
    614         1,
    615         1,
    616         1,
    617     )
    618     .map_err(|_| invalid_runtime_evidence())
    619 }
    620 
    621 fn invalid_runtime_evidence() -> TestBridgeError {
    622     TestBridgeError::Failure {
    623         safe_message: "The integration test runtime evidence is invalid.".to_owned(),
    624     }
    625 }
    626 
    627 fn to_identity(identity: &harvestcircle_domain::NostrIdentity) -> TestIdentity {
    628     TestIdentity {
    629         public_key_hex: identity.public_key().to_hex(),
    630         npub: identity.npub().as_str().to_owned(),
    631         display_label: identity.display_label(),
    632     }
    633 }
    634 
    635 fn to_snapshot(snapshot: AppSnapshot) -> TestSnapshot {
    636     let lifecycle = match snapshot.lifecycle() {
    637         AppLifecycle::Booting => TestLifecycle::Booting,
    638         AppLifecycle::Ready => TestLifecycle::Ready,
    639         AppLifecycle::Fatal(_) => TestLifecycle::Fatal,
    640     };
    641     let session = match snapshot.session() {
    642         SessionState::SignedOut => TestSession::SignedOut,
    643         SessionState::Activating(_) => TestSession::Activating,
    644         SessionState::Active => TestSession::Active,
    645         SessionState::SigningOut => TestSession::SigningOut,
    646         SessionState::Failed(_) => TestSession::Failed,
    647     };
    648     let profile_display_name = snapshot
    649         .active_identity()
    650         .and_then(|active| active.profile())
    651         .and_then(|profile| profile.display_name())
    652         .map(ToOwned::to_owned);
    653     TestSnapshot {
    654         revision: snapshot.revision().value(),
    655         lifecycle,
    656         identities: snapshot.identities().iter().map(to_identity).collect(),
    657         selected_public_key_hex: snapshot.selected_identity().map(PublicKey::to_hex),
    658         session,
    659         profile_display_name,
    660     }
    661 }
    662 
    663 fn request_unavailable() -> TestBridgeError {
    664     TestBridgeError::Failure {
    665         safe_message: "The integration test request is no longer available.".to_owned(),
    666     }
    667 }
    668 
    669 fn invalid_secret() -> TestBridgeError {
    670     TestBridgeError::Failure {
    671         safe_message: "The integration test secret key is invalid.".to_owned(),
    672     }
    673 }
    674 
    675 fn relay_failed() -> TestBridgeError {
    676     TestBridgeError::Failure {
    677         safe_message: "The local integration relay operation failed.".to_owned(),
    678     }
    679 }
    680 
    681 uniffi::setup_scaffolding!();