commit 72a4c5bca0fccd89c3187a723b46eed790846b85
parent 4332d6d12397bb942e47bb7e4d38652e4f2b1778
Author: triesap <tyson@radroots.org>
Date: Fri, 10 Apr 2026 18:47:48 +0000
nostr_connect: close coverage gaps
Diffstat:
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,