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 }