lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 7b798f4dc5771dd052e494ee0311db3a1112608f
parent 93744414d00ba8b79992a7686943c3bdb9f90ad3
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 19:28:36 +0000

identity: add exhaustive coverage tests for `radroots-identity` and username paths



- add trait and accessor path tests for identity ids and key projections
- add missing error path tests for file parsing and load behavior branches
- add profile state matrix tests to cover metadata handler empty checks
- extend username tests for non-ascii trim-empty and max-length rejection

Diffstat:
Mcrates/identity/src/username.rs | 3+++
Mcrates/identity/tests/identity.rs | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 207 insertions(+), 0 deletions(-)

diff --git a/crates/identity/src/username.rs b/crates/identity/src/username.rs @@ -72,6 +72,8 @@ mod tests { "radroots..test", "radroots!", "RADROOTS", + "rädroots", + "radroots-radroots-radroots-radroots", ] { assert!(!radroots_username_is_valid(name)); } @@ -84,6 +86,7 @@ mod tests { Some("radroots".to_string()) ); assert_eq!(radroots_username_normalize("ra"), None); + assert_eq!(radroots_username_normalize(" "), None); } } diff --git a/crates/identity/tests/identity.rs b/crates/identity/tests/identity.rs @@ -1,7 +1,22 @@ use radroots_events::profile::RadrootsProfile; use radroots_identity::{ IdentityError, RadrootsIdentity, RadrootsIdentityId, RadrootsIdentityProfile, + RadrootsIdentityPublic, RadrootsIdentitySecretKeyFormat, DEFAULT_IDENTITY_PATH, }; +use std::path::PathBuf; + +fn profile_with_identifier(value: &str) -> RadrootsIdentityProfile { + RadrootsIdentityProfile { + identifier: Some(value.to_string()), + ..Default::default() + } +} + +fn sample_event(content: &str) -> nostr::Event { + nostr::EventBuilder::text_note(content) + .sign_with_keys(&nostr::Keys::generate()) + .unwrap() +} #[test] fn load_from_json_file_hex() { @@ -185,6 +200,195 @@ fn to_public_projection_excludes_secret_key_fields() { assert!(!json.contains(&identity.secret_key_hex())); } +#[test] +fn identity_id_trait_paths_and_string_conversions() { + let keys = nostr::Keys::generate(); + let public_key = keys.public_key(); + let public_key_hex = public_key.to_hex(); + + let from_impl = RadrootsIdentityId::from(public_key); + assert_eq!(from_impl.as_ref(), public_key_hex); + + let from_try = RadrootsIdentityId::try_from(public_key_hex.as_str()).unwrap(); + assert_eq!(from_try.to_string(), public_key_hex); + assert_eq!(from_try.clone().into_string(), public_key_hex); +} + +#[test] +fn identity_profile_state_mutation_paths() { + let keys = nostr::Keys::generate(); + let mut identity = + RadrootsIdentity::with_profile(keys.clone(), RadrootsIdentityProfile::default()); + assert!(identity.profile().is_none()); + + identity.set_profile(RadrootsIdentityProfile::default()); + assert!(identity.profile().is_none()); + + let profile = profile_with_identifier("radroots-user"); + identity.set_profile(profile.clone()); + assert!(identity.profile().is_some()); + + let profile_mut = identity.profile_mut().unwrap(); + profile_mut.identifier = Some("radroots-user-updated".to_string()); + assert_eq!( + identity.profile().and_then(|p| p.identifier.as_deref()), + Some("radroots-user-updated") + ); + + let public = identity.to_public(); + assert!(public.profile.is_some()); + + identity.clear_profile(); + assert!(identity.profile().is_none()); + + let public_without_profile = RadrootsIdentityPublic::new(keys.public_key()) + .with_profile(RadrootsIdentityProfile::default()); + assert!(public_without_profile.profile.is_none()); + + let public_with_profile = RadrootsIdentityPublic::new(keys.public_key()).with_profile(profile); + assert!(public_with_profile.profile.is_some()); +} + +#[test] +fn identity_accessor_paths_and_secret_formats() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + + assert_eq!(identity.keys().public_key(), keys.public_key()); + assert_eq!(identity.public_key(), keys.public_key()); + assert!(identity.npub().starts_with("npub1")); + assert!(identity.nsec().starts_with("nsec1")); + + let file_nsec = identity.to_file_with_secret_format(RadrootsIdentitySecretKeyFormat::Nsec); + assert!(file_nsec.secret_key.starts_with("nsec1")); + + let from_keys: RadrootsIdentity = keys.clone().into(); + let roundtrip_keys = from_keys.clone().into_keys(); + assert_eq!(roundtrip_keys.public_key(), keys.public_key()); +} + +#[test] +fn parse_failures_cover_public_key_errors() { + let err_empty = RadrootsIdentityId::parse(" ").unwrap_err(); + assert!(matches!(err_empty, IdentityError::InvalidPublicKey(_))); + + let err_invalid = RadrootsIdentityId::parse("invalid-public-key-value").unwrap_err(); + assert!(matches!(err_invalid, IdentityError::InvalidPublicKey(_))); +} + +#[test] +fn from_secret_key_bytes_rejects_wrong_length() { + let err = RadrootsIdentity::from_secret_key_bytes(&[1, 2, 3]).unwrap_err(); + assert!(matches!(err, IdentityError::InvalidIdentityFormat)); +} + +#[test] +fn load_from_path_reports_not_found_and_read_errors() { + let dir = tempfile::tempdir().unwrap(); + let missing = dir.path().join("missing-identity.json"); + let not_found = RadrootsIdentity::load_from_path_auto(&missing).unwrap_err(); + assert!(matches!(not_found, IdentityError::NotFound(path) if path == missing)); + + let read_error = RadrootsIdentity::load_from_path_auto(dir.path()).unwrap_err(); + assert!(matches!(read_error, IdentityError::Read(path, _) if path == dir.path())); +} + +#[test] +fn load_from_path_rejects_invalid_payloads() { + let dir = tempfile::tempdir().unwrap(); + + let blank_path = dir.path().join("identity-blank.txt"); + std::fs::write(&blank_path, " \n\t ").unwrap(); + let blank_err = RadrootsIdentity::load_from_path_auto(&blank_path).unwrap_err(); + assert!(matches!(blank_err, IdentityError::InvalidIdentityFormat)); + + let invalid_utf8_path = dir.path().join("identity-invalid-utf8.bin"); + std::fs::write(&invalid_utf8_path, [0xff, 0xfe, 0xfd]).unwrap(); + let utf8_err = RadrootsIdentity::load_from_path_auto(&invalid_utf8_path).unwrap_err(); + assert!(matches!(utf8_err, IdentityError::InvalidIdentityFormat)); + + let invalid_json_path = dir.path().join("identity-invalid-json.json"); + std::fs::write(&invalid_json_path, "{invalid").unwrap(); + let json_err = RadrootsIdentity::load_from_path_auto(&invalid_json_path).unwrap_err(); + assert!(matches!(json_err, IdentityError::InvalidJson(_))); +} + +#[test] +fn load_from_json_file_without_public_key_succeeds() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let mut file = identity.to_file(); + file.public_key = None; + let json = serde_json::to_string(&file).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, json).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_or_generate_uses_default_path_when_missing() { + let original = std::env::current_dir().unwrap(); + let dir = tempfile::tempdir().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let denied = RadrootsIdentity::load_or_generate::<&std::path::Path>(None, false).unwrap_err(); + assert!( + matches!(denied, IdentityError::GenerationNotAllowed(path) if path == PathBuf::from(DEFAULT_IDENTITY_PATH)) + ); + + let generated = RadrootsIdentity::load_or_generate::<&std::path::Path>(None, true).unwrap(); + let default_path = dir.path().join(DEFAULT_IDENTITY_PATH); + assert!(default_path.exists()); + + let loaded = RadrootsIdentity::load_from_path_auto(&default_path).unwrap(); + assert_eq!(generated.public_key(), loaded.public_key()); + + std::env::set_current_dir(original).unwrap(); +} + +#[test] +fn load_or_generate_prefers_existing_path() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let payload = serde_json::to_string(&identity.to_file()).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, payload).unwrap(); + + let loaded = RadrootsIdentity::load_or_generate(Some(&path), false).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn generate_with_profile_retains_profile() { + let profile = profile_with_identifier("runtime-user"); + let identity = RadrootsIdentity::generate_with_profile(profile); + assert_eq!( + identity.profile().and_then(|p| p.identifier.as_deref()), + Some("runtime-user") + ); +} + +#[test] +fn identity_profile_is_empty_checks_metadata_and_application_handler() { + let profile_with_metadata = RadrootsIdentityProfile { + metadata: Some(sample_event("metadata")), + ..Default::default() + }; + assert!(!profile_with_metadata.is_empty()); + + let profile_with_handler = RadrootsIdentityProfile { + application_handler: Some(sample_event("handler")), + ..Default::default() + }; + assert!(!profile_with_handler.is_empty()); +} + #[cfg(feature = "secrecy")] #[test] fn secret_key_hex_secret_returns_secret_string() {