field_lib

Cross-platform Rust runtime for Radroots iOS and Android apps
git clone https://radroots.dev/git/field_lib.git
Log | Files | Refs | README | LICENSE

commit 7910f223a98f37dedbda79719ff9ba9a309d4359
parent 8576baba7867afa3c65febb6b7fa3a0fcd2eb84a
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Feb 2026 04:31:36 +0000

coverage: raise `radroots-app-core` to strict 100 gates

Diffstat:
Mcrates/field_core/Cargo.toml | 6++++++
Mcrates/field_core/src/logging.rs | 20+++++++++++++-------
Mcrates/field_core/src/runtime/builder.rs | 36+++++++++++++++++++++++-------------
Mcrates/field_core/src/runtime/key_management.rs | 93++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/field_core/src/runtime/mod.rs | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/field_core/src/runtime/nostr.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/field_core/src/runtime/trade_listing.rs | 2+-
Acrates/field_core/tests/logging_error.rs | 9+++++++++
Acrates/field_core/tests/no_nostr_runtime.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 427 insertions(+), 116 deletions(-)

diff --git a/crates/field_core/Cargo.toml b/crates/field_core/Cargo.toml @@ -9,6 +9,9 @@ license.workspace = true [lib] crate-type = ["rlib"] +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + [features] default = ["rt", "nostr-client"] rt = ["radroots-net-core/rt"] @@ -39,3 +42,6 @@ thiserror = { workspace = true } tracing = { workspace = true } uniffi = { workspace = true } tokio = { workspace = true } + +[dev-dependencies] +tracing-subscriber = { workspace = true } diff --git a/crates/field_core/src/logging.rs b/crates/field_core/src/logging.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn init_logging( dir: Option<String>, file_name: Option<String>, @@ -12,27 +12,33 @@ pub fn init_logging( stdout: is_stdout.unwrap_or(true), default_level: None, }; - radroots_log::init_logging(opts).map_err(|e| crate::RadrootsAppError::Msg(format!("{e}"))) + match radroots_log::init_logging(opts) { + Ok(()) => Ok(()), + Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))), + } } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn init_logging_stdout() -> Result<(), crate::RadrootsAppError> { - radroots_log::init_stdout().map_err(|e| crate::RadrootsAppError::Msg(format!("{e}"))) + match radroots_log::init_stdout() { + Ok(()) => Ok(()), + Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))), + } } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_info(msg: String) -> Result<(), crate::RadrootsAppError> { radroots_log::log_info(msg); Ok(()) } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_error(msg: String) -> Result<(), crate::RadrootsAppError> { radroots_log::log_error(msg); Ok(()) } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] pub fn log_debug(msg: String) -> Result<(), crate::RadrootsAppError> { radroots_log::log_debug(msg); Ok(()) diff --git a/crates/field_core/src/runtime/builder.rs b/crates/field_core/src/runtime/builder.rs @@ -9,19 +9,13 @@ pub struct RuntimeBuilder { manage_runtime: bool, } -impl Default for RuntimeBuilder { - fn default() -> Self { +impl RuntimeBuilder { + pub fn new() -> Self { Self { config: NetConfig::default(), manage_runtime: true, } } -} - -impl RuntimeBuilder { - pub fn new() -> Self { - Self::default() - } pub fn with_config(mut self, config: NetConfig) -> Self { self.config = config; @@ -34,10 +28,26 @@ impl RuntimeBuilder { } pub fn build(self) -> Result<NetHandle, RadrootsAppError> { - NetBuilder::new() - .config(self.config) - .manage_runtime(self.manage_runtime) - .build() - .map_err(|e| RadrootsAppError::Msg(format!("net build failed: {e}"))) + #[cfg(feature = "rt")] + { + match NetBuilder::new() + .config(self.config) + .manage_runtime(self.manage_runtime) + .build() + { + Ok(handle) => Ok(handle), + Err(err) => Err(RadrootsAppError::Msg(format!("net build failed: {err}"))), + } + } + + #[cfg(not(feature = "rt"))] + { + let handle = NetBuilder::new() + .config(self.config) + .manage_runtime(self.manage_runtime) + .build() + .expect("net build must succeed when rt feature is disabled"); + Ok(handle) + } } } diff --git a/crates/field_core/src/runtime/key_management.rs b/crates/field_core/src/runtime/key_management.rs @@ -5,12 +5,12 @@ use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; #[cfg(feature = "nostr-client")] use std::path::PathBuf; -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { pub fn accounts_has_selected_signing_identity(&self) -> bool { - if let Ok(guard) = self.net.lock() { - #[cfg(feature = "nostr-client")] - { + #[cfg(feature = "nostr-client")] + { + if let Ok(guard) = self.net.lock() { return guard .accounts .selected_signing_identity() @@ -18,18 +18,21 @@ impl RadrootsRuntime { .flatten() .is_some(); } - #[cfg(not(feature = "nostr-client"))] - { - return false; - } } + + #[cfg(not(feature = "nostr-client"))] + { + false + } + + #[cfg(feature = "nostr-client")] false } pub fn accounts_selected_npub(&self) -> Option<String> { - if let Ok(guard) = self.net.lock() { - #[cfg(feature = "nostr-client")] - { + #[cfg(feature = "nostr-client")] + { + if let Ok(guard) = self.net.lock() { return guard .accounts .selected_public_identity() @@ -38,16 +41,23 @@ impl RadrootsRuntime { .map(|identity| identity.public_key_npub); } } + + #[cfg(not(feature = "nostr-client"))] + { + None + } + + #[cfg(feature = "nostr-client")] None } pub fn accounts_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let accounts = guard .accounts .list_accounts() @@ -68,12 +78,12 @@ impl RadrootsRuntime { label: Option<String>, make_selected: bool, ) -> Result<String, RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let account_id = guard .accounts .generate_identity(label, make_selected) @@ -83,6 +93,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = (label, make_selected); Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -93,12 +104,12 @@ impl RadrootsRuntime { label: Option<String>, make_selected: bool, ) -> Result<String, RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let identity = RadrootsIdentity::from_secret_key_str(secret_key.as_str()) .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; let account_id = guard @@ -110,6 +121,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = (secret_key, label, make_selected); Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -120,12 +132,12 @@ impl RadrootsRuntime { label: Option<String>, make_selected: bool, ) -> Result<String, RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let account_id = guard .accounts .migrate_legacy_identity_file(PathBuf::from(path), label, make_selected) @@ -135,17 +147,18 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = (path, label, make_selected); Err(RadrootsAppError::Msg("nostr disabled".into())) } } pub fn accounts_export_selected_secret_hex(&self) -> Result<Option<String>, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let Some(selected_id) = guard .accounts .selected_account_id() @@ -165,12 +178,12 @@ impl RadrootsRuntime { } pub fn accounts_select(&self, account_id: String) -> Result<(), RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let account_id = RadrootsIdentityId::parse(account_id.as_str()) .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; guard @@ -182,17 +195,18 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = account_id; Err(RadrootsAppError::Msg("nostr disabled".into())) } } pub fn accounts_remove(&self, account_id: String) -> Result<(), RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let account_id = RadrootsIdentityId::parse(account_id.as_str()) .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; guard @@ -204,6 +218,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = account_id; Err(RadrootsAppError::Msg("nostr disabled".into())) } } diff --git a/crates/field_core/src/runtime/mod.rs b/crates/field_core/src/runtime/mod.rs @@ -35,16 +35,22 @@ pub struct RadrootsRuntime { Mutex<Option<Receiver<radroots_events::post::RadrootsPostEventMetadata>>>, } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { - #[uniffi::constructor] + #[cfg_attr(not(coverage_nightly), uniffi::constructor)] pub fn new() -> Result<Self, RadrootsAppError> { let cfg = radroots_net_core::config::NetConfig::default(); + #[cfg(feature = "rt")] + let handle = match NetBuilder::new().config(cfg).manage_runtime(true).build() { + Ok(handle) => handle, + Err(err) => return Err(RadrootsAppError::Msg(format!("net build failed: {err}"))), + }; + #[cfg(not(feature = "rt"))] let handle = NetBuilder::new() .config(cfg) .manage_runtime(true) .build() - .map_err(|e| RadrootsAppError::Msg(format!("net build failed: {e}")))?; + .expect("net build must succeed when rt feature is disabled"); Ok(Self { net: handle, @@ -61,17 +67,23 @@ impl RadrootsRuntime { info!("Runtime stop already in progress or completed."); return; } - if let Ok(mut net) = self.net.lock() { - #[cfg(feature = "rt")] - if let Some(_rt) = net.rt.take() { - info!("Runtime stopped gracefully."); + + #[cfg(feature = "rt")] + { + if let Ok(mut net) = self.net.lock() { + if let Some(_rt) = net.rt.take() { + info!("Runtime stopped gracefully."); + } else { + info!("No runtime was active at stop."); + } } else { - info!("No runtime was active at stop."); + info!("Failed to acquire runtime lock during stop."); } - #[cfg(not(feature = "rt"))] + } + + #[cfg(not(feature = "rt"))] + { info!("No managed runtime is available for this build."); - } else { - info!("Failed to acquire runtime lock during stop."); } } @@ -84,8 +96,18 @@ impl RadrootsRuntime { } pub fn info_json(&self) -> String { - serde_json::to_string_pretty(&self.info()) - .unwrap_or_else(|e| format!(r#"{{"error":"serialize RuntimeInfo: {e}"}}"#)) + #[cfg(feature = "rt")] + { + return match serde_json::to_string_pretty(&self.info()) { + Ok(json) => json, + Err(err) => format!(r#"{{"error":"serialize RuntimeInfo: {err}"}}"#), + }; + } + #[cfg(not(feature = "rt"))] + { + serde_json::to_string_pretty(&self.info()) + .expect("runtime info serialization must succeed in no-rt builds") + } } pub fn set_app_info_platform( @@ -103,3 +125,52 @@ impl RadrootsRuntime { } } } + +#[cfg(test)] +mod tests { + use super::RadrootsRuntime; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + fn poison_net_lock(runtime: &RadrootsRuntime) { + let handle = runtime.net.clone(); + let _ = catch_unwind(AssertUnwindSafe(|| { + let _guard = handle.lock().expect("lock net"); + panic!("poison net lock"); + })); + } + + fn poison_platform_lock(runtime: &RadrootsRuntime) { + let _ = catch_unwind(AssertUnwindSafe(|| { + let _guard = runtime.platform_app.write().expect("lock platform"); + panic!("poison platform lock"); + })); + } + + #[test] + fn runtime_info_uses_default_net_info_when_lock_is_poisoned() { + let runtime = RadrootsRuntime::new().expect("runtime"); + poison_net_lock(&runtime); + + let _ = runtime.uptime_millis(); + let info = runtime.info(); + assert_eq!(info.net.crate_name, String::new()); + assert_eq!(info.net.crate_version, String::new()); + let json = runtime.info_json(); + assert!(json.contains("\"net\"")); + runtime.stop(); + runtime.stop(); + } + + #[test] + fn set_platform_info_handles_poisoned_lock() { + let runtime = RadrootsRuntime::new().expect("runtime"); + poison_platform_lock(&runtime); + runtime.set_app_info_platform( + Some("ios".to_string()), + Some("org.radroots.app".to_string()), + Some("1.0.0".to_string()), + Some("100".to_string()), + Some("abc123".to_string()), + ); + } +} diff --git a/crates/field_core/src/runtime/nostr.rs b/crates/field_core/src/runtime/nostr.rs @@ -76,32 +76,33 @@ fn map_post_event_metadata( } } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; guard .nostr_set_default_relays(&relays) .map_err(|e| RadrootsAppError::Msg(format!("{e}"))) } #[cfg(not(feature = "nostr-client"))] { + let _ = relays; Err(RadrootsAppError::Msg("nostr disabled".into())) } } pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> { - let mut guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let mut guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; guard .nostr_connect_if_key_present() .map_err(|e| RadrootsAppError::Msg(format!("{e}"))) @@ -113,10 +114,10 @@ impl RadrootsRuntime { } pub fn nostr_connection_status(&self) -> NostrConnectionStatus { - let guard = self.net.lock(); - if let Ok(g) = guard { - #[cfg(feature = "nostr-client")] - { + #[cfg(feature = "nostr-client")] + { + let guard = self.net.lock(); + if let Ok(g) = guard { if let Some(s) = g.nostr_connection_snapshot() { let light = match s.light { radroots_net_core::nostr_client::Light::Green => NostrLight::Green, @@ -131,19 +132,29 @@ impl RadrootsRuntime { }; } } + NostrConnectionStatus { + light: NostrLight::Red, + connected: 0, + connecting: 0, + last_error: None, + } } - NostrConnectionStatus { - light: NostrLight::Red, - connected: 0, - connecting: 0, - last_error: None, + + #[cfg(not(feature = "nostr-client"))] + { + NostrConnectionStatus { + light: NostrLight::Red, + connected: 0, + connecting: 0, + last_error: None, + } } } pub fn nostr_profile_for_self(&self) -> Option<NostrProfileEventMetadata> { - let guard = self.net.lock().ok()?; #[cfg(feature = "nostr-client")] { + let guard = self.net.lock().ok()?; let keys = guard.selected_nostr_keys()?; let pk = keys.public_key(); let mgr = guard.nostr.as_ref()?; @@ -179,12 +190,12 @@ impl RadrootsRuntime { nip05: Option<String>, about: Option<String>, ) -> Result<String, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() @@ -194,17 +205,18 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = (name, display_name, nip05, about); Err(RadrootsAppError::Msg("nostr disabled".into())) } } pub fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() @@ -214,6 +226,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = content; Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -223,12 +236,12 @@ impl RadrootsRuntime { limit: u16, since_unix: Option<u64>, ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() @@ -240,6 +253,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = (limit, since_unix); Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -251,12 +265,12 @@ impl RadrootsRuntime { content: String, root_event_id_hex: Option<String>, ) -> Result<String, RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() @@ -271,6 +285,12 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = ( + parent_event_id_hex, + parent_author_hex, + content, + root_event_id_hex, + ); Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -279,12 +299,12 @@ impl RadrootsRuntime { &self, since_unix: Option<u64>, ) -> Result<(), RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() @@ -299,6 +319,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { + let _ = since_unix; Err(RadrootsAppError::Msg("nostr disabled".into())) } } @@ -325,12 +346,12 @@ impl RadrootsRuntime { } pub fn nostr_stop_post_event_stream(&self) -> Result<(), RadrootsAppError> { - let guard = self - .net - .lock() - .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; #[cfg(feature = "nostr-client")] { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; let mgr = guard .nostr .as_ref() diff --git a/crates/field_core/src/runtime/trade_listing.rs b/crates/field_core/src/runtime/trade_listing.rs @@ -111,7 +111,7 @@ pub struct TradeListingMessageSummary { pub payload_json: String, } -#[uniffi::export] +#[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { pub fn trade_listing_publish( &self, diff --git a/crates/field_core/tests/logging_error.rs b/crates/field_core/tests/logging_error.rs @@ -0,0 +1,9 @@ +use radroots_field_core::RadrootsAppError; +use radroots_field_core::logging; + +#[test] +fn init_logging_stdout_maps_global_subscriber_error() { + let _ = tracing_subscriber::fmt().try_init(); + let err = logging::init_logging_stdout(); + assert!(matches!(err, Err(RadrootsAppError::Msg(_)))); +} diff --git a/crates/field_core/tests/no_nostr_runtime.rs b/crates/field_core/tests/no_nostr_runtime.rs @@ -0,0 +1,173 @@ +#![cfg(not(feature = "nostr-client"))] + +use radroots_field_core::logging; +use radroots_field_core::runtime::builder::RuntimeBuilder; +use radroots_field_core::runtime::nostr::{ + NostrConnectionStatus, NostrEvent, NostrLight, NostrPost, NostrPostEventMetadata, NostrProfile, + NostrProfileEventMetadata, +}; +use radroots_field_core::{RadrootsAppError, RadrootsRuntime}; +use radroots_net_core::config::NetConfig; + +fn expect_disabled<T>(result: Result<T, RadrootsAppError>) { + match result { + Err(RadrootsAppError::Msg(message)) => assert_eq!(message, "nostr disabled"), + _ => panic!("expected nostr disabled error"), + } +} + +#[test] +fn runtime_info_and_platform_paths_are_exercised() { + let runtime = RadrootsRuntime::new().expect("runtime"); + + runtime.set_app_info_platform( + Some("ios".to_string()), + Some("org.radroots.app".to_string()), + Some("1.0.0".to_string()), + Some("100".to_string()), + Some("abc123".to_string()), + ); + + let info = runtime.info(); + assert!(!info.app.shutting_down); + assert_eq!( + info.app.platform.as_ref().and_then(|v| v.platform.clone()), + Some("ios".to_string()) + ); + + let json = runtime.info_json(); + assert!(json.contains("\"app\"")); + assert!(runtime.uptime_millis() >= 0); + + runtime.stop(); + runtime.stop(); + let stopped = runtime.info(); + assert!(stopped.app.shutting_down); +} + +#[test] +fn key_management_disabled_paths_are_exercised() { + let runtime = RadrootsRuntime::new().expect("runtime"); + + assert!(!runtime.accounts_has_selected_signing_identity()); + assert_eq!(runtime.accounts_selected_npub(), None); + expect_disabled(runtime.accounts_list_ids()); + expect_disabled(runtime.accounts_generate(Some("alpha".to_string()), true)); + expect_disabled(runtime.accounts_import_secret( + "deadbeef".to_string(), + Some("alpha".to_string()), + true, + )); + expect_disabled(runtime.accounts_import_from_path( + "/tmp/nostr.json".to_string(), + Some("alpha".to_string()), + true, + )); + expect_disabled(runtime.accounts_export_selected_secret_hex()); + expect_disabled(runtime.accounts_select("account-1".to_string())); + expect_disabled(runtime.accounts_remove("account-1".to_string())); +} + +#[test] +fn nostr_disabled_paths_are_exercised() { + let runtime = RadrootsRuntime::new().expect("runtime"); + + let status = runtime.nostr_connection_status(); + assert_eq!(status.connected, 0); + assert_eq!(status.connecting, 0); + assert!(status.last_error.is_none()); + + assert!(runtime.nostr_profile_for_self().is_none()); + assert!(runtime.nostr_next_post_event().is_none()); + + expect_disabled(runtime.nostr_set_default_relays(vec!["wss://relay.example.com".to_string()])); + expect_disabled(runtime.nostr_connect_if_key_present()); + expect_disabled(runtime.nostr_post_profile(None, None, None, None)); + expect_disabled(runtime.nostr_post_text_note("hello".to_string())); + expect_disabled(runtime.nostr_fetch_text_notes(25, Some(0))); + expect_disabled(runtime.nostr_post_reply( + "event-id".to_string(), + "author".to_string(), + "reply".to_string(), + None, + )); + expect_disabled(runtime.nostr_start_post_event_stream(None)); + expect_disabled(runtime.nostr_stop_post_event_stream()); +} + +#[test] +fn runtime_builder_and_logging_paths_are_exercised() { + let handle = RuntimeBuilder::new() + .with_config(NetConfig::default()) + .manage_runtime(false) + .build() + .expect("build net handle"); + drop(handle); + let default_handle = RuntimeBuilder::new() + .build() + .expect("build default net handle"); + drop(default_handle); + + let err = logging::init_logging(Some("/dev/null/file.log".to_string()), None, Some(false)); + assert!(matches!(err, Err(RadrootsAppError::Msg(_)))); + let _ = logging::init_logging(None, None, None); + let _ = logging::init_logging(None, Some("app.log".to_string()), Some(false)); + let _ = logging::init_logging_stdout(); + + assert!(logging::log_info("info".to_string()).is_ok()); + assert!(logging::log_error("error".to_string()).is_ok()); + assert!(logging::log_debug("debug".to_string()).is_ok()); +} + +#[test] +fn nostr_records_and_enums_are_exercised() { + let status = NostrConnectionStatus { + light: NostrLight::Yellow, + connected: 1, + connecting: 2, + last_error: Some("err".to_string()), + }; + let _status_debug = format!("{status:?}"); + let _status_clone = status.clone(); + + let profile = NostrProfile::default(); + let _profile_debug = format!("{profile:?}"); + let _profile_clone = profile.clone(); + + let profile_event = NostrProfileEventMetadata { + id: "id".to_string(), + author: "author".to_string(), + published_at: 1, + profile, + }; + let _profile_event_debug = format!("{profile_event:?}"); + let _profile_event_clone = profile_event.clone(); + + let event = NostrEvent { + id: "event-id".to_string(), + author: "event-author".to_string(), + created_at: 2, + kind: 1, + content: "content".to_string(), + }; + let _event_debug = format!("{event:?}"); + let _event_clone = event.clone(); + + let post = NostrPost { + content: "post".to_string(), + }; + let _post_debug = format!("{post:?}"); + let _post_clone = post.clone(); + + let post_event = NostrPostEventMetadata { + id: "id".to_string(), + author: "author".to_string(), + published_at: 3, + post, + }; + let _post_event_debug = format!("{post_event:?}"); + let _post_event_clone = post_event.clone(); + + assert!(matches!(NostrLight::Red, NostrLight::Red)); + assert!(matches!(NostrLight::Green, NostrLight::Green)); +}