app

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

dto.rs (33679B)


      1 use std::fmt::{self, Formatter};
      2 
      3 use harvestcircle_application::{
      4     ActiveIdentitySnapshot, AppLifecycle, AppSnapshot, ProfileLoadState, RelayConnectionState,
      5     RelayEndpoint, RelayUrlPolicy, RuntimeLifecycle, SessionState,
      6 };
      7 use harvestcircle_domain::{
      8     NostrIdentity, ProfileMetadata, SafeError, SafeErrorCode, SignerAvailability,
      9 };
     10 use harvestcircle_nostr::{NostrReferenceKind, NostrReferenceParse};
     11 
     12 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     13 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
     14 pub enum NostrReferenceKindDto {
     15     EventId,
     16     PublicKey,
     17     Profile,
     18     Note,
     19     Event,
     20     Address,
     21     PrivateKeyRejected,
     22     Invalid,
     23 }
     24 
     25 impl From<NostrReferenceKind> for NostrReferenceKindDto {
     26     fn from(value: NostrReferenceKind) -> Self {
     27         match value {
     28             NostrReferenceKind::EventId => Self::EventId,
     29             NostrReferenceKind::PublicKey => Self::PublicKey,
     30             NostrReferenceKind::Profile => Self::Profile,
     31             NostrReferenceKind::Note => Self::Note,
     32             NostrReferenceKind::NostrEvent => Self::Event,
     33             NostrReferenceKind::Address => Self::Address,
     34             NostrReferenceKind::PrivateKeyRejected => Self::PrivateKeyRejected,
     35             NostrReferenceKind::Invalid => Self::Invalid,
     36         }
     37     }
     38 }
     39 
     40 #[derive(Clone, Debug, Eq, PartialEq)]
     41 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
     42 pub struct NostrReferenceParseDto {
     43     pub classification: NostrReferenceKindDto,
     44     pub canonical_reference: Option<String>,
     45 }
     46 
     47 impl From<NostrReferenceParse> for NostrReferenceParseDto {
     48     fn from(value: NostrReferenceParse) -> Self {
     49         Self {
     50             classification: value.classification.into(),
     51             canonical_reference: value.canonical_reference,
     52         }
     53     }
     54 }
     55 
     56 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     57 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
     58 pub enum WireErrorCode {
     59     InvalidPublicKey,
     60     InvalidSecretKey,
     61     InvalidIdentityMetadata,
     62     InvalidProfileMetadata,
     63     InvalidApplicationState,
     64     IdentityAlreadyExists,
     65     IdentityNotFound,
     66     KeyringUnavailable,
     67     CredentialMissing,
     68     StorageUnavailable,
     69     StorageCorrupt,
     70     StorageQuarantined,
     71     StorageBackupInvalid,
     72     UnsupportedSchemaVersion,
     73     RepairUnauthorized,
     74     PendingOperationRecoveryRequired,
     75     InvalidRelayConfiguration,
     76     RelayConnectionFailed,
     77     ProfileRefreshFailed,
     78     ObserverRegistrationFailed,
     79     NativeLibraryLoadFailed,
     80     CompatibilityMismatch,
     81     Internal,
     82     AvailabilityInvalidInput,
     83     AvailabilityUnsupportedProfile,
     84     AvailabilityScopeMismatch,
     85     AvailabilityStaleQuery,
     86     AvailabilityCapacity,
     87     AvailabilityUnavailable,
     88 }
     89 
     90 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     91 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
     92 pub enum WireErrorCategory {
     93     Input,
     94     Conflict,
     95     Credential,
     96     Storage,
     97     Network,
     98     Lifecycle,
     99     Compatibility,
    100     Internal,
    101 }
    102 
    103 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    104 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    105 pub enum WireRecoveryAction {
    106     None,
    107     Retry,
    108     RepairCredential,
    109     Authenticate,
    110     RepairStorage,
    111     RestoreBackup,
    112     CheckConfiguration,
    113     RestartApplication,
    114     UpdateApplication,
    115 }
    116 
    117 #[derive(Clone, Debug, Eq, PartialEq)]
    118 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    119 pub struct SafeErrorDto {
    120     pub code: WireErrorCode,
    121     pub category: WireErrorCategory,
    122     pub retryable: bool,
    123     pub recovery_action: WireRecoveryAction,
    124     pub message: String,
    125 }
    126 
    127 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    128 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    129 pub enum AppLifecycleDto {
    130     Opening,
    131     CompatibilityChecking,
    132     AcquiringOwnership,
    133     Migrating,
    134     Recovering,
    135     Ready,
    136     Degraded,
    137     Blocked,
    138     ShuttingDown,
    139     Closed,
    140     Fatal,
    141 }
    142 
    143 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    144 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    145 pub enum SessionStateDto {
    146     SignedOut,
    147     Activating,
    148     Active,
    149     SigningOut,
    150     Failed,
    151 }
    152 
    153 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    154 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    155 pub enum RelayConnectionStateDto {
    156     Disconnected,
    157     Connecting,
    158     Connected,
    159     Degraded,
    160     Error,
    161 }
    162 
    163 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    164 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    165 pub enum ProfileLoadStateDto {
    166     Empty,
    167     Loading,
    168     Cached,
    169     Fresh,
    170     Error,
    171 }
    172 
    173 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    174 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    175 pub enum RelayDestinationDto {
    176     Local,
    177     PrivateNetwork,
    178     Public,
    179 }
    180 
    181 #[derive(Clone, Eq, PartialEq)]
    182 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    183 pub struct RelayEndpointDto {
    184     pub url: String,
    185     pub destination: RelayDestinationDto,
    186     pub read: bool,
    187     pub write: bool,
    188 }
    189 
    190 impl fmt::Debug for RelayEndpointDto {
    191     fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    192         formatter
    193             .debug_struct("RelayEndpointDto")
    194             .field("url", &"<redacted>")
    195             .field("destination", &self.destination)
    196             .field("read", &self.read)
    197             .field("write", &self.write)
    198             .finish()
    199     }
    200 }
    201 
    202 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    203 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    204 pub enum SignerBindingKindDto {
    205     LocalKeyring,
    206 }
    207 
    208 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    209 #[cfg_attr(not(coverage_nightly), derive(uniffi::Enum))]
    210 pub enum SignerAvailabilityDto {
    211     Available,
    212     CredentialMissing,
    213     StoreUnavailable,
    214 }
    215 
    216 #[derive(Clone, Debug, Eq, PartialEq)]
    217 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    218 pub struct ProfileDto {
    219     pub name: Option<String>,
    220     pub display_name: Option<String>,
    221     pub nip05: Option<String>,
    222     pub about: Option<String>,
    223     pub picture: Option<String>,
    224 }
    225 
    226 #[derive(Clone, Debug, Eq, PartialEq)]
    227 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    228 pub struct IdentityDto {
    229     pub public_key_hex: String,
    230     pub npub: String,
    231     pub display_label: String,
    232     pub signer_binding_kind: SignerBindingKindDto,
    233     pub signer_availability: SignerAvailabilityDto,
    234     pub created_at_seconds: i64,
    235     pub last_used_at_seconds: Option<i64>,
    236 }
    237 
    238 #[derive(Clone, Debug, Eq, PartialEq)]
    239 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    240 pub struct ActiveIdentityDto {
    241     pub identity: IdentityDto,
    242     pub relay_state: RelayConnectionStateDto,
    243     pub profile_state: ProfileLoadStateDto,
    244     pub profile: Option<ProfileDto>,
    245 }
    246 
    247 #[derive(Clone, Debug, Eq, PartialEq)]
    248 #[cfg_attr(not(coverage_nightly), derive(uniffi::Record))]
    249 pub struct AppSnapshotDto {
    250     pub revision: u64,
    251     pub lifecycle: AppLifecycleDto,
    252     pub lifecycle_error: Option<SafeErrorDto>,
    253     pub configured_relays: Vec<RelayEndpointDto>,
    254     pub identities: Vec<IdentityDto>,
    255     pub selected_public_key_hex: Option<String>,
    256     pub session: SessionStateDto,
    257     pub session_subject_public_key_hex: Option<String>,
    258     pub session_error: Option<SafeErrorDto>,
    259     pub active_identity: Option<ActiveIdentityDto>,
    260     pub recoverable_problem: Option<SafeErrorDto>,
    261 }
    262 
    263 impl From<&AppSnapshot> for AppSnapshotDto {
    264     fn from(snapshot: &AppSnapshot) -> Self {
    265         let (lifecycle, lifecycle_error) = match snapshot.lifecycle() {
    266             AppLifecycle::Booting => (AppLifecycleDto::Opening, None),
    267             AppLifecycle::Ready => (AppLifecycleDto::Ready, None),
    268             AppLifecycle::Fatal(error) => (AppLifecycleDto::Fatal, Some(error.into())),
    269         };
    270         let (session, session_subject_public_key_hex, session_error) = match snapshot.session() {
    271             SessionState::SignedOut => (SessionStateDto::SignedOut, None, None),
    272             SessionState::Activating(public_key) => {
    273                 (SessionStateDto::Activating, Some(public_key.to_hex()), None)
    274             }
    275             SessionState::Active => (SessionStateDto::Active, None, None),
    276             SessionState::SigningOut => (SessionStateDto::SigningOut, None, None),
    277             SessionState::Failed(error) => (SessionStateDto::Failed, None, Some(error.into())),
    278         };
    279         Self {
    280             revision: snapshot.revision().value(),
    281             lifecycle,
    282             lifecycle_error,
    283             configured_relays: snapshot
    284                 .relay_configuration()
    285                 .relays()
    286                 .iter()
    287                 .map(RelayEndpointDto::from)
    288                 .collect(),
    289             identities: snapshot
    290                 .identities()
    291                 .iter()
    292                 .map(IdentityDto::from)
    293                 .collect(),
    294             selected_public_key_hex: snapshot
    295                 .selected_identity()
    296                 .map(harvestcircle_domain::PublicKey::to_hex),
    297             session,
    298             session_subject_public_key_hex,
    299             session_error,
    300             active_identity: snapshot.active_identity().map(ActiveIdentityDto::from),
    301             recoverable_problem: snapshot.recoverable_problem().map(SafeErrorDto::from),
    302         }
    303     }
    304 }
    305 
    306 impl AppSnapshotDto {
    307     pub(crate) fn from_runtime(snapshot: &AppSnapshot, runtime: RuntimeLifecycle) -> Self {
    308         let mut dto = Self::from(snapshot);
    309         let (lifecycle, problem) = match runtime {
    310             RuntimeLifecycle::Opening => (AppLifecycleDto::Opening, None),
    311             RuntimeLifecycle::CompatibilityChecking => {
    312                 (AppLifecycleDto::CompatibilityChecking, None)
    313             }
    314             RuntimeLifecycle::AcquiringOwnership => (AppLifecycleDto::AcquiringOwnership, None),
    315             RuntimeLifecycle::Migrating => (AppLifecycleDto::Migrating, None),
    316             RuntimeLifecycle::Recovering => (AppLifecycleDto::Recovering, None),
    317             RuntimeLifecycle::Ready => (AppLifecycleDto::Ready, None),
    318             RuntimeLifecycle::Degraded(error) => {
    319                 (AppLifecycleDto::Degraded, Some(SafeErrorDto::from(error)))
    320             }
    321             RuntimeLifecycle::Blocked(error) => {
    322                 (AppLifecycleDto::Blocked, Some(SafeErrorDto::from(error)))
    323             }
    324             RuntimeLifecycle::ShuttingDown => (AppLifecycleDto::ShuttingDown, None),
    325             RuntimeLifecycle::Closed => (AppLifecycleDto::Closed, None),
    326             RuntimeLifecycle::Fatal(error) => {
    327                 (AppLifecycleDto::Fatal, Some(SafeErrorDto::from(error)))
    328             }
    329         };
    330         dto.lifecycle = lifecycle;
    331         dto.lifecycle_error = problem;
    332         dto
    333     }
    334 }
    335 
    336 impl From<&NostrIdentity> for IdentityDto {
    337     fn from(identity: &NostrIdentity) -> Self {
    338         Self {
    339             public_key_hex: identity.public_key().to_hex(),
    340             npub: identity.npub().as_str().to_owned(),
    341             display_label: identity.display_label(),
    342             signer_binding_kind: SignerBindingKindDto::LocalKeyring,
    343             signer_availability: identity.signer_binding().availability().into(),
    344             created_at_seconds: identity.created_at().timestamp().as_seconds(),
    345             last_used_at_seconds: identity
    346                 .last_used_at()
    347                 .map(harvestcircle_domain::UnixTimestamp::as_seconds),
    348         }
    349     }
    350 }
    351 
    352 impl From<&RelayEndpoint> for RelayEndpointDto {
    353     fn from(endpoint: &RelayEndpoint) -> Self {
    354         Self {
    355             url: endpoint.url().as_str().to_owned(),
    356             destination: endpoint.policy().into(),
    357             read: endpoint.access().can_read(),
    358             write: endpoint.access().can_write(),
    359         }
    360     }
    361 }
    362 
    363 impl From<RelayUrlPolicy> for RelayDestinationDto {
    364     fn from(destination: RelayUrlPolicy) -> Self {
    365         match destination {
    366             RelayUrlPolicy::Local => Self::Local,
    367             RelayUrlPolicy::PrivateNetwork => Self::PrivateNetwork,
    368             RelayUrlPolicy::Public => Self::Public,
    369             _ => unreachable!("validated relay policy is outside the governed v1 vocabulary"),
    370         }
    371     }
    372 }
    373 
    374 impl From<&ActiveIdentitySnapshot> for ActiveIdentityDto {
    375     fn from(active: &ActiveIdentitySnapshot) -> Self {
    376         Self {
    377             identity: active.identity().into(),
    378             relay_state: active.relay_state().into(),
    379             profile_state: active.profile_state().into(),
    380             profile: active.profile().map(ProfileDto::from),
    381         }
    382     }
    383 }
    384 
    385 impl From<&ProfileMetadata> for ProfileDto {
    386     fn from(profile: &ProfileMetadata) -> Self {
    387         Self {
    388             name: profile.name().map(str::to_owned),
    389             display_name: profile.display_name().map(str::to_owned),
    390             nip05: profile.nip05().map(str::to_owned),
    391             about: profile.about().map(str::to_owned),
    392             picture: profile.picture().map(str::to_owned),
    393         }
    394     }
    395 }
    396 
    397 impl From<SafeError> for SafeErrorDto {
    398     fn from(error: SafeError) -> Self {
    399         let (category, retryable, recovery_action) = error_policy(error.code());
    400         Self {
    401             code: error.code().into(),
    402             category,
    403             retryable,
    404             recovery_action,
    405             message: error.message().as_str().to_owned(),
    406         }
    407     }
    408 }
    409 
    410 impl From<SafeErrorCode> for WireErrorCode {
    411     fn from(code: SafeErrorCode) -> Self {
    412         match code {
    413             SafeErrorCode::InvalidPublicKey => Self::InvalidPublicKey,
    414             SafeErrorCode::InvalidSecretKey => Self::InvalidSecretKey,
    415             SafeErrorCode::InvalidIdentityMetadata => Self::InvalidIdentityMetadata,
    416             SafeErrorCode::InvalidProfileMetadata => Self::InvalidProfileMetadata,
    417             SafeErrorCode::InvalidApplicationState => Self::InvalidApplicationState,
    418             SafeErrorCode::IdentityAlreadyExists => Self::IdentityAlreadyExists,
    419             SafeErrorCode::IdentityNotFound => Self::IdentityNotFound,
    420             SafeErrorCode::KeyringUnavailable => Self::KeyringUnavailable,
    421             SafeErrorCode::CredentialMissing => Self::CredentialMissing,
    422             SafeErrorCode::StorageUnavailable => Self::StorageUnavailable,
    423             SafeErrorCode::StorageCorrupt => Self::StorageCorrupt,
    424             SafeErrorCode::StorageQuarantined => Self::StorageQuarantined,
    425             SafeErrorCode::StorageBackupInvalid => Self::StorageBackupInvalid,
    426             SafeErrorCode::UnsupportedSchemaVersion => Self::UnsupportedSchemaVersion,
    427             SafeErrorCode::RepairUnauthorized => Self::RepairUnauthorized,
    428             SafeErrorCode::PendingOperationRecoveryRequired => {
    429                 Self::PendingOperationRecoveryRequired
    430             }
    431             SafeErrorCode::InvalidRelayConfiguration => Self::InvalidRelayConfiguration,
    432             SafeErrorCode::RelayConnectionFailed => Self::RelayConnectionFailed,
    433             SafeErrorCode::ProfileRefreshFailed => Self::ProfileRefreshFailed,
    434             SafeErrorCode::ObserverRegistrationFailed => Self::ObserverRegistrationFailed,
    435             SafeErrorCode::NativeLibraryLoadFailed => Self::NativeLibraryLoadFailed,
    436             SafeErrorCode::AvailabilityInvalidInput => Self::AvailabilityInvalidInput,
    437             SafeErrorCode::AvailabilityUnsupportedProfile => Self::AvailabilityUnsupportedProfile,
    438             SafeErrorCode::AvailabilityScopeMismatch => Self::AvailabilityScopeMismatch,
    439             SafeErrorCode::AvailabilityStaleQuery => Self::AvailabilityStaleQuery,
    440             SafeErrorCode::AvailabilityCapacity => Self::AvailabilityCapacity,
    441             SafeErrorCode::AvailabilityUnavailable => Self::AvailabilityUnavailable,
    442         }
    443     }
    444 }
    445 
    446 pub(crate) const fn error_policy(
    447     code: SafeErrorCode,
    448 ) -> (WireErrorCategory, bool, WireRecoveryAction) {
    449     match code {
    450         SafeErrorCode::InvalidPublicKey
    451         | SafeErrorCode::InvalidSecretKey
    452         | SafeErrorCode::InvalidIdentityMetadata
    453         | SafeErrorCode::InvalidProfileMetadata => {
    454             (WireErrorCategory::Input, false, WireRecoveryAction::None)
    455         }
    456         SafeErrorCode::IdentityAlreadyExists | SafeErrorCode::IdentityNotFound => {
    457             (WireErrorCategory::Conflict, false, WireRecoveryAction::None)
    458         }
    459         SafeErrorCode::KeyringUnavailable => (
    460             WireErrorCategory::Credential,
    461             true,
    462             WireRecoveryAction::Retry,
    463         ),
    464         SafeErrorCode::CredentialMissing => (
    465             WireErrorCategory::Credential,
    466             false,
    467             WireRecoveryAction::RepairCredential,
    468         ),
    469         SafeErrorCode::StorageUnavailable => (
    470             WireErrorCategory::Storage,
    471             true,
    472             WireRecoveryAction::RestartApplication,
    473         ),
    474         SafeErrorCode::StorageCorrupt | SafeErrorCode::PendingOperationRecoveryRequired => (
    475             WireErrorCategory::Storage,
    476             false,
    477             WireRecoveryAction::RestartApplication,
    478         ),
    479         SafeErrorCode::StorageQuarantined => (
    480             WireErrorCategory::Storage,
    481             false,
    482             WireRecoveryAction::RepairStorage,
    483         ),
    484         SafeErrorCode::StorageBackupInvalid => (
    485             WireErrorCategory::Storage,
    486             false,
    487             WireRecoveryAction::RestoreBackup,
    488         ),
    489         SafeErrorCode::UnsupportedSchemaVersion => (
    490             WireErrorCategory::Compatibility,
    491             false,
    492             WireRecoveryAction::UpdateApplication,
    493         ),
    494         SafeErrorCode::RepairUnauthorized => (
    495             WireErrorCategory::Credential,
    496             false,
    497             WireRecoveryAction::Authenticate,
    498         ),
    499         SafeErrorCode::InvalidRelayConfiguration => (
    500             WireErrorCategory::Network,
    501             false,
    502             WireRecoveryAction::CheckConfiguration,
    503         ),
    504         SafeErrorCode::RelayConnectionFailed | SafeErrorCode::ProfileRefreshFailed => {
    505             (WireErrorCategory::Network, true, WireRecoveryAction::Retry)
    506         }
    507         SafeErrorCode::InvalidApplicationState | SafeErrorCode::ObserverRegistrationFailed => (
    508             WireErrorCategory::Lifecycle,
    509             true,
    510             WireRecoveryAction::Retry,
    511         ),
    512         SafeErrorCode::NativeLibraryLoadFailed => (
    513             WireErrorCategory::Internal,
    514             false,
    515             WireRecoveryAction::RestartApplication,
    516         ),
    517         SafeErrorCode::AvailabilityInvalidInput => {
    518             (WireErrorCategory::Input, false, WireRecoveryAction::None)
    519         }
    520         SafeErrorCode::AvailabilityUnsupportedProfile => (
    521             WireErrorCategory::Compatibility,
    522             false,
    523             WireRecoveryAction::None,
    524         ),
    525         SafeErrorCode::AvailabilityScopeMismatch | SafeErrorCode::AvailabilityStaleQuery => {
    526             (WireErrorCategory::Conflict, false, WireRecoveryAction::None)
    527         }
    528         SafeErrorCode::AvailabilityCapacity => (
    529             WireErrorCategory::Lifecycle,
    530             true,
    531             WireRecoveryAction::Retry,
    532         ),
    533         SafeErrorCode::AvailabilityUnavailable => {
    534             (WireErrorCategory::Network, true, WireRecoveryAction::Retry)
    535         }
    536     }
    537 }
    538 
    539 impl From<SignerAvailability> for SignerAvailabilityDto {
    540     fn from(value: SignerAvailability) -> Self {
    541         match value {
    542             SignerAvailability::Available => Self::Available,
    543             SignerAvailability::CredentialMissing => Self::CredentialMissing,
    544             SignerAvailability::StoreUnavailable => Self::StoreUnavailable,
    545         }
    546     }
    547 }
    548 
    549 impl From<RelayConnectionState> for RelayConnectionStateDto {
    550     fn from(value: RelayConnectionState) -> Self {
    551         match value {
    552             RelayConnectionState::Disconnected => Self::Disconnected,
    553             RelayConnectionState::Connecting => Self::Connecting,
    554             RelayConnectionState::Connected => Self::Connected,
    555             RelayConnectionState::Degraded => Self::Degraded,
    556             RelayConnectionState::Error(_) => Self::Error,
    557         }
    558     }
    559 }
    560 
    561 impl From<ProfileLoadState> for ProfileLoadStateDto {
    562     fn from(value: ProfileLoadState) -> Self {
    563         match value {
    564             ProfileLoadState::Empty => Self::Empty,
    565             ProfileLoadState::Loading => Self::Loading,
    566             ProfileLoadState::Cached => Self::Cached,
    567             ProfileLoadState::Fresh => Self::Fresh,
    568             ProfileLoadState::Error(_) => Self::Error,
    569         }
    570     }
    571 }
    572 
    573 #[cfg(test)]
    574 #[cfg_attr(coverage_nightly, coverage(off))]
    575 mod tests {
    576     use std::sync::Arc;
    577 
    578     use harvestcircle_application::{
    579         AppCore, ProfileLoadState, RelayConfiguration, RelayConnectionState, RuntimeLifecycle,
    580     };
    581     use harvestcircle_nostr::NostrKeyMaterialProvider;
    582 
    583     use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage, SignerAvailability};
    584 
    585     use super::{
    586         AppLifecycleDto, AppSnapshotDto, ProfileLoadStateDto, RelayConnectionStateDto,
    587         SafeErrorDto, SignerAvailabilityDto, WireErrorCategory, WireErrorCode, WireRecoveryAction,
    588         error_policy,
    589     };
    590 
    591     fn safe_error(code: SafeErrorCode) -> SafeError {
    592         SafeError::new(code, SafeMessage::new("Safe compatibility failure."))
    593     }
    594 
    595     #[test]
    596     fn snapshot_dto_is_revisioned_public_and_secret_free() {
    597         let core = AppCore::new(
    598             RelayConfiguration::default(),
    599             Arc::new(NostrKeyMaterialProvider),
    600         );
    601         let snapshot = core.bootstrap().expect("bootstrap");
    602         let dto = AppSnapshotDto::from(&snapshot);
    603         let debug = format!("{dto:?}");
    604 
    605         assert_eq!(dto.revision, 1);
    606         assert!(dto.identities.is_empty());
    607         assert!(!debug.contains("nsec"));
    608         assert!(!debug.contains("secret_key"));
    609         assert!(!debug.contains("server_url"));
    610     }
    611 
    612     #[test]
    613     fn security_errors_have_explicit_stable_wire_mappings() {
    614         for (code, expected_code, expected_recovery) in [
    615             (
    616                 SafeErrorCode::StorageQuarantined,
    617                 WireErrorCode::StorageQuarantined,
    618                 WireRecoveryAction::RepairStorage,
    619             ),
    620             (
    621                 SafeErrorCode::StorageBackupInvalid,
    622                 WireErrorCode::StorageBackupInvalid,
    623                 WireRecoveryAction::RestoreBackup,
    624             ),
    625             (
    626                 SafeErrorCode::UnsupportedSchemaVersion,
    627                 WireErrorCode::UnsupportedSchemaVersion,
    628                 WireRecoveryAction::UpdateApplication,
    629             ),
    630             (
    631                 SafeErrorCode::RepairUnauthorized,
    632                 WireErrorCode::RepairUnauthorized,
    633                 WireRecoveryAction::Authenticate,
    634             ),
    635         ] {
    636             let dto = SafeErrorDto::from(harvestcircle_domain::SafeError::new(
    637                 code,
    638                 SafeMessage::new("Safe compatibility failure."),
    639             ));
    640             assert_eq!(dto.code, expected_code);
    641             assert_eq!(dto.recovery_action, expected_recovery);
    642             assert!(!dto.retryable);
    643         }
    644     }
    645 
    646     #[test]
    647     fn every_safe_error_has_an_explicit_wire_code_and_policy() {
    648         let cases = [
    649             (
    650                 SafeErrorCode::InvalidPublicKey,
    651                 WireErrorCode::InvalidPublicKey,
    652             ),
    653             (
    654                 SafeErrorCode::InvalidSecretKey,
    655                 WireErrorCode::InvalidSecretKey,
    656             ),
    657             (
    658                 SafeErrorCode::InvalidIdentityMetadata,
    659                 WireErrorCode::InvalidIdentityMetadata,
    660             ),
    661             (
    662                 SafeErrorCode::InvalidProfileMetadata,
    663                 WireErrorCode::InvalidProfileMetadata,
    664             ),
    665             (
    666                 SafeErrorCode::InvalidApplicationState,
    667                 WireErrorCode::InvalidApplicationState,
    668             ),
    669             (
    670                 SafeErrorCode::IdentityAlreadyExists,
    671                 WireErrorCode::IdentityAlreadyExists,
    672             ),
    673             (
    674                 SafeErrorCode::IdentityNotFound,
    675                 WireErrorCode::IdentityNotFound,
    676             ),
    677             (
    678                 SafeErrorCode::KeyringUnavailable,
    679                 WireErrorCode::KeyringUnavailable,
    680             ),
    681             (
    682                 SafeErrorCode::CredentialMissing,
    683                 WireErrorCode::CredentialMissing,
    684             ),
    685             (
    686                 SafeErrorCode::StorageUnavailable,
    687                 WireErrorCode::StorageUnavailable,
    688             ),
    689             (SafeErrorCode::StorageCorrupt, WireErrorCode::StorageCorrupt),
    690             (
    691                 SafeErrorCode::StorageQuarantined,
    692                 WireErrorCode::StorageQuarantined,
    693             ),
    694             (
    695                 SafeErrorCode::StorageBackupInvalid,
    696                 WireErrorCode::StorageBackupInvalid,
    697             ),
    698             (
    699                 SafeErrorCode::UnsupportedSchemaVersion,
    700                 WireErrorCode::UnsupportedSchemaVersion,
    701             ),
    702             (
    703                 SafeErrorCode::RepairUnauthorized,
    704                 WireErrorCode::RepairUnauthorized,
    705             ),
    706             (
    707                 SafeErrorCode::PendingOperationRecoveryRequired,
    708                 WireErrorCode::PendingOperationRecoveryRequired,
    709             ),
    710             (
    711                 SafeErrorCode::InvalidRelayConfiguration,
    712                 WireErrorCode::InvalidRelayConfiguration,
    713             ),
    714             (
    715                 SafeErrorCode::RelayConnectionFailed,
    716                 WireErrorCode::RelayConnectionFailed,
    717             ),
    718             (
    719                 SafeErrorCode::ProfileRefreshFailed,
    720                 WireErrorCode::ProfileRefreshFailed,
    721             ),
    722             (
    723                 SafeErrorCode::ObserverRegistrationFailed,
    724                 WireErrorCode::ObserverRegistrationFailed,
    725             ),
    726             (
    727                 SafeErrorCode::NativeLibraryLoadFailed,
    728                 WireErrorCode::NativeLibraryLoadFailed,
    729             ),
    730             (
    731                 SafeErrorCode::AvailabilityInvalidInput,
    732                 WireErrorCode::AvailabilityInvalidInput,
    733             ),
    734             (
    735                 SafeErrorCode::AvailabilityUnsupportedProfile,
    736                 WireErrorCode::AvailabilityUnsupportedProfile,
    737             ),
    738             (
    739                 SafeErrorCode::AvailabilityScopeMismatch,
    740                 WireErrorCode::AvailabilityScopeMismatch,
    741             ),
    742             (
    743                 SafeErrorCode::AvailabilityStaleQuery,
    744                 WireErrorCode::AvailabilityStaleQuery,
    745             ),
    746             (
    747                 SafeErrorCode::AvailabilityCapacity,
    748                 WireErrorCode::AvailabilityCapacity,
    749             ),
    750             (
    751                 SafeErrorCode::AvailabilityUnavailable,
    752                 WireErrorCode::AvailabilityUnavailable,
    753             ),
    754         ];
    755         for (code, expected_wire_code) in cases {
    756             let dto = SafeErrorDto::from(safe_error(code));
    757             assert_eq!(dto.code, expected_wire_code);
    758             assert_eq!(
    759                 (dto.category, dto.retryable, dto.recovery_action),
    760                 error_policy(code)
    761             );
    762         }
    763         assert_eq!(
    764             error_policy(SafeErrorCode::KeyringUnavailable),
    765             (
    766                 WireErrorCategory::Credential,
    767                 true,
    768                 WireRecoveryAction::Retry,
    769             )
    770         );
    771         assert_eq!(
    772             error_policy(SafeErrorCode::NativeLibraryLoadFailed),
    773             (
    774                 WireErrorCategory::Internal,
    775                 false,
    776                 WireRecoveryAction::RestartApplication,
    777             )
    778         );
    779     }
    780 
    781     #[test]
    782     fn runtime_and_connection_states_map_exhaustively_to_wire_states() {
    783         let core = AppCore::new(
    784             RelayConfiguration::default(),
    785             Arc::new(NostrKeyMaterialProvider),
    786         );
    787         let snapshot = core.bootstrap().expect("bootstrap");
    788         for (runtime, expected) in [
    789             (RuntimeLifecycle::Opening, AppLifecycleDto::Opening),
    790             (
    791                 RuntimeLifecycle::CompatibilityChecking,
    792                 AppLifecycleDto::CompatibilityChecking,
    793             ),
    794             (
    795                 RuntimeLifecycle::AcquiringOwnership,
    796                 AppLifecycleDto::AcquiringOwnership,
    797             ),
    798             (RuntimeLifecycle::Migrating, AppLifecycleDto::Migrating),
    799             (RuntimeLifecycle::Recovering, AppLifecycleDto::Recovering),
    800             (RuntimeLifecycle::Ready, AppLifecycleDto::Ready),
    801             (
    802                 RuntimeLifecycle::Degraded(safe_error(SafeErrorCode::RelayConnectionFailed)),
    803                 AppLifecycleDto::Degraded,
    804             ),
    805             (
    806                 RuntimeLifecycle::Blocked(safe_error(SafeErrorCode::StorageUnavailable)),
    807                 AppLifecycleDto::Blocked,
    808             ),
    809             (
    810                 RuntimeLifecycle::ShuttingDown,
    811                 AppLifecycleDto::ShuttingDown,
    812             ),
    813             (RuntimeLifecycle::Closed, AppLifecycleDto::Closed),
    814             (
    815                 RuntimeLifecycle::Fatal(safe_error(SafeErrorCode::StorageCorrupt)),
    816                 AppLifecycleDto::Fatal,
    817             ),
    818         ] {
    819             let dto = AppSnapshotDto::from_runtime(&snapshot, runtime);
    820             assert_eq!(dto.lifecycle, expected);
    821             assert_eq!(
    822                 dto.lifecycle_error.is_some(),
    823                 matches!(
    824                     expected,
    825                     AppLifecycleDto::Degraded | AppLifecycleDto::Blocked | AppLifecycleDto::Fatal
    826                 )
    827             );
    828         }
    829 
    830         for (source, expected) in [
    831             (
    832                 SignerAvailability::Available,
    833                 SignerAvailabilityDto::Available,
    834             ),
    835             (
    836                 SignerAvailability::CredentialMissing,
    837                 SignerAvailabilityDto::CredentialMissing,
    838             ),
    839             (
    840                 SignerAvailability::StoreUnavailable,
    841                 SignerAvailabilityDto::StoreUnavailable,
    842             ),
    843         ] {
    844             assert_eq!(SignerAvailabilityDto::from(source), expected);
    845         }
    846         for (source, expected) in [
    847             (
    848                 RelayConnectionState::Disconnected,
    849                 RelayConnectionStateDto::Disconnected,
    850             ),
    851             (
    852                 RelayConnectionState::Connecting,
    853                 RelayConnectionStateDto::Connecting,
    854             ),
    855             (
    856                 RelayConnectionState::Connected,
    857                 RelayConnectionStateDto::Connected,
    858             ),
    859             (
    860                 RelayConnectionState::Degraded,
    861                 RelayConnectionStateDto::Degraded,
    862             ),
    863             (
    864                 RelayConnectionState::Error(safe_error(SafeErrorCode::RelayConnectionFailed)),
    865                 RelayConnectionStateDto::Error,
    866             ),
    867         ] {
    868             assert_eq!(RelayConnectionStateDto::from(source), expected);
    869         }
    870         for (source, expected) in [
    871             (ProfileLoadState::Empty, ProfileLoadStateDto::Empty),
    872             (ProfileLoadState::Loading, ProfileLoadStateDto::Loading),
    873             (ProfileLoadState::Cached, ProfileLoadStateDto::Cached),
    874             (ProfileLoadState::Fresh, ProfileLoadStateDto::Fresh),
    875             (
    876                 ProfileLoadState::Error(safe_error(SafeErrorCode::ProfileRefreshFailed)),
    877                 ProfileLoadStateDto::Error,
    878             ),
    879         ] {
    880             assert_eq!(ProfileLoadStateDto::from(source), expected);
    881         }
    882     }
    883 
    884     #[test]
    885     fn availability_errors_have_exact_wire_codes_and_recovery_policy() {
    886         use harvestcircle_domain::error::AvailabilityFailure;
    887 
    888         for (failure, code, category, retryable, recovery, message) in [
    889             (
    890                 AvailabilityFailure::InvalidInput,
    891                 WireErrorCode::AvailabilityInvalidInput,
    892                 WireErrorCategory::Input,
    893                 false,
    894                 WireRecoveryAction::None,
    895                 "The availability request is invalid.",
    896             ),
    897             (
    898                 AvailabilityFailure::UnsupportedProfile,
    899                 WireErrorCode::AvailabilityUnsupportedProfile,
    900                 WireErrorCategory::Compatibility,
    901                 false,
    902                 WireRecoveryAction::None,
    903                 "This availability profile is unsupported.",
    904             ),
    905             (
    906                 AvailabilityFailure::ScopeMismatch,
    907                 WireErrorCode::AvailabilityScopeMismatch,
    908                 WireErrorCategory::Conflict,
    909                 false,
    910                 WireRecoveryAction::None,
    911                 "The availability request belongs to another scope.",
    912             ),
    913             (
    914                 AvailabilityFailure::StaleQuery,
    915                 WireErrorCode::AvailabilityStaleQuery,
    916                 WireErrorCategory::Conflict,
    917                 false,
    918                 WireRecoveryAction::None,
    919                 "The availability query is stale.",
    920             ),
    921             (
    922                 AvailabilityFailure::Capacity,
    923                 WireErrorCode::AvailabilityCapacity,
    924                 WireErrorCategory::Lifecycle,
    925                 true,
    926                 WireRecoveryAction::Retry,
    927                 "The availability operation reached its capacity.",
    928             ),
    929             (
    930                 AvailabilityFailure::Unavailable,
    931                 WireErrorCode::AvailabilityUnavailable,
    932                 WireErrorCategory::Network,
    933                 true,
    934                 WireRecoveryAction::Retry,
    935                 "Availability discovery is unavailable.",
    936             ),
    937         ] {
    938             let dto = SafeErrorDto::from(SafeError::from(failure));
    939             assert_eq!(dto.code, code);
    940             assert_eq!(
    941                 (dto.category, dto.retryable, dto.recovery_action),
    942                 (category, retryable, recovery)
    943             );
    944             assert_eq!(dto.message, message);
    945             assert!(!format!("{dto:?}").contains("HCAV_PRIVATE_REMOTE_PAYLOAD"));
    946         }
    947     }
    948 }