lib

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

protocol.rs (10849B)


      1 #[path = "../src/test_fixtures.rs"]
      2 mod test_fixtures;
      3 
      4 use nostr::{EventBuilder, Keys, PublicKey, RelayUrl, SecretKey, Timestamp, UnsignedEvent};
      5 use radroots_nostr_connect::prelude::{
      6     RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RadrootsNostrConnectMethod,
      7     RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
      8     RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
      9     RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri,
     10 };
     11 use serde_json::{Value, json};
     12 use test_fixtures::{
     13     APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS,
     14     RELAY_TERTIARY_WSS,
     15 };
     16 
     17 fn test_public_key() -> PublicKey {
     18     PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key")
     19 }
     20 
     21 fn test_keys() -> Keys {
     22     let secret_key = SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).expect("secret key");
     23     Keys::new(secret_key)
     24 }
     25 
     26 fn encode_uri_component(value: &str) -> String {
     27     url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
     28 }
     29 
     30 fn logo_url() -> String {
     31     format!("{CDN_PRIMARY_HTTPS}/logo.png")
     32 }
     33 
     34 fn remote_session_capability()
     35 -> radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
     36     radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
     37         user_public_key: test_public_key(),
     38         relays: vec![
     39             RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay 1"),
     40             RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 2"),
     41         ],
     42         permissions: RadrootsNostrConnectPermissions::from(vec![
     43             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
     44             RadrootsNostrConnectPermission::with_parameter(
     45                 RadrootsNostrConnectMethod::SignEvent,
     46                 "kind:1",
     47             ),
     48         ]),
     49     }
     50 }
     51 
     52 #[test]
     53 fn parses_client_uri_with_current_spec_query_fields() {
     54     let uri = format!(
     55         "nostrconnect://{}?relay={}&relay={}&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1059&name=My+Client&url={}&image={}",
     56         FIXTURE_ALICE.public_key_hex,
     57         encode_uri_component(RELAY_SECONDARY_WSS),
     58         encode_uri_component(RELAY_TERTIARY_WSS),
     59         encode_uri_component(APP_PRIMARY_HTTPS),
     60         encode_uri_component(&logo_url()),
     61     );
     62     let parsed = RadrootsNostrConnectUri::parse(&uri).expect("parse client uri");
     63 
     64     match parsed {
     65         RadrootsNostrConnectUri::Client(client) => {
     66             assert_eq!(client.client_public_key, test_public_key());
     67             assert_eq!(client.relays.len(), 2);
     68             assert_eq!(client.secret, "0s8j2djs");
     69             assert_eq!(client.metadata.name.as_deref(), Some("My Client"));
     70             assert_eq!(
     71                 client.metadata.requested_permissions,
     72                 RadrootsNostrConnectPermissions::from(vec![
     73                     RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt,),
     74                     RadrootsNostrConnectPermission::with_parameter(
     75                         RadrootsNostrConnectMethod::SignEvent,
     76                         "1059",
     77                     ),
     78                 ])
     79             );
     80             assert_eq!(
     81                 client.metadata.url.as_deref(),
     82                 Some(format!("{APP_PRIMARY_HTTPS}/").as_str())
     83             );
     84             assert_eq!(client.metadata.image.as_deref(), Some(logo_url().as_str()));
     85         }
     86         other => panic!("expected client uri, got {other:?}"),
     87     }
     88 }
     89 
     90 #[test]
     91 fn parses_bunker_uri_and_roundtrips() {
     92     let source = format!(
     93         "bunker://{}?relay={}&secret=abcd",
     94         FIXTURE_ALICE.public_key_hex,
     95         encode_uri_component(RELAY_PRIMARY_WSS),
     96     );
     97     let parsed = RadrootsNostrConnectUri::parse(&source).expect("parse bunker uri");
     98     let rendered = parsed.to_string();
     99     let reparsed = RadrootsNostrConnectUri::parse(&rendered).expect("reparse bunker uri");
    100     assert_eq!(parsed, reparsed);
    101 }
    102 
    103 #[test]
    104 fn rejects_client_uri_without_required_secret() {
    105     let source = format!(
    106         "nostrconnect://{}?relay={}",
    107         FIXTURE_ALICE.public_key_hex,
    108         encode_uri_component(RELAY_PRIMARY_WSS),
    109     );
    110     assert!(RadrootsNostrConnectUri::parse(&source).is_err());
    111 }
    112 
    113 #[test]
    114 fn requested_permissions_roundtrip_as_csv() {
    115     let permissions = RadrootsNostrConnectPermissions::from(vec![
    116         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt),
    117         RadrootsNostrConnectPermission::with_parameter(RadrootsNostrConnectMethod::SignEvent, "13"),
    118     ]);
    119 
    120     let rendered = permissions.to_string();
    121     assert_eq!(rendered, "nip44_encrypt,sign_event:13");
    122     let reparsed: RadrootsNostrConnectPermissions = rendered.parse().expect("parse permissions");
    123     assert_eq!(permissions, reparsed);
    124 }
    125 
    126 #[test]
    127 fn connect_request_roundtrips_requested_permissions() {
    128     let request = RadrootsNostrConnectRequest::Connect {
    129         remote_signer_public_key: test_public_key(),
    130         secret: Some("abcd".to_owned()),
    131         requested_permissions: RadrootsNostrConnectPermissions::from(vec![
    132             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt),
    133             RadrootsNostrConnectPermission::with_parameter(
    134                 RadrootsNostrConnectMethod::SignEvent,
    135                 "1059",
    136             ),
    137         ]),
    138     };
    139     let message = RadrootsNostrConnectRequestMessage::new("req-1", request);
    140     let encoded = serde_json::to_value(&message).expect("serialize request");
    141     assert_eq!(
    142         encoded,
    143         json!({
    144             "id": "req-1",
    145             "method": "connect",
    146             "params": [
    147                 FIXTURE_ALICE.public_key_hex,
    148                 "abcd",
    149                 "nip44_encrypt,sign_event:1059"
    150             ]
    151         })
    152     );
    153 
    154     let decoded: RadrootsNostrConnectRequestMessage =
    155         serde_json::from_value(encoded).expect("deserialize request");
    156     assert_eq!(decoded, message);
    157 }
    158 
    159 #[test]
    160 fn sign_event_request_roundtrips_unsigned_event_payload() {
    161     let unsigned_event: UnsignedEvent = serde_json::from_value(json!({
    162         "pubkey": test_public_key().to_hex(),
    163         "created_at": 1714078911u64,
    164         "kind": 1u16,
    165         "tags": [],
    166         "content": "Hello, I'm signing remotely"
    167     }))
    168     .expect("unsigned event");
    169 
    170     let message = RadrootsNostrConnectRequestMessage::new(
    171         "req-sign",
    172         RadrootsNostrConnectRequest::SignEvent(unsigned_event.clone()),
    173     );
    174     let encoded = serde_json::to_value(&message).expect("serialize sign request");
    175     assert_eq!(encoded["method"], "sign_event");
    176 
    177     let decoded: RadrootsNostrConnectRequestMessage =
    178         serde_json::from_value(encoded).expect("deserialize sign request");
    179     assert_eq!(decoded, message);
    180     assert_eq!(
    181         decoded.request,
    182         RadrootsNostrConnectRequest::SignEvent(unsigned_event)
    183     );
    184 }
    185 
    186 #[test]
    187 fn switch_relays_response_accepts_array_or_null() {
    188     let relays_response = RadrootsNostrConnectResponseEnvelope {
    189         id: "req-switch".to_owned(),
    190         result: Some(json!([RELAY_SECONDARY_WSS, RELAY_TERTIARY_WSS])),
    191         error: None,
    192     };
    193     let parsed = RadrootsNostrConnectResponse::from_envelope(
    194         &RadrootsNostrConnectMethod::SwitchRelays,
    195         relays_response,
    196     )
    197     .expect("parse relay list");
    198     assert_eq!(
    199         parsed,
    200         RadrootsNostrConnectResponse::RelayList(vec![
    201             RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 1"),
    202             RelayUrl::parse(RELAY_TERTIARY_WSS).expect("relay 2"),
    203         ])
    204     );
    205 
    206     let unchanged = RadrootsNostrConnectResponse::from_envelope(
    207         &RadrootsNostrConnectMethod::SwitchRelays,
    208         RadrootsNostrConnectResponseEnvelope {
    209             id: "req-switch".to_owned(),
    210             result: Some(Value::Null),
    211             error: None,
    212         },
    213     )
    214     .expect("parse null relay result");
    215     assert_eq!(unchanged, RadrootsNostrConnectResponse::RelayListUnchanged);
    216 }
    217 
    218 #[test]
    219 fn get_session_capability_request_and_response_roundtrip() {
    220     let request_message = RadrootsNostrConnectRequestMessage::new(
    221         "req-cap",
    222         RadrootsNostrConnectRequest::GetSessionCapability,
    223     );
    224     let encoded_request = serde_json::to_value(&request_message).expect("serialize request");
    225     let decoded_request: RadrootsNostrConnectRequestMessage =
    226         serde_json::from_value(encoded_request).expect("deserialize request");
    227     assert_eq!(decoded_request, request_message);
    228 
    229     let capability = remote_session_capability();
    230     let response_envelope =
    231         RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone())
    232             .into_envelope("resp-cap")
    233             .expect("serialize response");
    234     let decoded_response = RadrootsNostrConnectResponse::from_envelope(
    235         &RadrootsNostrConnectMethod::GetSessionCapability,
    236         response_envelope,
    237     )
    238     .expect("deserialize response");
    239     assert_eq!(
    240         decoded_response,
    241         RadrootsNostrConnectResponse::RemoteSessionCapability(capability)
    242     );
    243 }
    244 
    245 #[test]
    246 fn auth_url_response_parses_from_result_and_error_fields() {
    247     let response = RadrootsNostrConnectResponse::from_envelope(
    248         &RadrootsNostrConnectMethod::SignEvent,
    249         RadrootsNostrConnectResponseEnvelope {
    250             id: "req-auth".to_owned(),
    251             result: Some(json!("auth_url")),
    252             error: Some("https://auth.example.com/challenge".to_owned()),
    253         },
    254     )
    255     .expect("parse auth challenge");
    256 
    257     assert_eq!(
    258         response,
    259         RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/challenge".to_owned())
    260     );
    261 }
    262 
    263 #[test]
    264 fn get_public_key_pending_response_parses_as_typed_pending_connection() {
    265     let response = RadrootsNostrConnectResponse::from_envelope(
    266         &RadrootsNostrConnectMethod::GetPublicKey,
    267         RadrootsNostrConnectResponseEnvelope {
    268             id: "req-pending".to_owned(),
    269             result: None,
    270             error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()),
    271         },
    272     )
    273     .expect("parse pending get_public_key response");
    274 
    275     assert_eq!(response, RadrootsNostrConnectResponse::PendingConnection);
    276 }
    277 
    278 #[test]
    279 fn sign_event_response_roundtrips_signed_event_json_string() {
    280     let keys = test_keys();
    281     let event = EventBuilder::text_note("hello world")
    282         .custom_created_at(Timestamp::from(1_714_078_911))
    283         .sign_with_keys(&keys)
    284         .expect("sign event");
    285 
    286     let envelope = RadrootsNostrConnectResponse::SignedEvent(event.clone())
    287         .into_envelope("req-sign")
    288         .expect("serialize response");
    289     let parsed = RadrootsNostrConnectResponse::from_envelope(
    290         &RadrootsNostrConnectMethod::SignEvent,
    291         envelope,
    292     )
    293     .expect("parse signed event response");
    294 
    295     assert_eq!(parsed, RadrootsNostrConnectResponse::SignedEvent(event));
    296 }