client.rs (13511B)
1 #[path = "../src/test_fixtures.rs"] 2 mod test_fixtures; 3 4 use nostr::nips::nip44::{self, Version}; 5 use nostr::{ 6 Event, EventBuilder, Keys, Kind, PublicKey, RelayUrl, SecretKey, Tag, Timestamp, UnsignedEvent, 7 }; 8 use radroots_nostr_connect::prelude::{ 9 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientEventOutcome, 10 RadrootsNostrConnectClientProgress, RadrootsNostrConnectClientRequest, 11 RadrootsNostrConnectClientTarget, RadrootsNostrConnectClientTransport, 12 RadrootsNostrConnectClientTransportFuture, RadrootsNostrConnectError, 13 RadrootsNostrConnectMethod, RadrootsNostrConnectRemoteSessionCapability, 14 RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, 15 build_request_event, execute_request_with_transport, parse_response_event, 16 }; 17 use std::collections::VecDeque; 18 use test_fixtures::{FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, RELAY_PRIMARY_WSS}; 19 20 fn keys(secret_key_hex: &str) -> Keys { 21 let secret_key = SecretKey::from_hex(secret_key_hex).expect("secret key"); 22 Keys::new(secret_key) 23 } 24 25 fn client_keys() -> Keys { 26 keys(FIXTURE_ALICE.secret_key_hex) 27 } 28 29 fn remote_signer_keys() -> Keys { 30 keys(FIXTURE_BOB.secret_key_hex) 31 } 32 33 fn other_keys() -> Keys { 34 keys(FIXTURE_CAROL.secret_key_hex) 35 } 36 37 fn relay() -> RelayUrl { 38 RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay") 39 } 40 41 fn target(remote_keys: &Keys) -> RadrootsNostrConnectClientTarget { 42 RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), vec![relay()]) 43 } 44 45 fn unsigned_event(pubkey: PublicKey) -> UnsignedEvent { 46 EventBuilder::text_note("remote signing") 47 .custom_created_at(Timestamp::from(1_714_078_911)) 48 .build(pubkey) 49 } 50 51 fn signed_event(keys: &Keys) -> Event { 52 EventBuilder::text_note("signed remotely") 53 .custom_created_at(Timestamp::from(1_714_078_911)) 54 .sign_with_keys(keys) 55 .expect("signed event") 56 } 57 58 fn response_event( 59 remote_keys: &Keys, 60 client_public_key: PublicKey, 61 request_id: &str, 62 response: RadrootsNostrConnectResponse, 63 ) -> Event { 64 let envelope = response 65 .into_envelope(request_id) 66 .expect("response envelope"); 67 let payload = serde_json::to_string(&envelope).expect("response payload"); 68 let ciphertext = nip44::encrypt( 69 remote_keys.secret_key(), 70 &client_public_key, 71 payload, 72 Version::V2, 73 ) 74 .expect("response ciphertext"); 75 76 EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext) 77 .tag(Tag::public_key(client_public_key)) 78 .sign_with_keys(remote_keys) 79 .expect("response event") 80 } 81 82 fn untagged_response_event( 83 remote_keys: &Keys, 84 client_public_key: PublicKey, 85 request_id: &str, 86 response: RadrootsNostrConnectResponse, 87 ) -> Event { 88 let envelope = response 89 .into_envelope(request_id) 90 .expect("response envelope"); 91 let payload = serde_json::to_string(&envelope).expect("response payload"); 92 let ciphertext = nip44::encrypt( 93 remote_keys.secret_key(), 94 &client_public_key, 95 payload, 96 Version::V2, 97 ) 98 .expect("response ciphertext"); 99 100 EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext) 101 .sign_with_keys(remote_keys) 102 .expect("response event") 103 } 104 105 fn remote_session_capability(remote_keys: &Keys) -> RadrootsNostrConnectRemoteSessionCapability { 106 RadrootsNostrConnectRemoteSessionCapability { 107 user_public_key: remote_keys.public_key(), 108 relays: vec![relay()], 109 permissions: Vec::new().into(), 110 } 111 } 112 113 struct MockTransport { 114 published: Vec<Event>, 115 inbound: VecDeque<Event>, 116 } 117 118 impl MockTransport { 119 fn new(inbound: Vec<Event>) -> Self { 120 Self { 121 published: Vec::new(), 122 inbound: inbound.into(), 123 } 124 } 125 } 126 127 impl RadrootsNostrConnectClientTransport for MockTransport { 128 fn publish_request_event<'a>( 129 &'a mut self, 130 event: Event, 131 ) -> RadrootsNostrConnectClientTransportFuture<'a, ()> { 132 self.published.push(event); 133 Box::pin(async { Ok(()) }) 134 } 135 136 fn next_response_event<'a>( 137 &'a mut self, 138 ) -> RadrootsNostrConnectClientTransportFuture<'a, Event> { 139 let next = self.inbound.pop_front(); 140 Box::pin(async move { next.ok_or(RadrootsNostrConnectError::RequestTimedOut) }) 141 } 142 } 143 144 #[tokio::test] 145 async fn executes_connect_request_and_secret_echo_response() { 146 let client_keys = client_keys(); 147 let remote_keys = remote_signer_keys(); 148 let target = target(&remote_keys); 149 let mut transport = MockTransport::new(vec![response_event( 150 &remote_keys, 151 client_keys.public_key(), 152 "req-connect", 153 RadrootsNostrConnectResponse::ConnectSecretEcho("connect-secret".to_owned()), 154 )]); 155 156 let response = execute_request_with_transport( 157 &client_keys, 158 &target, 159 RadrootsNostrConnectClientRequest::new( 160 "req-connect", 161 RadrootsNostrConnectRequest::Connect { 162 remote_signer_public_key: remote_keys.public_key(), 163 secret: Some("connect-secret".to_owned()), 164 requested_permissions: Vec::new().into(), 165 }, 166 ), 167 &mut transport, 168 |_| Ok(()), 169 ) 170 .await 171 .expect("connect response"); 172 173 assert_eq!( 174 response, 175 RadrootsNostrConnectResponse::ConnectSecretEcho("connect-secret".to_owned()) 176 ); 177 assert_eq!(transport.published.len(), 1); 178 } 179 180 #[tokio::test] 181 async fn executes_capability_request_and_typed_response() { 182 let client_keys = client_keys(); 183 let remote_keys = remote_signer_keys(); 184 let target = target(&remote_keys); 185 let capability = remote_session_capability(&remote_keys); 186 let mut transport = MockTransport::new(vec![response_event( 187 &remote_keys, 188 client_keys.public_key(), 189 "req-capability", 190 RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()), 191 )]); 192 193 let response = execute_request_with_transport( 194 &client_keys, 195 &target, 196 RadrootsNostrConnectClientRequest::new( 197 "req-capability", 198 RadrootsNostrConnectRequest::GetSessionCapability, 199 ), 200 &mut transport, 201 |_| Ok(()), 202 ) 203 .await 204 .expect("capability response"); 205 206 assert_eq!( 207 response, 208 RadrootsNostrConnectResponse::RemoteSessionCapability(capability) 209 ); 210 assert_eq!(transport.published.len(), 1); 211 } 212 213 #[tokio::test] 214 async fn reports_timeout_when_transport_has_no_matching_response() { 215 let client_keys = client_keys(); 216 let remote_keys = remote_signer_keys(); 217 let target = target(&remote_keys); 218 let mut transport = MockTransport::new(Vec::new()); 219 220 let error = execute_request_with_transport( 221 &client_keys, 222 &target, 223 RadrootsNostrConnectClientRequest::new("req-timeout", RadrootsNostrConnectRequest::Ping), 224 &mut transport, 225 |_| Ok(()), 226 ) 227 .await 228 .expect_err("timeout"); 229 230 assert_eq!(error, RadrootsNostrConnectError::RequestTimedOut); 231 assert_eq!(transport.published.len(), 1); 232 } 233 234 #[test] 235 fn builds_encrypted_request_event_for_remote_signer() { 236 let client_keys = client_keys(); 237 let remote_keys = remote_signer_keys(); 238 let target = target(&remote_keys); 239 let message = 240 RadrootsNostrConnectRequestMessage::new("req-ping", RadrootsNostrConnectRequest::Ping); 241 242 let event = build_request_event(&client_keys, &target, message.clone()).expect("event"); 243 244 assert_eq!(event.kind, Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)); 245 assert_eq!(event.pubkey, client_keys.public_key()); 246 assert!( 247 event 248 .tags 249 .public_keys() 250 .any(|public_key| *public_key == remote_keys.public_key()) 251 ); 252 assert!(!event.content.contains("ping")); 253 254 let decrypted = nip44::decrypt( 255 remote_keys.secret_key(), 256 &client_keys.public_key(), 257 &event.content, 258 ) 259 .expect("decrypt request"); 260 let decoded: RadrootsNostrConnectRequestMessage = 261 serde_json::from_str(&decrypted).expect("decode request"); 262 assert_eq!(decoded, message); 263 } 264 265 #[test] 266 fn ignores_response_from_unexpected_signer_identity() { 267 let client_keys = client_keys(); 268 let remote_keys = remote_signer_keys(); 269 let other_keys = other_keys(); 270 let target = target(&remote_keys); 271 let response = response_event( 272 &other_keys, 273 client_keys.public_key(), 274 "req-ping", 275 RadrootsNostrConnectResponse::Pong, 276 ); 277 278 let outcome = parse_response_event( 279 &client_keys, 280 &target, 281 "req-ping", 282 &RadrootsNostrConnectMethod::Ping, 283 &response, 284 ) 285 .expect("parse response"); 286 287 assert_eq!(outcome, RadrootsNostrConnectClientEventOutcome::Ignore); 288 } 289 290 #[tokio::test] 291 async fn executes_request_through_transport_with_auth_progress() { 292 let client_keys = client_keys(); 293 let remote_keys = remote_signer_keys(); 294 let target = target(&remote_keys); 295 let signed = signed_event(&remote_keys); 296 let inbound = vec![ 297 response_event( 298 &remote_keys, 299 client_keys.public_key(), 300 "other-request", 301 RadrootsNostrConnectResponse::Pong, 302 ), 303 response_event( 304 &remote_keys, 305 client_keys.public_key(), 306 "req-sign", 307 RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/challenge".to_owned()), 308 ), 309 response_event( 310 &remote_keys, 311 client_keys.public_key(), 312 "req-sign", 313 RadrootsNostrConnectResponse::SignedEvent(signed.clone()), 314 ), 315 ]; 316 let mut transport = MockTransport::new(inbound); 317 let mut progress = Vec::new(); 318 319 let response = execute_request_with_transport( 320 &client_keys, 321 &target, 322 RadrootsNostrConnectClientRequest::new( 323 "req-sign", 324 RadrootsNostrConnectRequest::SignEvent(unsigned_event(remote_keys.public_key())), 325 ), 326 &mut transport, 327 |event| { 328 progress.push(event); 329 Ok(()) 330 }, 331 ) 332 .await 333 .expect("response"); 334 335 assert_eq!(transport.published.len(), 1); 336 assert_eq!( 337 progress, 338 vec![RadrootsNostrConnectClientProgress::AuthChallenge { 339 url: "https://auth.example.com/challenge".to_owned() 340 }] 341 ); 342 assert_eq!(response, RadrootsNostrConnectResponse::SignedEvent(signed)); 343 } 344 345 #[tokio::test] 346 async fn ignores_events_not_addressed_by_expected_signer_and_client() { 347 let client_keys = client_keys(); 348 let remote_keys = remote_signer_keys(); 349 let other_keys = other_keys(); 350 let target = target(&remote_keys); 351 let wrong_author = EventBuilder::new( 352 Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), 353 "not encrypted for this client", 354 ) 355 .tag(Tag::public_key(client_keys.public_key())) 356 .sign_with_keys(&other_keys) 357 .expect("wrong author event"); 358 let missing_client_tag = untagged_response_event( 359 &remote_keys, 360 client_keys.public_key(), 361 "req-ping", 362 RadrootsNostrConnectResponse::Pong, 363 ); 364 let valid = response_event( 365 &remote_keys, 366 client_keys.public_key(), 367 "req-ping", 368 RadrootsNostrConnectResponse::Pong, 369 ); 370 let mut transport = MockTransport::new(vec![wrong_author, missing_client_tag, valid]); 371 372 let response = execute_request_with_transport( 373 &client_keys, 374 &target, 375 RadrootsNostrConnectClientRequest::new("req-ping", RadrootsNostrConnectRequest::Ping), 376 &mut transport, 377 |_| Ok(()), 378 ) 379 .await 380 .expect("response"); 381 382 assert_eq!(response, RadrootsNostrConnectResponse::Pong); 383 assert_eq!(transport.published.len(), 1); 384 } 385 386 #[test] 387 fn reports_decryption_failure_from_expected_signer() { 388 let client_keys = client_keys(); 389 let remote_keys = remote_signer_keys(); 390 let target = target(&remote_keys); 391 let malformed = EventBuilder::new( 392 Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), 393 "not nip44 ciphertext", 394 ) 395 .tag(Tag::public_key(client_keys.public_key())) 396 .sign_with_keys(&remote_keys) 397 .expect("malformed response"); 398 399 let error = parse_response_event( 400 &client_keys, 401 &target, 402 "req-ping", 403 &RadrootsNostrConnectMethod::Ping, 404 &malformed, 405 ) 406 .expect_err("decrypt failure"); 407 408 assert!(matches!( 409 error, 410 RadrootsNostrConnectError::Decrypt { reason } if !reason.is_empty() 411 )); 412 } 413 414 #[test] 415 fn parses_auth_challenge_as_progress_without_consuming_final_response() { 416 let client_keys = client_keys(); 417 let remote_keys = remote_signer_keys(); 418 let target = target(&remote_keys); 419 let auth = response_event( 420 &remote_keys, 421 client_keys.public_key(), 422 "req-sign", 423 RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/continue".to_owned()), 424 ); 425 426 let outcome = parse_response_event( 427 &client_keys, 428 &target, 429 "req-sign", 430 &RadrootsNostrConnectMethod::SignEvent, 431 &auth, 432 ) 433 .expect("parse auth"); 434 435 assert_eq!( 436 outcome, 437 RadrootsNostrConnectClientEventOutcome::Progress( 438 RadrootsNostrConnectClientProgress::AuthChallenge { 439 url: "https://auth.example.com/continue".to_owned() 440 } 441 ) 442 ); 443 }