lib

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

commit 72a4c5bca0fccd89c3187a723b46eed790846b85
parent 4332d6d12397bb942e47bb7e4d38652e4f2b1778
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Apr 2026 18:47:48 +0000

nostr_connect: close coverage gaps

Diffstat:
Mcrates/nostr_connect/src/message.rs | 95++++++++++++++-----------------------------------------------------------------
Mcrates/nostr_connect/tests/coverage.rs | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/nostr_connect/tests/protocol.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 198 insertions(+), 79 deletions(-)

diff --git a/crates/nostr_connect/src/message.rs b/crates/nostr_connect/src/message.rs @@ -3,7 +3,7 @@ use crate::method::RadrootsNostrConnectMethod; use crate::permission::RadrootsNostrConnectPermissions; use nostr::{Event, JsonUtil, PublicKey, RelayUrl, UnsignedEvent}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; +use serde_json::{Value, json}; use std::str::FromStr; use url::Url; @@ -353,12 +353,7 @@ impl RadrootsNostrConnectResponse { }, Self::RemoteSessionCapability(capability) => RadrootsNostrConnectResponseEnvelope { id, - result: Some(serde_json::to_value(capability).map_err(|error| { - RadrootsNostrConnectError::InvalidResponsePayload { - method: RadrootsNostrConnectMethod::GetSessionCapability.to_string(), - reason: error.to_string(), - } - })?), + result: Some(remote_session_capability_value(capability)), error: None, }, Self::SignedEvent(event) => RadrootsNostrConnectResponseEnvelope { @@ -462,9 +457,10 @@ impl RadrootsNostrConnectResponse { let result = expect_string_result(method, envelope.result)?; Ok(Self::UserPublicKey(parse_public_key(&result)?)) } - RadrootsNostrConnectMethod::GetSessionCapability => Ok(Self::RemoteSessionCapability( - parse_json_string_result(method, envelope.result)?, - )), + RadrootsNostrConnectMethod::GetSessionCapability => { + let capability = parse_json_string_result(method, envelope.result)?; + Ok(Self::RemoteSessionCapability(capability)) + } RadrootsNostrConnectMethod::SignEvent => { let event = parse_json_string_result::<Event>(method, envelope.result)?; Ok(Self::SignedEvent(event)) @@ -502,6 +498,16 @@ impl RadrootsNostrConnectResponse { } } +fn remote_session_capability_value( + capability: RadrootsNostrConnectRemoteSessionCapability, +) -> Value { + json!({ + "user_public_key": capability.user_public_key, + "relays": capability.relays, + "permissions": capability.permissions, + }) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] struct RawRequestMessage { id: String, @@ -633,72 +639,3 @@ fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> { reason: error.to_string(), }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::method::RadrootsNostrConnectMethod; - use crate::permission::RadrootsNostrConnectPermission; - - fn relay(value: &str) -> RelayUrl { - RelayUrl::parse(value).expect("relay url") - } - - fn public_key() -> PublicKey { - PublicKey::from_hex("4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa") - .expect("public key") - } - - #[test] - fn get_session_capability_request_round_trips_without_params() { - let request = RadrootsNostrConnectRequest::GetSessionCapability; - let message = RadrootsNostrConnectRequestMessage::new("req-cap", request.clone()); - let encoded = serde_json::to_string(&message).expect("encode request"); - let decoded: RadrootsNostrConnectRequestMessage = - serde_json::from_str(&encoded).expect("decode request"); - - assert_eq!(decoded.request, request); - assert_eq!( - decoded.request.method(), - RadrootsNostrConnectMethod::GetSessionCapability - ); - } - - #[test] - fn get_session_capability_response_round_trips() { - let capability = RadrootsNostrConnectRemoteSessionCapability { - user_public_key: public_key(), - relays: vec![ - relay("wss://relay.example.com"), - relay("wss://relay2.example.com"), - ], - permissions: vec![ - RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "kind:1", - ), - RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), - ] - .into(), - }; - let response = RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()); - let envelope = response - .into_envelope("resp-cap") - .expect("encode response envelope"); - let decoded = RadrootsNostrConnectResponse::from_envelope( - &RadrootsNostrConnectMethod::GetSessionCapability, - envelope, - ) - .expect("decode capability response"); - - assert_eq!( - decoded, - RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()) - ); - assert_eq!( - RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()) - .into_pending_connection_poll_outcome(), - RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) - ); - } -} diff --git a/crates/nostr_connect/tests/coverage.rs b/crates/nostr_connect/tests/coverage.rs @@ -63,6 +63,10 @@ fn error_method_and_permission_surfaces_cover_public_paths() { let methods = [ (RadrootsNostrConnectMethod::Connect, "connect"), (RadrootsNostrConnectMethod::GetPublicKey, "get_public_key"), + ( + RadrootsNostrConnectMethod::GetSessionCapability, + "get_session_capability", + ), (RadrootsNostrConnectMethod::SignEvent, "sign_event"), (RadrootsNostrConnectMethod::Nip04Encrypt, "nip04_encrypt"), (RadrootsNostrConnectMethod::Nip04Decrypt, "nip04_decrypt"), @@ -325,6 +329,11 @@ fn request_surface_covers_variant_methods_serialization_and_validation() { Vec::new(), ), ( + RadrootsNostrConnectRequest::GetSessionCapability, + RadrootsNostrConnectMethod::GetSessionCapability, + Vec::new(), + ), + ( RadrootsNostrConnectRequest::SignEvent(unsigned_event()), RadrootsNostrConnectMethod::SignEvent, vec![serde_json::to_string(&unsigned_event()).expect("serialize unsigned event")], @@ -421,6 +430,14 @@ fn request_surface_covers_variant_methods_serialization_and_validation() { ); assert_eq!( RadrootsNostrConnectRequest::from_parts( + RadrootsNostrConnectMethod::GetSessionCapability, + Vec::new(), + ) + .expect("get_session_capability from parts"), + RadrootsNostrConnectRequest::GetSessionCapability + ); + assert_eq!( + RadrootsNostrConnectRequest::from_parts( RadrootsNostrConnectMethod::Nip04Encrypt, vec![test_public_key().to_hex(), "hello".to_owned()], ) @@ -484,6 +501,11 @@ fn request_surface_covers_variant_methods_serialization_and_validation() { "no params", ), ( + RadrootsNostrConnectMethod::GetSessionCapability, + vec!["oops".to_owned()], + "no params", + ), + ( RadrootsNostrConnectMethod::SignEvent, Vec::new(), "exactly 1 param", @@ -622,6 +644,18 @@ fn request_surface_covers_variant_methods_serialization_and_validation() { #[test] fn response_surface_covers_success_and_error_paths() { let event = signed_event(); + let remote_session_capability = + radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability { + user_public_key: test_public_key(), + relays: vec![relay(RELAY_PRIMARY_WSS), relay(RELAY_SECONDARY_WSS)], + permissions: RadrootsNostrConnectPermissions::from(vec![ + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping), + RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + "kind:1", + ), + ]), + }; let cases = vec![ ( RadrootsNostrConnectResponse::ConnectAcknowledged, @@ -639,6 +673,20 @@ fn response_surface_covers_success_and_error_paths() { RadrootsNostrConnectResponse::UserPublicKey(test_public_key()), ), ( + RadrootsNostrConnectResponse::PendingConnection, + RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponse::PendingConnection, + ), + ( + RadrootsNostrConnectResponse::RemoteSessionCapability( + remote_session_capability.clone(), + ), + RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponse::RemoteSessionCapability( + remote_session_capability.clone(), + ), + ), + ( RadrootsNostrConnectResponse::SignedEvent(event.clone()), RadrootsNostrConnectMethod::SignEvent, RadrootsNostrConnectResponse::SignedEvent(event.clone()), @@ -777,6 +825,75 @@ fn response_surface_covers_success_and_error_paths() { ); assert_eq!( RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetPublicKey, + RadrootsNostrConnectResponseEnvelope { + id: "req-nonpending-public-key".to_owned(), + result: None, + error: Some("denied".to_owned()), + }, + ) + .expect("parse non-pending public key error"), + RadrootsNostrConnectResponse::Error { + result: None, + error: "denied".to_owned(), + } + ); + assert_eq!( + RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponseEnvelope { + id: "req-capability-error-with-result".to_owned(), + result: Some(json!({"code": "retry"})), + error: Some("denied".to_owned()), + }, + ) + .expect("parse capability error with result"), + RadrootsNostrConnectResponse::Error { + result: Some(json!({"code": "retry"})), + error: "denied".to_owned(), + } + ); + assert!(matches!( + RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponseEnvelope { + id: "req-capability-invalid-result".to_owned(), + result: Some(json!({"permissions": "ping"})), + error: None, + }, + ), + Err(RadrootsNostrConnectError::InvalidResponsePayload { method, .. }) + if method == "get_session_capability" + )); + assert_eq!( + RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponseEnvelope { + id: "req-capability-string-result".to_owned(), + result: Some(json!( + serde_json::to_string(&remote_session_capability) + .expect("serialize remote session capability") + )), + error: None, + }, + ) + .expect("parse stringified capability result"), + RadrootsNostrConnectResponse::RemoteSessionCapability(remote_session_capability.clone(),) + ); + assert!(matches!( + RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectResponseEnvelope { + id: "req-capability-invalid-string".to_owned(), + result: Some(json!("{")), + error: None, + }, + ), + Err(RadrootsNostrConnectError::InvalidResponsePayload { method, .. }) + if method == "get_session_capability" + )); + assert_eq!( + RadrootsNostrConnectResponse::from_envelope( &RadrootsNostrConnectMethod::Ping, RadrootsNostrConnectResponseEnvelope { id: "req-error".to_owned(), @@ -1033,6 +1150,19 @@ fn response_surface_covers_success_and_error_paths() { #[test] fn pending_connection_poll_outcome_uses_typed_variants() { + let remote_session_capability = + radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability { + user_public_key: test_public_key(), + relays: vec![relay(RELAY_PRIMARY_WSS), relay(RELAY_SECONDARY_WSS)], + permissions: RadrootsNostrConnectPermissions::from(vec![ + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping), + RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + "kind:1", + ), + ]), + }; + assert_eq!( RadrootsNostrConnectResponse::PendingConnection.into_pending_connection_poll_outcome(), RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval @@ -1043,6 +1173,13 @@ fn pending_connection_poll_outcome_uses_typed_variants() { .into_pending_connection_poll_outcome(), RadrootsNostrConnectPendingConnectionPollOutcome::Approved(test_public_key()) ); + assert_eq!( + RadrootsNostrConnectResponse::RemoteSessionCapability(remote_session_capability.clone()) + .into_pending_connection_poll_outcome(), + RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability( + remote_session_capability + ) + ); assert_eq!( RadrootsNostrConnectResponse::Error { diff --git a/crates/nostr_connect/tests/protocol.rs b/crates/nostr_connect/tests/protocol.rs @@ -28,6 +28,24 @@ fn logo_url() -> String { format!("{CDN_PRIMARY_HTTPS}/logo.png") } +fn remote_session_capability() +-> radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability { + radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability { + user_public_key: test_public_key(), + relays: vec![ + RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay 1"), + RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 2"), + ], + permissions: RadrootsNostrConnectPermissions::from(vec![ + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping), + RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + "kind:1", + ), + ]), + } +} + #[test] fn parses_client_uri_with_current_spec_query_fields() { let uri = format!( @@ -195,6 +213,33 @@ fn switch_relays_response_accepts_array_or_null() { } #[test] +fn get_session_capability_request_and_response_roundtrip() { + let request_message = RadrootsNostrConnectRequestMessage::new( + "req-cap", + RadrootsNostrConnectRequest::GetSessionCapability, + ); + let encoded_request = serde_json::to_value(&request_message).expect("serialize request"); + let decoded_request: RadrootsNostrConnectRequestMessage = + serde_json::from_value(encoded_request).expect("deserialize request"); + assert_eq!(decoded_request, request_message); + + let capability = remote_session_capability(); + let response_envelope = + RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()) + .into_envelope("resp-cap") + .expect("serialize response"); + let decoded_response = RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + response_envelope, + ) + .expect("deserialize response"); + assert_eq!( + decoded_response, + RadrootsNostrConnectResponse::RemoteSessionCapability(capability) + ); +} + +#[test] fn auth_url_response_parses_from_result_and_error_fields() { let response = RadrootsNostrConnectResponse::from_envelope( &RadrootsNostrConnectMethod::SignEvent,