app

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

commit 25d9dccc1574274d217d6592e206f9369dfa5388
parent 45db999099edb054f58993aa079c10567358878c
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 19:22:30 +0000

app: strengthen keystore health checks

- require datastore public key for keystore validation
- verify keystore contains the stored public key
- add coverage for missing and mismatch cases
- keep aggregate health wiring consistent

Diffstat:
Mapp/src/health.rs | 215++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 196 insertions(+), 19 deletions(-)

diff --git a/app/src/health.rs b/app/src/health.rs @@ -76,10 +76,11 @@ impl AppHealthReport { use crate::{ app_datastore_has_app_data, app_datastore_has_config, + app_datastore_key_nostr_key, app_key_maps_validate, AppKeyMapConfig, }; -use radroots_app_core::datastore::RadrootsClientDatastore; +use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError}; use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr}; pub fn app_health_check_key_maps(key_maps: &AppKeyMapConfig) -> AppHealthCheckResult { @@ -134,12 +135,25 @@ pub async fn app_health_check_datastore_roundtrip<T: RadrootsClientDatastore>( AppHealthCheckResult::ok() } -pub async fn app_health_check_keystore_access<T: RadrootsClientKeystoreNostr>( - keystore: &T, +pub async fn app_health_check_keystore_access<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>( + datastore: &T, + keystore: &K, + key_maps: &AppKeyMapConfig, ) -> AppHealthCheckResult { - match keystore.keys().await { + let key_name = match app_datastore_key_nostr_key(key_maps) { + Ok(value) => value, + Err(err) => return AppHealthCheckResult::error(err.to_string()), + }; + let public_key = match datastore.get(key_name).await { + Ok(value) if !value.is_empty() => value, + Ok(_) => return AppHealthCheckResult::error("missing"), + Err(RadrootsClientDatastoreError::NoResult) => return AppHealthCheckResult::error("missing"), + Err(err) => return AppHealthCheckResult::error(err.to_string()), + }; + match keystore.read(&public_key).await { Ok(_) => AppHealthCheckResult::ok(), - Err(RadrootsClientKeystoreError::NostrNoResults) => AppHealthCheckResult::ok(), + Err(RadrootsClientKeystoreError::MissingKey) => AppHealthCheckResult::error("missing"), + Err(RadrootsClientKeystoreError::NostrNoResults) => AppHealthCheckResult::error("missing"), Err(err) => AppHealthCheckResult::error(err.to_string()), } } @@ -154,7 +168,7 @@ pub async fn app_health_check_all<T: RadrootsClientDatastore, K: RadrootsClientK bootstrap_config: app_health_check_bootstrap_config(datastore, key_maps).await, bootstrap_app_data: app_health_check_bootstrap_app_data(datastore, key_maps).await, datastore_roundtrip: app_health_check_datastore_roundtrip(datastore).await, - keystore: app_health_check_keystore_access(keystore).await, + keystore: app_health_check_keystore_access(datastore, keystore, key_maps).await, } } @@ -173,13 +187,22 @@ mod tests { }; use crate::AppKeyMapConfig; use async_trait::async_trait; - use radroots_app_core::datastore::RadrootsClientWebDatastore; + use radroots_app_core::datastore::{ + RadrootsClientDatastore, + RadrootsClientDatastoreEntries, + RadrootsClientDatastoreError, + RadrootsClientDatastoreResult, + RadrootsClientWebDatastore, + }; use radroots_app_core::keystore::{ RadrootsClientKeystoreError, RadrootsClientKeystoreNostr, RadrootsClientKeystoreResult, RadrootsClientWebKeystoreNostr, }; + use radroots_app_core::idb::IDB_CONFIG_DATASTORE; + use radroots_app_core::backup::RadrootsClientBackupDatastorePayload; + use radroots_app_core::idb::RadrootsClientIdbConfig; #[test] fn health_status_as_str() { @@ -244,8 +267,114 @@ mod tests { assert_eq!(result.status, AppHealthCheckStatus::Error); } + struct TestDatastore { + get_result: RadrootsClientDatastoreResult<String>, + } + + fn datastore_err<T>() -> RadrootsClientDatastoreResult<T> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + #[async_trait(?Send)] + impl RadrootsClientDatastore for TestDatastore { + fn get_config(&self) -> RadrootsClientIdbConfig { + IDB_CONFIG_DATASTORE + } + + fn get_store_id(&self) -> &str { + "test" + } + + async fn init(&self) -> RadrootsClientDatastoreResult<()> { + datastore_err() + } + + async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> { + datastore_err() + } + + async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + self.get_result.clone() + } + + async fn set_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T> + where + T: serde::Serialize + serde::de::DeserializeOwned + Clone, + { + datastore_err() + } + + async fn update_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T> + where + T: serde::Serialize + serde::de::DeserializeOwned + Clone, + { + datastore_err() + } + + async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T> + where + T: serde::de::DeserializeOwned, + { + datastore_err() + } + + async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + datastore_err() + } + + async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + datastore_err() + } + + async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { + datastore_err() + } + + async fn set_param( + &self, + _key: &str, + _key_param: &str, + _value: &str, + ) -> RadrootsClientDatastoreResult<String> { + datastore_err() + } + + async fn get_param( + &self, + _key: &str, + _key_param: &str, + ) -> RadrootsClientDatastoreResult<String> { + datastore_err() + } + + async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> { + datastore_err() + } + + async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + datastore_err() + } + + async fn reset(&self) -> RadrootsClientDatastoreResult<()> { + datastore_err() + } + + async fn export_backup( + &self, + ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> { + datastore_err() + } + + async fn import_backup( + &self, + _payload: RadrootsClientBackupDatastorePayload, + ) -> RadrootsClientDatastoreResult<()> { + datastore_err() + } + } + struct TestKeystore { - result: RadrootsClientKeystoreResult<Vec<String>>, + read_result: RadrootsClientKeystoreResult<String>, } #[async_trait(?Send)] @@ -259,11 +388,11 @@ mod tests { } async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { - Err(RadrootsClientKeystoreError::IdbUndefined) + self.read_result.clone() } async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> { - self.result.clone() + Err(RadrootsClientKeystoreError::IdbUndefined) } async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { @@ -276,21 +405,69 @@ mod tests { } #[test] - fn health_check_keystore_maps_empty_ok() { + fn health_check_keystore_maps_idb_errors() { + let datastore = RadrootsClientWebDatastore::new(None); + let keystore = RadrootsClientWebKeystoreNostr::new(None); + let key_maps = crate::app_key_maps_default(); + let result = futures::executor::block_on(app_health_check_keystore_access( + &datastore, + &keystore, + &key_maps, + )); + assert_eq!(result.status, AppHealthCheckStatus::Error); + } + + #[test] + fn health_check_keystore_reports_missing_datastore_key() { + let datastore = TestDatastore { + get_result: Err(RadrootsClientDatastoreError::NoResult), + }; let keystore = TestKeystore { - result: Err(RadrootsClientKeystoreError::NostrNoResults), + read_result: Err(RadrootsClientKeystoreError::MissingKey), }; - let result = - futures::executor::block_on(app_health_check_keystore_access(&keystore)); - assert_eq!(result.status, AppHealthCheckStatus::Ok); + let key_maps = crate::app_key_maps_default(); + let result = futures::executor::block_on(app_health_check_keystore_access( + &datastore, + &keystore, + &key_maps, + )); + assert_eq!(result.status, AppHealthCheckStatus::Error); + assert_eq!(result.message.as_deref(), Some("missing")); } #[test] - fn health_check_keystore_maps_idb_errors() { - let keystore = RadrootsClientWebKeystoreNostr::new(None); - let result = - futures::executor::block_on(app_health_check_keystore_access(&keystore)); + fn health_check_keystore_reports_missing_keystore_key() { + let datastore = TestDatastore { + get_result: Ok("pub".to_string()), + }; + let keystore = TestKeystore { + read_result: Err(RadrootsClientKeystoreError::MissingKey), + }; + let key_maps = crate::app_key_maps_default(); + let result = futures::executor::block_on(app_health_check_keystore_access( + &datastore, + &keystore, + &key_maps, + )); assert_eq!(result.status, AppHealthCheckStatus::Error); + assert_eq!(result.message.as_deref(), Some("missing")); + } + + #[test] + fn health_check_keystore_accepts_matching_key() { + let datastore = TestDatastore { + get_result: Ok("pub".to_string()), + }; + let keystore = TestKeystore { + read_result: Ok("secret".to_string()), + }; + let key_maps = crate::app_key_maps_default(); + let result = futures::executor::block_on(app_health_check_keystore_access( + &datastore, + &keystore, + &key_maps, + )); + assert_eq!(result.status, AppHealthCheckStatus::Ok); } #[test]