lib

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

commit f043868a55b261553827ad3a9cfe7c41895c9280
parent aa50e41dad8884372c6c0258d256301c405f98f4
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 16:02:41 +0000

tests: migrate identity fixtures to approved values

- add approved test-fixture dev-dependencies to identity, nostr, nostr-connect, and nostr-accounts
- replace generated and copied identity and relay literals in the relevant tests with approved fixture imports
- keep generation and invalid-shape cases only where the behavior under test depends on them
- verify targeted Nix test lanes; nix run .#contract remains blocked by no space on device

Diffstat:
MCargo.lock | 4++++
Mcrates/identity/Cargo.toml | 1+
Mcrates/identity/tests/identity.rs | 145++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcrates/nostr-accounts/Cargo.toml | 1+
Mcrates/nostr-accounts/src/vault.rs | 20++++++++------------
Mcrates/nostr-connect/Cargo.toml | 3+++
Mcrates/nostr-connect/tests/coverage.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcrates/nostr-connect/tests/protocol.rs | 63++++++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/nostr/Cargo.toml | 1+
Mcrates/nostr/src/events/mod.rs | 3++-
Mcrates/nostr/src/nip17.rs | 7++++---
Mcrates/nostr/tests/coverage.rs | 3++-
12 files changed, 219 insertions(+), 161 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2168,6 +2168,7 @@ dependencies = [ "nostr", "radroots-events", "radroots-runtime", + "radroots-test-fixtures", "secrecy", "serde", "serde_json", @@ -2227,6 +2228,7 @@ dependencies = [ "radroots-events", "radroots-events-codec", "radroots-identity", + "radroots-test-fixtures", "reqwest", "serde", "serde_json", @@ -2243,6 +2245,7 @@ dependencies = [ "radroots-nostr-ndb", "radroots-nostr-signer", "radroots-runtime", + "radroots-test-fixtures", "serde", "serde_json", "tempfile", @@ -2255,6 +2258,7 @@ name = "radroots-nostr-connect" version = "0.1.0-alpha.1" dependencies = [ "nostr", + "radroots-test-fixtures", "serde", "serde_json", "thiserror 1.0.69", diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml @@ -39,4 +39,5 @@ ts-rs = { workspace = true, optional = true } zeroize = { workspace = true, optional = true } [dev-dependencies] +radroots-test-fixtures = { workspace = true } tempfile = { workspace = true } diff --git a/crates/identity/tests/identity.rs b/crates/identity/tests/identity.rs @@ -7,8 +7,18 @@ use radroots_identity::{ use radroots_identity::{ RadrootsIdentityEncryptedSecretKeyOptions, RadrootsIdentityEncryptedSecretKeySecurity, }; +use radroots_test_fixtures::{ApprovedFixtureIdentity, FIXTURE_ALICE, FIXTURE_BOB}; use std::path::PathBuf; +fn fixture_keys(fixture: ApprovedFixtureIdentity) -> nostr::Keys { + let secret = nostr::SecretKey::from_hex(fixture.secret_key_hex).unwrap(); + nostr::Keys::new(secret) +} + +fn fixture_identity(fixture: ApprovedFixtureIdentity) -> RadrootsIdentity { + RadrootsIdentity::from_secret_key_str(fixture.secret_key_hex).unwrap() +} + fn profile_with_identifier(value: &str) -> RadrootsIdentityProfile { RadrootsIdentityProfile { identifier: Some(value.to_string()), @@ -18,14 +28,13 @@ fn profile_with_identifier(value: &str) -> RadrootsIdentityProfile { fn sample_event(content: &str) -> nostr::Event { nostr::EventBuilder::text_note(content) - .sign_with_keys(&nostr::Keys::generate()) + .sign_with_keys(&fixture_keys(FIXTURE_ALICE)) .unwrap() } #[test] fn load_from_json_file_hex() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let json = serde_json::to_string(&identity.to_file()).unwrap(); let dir = tempfile::tempdir().unwrap(); @@ -33,13 +42,12 @@ fn load_from_json_file_hex() { std::fs::write(&path, json).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] fn load_from_json_file_profile() { - let keys = nostr::Keys::generate(); - let mut identity = RadrootsIdentity::new(keys.clone()); + let mut identity = fixture_identity(FIXTURE_ALICE); let profile = RadrootsProfile { name: "relay-agent".to_string(), display_name: Some("Relay Agent".to_string()), @@ -71,8 +79,7 @@ fn load_from_json_file_profile() { #[test] fn load_from_text_file_hex() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let secret = identity.secret_key_hex(); let dir = tempfile::tempdir().unwrap(); @@ -80,13 +87,12 @@ fn load_from_text_file_hex() { std::fs::write(&path, secret).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] fn load_from_text_file_nsec() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let secret = identity.secret_key_nsec(); let dir = tempfile::tempdir().unwrap(); @@ -94,13 +100,12 @@ fn load_from_text_file_nsec() { std::fs::write(&path, secret).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] fn load_from_binary_file() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let secret = identity.secret_key_bytes(); let dir = tempfile::tempdir().unwrap(); @@ -108,7 +113,7 @@ fn load_from_binary_file() { std::fs::write(&path, secret).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] @@ -134,8 +139,7 @@ fn load_or_generate_missing_allowed_creates_json() { #[test] fn load_from_json_file_public_key_npub() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let mut file = identity.to_file(); file.public_key = Some(identity.public_key_npub()); let json = serde_json::to_string(&file).unwrap(); @@ -145,16 +149,14 @@ fn load_from_json_file_public_key_npub() { std::fs::write(&path, json).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] fn load_from_json_file_public_key_mismatch() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys); - let other_keys = nostr::Keys::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let mut file = identity.to_file(); - file.public_key = Some(other_keys.public_key().to_hex()); + file.public_key = Some(FIXTURE_BOB.public_key_hex.to_string()); let json = serde_json::to_string(&file).unwrap(); let dir = tempfile::tempdir().unwrap(); @@ -167,36 +169,28 @@ fn load_from_json_file_public_key_mismatch() { #[test] fn identity_id_matches_public_key_hex() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let id = identity.id(); - assert_eq!(id.as_str(), keys.public_key().to_hex()); + assert_eq!(id.as_str(), FIXTURE_ALICE.public_key_hex); } #[test] fn identity_id_parses_hex_and_npub() { - use nostr::nips::nip19::ToBech32; - - let keys = nostr::Keys::generate(); - let public_key = keys.public_key(); - let hex = public_key.to_hex(); - let npub = public_key.to_bech32().unwrap(); - - let from_hex = RadrootsIdentityId::parse(hex.as_str()).unwrap(); - let from_npub = RadrootsIdentityId::parse(npub.as_str()).unwrap(); - assert_eq!(from_hex.as_str(), hex); - assert_eq!(from_npub.as_str(), hex); + let from_hex = RadrootsIdentityId::parse(FIXTURE_ALICE.public_key_hex).unwrap(); + let from_npub = RadrootsIdentityId::parse(FIXTURE_ALICE.npub).unwrap(); + assert_eq!(from_hex.as_str(), FIXTURE_ALICE.public_key_hex); + assert_eq!(from_npub.as_str(), FIXTURE_ALICE.public_key_hex); } #[test] fn to_public_projection_excludes_secret_key_fields() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let public = identity.to_public(); - assert_eq!(public.id.as_str(), keys.public_key().to_hex()); - assert_eq!(public.public_key_hex, keys.public_key().to_hex()); + assert_eq!(public.id.as_str(), FIXTURE_ALICE.public_key_hex); + assert_eq!(public.public_key_hex, FIXTURE_ALICE.public_key_hex); + assert_eq!(public.public_key_npub, FIXTURE_ALICE.npub); assert!(public.profile.is_none()); let json = serde_json::to_string(&public).unwrap(); @@ -206,9 +200,8 @@ fn to_public_projection_excludes_secret_key_fields() { #[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 public_key = fixture_identity(FIXTURE_ALICE).public_key(); + let public_key_hex = FIXTURE_ALICE.public_key_hex.to_string(); let from_impl = RadrootsIdentityId::from(public_key); assert_eq!(from_impl.as_ref(), public_key_hex); @@ -220,9 +213,10 @@ fn identity_id_trait_paths_and_string_conversions() { #[test] fn identity_profile_state_mutation_paths() { - let keys = nostr::Keys::generate(); - let mut identity = - RadrootsIdentity::with_profile(keys.clone(), RadrootsIdentityProfile::default()); + let mut identity = RadrootsIdentity::with_profile( + fixture_keys(FIXTURE_ALICE), + RadrootsIdentityProfile::default(), + ); assert!(identity.profile().is_none()); identity.set_profile(RadrootsIdentityProfile::default()); @@ -245,36 +239,42 @@ fn identity_profile_state_mutation_paths() { identity.clear_profile(); assert!(identity.profile().is_none()); - let public_without_profile = RadrootsIdentityPublic::new(keys.public_key()) + let public_without_profile = RadrootsIdentityPublic::new(identity.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); + let public_with_profile = + RadrootsIdentityPublic::new(identity.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()); + let identity = fixture_identity(FIXTURE_ALICE); - 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")); + assert_eq!( + identity.keys().public_key().to_hex(), + FIXTURE_ALICE.public_key_hex + ); + assert_eq!(identity.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); + assert_eq!(identity.npub(), FIXTURE_ALICE.npub); + assert_eq!(identity.nsec(), FIXTURE_ALICE.nsec); let file_nsec = identity.to_file_with_secret_format(RadrootsIdentitySecretKeyFormat::Nsec); - assert!(file_nsec.secret_key.starts_with("nsec1")); + assert_eq!(file_nsec.secret_key, FIXTURE_ALICE.nsec); - let from_keys: RadrootsIdentity = keys.clone().into(); + let from_keys: RadrootsIdentity = fixture_keys(FIXTURE_ALICE).into(); let roundtrip_keys = from_keys.clone().into_keys(); - assert_eq!(roundtrip_keys.public_key(), keys.public_key()); + assert_eq!( + roundtrip_keys.public_key().to_hex(), + FIXTURE_ALICE.public_key_hex + ); } #[cfg(feature = "nip49")] #[test] fn encrypted_secret_key_round_trips_to_identity() { - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let encrypted = identity .encrypt_secret_key_ncryptsec("fixture-password") .unwrap(); @@ -291,7 +291,7 @@ fn encrypted_secret_key_options_propagate_to_output() { use nostr::nips::nip19::FromBech32; use nostr::nips::nip49::{EncryptedSecretKey, KeySecurity}; - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let encrypted = identity .encrypt_secret_key_ncryptsec_with_options( "fixture-password", @@ -309,7 +309,7 @@ fn encrypted_secret_key_options_propagate_to_output() { #[cfg(feature = "nip49")] #[test] fn encrypted_secret_key_rejects_invalid_and_wrong_password_inputs() { - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let encrypted = identity .encrypt_secret_key_ncryptsec("fixture-password") .unwrap(); @@ -390,8 +390,7 @@ fn load_from_path_rejects_invalid_payloads() { #[test] fn load_from_json_file_without_public_key_succeeds() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let mut file = identity.to_file(); file.public_key = None; let json = serde_json::to_string(&file).unwrap(); @@ -401,7 +400,7 @@ fn load_from_json_file_without_public_key_succeeds() { std::fs::write(&path, json).unwrap(); let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] @@ -420,8 +419,7 @@ fn load_from_json_file_rejects_invalid_secret_key_string() { #[test] fn load_from_json_file_rejects_invalid_public_key_value() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys); + let identity = fixture_identity(FIXTURE_ALICE); let mut file = identity.to_file(); file.public_key = Some("invalid-public-key".to_string()); let json = serde_json::to_string(&file).unwrap(); @@ -436,7 +434,7 @@ fn load_from_json_file_rejects_invalid_public_key_value() { #[test] fn save_json_rejects_directory_target() { - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let dir = tempfile::tempdir().unwrap(); let err = identity.save_json(dir.path()).unwrap_err(); assert!(matches!(err, IdentityError::Store(_))); @@ -447,7 +445,7 @@ fn save_json_rejects_directory_target() { fn save_json_reports_write_failure_on_read_only_directory() { use std::os::unix::fs::PermissionsExt; - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("identity.json"); identity.save_json(path.as_path()).unwrap(); @@ -502,8 +500,7 @@ fn load_or_generate_uses_default_path_when_missing() { #[test] fn load_or_generate_prefers_existing_path() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys.clone()); + let identity = fixture_identity(FIXTURE_ALICE); let payload = serde_json::to_string(&identity.to_file()).unwrap(); let dir = tempfile::tempdir().unwrap(); @@ -511,12 +508,12 @@ fn load_or_generate_prefers_existing_path() { std::fs::write(&path, payload).unwrap(); let loaded = RadrootsIdentity::load_or_generate(Some(&path), false).unwrap(); - assert_eq!(loaded.public_key(), keys.public_key()); + assert_eq!(loaded.public_key().to_hex(), FIXTURE_ALICE.public_key_hex); } #[test] fn path_ref_variants_cover_success_paths() { - let identity = RadrootsIdentity::generate(); + let identity = fixture_identity(FIXTURE_ALICE); let dir = tempfile::tempdir().unwrap(); let saved_path = dir.path().join("saved.json"); @@ -562,8 +559,7 @@ fn identity_profile_is_empty_checks_metadata_and_application_handler() { fn secret_key_hex_secret_returns_secret_string() { use secrecy::ExposeSecret; - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys); + let identity = fixture_identity(FIXTURE_ALICE); let secret = identity.secret_key_hex_secret(); assert_eq!(secret.expose_secret(), &identity.secret_key_hex()); } @@ -571,8 +567,7 @@ fn secret_key_hex_secret_returns_secret_string() { #[cfg(feature = "zeroize")] #[test] fn secret_key_zeroizing_bytes_matches_raw_secret() { - let keys = nostr::Keys::generate(); - let identity = RadrootsIdentity::new(keys); + let identity = fixture_identity(FIXTURE_ALICE); let raw = identity.secret_key_bytes(); let protected = identity.secret_key_bytes_zeroizing(); assert_eq!(&*protected, &raw); diff --git a/crates/nostr-accounts/Cargo.toml b/crates/nostr-accounts/Cargo.toml @@ -47,4 +47,5 @@ thiserror = { workspace = true } zeroize = { workspace = true } [dev-dependencies] +radroots-test-fixtures = { workspace = true } tempfile = { workspace = true } diff --git a/crates/nostr-accounts/src/vault.rs b/crates/nostr-accounts/src/vault.rs @@ -134,15 +134,17 @@ impl RadrootsNostrSecretVault for RadrootsNostrSecretVaultOsKeyring { mod tests { use super::*; use radroots_identity::RadrootsIdentityId; + use radroots_test_fixtures::FIXTURE_ALICE; use std::thread; + fn fixture_account_id() -> RadrootsIdentityId { + RadrootsIdentityId::parse(FIXTURE_ALICE.public_key_hex).expect("account id") + } + #[test] fn memory_vault_round_trip() { let vault = RadrootsNostrSecretVaultMemory::new(); - let account_id = RadrootsIdentityId::parse( - "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606", - ) - .expect("account id"); + let account_id = fixture_account_id(); vault .store_secret_hex(&account_id, "abc123") .expect("store"); @@ -156,10 +158,7 @@ mod tests { #[test] fn memory_vault_distinguishes_present_and_missing_entries() { let vault = RadrootsNostrSecretVaultMemory::new(); - let account_id = RadrootsIdentityId::parse( - "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606", - ) - .expect("account id"); + let account_id = fixture_account_id(); assert!( vault @@ -184,10 +183,7 @@ mod tests { #[test] fn memory_vault_reports_poisoned_lock() { let vault = RadrootsNostrSecretVaultMemory::new(); - let account_id = RadrootsIdentityId::parse( - "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606", - ) - .expect("account id"); + let account_id = fixture_account_id(); let shared = vault.entries.clone(); let _ = thread::spawn(move || { diff --git a/crates/nostr-connect/Cargo.toml b/crates/nostr-connect/Cargo.toml @@ -19,3 +19,6 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } url = { workspace = true } + +[dev-dependencies] +radroots-test-fixtures = { workspace = true } diff --git a/crates/nostr-connect/tests/coverage.rs b/crates/nostr-connect/tests/coverage.rs @@ -5,21 +5,30 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri, }; +use radroots_test_fixtures::{ + APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS, + RELAY_TERTIARY_WSS, +}; use serde_json::{Value, json}; use std::str::FromStr; fn test_public_key() -> PublicKey { - PublicKey::parse("83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5") - .expect("public key") + PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key") } fn test_keys() -> Keys { - let secret_key = - SecretKey::from_hex("6d5f4530cbf6a9e8f021eb409c8c5f2ee7ea123c76364b6f53c2d8a3507f7f5b") - .expect("secret key"); + let secret_key = SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).expect("secret key"); Keys::new(secret_key) } +fn encode_uri_component(value: &str) -> String { + url::form_urlencoded::byte_serialize(value.as_bytes()).collect() +} + +fn logo_url() -> String { + format!("{CDN_PRIMARY_HTTPS}/logo.png") +} + fn unsigned_event() -> UnsignedEvent { serde_json::from_value(json!({ "pubkey": test_public_key().to_hex(), @@ -156,18 +165,26 @@ fn error_method_and_permission_surfaces_cover_public_paths() { #[test] fn uri_surface_covers_rendering_ignored_queries_and_error_paths() { - let bunker = RadrootsNostrConnectUri::parse( - "bunker://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&foo=bar", - ) + let bunker = RadrootsNostrConnectUri::parse(&format!( + "bunker://{}?relay={}&foo=bar", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + )) .expect("parse bunker"); let bunker_rendered = bunker.to_string(); - assert!(bunker_rendered.contains("relay=wss%3A%2F%2Frelay.example.com")); + assert!(bunker_rendered.contains(&format!( + "relay={}", + encode_uri_component(RELAY_PRIMARY_WSS) + ))); assert!(!bunker_rendered.contains("secret=")); - let minimal_client: RadrootsNostrConnectUri = - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=shared" - .parse() - .expect("parse minimal client"); + let minimal_client: RadrootsNostrConnectUri = format!( + "nostrconnect://{}?relay={}&secret=shared", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + ) + .parse() + .expect("parse minimal client"); let minimal_client_rendered = minimal_client.to_string(); assert!(minimal_client_rendered.contains("secret=shared")); assert!(!minimal_client_rendered.contains("perms=")); @@ -175,15 +192,22 @@ fn uri_surface_covers_rendering_ignored_queries_and_error_paths() { assert!(!minimal_client_rendered.contains("url=")); assert!(!minimal_client_rendered.contains("image=")); - let metadata_client = RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=shared&perms=ping&name=myc&url=https%3A%2F%2Fexample.com&image=https%3A%2F%2Fexample.com%2Flogo.png&ignored=value", - ) + let metadata_client = RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay={}&secret=shared&perms=ping&name=myc&url={}&image={}&ignored=value", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + encode_uri_component(APP_PRIMARY_HTTPS), + encode_uri_component(&logo_url()), + )) .expect("parse metadata client"); let metadata_rendered = metadata_client.to_string(); assert!(metadata_rendered.contains("perms=ping")); assert!(metadata_rendered.contains("name=myc")); - assert!(metadata_rendered.contains("url=https%3A%2F%2Fexample.com%2F")); - assert!(metadata_rendered.contains("image=https%3A%2F%2Fexample.com%2Flogo.png")); + assert!(metadata_rendered.contains(&format!( + "url={}", + encode_uri_component(&format!("{APP_PRIMARY_HTTPS}/")) + ))); + assert!(metadata_rendered.contains(&format!("image={}", encode_uri_component(&logo_url())))); assert!(matches!( RadrootsNostrConnectUri::parse("not a uri"), @@ -196,21 +220,22 @@ fn uri_surface_covers_rendering_ignored_queries_and_error_paths() { Err(RadrootsNostrConnectError::MissingPublicKey) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "bunker://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5" - ), + RadrootsNostrConnectUri::parse(&format!("bunker://{}", FIXTURE_ALICE.public_key_hex)), Err(RadrootsNostrConnectError::MissingRelay) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?secret=abc" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?secret=abc", + FIXTURE_ALICE.public_key_hex + )), Err(RadrootsNostrConnectError::MissingRelay) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay={}", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + )), Err(RadrootsNostrConnectError::MissingSecret) )); assert!(matches!( @@ -224,15 +249,18 @@ fn uri_surface_covers_rendering_ignored_queries_and_error_paths() { Err(RadrootsNostrConnectError::InvalidPublicKey { .. }) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=http%3A%2F%2Frelay.example.com&secret=abc" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay=http%3A%2F%2Frelay.example.com&secret=abc", + FIXTURE_ALICE.public_key_hex + )), Err(RadrootsNostrConnectError::InvalidRelayUrl { .. }) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=abc&url=not-a-url" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay={}&secret=abc&url=not-a-url", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + )), Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url" )); assert!(matches!( @@ -240,21 +268,26 @@ fn uri_surface_covers_rendering_ignored_queries_and_error_paths() { Err(RadrootsNostrConnectError::InvalidPublicKey { .. }) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "bunker://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=http%3A%2F%2Frelay.example.com" - ), + RadrootsNostrConnectUri::parse(&format!( + "bunker://{}?relay=http%3A%2F%2Frelay.example.com", + FIXTURE_ALICE.public_key_hex + )), Err(RadrootsNostrConnectError::InvalidRelayUrl { .. }) )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=abc&perms=sign_event%3A" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay={}&secret=abc&perms=sign_event%3A", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + )), Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == "sign_event:" )); assert!(matches!( - RadrootsNostrConnectUri::parse( - "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=abc&image=not-a-url" - ), + RadrootsNostrConnectUri::parse(&format!( + "nostrconnect://{}?relay={}&secret=abc&image=not-a-url", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + )), Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url" )); } @@ -636,13 +669,13 @@ fn response_surface_covers_success_and_error_paths() { ), ( RadrootsNostrConnectResponse::RelayList(vec![ - relay("wss://relay1.example.com"), - relay("wss://relay2.example.com"), + relay(RELAY_SECONDARY_WSS), + relay(RELAY_TERTIARY_WSS), ]), RadrootsNostrConnectMethod::SwitchRelays, RadrootsNostrConnectResponse::RelayList(vec![ - relay("wss://relay1.example.com"), - relay("wss://relay2.example.com"), + relay(RELAY_SECONDARY_WSS), + relay(RELAY_TERTIARY_WSS), ]), ), ( @@ -761,12 +794,12 @@ fn response_surface_covers_success_and_error_paths() { &RadrootsNostrConnectMethod::SwitchRelays, RadrootsNostrConnectResponseEnvelope { id: "req-switch".to_owned(), - result: Some(json!("[\"wss://relay1.example.com\"]")), + result: Some(json!(format!("[\"{RELAY_SECONDARY_WSS}\"]"))), error: None, }, ) .expect("parse stringified relay list"), - RadrootsNostrConnectResponse::RelayList(vec![relay("wss://relay1.example.com")]) + RadrootsNostrConnectResponse::RelayList(vec![relay(RELAY_SECONDARY_WSS)]) ); assert!(matches!( diff --git a/crates/nostr-connect/tests/protocol.rs b/crates/nostr-connect/tests/protocol.rs @@ -4,24 +4,40 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri, }; +use radroots_test_fixtures::{ + APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS, + RELAY_TERTIARY_WSS, +}; use serde_json::{Value, json}; fn test_public_key() -> PublicKey { - PublicKey::parse("83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5") - .expect("public key") + PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key") } fn test_keys() -> Keys { - let secret_key = - SecretKey::from_hex("6d5f4530cbf6a9e8f021eb409c8c5f2ee7ea123c76364b6f53c2d8a3507f7f5b") - .expect("secret key"); + let secret_key = SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).expect("secret key"); Keys::new(secret_key) } +fn encode_uri_component(value: &str) -> String { + url::form_urlencoded::byte_serialize(value.as_bytes()).collect() +} + +fn logo_url() -> String { + format!("{CDN_PRIMARY_HTTPS}/logo.png") +} + #[test] fn parses_client_uri_with_current_spec_query_fields() { - let uri = "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&relay=wss%3A%2F%2Frelay2.example.com&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1059&name=My+Client&url=https%3A%2F%2Fexample.com&image=https%3A%2F%2Fexample.com%2Flogo.png"; - let parsed = RadrootsNostrConnectUri::parse(uri).expect("parse client uri"); + let uri = format!( + "nostrconnect://{}?relay={}&relay={}&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1059&name=My+Client&url={}&image={}", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_SECONDARY_WSS), + encode_uri_component(RELAY_TERTIARY_WSS), + encode_uri_component(APP_PRIMARY_HTTPS), + encode_uri_component(&logo_url()), + ); + let parsed = RadrootsNostrConnectUri::parse(&uri).expect("parse client uri"); match parsed { RadrootsNostrConnectUri::Client(client) => { @@ -39,11 +55,11 @@ fn parses_client_uri_with_current_spec_query_fields() { ), ]) ); - assert_eq!(client.metadata.url.as_deref(), Some("https://example.com/")); assert_eq!( - client.metadata.image.as_deref(), - Some("https://example.com/logo.png") + client.metadata.url.as_deref(), + Some(format!("{APP_PRIMARY_HTTPS}/").as_str()) ); + assert_eq!(client.metadata.image.as_deref(), Some(logo_url().as_str())); } other => panic!("expected client uri, got {other:?}"), } @@ -51,8 +67,12 @@ fn parses_client_uri_with_current_spec_query_fields() { #[test] fn parses_bunker_uri_and_roundtrips() { - let source = "bunker://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com&secret=abcd"; - let parsed = RadrootsNostrConnectUri::parse(source).expect("parse bunker uri"); + let source = format!( + "bunker://{}?relay={}&secret=abcd", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + ); + let parsed = RadrootsNostrConnectUri::parse(&source).expect("parse bunker uri"); let rendered = parsed.to_string(); let reparsed = RadrootsNostrConnectUri::parse(&rendered).expect("reparse bunker uri"); assert_eq!(parsed, reparsed); @@ -60,8 +80,12 @@ fn parses_bunker_uri_and_roundtrips() { #[test] fn rejects_client_uri_without_required_secret() { - let source = "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay.example.com"; - assert!(RadrootsNostrConnectUri::parse(source).is_err()); + let source = format!( + "nostrconnect://{}?relay={}", + FIXTURE_ALICE.public_key_hex, + encode_uri_component(RELAY_PRIMARY_WSS), + ); + assert!(RadrootsNostrConnectUri::parse(&source).is_err()); } #[test] @@ -98,7 +122,7 @@ fn connect_request_roundtrips_requested_permissions() { "id": "req-1", "method": "connect", "params": [ - "83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5", + FIXTURE_ALICE.public_key_hex, "abcd", "nip44_encrypt,sign_event:1059" ] @@ -141,10 +165,7 @@ fn sign_event_request_roundtrips_unsigned_event_payload() { fn switch_relays_response_accepts_array_or_null() { let relays_response = RadrootsNostrConnectResponseEnvelope { id: "req-switch".to_owned(), - result: Some(json!([ - "wss://relay1.example.com", - "wss://relay2.example.com" - ])), + result: Some(json!([RELAY_SECONDARY_WSS, RELAY_TERTIARY_WSS])), error: None, }; let parsed = RadrootsNostrConnectResponse::from_envelope( @@ -155,8 +176,8 @@ fn switch_relays_response_accepts_array_or_null() { assert_eq!( parsed, RadrootsNostrConnectResponse::RelayList(vec![ - RelayUrl::parse("wss://relay1.example.com").expect("relay 1"), - RelayUrl::parse("wss://relay2.example.com").expect("relay 2"), + RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 1"), + RelayUrl::parse(RELAY_TERTIARY_WSS).expect("relay 2"), ]) ); diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml @@ -47,4 +47,5 @@ serde_json = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +radroots-test-fixtures = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/nostr/src/events/mod.rs b/crates/nostr/src/events/mod.rs @@ -40,10 +40,11 @@ pub fn radroots_nostr_build_event( mod tests { use super::radroots_nostr_build_event; use crate::types::{RadrootsNostrPublicKey, RadrootsNostrTagKind}; + use radroots_test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX; #[test] fn build_event_preserves_self_p_tag() { - let pubkey_hex = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; + let pubkey_hex = FIXTURE_ALICE_PUBLIC_KEY_HEX; let pubkey = RadrootsNostrPublicKey::from_hex(pubkey_hex).expect("pubkey"); let tags = vec![ vec!["x".to_string(), "v".to_string()], diff --git a/crates/nostr/src/nip17.rs b/crates/nostr/src/nip17.rs @@ -214,16 +214,17 @@ where #[cfg(all(test, feature = "nip17"))] mod tests { use super::*; - use nostr::Keys; + use nostr::{Keys, SecretKey}; use radroots_events::message::{RadrootsMessage, RadrootsMessageRecipient}; use radroots_events::message_file::{RadrootsMessageFile, RadrootsMessageFileDimensions}; + use radroots_test_fixtures::{FIXTURE_ALICE, FIXTURE_BOB}; fn sender_keys() -> Keys { - Keys::parse("6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e").unwrap() + Keys::new(SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).unwrap()) } fn receiver_keys() -> Keys { - Keys::parse("7b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e").unwrap() + Keys::new(SecretKey::from_hex(FIXTURE_BOB.secret_key_hex).unwrap()) } #[tokio::test] diff --git a/crates/nostr/tests/coverage.rs b/crates/nostr/tests/coverage.rs @@ -31,6 +31,7 @@ use radroots_nostr::types::{ use radroots_nostr::util::{ created_at_u32_saturating, event_created_at_u32_saturating, radroots_nostr_npub_string, }; +use radroots_test_fixtures::RELAY_PRIMARY_WSS; fn make_keys() -> RadrootsNostrKeys { RadrootsNostrKeys::generate() @@ -254,7 +255,7 @@ fn tag_helpers_cover_matchers_and_resolve_paths() { assert_eq!(matched.1, ["v1".to_string(), "v2".to_string()]); let relays_tag = RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Relays(vec![ - RadrootsNostrRelayUrl::parse("wss://relay.example.com").expect("relay"), + RadrootsNostrRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"), ])); assert!(radroots_nostr_tag_relays_parse(&relays_tag).is_some()); let relays_non_match =