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!();