commit a2a4161f68fd772450558427f06d9516dc0482a1
parent 366a596afec3f39bae98a25943da096b91d6ac43
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Jul 2026 09:04:45 +0000
nostr_connect: close semantic protocol coverage
- Cover malformed metadata JSON and direct metadata deserialization.
- Reject empty secrets, unsafe client URLs, and credential-bearing URLs.
- Exercise display metadata short circuits and invalid logout acknowledgements.
- Verify strict Clippy, all protocol tests, and 100 percent coverage.
Diffstat:
1 file changed, 95 insertions(+), 3 deletions(-)
diff --git a/crates/nostr_connect/tests/coverage.rs b/crates/nostr_connect/tests/coverage.rs
@@ -3,9 +3,10 @@ mod test_fixtures;
use nostr::{Event, EventBuilder, Keys, PublicKey, RelayUrl, SecretKey, Timestamp, UnsignedEvent};
use radroots_nostr_connect::prelude::{
- RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RadrootsNostrConnectError,
- RadrootsNostrConnectMethod, RadrootsNostrConnectPendingConnectionPollOutcome,
- RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
+ RADROOTS_NOSTR_CONNECT_CLIENT_URL_MAX_BYTES, RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR,
+ RadrootsNostrConnectClientMetadata, RadrootsNostrConnectError, RadrootsNostrConnectMethod,
+ RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectPermission,
+ RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri,
};
@@ -361,6 +362,85 @@ fn uri_surface_covers_rendering_ignored_queries_and_error_paths() {
)),
Err(RadrootsNostrConnectError::InvalidClientMetadata { field: "image", .. })
));
+ assert!(matches!(
+ RadrootsNostrConnectUri::parse(&format!(
+ "nostrconnect://{}?relay={}&secret=",
+ FIXTURE_ALICE.public_key_hex,
+ encode_uri_component(RELAY_PRIMARY_WSS),
+ )),
+ Err(RadrootsNostrConnectError::MissingSecret)
+ ));
+}
+
+#[test]
+fn client_metadata_rejects_malformed_and_unsafe_display_fields() {
+ let empty = RadrootsNostrConnectClientMetadata::default();
+ assert!(empty.is_display_empty());
+ let decoded: RadrootsNostrConnectClientMetadata = serde_json::from_value(json!({
+ "requested_permissions": "ping",
+ "name": " client ",
+ "url": APP_PRIMARY_HTTPS,
+ "image": logo_url(),
+ }))
+ .expect("deserialize and normalize client metadata");
+ assert_eq!(decoded.name.as_deref(), Some("client"));
+ assert_eq!(
+ decoded.url.as_deref(),
+ Some(format!("{APP_PRIMARY_HTTPS}/").as_str())
+ );
+ assert!(
+ serde_json::from_value::<RadrootsNostrConnectClientMetadata>(json!({
+ "name": "line\nbreak"
+ }))
+ .is_err()
+ );
+ for metadata in [
+ RadrootsNostrConnectClientMetadata {
+ name: Some("client".to_owned()),
+ ..empty.clone()
+ },
+ RadrootsNostrConnectClientMetadata {
+ url: Some(APP_PRIMARY_HTTPS.to_owned()),
+ ..empty.clone()
+ },
+ RadrootsNostrConnectClientMetadata {
+ image: Some(logo_url()),
+ ..empty.clone()
+ },
+ ] {
+ assert!(!metadata.is_display_empty());
+ }
+
+ assert!(matches!(
+ RadrootsNostrConnectClientMetadata::from_connect_param("{"),
+ Err(RadrootsNostrConnectError::InvalidClientMetadata {
+ field: "payload",
+ ..
+ })
+ ));
+
+ for (value, field) in [
+ (
+ "x".repeat(RADROOTS_NOSTR_CONNECT_CLIENT_URL_MAX_BYTES + 1),
+ "url",
+ ),
+ ("https://example.com/\n".to_owned(), "url"),
+ ("https://user@example.com".to_owned(), "url"),
+ ("https://:secret@example.com".to_owned(), "image"),
+ ] {
+ let metadata = RadrootsNostrConnectClientMetadata {
+ url: (field == "url").then_some(value.clone()),
+ image: (field == "image").then_some(value),
+ ..empty.clone()
+ };
+ assert!(matches!(
+ metadata.normalized(),
+ Err(RadrootsNostrConnectError::InvalidClientMetadata {
+ field: actual,
+ ..
+ }) if actual == field
+ ));
+ }
}
#[test]
@@ -1227,6 +1307,18 @@ fn response_surface_covers_success_and_error_paths() {
),
Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
));
+ assert!(matches!(
+ RadrootsNostrConnectResponse::from_envelope(
+ &RadrootsNostrConnectMethod::Logout,
+ RadrootsNostrConnectResponseEnvelope {
+ id: "req-logout".to_owned(),
+ result: Some(json!("not-ack")),
+ error: None,
+ },
+ ),
+ Err(RadrootsNostrConnectError::InvalidResponsePayload { method, .. })
+ if method == "logout"
+ ));
}
#[test]