protocol.rs (22004B)
1 #[path = "../src/test_fixtures.rs"] 2 mod test_fixtures; 3 4 use nostr::{EventBuilder, JsonUtil, Keys, PublicKey, SecretKey, Timestamp, UnsignedEvent}; 5 use radroots_nostr_connect::message::{ 6 PENDING_CONNECTION_ERROR, RemoteSessionCapability, RequestMessage, ResponseEnvelope, 7 SignedEvent as ConnectSignedEvent, UnsignedEvent as ConnectUnsignedEvent, 8 }; 9 use radroots_nostr_connect::permission::Permissions; 10 use radroots_nostr_connect::uri::{ 11 CLIENT_METADATA_JSON_MAX_BYTES, CLIENT_NAME_MAX_BYTES, ClientMetadata, ClientUri, 12 RelayUrl as ConnectRelayUrl, Uri, 13 }; 14 use radroots_nostr_connect::{Error, Method, Permission, Request, Response}; 15 use serde_json::{Value, json}; 16 use test_fixtures::{ 17 APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS, 18 RELAY_TERTIARY_WSS, 19 }; 20 21 fn test_public_key() -> PublicKey { 22 PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key") 23 } 24 25 #[test] 26 fn constructs_client_uri_from_validated_values() { 27 let relay = ConnectRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"); 28 let metadata = ClientMetadata::new() 29 .with_name("Radroots") 30 .expect("metadata"); 31 let client = ClientUri::try_new( 32 test_identity_public_key(), 33 [relay.clone(), relay], 34 "shared-secret", 35 metadata, 36 ) 37 .expect("client URI"); 38 39 assert_eq!(client.relays().len(), 1); 40 assert_eq!(client.secret(), "shared-secret"); 41 assert_eq!(client.metadata().name(), Some("Radroots")); 42 assert!( 43 ClientUri::try_new( 44 test_identity_public_key(), 45 Vec::<ConnectRelayUrl>::new(), 46 "shared-secret", 47 ClientMetadata::new(), 48 ) 49 .is_err() 50 ); 51 } 52 53 fn test_identity_public_key() -> radroots_identity::PublicKey { 54 radroots_identity::PublicKey::from_hex(FIXTURE_ALICE.public_key_hex) 55 .expect("identity public key") 56 } 57 58 fn test_keys() -> Keys { 59 let secret_key = SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).expect("secret key"); 60 Keys::new(secret_key) 61 } 62 63 fn encode_uri_component(value: &str) -> String { 64 url::form_urlencoded::byte_serialize(value.as_bytes()).collect() 65 } 66 67 fn logo_url() -> String { 68 format!("{CDN_PRIMARY_HTTPS}/logo.png") 69 } 70 71 fn remote_session_capability() -> RemoteSessionCapability { 72 RemoteSessionCapability { 73 user_public_key: test_identity_public_key(), 74 relays: vec![ 75 ConnectRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay 1"), 76 ConnectRelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 2"), 77 ], 78 permissions: Permissions::from(vec![ 79 Permission::new(Method::Ping), 80 Permission::with_parameter(Method::SignEvent, "kind:1"), 81 ]), 82 } 83 } 84 85 #[test] 86 fn parses_client_uri_with_current_spec_query_fields() { 87 let uri = format!( 88 "nostrconnect://{}?relay={}&relay={}&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1059&name=My+Client&url={}&image={}", 89 FIXTURE_ALICE.public_key_hex, 90 encode_uri_component(RELAY_SECONDARY_WSS), 91 encode_uri_component(RELAY_TERTIARY_WSS), 92 encode_uri_component(APP_PRIMARY_HTTPS), 93 encode_uri_component(&logo_url()), 94 ); 95 let parsed = Uri::parse(&uri).expect("parse client uri"); 96 97 match parsed { 98 Uri::Client(client) => { 99 assert_eq!(client.client_public_key(), test_identity_public_key()); 100 assert_eq!(client.relays().len(), 2); 101 assert_eq!(client.secret(), "0s8j2djs"); 102 assert_eq!(client.metadata().name.as_deref(), Some("My Client")); 103 assert_eq!( 104 client.metadata().requested_permissions, 105 Permissions::from(vec![ 106 Permission::new(Method::Nip44Encrypt,), 107 Permission::with_parameter(Method::SignEvent, "1059",), 108 ]) 109 ); 110 assert_eq!( 111 client.metadata().url.as_deref(), 112 Some(format!("{APP_PRIMARY_HTTPS}/").as_str()) 113 ); 114 assert_eq!( 115 client.metadata().image.as_deref(), 116 Some(logo_url().as_str()) 117 ); 118 } 119 other => panic!("expected client uri, got {other:?}"), 120 } 121 } 122 123 #[test] 124 fn parses_bunker_uri_and_roundtrips() { 125 let source = format!( 126 "bunker://{}?relay={}&secret=abcd", 127 FIXTURE_ALICE.public_key_hex, 128 encode_uri_component(RELAY_PRIMARY_WSS), 129 ); 130 let parsed = Uri::parse(&source).expect("parse bunker uri"); 131 let rendered = parsed.to_string(); 132 let reparsed = Uri::parse(&rendered).expect("reparse bunker uri"); 133 assert_eq!(parsed, reparsed); 134 } 135 136 #[test] 137 fn rejects_client_uri_without_required_secret() { 138 let source = format!( 139 "nostrconnect://{}?relay={}", 140 FIXTURE_ALICE.public_key_hex, 141 encode_uri_component(RELAY_PRIMARY_WSS), 142 ); 143 assert!(Uri::parse(&source).is_err()); 144 } 145 146 #[test] 147 fn requested_permissions_roundtrip_as_csv() { 148 let permissions = Permissions::from(vec![ 149 Permission::new(Method::Nip44Encrypt), 150 Permission::with_parameter(Method::SignEvent, "13"), 151 ]); 152 153 let rendered = permissions.to_string(); 154 assert_eq!(rendered, "nip44_encrypt,sign_event:13"); 155 let reparsed: Permissions = rendered.parse().expect("parse permissions"); 156 assert_eq!(permissions, reparsed); 157 } 158 159 #[test] 160 fn connect_request_roundtrips_requested_permissions() { 161 let request = Request::Connect { 162 remote_signer_public_key: test_identity_public_key(), 163 secret: Some("abcd".to_owned()), 164 requested_permissions: Permissions::from(vec![ 165 Permission::new(Method::Nip44Encrypt), 166 Permission::with_parameter(Method::SignEvent, "1059"), 167 ]), 168 client_metadata: None, 169 }; 170 let message = RequestMessage::new("req-1", request); 171 let encoded = serde_json::to_value(&message).expect("serialize request"); 172 assert_eq!( 173 encoded, 174 json!({ 175 "id": "req-1", 176 "method": "connect", 177 "params": [ 178 FIXTURE_ALICE.public_key_hex, 179 "abcd", 180 "nip44_encrypt,sign_event:1059" 181 ] 182 }) 183 ); 184 185 let decoded: RequestMessage = serde_json::from_value(encoded).expect("deserialize request"); 186 assert_eq!(decoded, message); 187 } 188 189 #[test] 190 fn connect_request_roundtrips_client_metadata_in_fourth_parameter() { 191 let request = Request::Connect { 192 remote_signer_public_key: test_identity_public_key(), 193 secret: None, 194 requested_permissions: Permissions::default(), 195 client_metadata: Some(ClientMetadata { 196 requested_permissions: Permissions::default(), 197 name: Some(" My Client ".to_owned()), 198 url: Some(APP_PRIMARY_HTTPS.to_owned()), 199 image: Some(logo_url()), 200 }), 201 }; 202 let message = RequestMessage::new("req-metadata", request); 203 let encoded = serde_json::to_value(&message).expect("serialize metadata request"); 204 assert_eq!(encoded["params"][1], ""); 205 assert_eq!(encoded["params"][2], ""); 206 let encoded_metadata: Value = serde_json::from_str( 207 encoded["params"][3] 208 .as_str() 209 .expect("metadata parameter string"), 210 ) 211 .expect("metadata parameter json"); 212 assert_eq!( 213 encoded_metadata, 214 json!({ 215 "name": "My Client", 216 "url": format!("{APP_PRIMARY_HTTPS}/"), 217 "image": logo_url(), 218 }) 219 ); 220 221 let decoded: RequestMessage = 222 serde_json::from_value(encoded.clone()).expect("deserialize metadata request"); 223 match &decoded.request { 224 Request::Connect { 225 client_metadata: Some(metadata), 226 .. 227 } => { 228 assert_eq!(metadata.name.as_deref(), Some("My Client")); 229 assert!(metadata.requested_permissions.is_empty()); 230 } 231 other => panic!("expected connect metadata, got {other:?}"), 232 } 233 assert_eq!( 234 serde_json::to_value(&decoded).expect("re-encode normalized metadata"), 235 encoded 236 ); 237 } 238 239 #[test] 240 fn logout_request_and_acknowledgement_roundtrip() { 241 let message = RequestMessage::new("req-logout", Request::Logout); 242 assert_eq!( 243 serde_json::to_value(&message).expect("serialize logout"), 244 json!({"id": "req-logout", "method": "logout", "params": []}) 245 ); 246 247 let response = Response::from_envelope( 248 &Method::Logout, 249 ResponseEnvelope { 250 id: "req-logout".to_owned(), 251 result: Some(Value::String("ack".to_owned())), 252 error: None, 253 }, 254 ) 255 .expect("parse logout acknowledgement"); 256 assert_eq!(response, Response::LogoutAcknowledged); 257 assert_eq!( 258 response 259 .into_envelope("req-logout") 260 .expect("encode logout acknowledgement") 261 .result, 262 Some(Value::String("ack".to_owned())) 263 ); 264 } 265 266 #[test] 267 fn rejects_invalid_client_metadata() { 268 let invalid_name = json!({ 269 "id": "req-invalid-name", 270 "method": "connect", 271 "params": [ 272 FIXTURE_ALICE.public_key_hex, 273 "", 274 "", 275 serde_json::to_string(&json!({"name": "line\nbreak"})).expect("metadata") 276 ] 277 }); 278 assert!(serde_json::from_value::<RequestMessage>(invalid_name).is_err()); 279 280 let invalid_scheme = format!( 281 "nostrconnect://{}?relay={}&secret=secret&url={}", 282 FIXTURE_ALICE.public_key_hex, 283 encode_uri_component(RELAY_PRIMARY_WSS), 284 encode_uri_component("file:///tmp/client"), 285 ); 286 assert!(Uri::parse(&invalid_scheme).is_err()); 287 288 let oversized_name = ClientMetadata { 289 requested_permissions: Permissions::default(), 290 name: Some("a".repeat(CLIENT_NAME_MAX_BYTES + 1)), 291 url: None, 292 image: None, 293 }; 294 assert!(matches!( 295 oversized_name.to_connect_param(), 296 Err(Error::InvalidClientMetadata { field: "name", .. }) 297 )); 298 299 let oversized_payload = "x".repeat(CLIENT_METADATA_JSON_MAX_BYTES + 1); 300 assert!(matches!( 301 Request::from_parts( 302 Method::Connect, 303 vec![ 304 test_public_key().to_hex(), 305 String::new(), 306 String::new(), 307 oversized_payload, 308 ], 309 ), 310 Err(Error::ClientMetadataTooLarge { .. }) 311 )); 312 } 313 314 #[test] 315 fn sign_event_request_roundtrips_unsigned_event_payload() { 316 let unsigned_event: UnsignedEvent = serde_json::from_value(json!({ 317 "pubkey": test_public_key().to_hex(), 318 "created_at": 1714078911u64, 319 "kind": 1u16, 320 "tags": [], 321 "content": "Hello, I'm signing remotely" 322 })) 323 .expect("unsigned event"); 324 325 let message = RequestMessage::new( 326 "req-sign", 327 Request::SignEvent( 328 ConnectUnsignedEvent::from_json(&unsigned_event.as_json()) 329 .expect("unsigned event payload"), 330 ), 331 ); 332 let encoded = serde_json::to_value(&message).expect("serialize sign request"); 333 assert_eq!(encoded["method"], "sign_event"); 334 335 let decoded: RequestMessage = 336 serde_json::from_value(encoded).expect("deserialize sign request"); 337 assert_eq!(decoded, message); 338 assert_eq!( 339 decoded.request, 340 Request::SignEvent( 341 ConnectUnsignedEvent::from_json(&unsigned_event.as_json()) 342 .expect("unsigned event payload"), 343 ) 344 ); 345 } 346 347 #[test] 348 fn switch_relays_response_accepts_array_or_null() { 349 let relays_response = ResponseEnvelope { 350 id: "req-switch".to_owned(), 351 result: Some(json!([RELAY_SECONDARY_WSS, RELAY_TERTIARY_WSS])), 352 error: None, 353 }; 354 let parsed = 355 Response::from_envelope(&Method::SwitchRelays, relays_response).expect("parse relay list"); 356 assert_eq!( 357 parsed, 358 Response::RelayList(vec![ 359 ConnectRelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 1"), 360 ConnectRelayUrl::parse(RELAY_TERTIARY_WSS).expect("relay 2"), 361 ]) 362 ); 363 364 let unchanged = Response::from_envelope( 365 &Method::SwitchRelays, 366 ResponseEnvelope { 367 id: "req-switch".to_owned(), 368 result: Some(Value::Null), 369 error: None, 370 }, 371 ) 372 .expect("parse null relay result"); 373 assert_eq!(unchanged, Response::RelayListUnchanged); 374 } 375 376 #[test] 377 fn get_session_capability_request_and_response_roundtrip() { 378 let request_message = RequestMessage::new("req-cap", Request::GetSessionCapability); 379 let encoded_request = serde_json::to_value(&request_message).expect("serialize request"); 380 let decoded_request: RequestMessage = 381 serde_json::from_value(encoded_request).expect("deserialize request"); 382 assert_eq!(decoded_request, request_message); 383 384 let capability = remote_session_capability(); 385 let response_envelope = Response::RemoteSessionCapability(capability.clone()) 386 .into_envelope("resp-cap") 387 .expect("serialize response"); 388 let decoded_response = 389 Response::from_envelope(&Method::GetSessionCapability, response_envelope) 390 .expect("deserialize response"); 391 assert_eq!( 392 decoded_response, 393 Response::RemoteSessionCapability(capability) 394 ); 395 } 396 397 #[test] 398 fn auth_url_response_parses_from_result_and_error_fields() { 399 let response = Response::from_envelope( 400 &Method::SignEvent, 401 ResponseEnvelope { 402 id: "req-auth".to_owned(), 403 result: Some(json!("auth_url")), 404 error: Some("https://auth.example.com/challenge".to_owned()), 405 }, 406 ) 407 .expect("parse auth challenge"); 408 409 assert_eq!( 410 response, 411 Response::AuthUrl("https://auth.example.com/challenge".to_owned()) 412 ); 413 } 414 415 #[test] 416 fn connect_and_poll_pending_responses_parse_as_typed_pending_connection() { 417 for method in [ 418 Method::Connect, 419 Method::GetPublicKey, 420 Method::GetSessionCapability, 421 ] { 422 let response = Response::from_envelope( 423 &method, 424 ResponseEnvelope { 425 id: "req-pending".to_owned(), 426 result: None, 427 error: Some(PENDING_CONNECTION_ERROR.to_owned()), 428 }, 429 ) 430 .expect("parse pending connection response"); 431 432 assert_eq!(response, Response::PendingConnection); 433 } 434 } 435 436 #[test] 437 fn sign_event_response_roundtrips_signed_event_json_string() { 438 let keys = test_keys(); 439 let event = EventBuilder::text_note("hello world") 440 .custom_created_at(Timestamp::from(1_714_078_911)) 441 .sign_with_keys(&keys) 442 .expect("sign event"); 443 444 let envelope = Response::SignedEvent( 445 ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload"), 446 ) 447 .into_envelope("req-sign") 448 .expect("serialize response"); 449 let parsed = 450 Response::from_envelope(&Method::SignEvent, envelope).expect("parse signed event response"); 451 452 assert_eq!( 453 parsed, 454 Response::SignedEvent( 455 ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload") 456 ) 457 ); 458 } 459 460 #[test] 461 fn checked_in_current_session_vectors_match_protocol_behavior() { 462 let vectors = include_str!("fixtures/current_session.v1.json"); 463 let document: Value = serde_json::from_str(vectors).expect("NIP-46 vector JSON"); 464 assert_eq!(document["suite"], "nip46_current_session"); 465 assert_eq!(document["contract_version"], "1.0.0"); 466 let entries = document["vectors"].as_array().expect("NIP-46 vectors"); 467 468 for entry in entries { 469 let id = entry["id"].as_str().expect("vector id"); 470 let kind = entry["kind"].as_str().expect("vector kind"); 471 let input = &entry["input"]; 472 let expected = &entry["expected"]; 473 474 match kind { 475 "nip46.request.valid" => { 476 let message: RequestMessage = serde_json::from_value(input["message"].clone()) 477 .unwrap_or_else(|error| panic!("{id}: parse request: {error}")); 478 let normalized = serde_json::to_value(message) 479 .unwrap_or_else(|error| panic!("{id}: serialize request: {error}")); 480 assert_eq!(normalized, expected["normalized_message"], "{id}"); 481 } 482 "nip46.request.invalid" => { 483 let error = serde_json::from_value::<RequestMessage>(input["message"].clone()) 484 .expect_err("invalid request vector"); 485 assert_vector_error(id, expected, error); 486 } 487 "nip46.metadata.invalid" => { 488 let count = input["count"].as_u64().expect("metadata repeat count") as usize; 489 let repeat = input["repeat"].as_str().expect("metadata repeat value"); 490 let metadata = ClientMetadata { 491 name: Some(repeat.repeat(count)), 492 ..ClientMetadata::default() 493 }; 494 let error = metadata.normalized().expect_err("invalid metadata vector"); 495 assert_vector_error(id, expected, error); 496 } 497 "nip46.uri.valid" => { 498 let uri = input["uri"].as_str().expect("NIP-46 URI"); 499 let parsed = 500 Uri::parse(uri).unwrap_or_else(|error| panic!("{id}: parse URI: {error}")); 501 assert_uri_vector(id, parsed, expected); 502 } 503 "nip46.uri.invalid" => { 504 let uri = input["uri"].as_str().expect("NIP-46 URI"); 505 let error = Uri::parse(uri).expect_err("invalid URI vector"); 506 assert_vector_error(id, expected, error); 507 } 508 "nip46.response.valid" => { 509 let method = input["method"] 510 .as_str() 511 .expect("response method") 512 .parse::<Method>() 513 .expect("typed response method"); 514 let envelope: ResponseEnvelope = serde_json::from_value(input["envelope"].clone()) 515 .unwrap_or_else(|error| panic!("{id}: parse envelope: {error}")); 516 let request_id = envelope.id.clone(); 517 let response = Response::from_envelope(&method, envelope) 518 .unwrap_or_else(|error| panic!("{id}: parse response: {error}")); 519 let normalized = response 520 .into_envelope(request_id) 521 .unwrap_or_else(|error| panic!("{id}: serialize response: {error}")); 522 let normalized = serde_json::to_value(normalized) 523 .unwrap_or_else(|error| panic!("{id}: serialize envelope: {error}")); 524 assert_eq!(normalized, expected["normalized_envelope"], "{id}"); 525 } 526 "nip46.response.invalid" => { 527 let method = input["method"] 528 .as_str() 529 .expect("response method") 530 .parse::<Method>() 531 .expect("typed response method"); 532 let envelope: ResponseEnvelope = serde_json::from_value(input["envelope"].clone()) 533 .unwrap_or_else(|error| panic!("{id}: parse envelope: {error}")); 534 let error = Response::from_envelope(&method, envelope) 535 .expect_err("invalid response vector"); 536 assert_vector_error(id, expected, error); 537 } 538 other => panic!("{id}: unknown NIP-46 vector kind {other}"), 539 } 540 } 541 } 542 543 fn assert_uri_vector(id: &str, parsed: Uri, expected: &Value) { 544 let expected_relays = expected["relays"] 545 .as_array() 546 .expect("expected relays") 547 .iter() 548 .map(|relay| relay.as_str().expect("expected relay")) 549 .collect::<Vec<_>>(); 550 551 match parsed { 552 Uri::Bunker(uri) => { 553 assert_eq!(expected["variant"], "bunker", "{id}"); 554 let relays = uri 555 .relays() 556 .iter() 557 .map(ToString::to_string) 558 .collect::<Vec<_>>(); 559 assert_eq!(relays, expected_relays, "{id}"); 560 assert_eq!(uri.secret(), expected["secret"].as_str(), "{id}"); 561 } 562 Uri::Client(uri) => { 563 assert_eq!(expected["variant"], "nostrconnect", "{id}"); 564 let relays = uri 565 .relays() 566 .iter() 567 .map(ToString::to_string) 568 .collect::<Vec<_>>(); 569 assert_eq!(relays, expected_relays, "{id}"); 570 assert_eq!(uri.secret(), expected["secret"].as_str().expect("secret")); 571 assert_eq!( 572 uri.metadata().name.as_deref(), 573 expected["metadata"]["name"].as_str(), 574 "{id}" 575 ); 576 assert_eq!( 577 uri.metadata().url.as_deref(), 578 expected["metadata"]["url"].as_str(), 579 "{id}" 580 ); 581 assert_eq!( 582 uri.metadata().image.as_deref(), 583 expected["metadata"]["image"].as_str(), 584 "{id}" 585 ); 586 assert_eq!( 587 uri.metadata().requested_permissions.to_string(), 588 expected["metadata"]["permissions"] 589 .as_str() 590 .expect("permissions"), 591 "{id}" 592 ); 593 } 594 } 595 } 596 597 fn assert_vector_error(id: &str, expected: &Value, error: impl ToString) { 598 let class = expected["error"].as_str().expect("expected error class"); 599 let needle = match class { 600 "invalid_client_metadata" => "invalid NIP-46 client metadata", 601 "invalid_params" => "invalid parameter count", 602 "invalid_response_payload" => "invalid response payload", 603 "missing_result" => "missing response result", 604 "missing_secret" => "missing secret", 605 other => panic!("{id}: unknown expected error class {other}"), 606 }; 607 let message = error.to_string(); 608 assert!( 609 message.contains(needle), 610 "{id}: expected {class}, got {message}" 611 ); 612 }