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 }