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:
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() {