lib

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

coverage.rs (47616B)


      1 #[path = "../src/test_fixtures.rs"]
      2 mod test_fixtures;
      3 
      4 use nostr::{Event, EventBuilder, Keys, PublicKey, RelayUrl, SecretKey, Timestamp, UnsignedEvent};
      5 use radroots_nostr_connect::prelude::{
      6     RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RadrootsNostrConnectError,
      7     RadrootsNostrConnectMethod, RadrootsNostrConnectPendingConnectionPollOutcome,
      8     RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
      9     RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
     10     RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectUri,
     11 };
     12 use serde_json::{Value, json};
     13 use std::str::FromStr;
     14 use test_fixtures::{
     15     APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS,
     16     RELAY_TERTIARY_WSS,
     17 };
     18 
     19 fn test_public_key() -> PublicKey {
     20     PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key")
     21 }
     22 
     23 fn test_keys() -> Keys {
     24     let secret_key = SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).expect("secret key");
     25     Keys::new(secret_key)
     26 }
     27 
     28 fn encode_uri_component(value: &str) -> String {
     29     url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
     30 }
     31 
     32 fn logo_url() -> String {
     33     format!("{CDN_PRIMARY_HTTPS}/logo.png")
     34 }
     35 
     36 fn unsigned_event() -> UnsignedEvent {
     37     serde_json::from_value(json!({
     38         "pubkey": test_public_key().to_hex(),
     39         "created_at": 1714078911u64,
     40         "kind": 1u16,
     41         "tags": [],
     42         "content": "hello"
     43     }))
     44     .expect("unsigned event")
     45 }
     46 
     47 fn signed_event() -> Event {
     48     EventBuilder::text_note("hello world")
     49         .custom_created_at(Timestamp::from(1_714_078_911))
     50         .sign_with_keys(&test_keys())
     51         .expect("sign event")
     52 }
     53 
     54 fn relay(value: &str) -> RelayUrl {
     55     RelayUrl::parse(value).expect("relay")
     56 }
     57 
     58 #[test]
     59 fn error_method_and_permission_surfaces_cover_public_paths() {
     60     let json_error = serde_json::from_str::<Value>("{").expect_err("invalid json");
     61     assert!(matches!(
     62         RadrootsNostrConnectError::from(json_error),
     63         RadrootsNostrConnectError::Json(message) if !message.is_empty()
     64     ));
     65 
     66     let methods = [
     67         (RadrootsNostrConnectMethod::Connect, "connect"),
     68         (RadrootsNostrConnectMethod::GetPublicKey, "get_public_key"),
     69         (
     70             RadrootsNostrConnectMethod::GetSessionCapability,
     71             "get_session_capability",
     72         ),
     73         (RadrootsNostrConnectMethod::SignEvent, "sign_event"),
     74         (RadrootsNostrConnectMethod::Nip04Encrypt, "nip04_encrypt"),
     75         (RadrootsNostrConnectMethod::Nip04Decrypt, "nip04_decrypt"),
     76         (RadrootsNostrConnectMethod::Nip44Encrypt, "nip44_encrypt"),
     77         (RadrootsNostrConnectMethod::Nip44Decrypt, "nip44_decrypt"),
     78         (RadrootsNostrConnectMethod::Ping, "ping"),
     79         (RadrootsNostrConnectMethod::SwitchRelays, "switch_relays"),
     80     ];
     81     for (method, raw) in methods {
     82         assert_eq!(method.as_str(), raw);
     83         assert_eq!(method.to_string(), raw);
     84         assert_eq!(
     85             RadrootsNostrConnectMethod::from_str(raw).expect("parse method"),
     86             method
     87         );
     88     }
     89     assert_eq!(
     90         RadrootsNostrConnectMethod::from_str("publish_note").expect("custom method"),
     91         RadrootsNostrConnectMethod::Custom("publish_note".to_owned())
     92     );
     93     assert!(matches!(
     94         RadrootsNostrConnectMethod::from_str(" "),
     95         Err(RadrootsNostrConnectError::InvalidMethod(value)) if value == " "
     96     ));
     97     assert_eq!(
     98         serde_json::from_str::<RadrootsNostrConnectMethod>("\"do_work\"")
     99             .expect("deserialize custom method"),
    100         RadrootsNostrConnectMethod::Custom("do_work".to_owned())
    101     );
    102     assert!(
    103         serde_json::from_str::<RadrootsNostrConnectMethod>("123")
    104             .expect_err("non-string method")
    105             .to_string()
    106             .contains("invalid type")
    107     );
    108     assert!(
    109         serde_json::from_str::<RadrootsNostrConnectMethod>("\"\"")
    110             .expect_err("blank method")
    111             .to_string()
    112             .contains("invalid NIP-46 method")
    113     );
    114 
    115     let simple = RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping);
    116     assert_eq!(simple.to_string(), "ping");
    117     let parameterized = RadrootsNostrConnectPermission::with_parameter(
    118         RadrootsNostrConnectMethod::SignEvent,
    119         "1059",
    120     );
    121     assert_eq!(parameterized.to_string(), "sign_event:1059");
    122     assert_eq!(
    123         RadrootsNostrConnectPermission::from_str("sign_event:1059").expect("parse permission"),
    124         parameterized
    125     );
    126     assert!(matches!(
    127         RadrootsNostrConnectPermission::from_str(" "),
    128         Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == " "
    129     ));
    130     assert!(matches!(
    131         RadrootsNostrConnectPermission::from_str("sign_event:"),
    132         Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == "sign_event:"
    133     ));
    134     assert!(matches!(
    135         RadrootsNostrConnectPermission::from_str(" :kind"),
    136         Err(RadrootsNostrConnectError::InvalidMethod(_))
    137     ));
    138 
    139     let empty = RadrootsNostrConnectPermissions::new();
    140     assert!(empty.is_empty());
    141     assert!(empty.as_slice().is_empty());
    142     assert!(empty.clone().into_vec().is_empty());
    143     assert_eq!(
    144         RadrootsNostrConnectPermissions::from_str("  ").expect("empty permissions"),
    145         empty
    146     );
    147 
    148     let permissions = RadrootsNostrConnectPermissions::from(vec![
    149         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt),
    150         RadrootsNostrConnectPermission::with_parameter(RadrootsNostrConnectMethod::SignEvent, "13"),
    151     ]);
    152     assert_eq!(permissions.to_string(), "nip44_encrypt,sign_event:13");
    153     assert_eq!(
    154         serde_json::to_string(&permissions).expect("serialize permissions"),
    155         "\"nip44_encrypt,sign_event:13\""
    156     );
    157     assert_eq!(
    158         serde_json::from_str::<RadrootsNostrConnectPermissions>("\"nip44_encrypt,sign_event:13\"")
    159             .expect("deserialize permissions"),
    160         permissions
    161     );
    162     assert!(
    163         serde_json::from_str::<RadrootsNostrConnectPermissions>("123")
    164             .expect_err("non-string permissions")
    165             .to_string()
    166             .contains("invalid type")
    167     );
    168     assert!(matches!(
    169         RadrootsNostrConnectPermissions::from_str("sign_event:,ping"),
    170         Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == "sign_event:"
    171     ));
    172 
    173     let all_sign_events =
    174         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SignEvent);
    175     assert!(all_sign_events.matches_sign_event_kind(30402));
    176     assert!(all_sign_events.matches_request(&RadrootsNostrConnectMethod::SignEvent, None));
    177     assert!(!all_sign_events.matches_request(&RadrootsNostrConnectMethod::Ping, None));
    178 
    179     let numeric_sign_event = RadrootsNostrConnectPermission::with_parameter(
    180         RadrootsNostrConnectMethod::SignEvent,
    181         "30402",
    182     );
    183     let kind_prefixed_sign_event = RadrootsNostrConnectPermission::with_parameter(
    184         RadrootsNostrConnectMethod::SignEvent,
    185         "kind:30402",
    186     );
    187     assert!(numeric_sign_event.matches_sign_event_kind(30402));
    188     assert!(kind_prefixed_sign_event.matches_sign_event_kind(30402));
    189     assert!(
    190         numeric_sign_event
    191             .matches_request(&RadrootsNostrConnectMethod::SignEvent, Some("kind:30402"))
    192     );
    193     assert!(!numeric_sign_event.matches_sign_event_kind(3040));
    194     assert!(
    195         !RadrootsNostrConnectPermission::with_parameter(
    196             RadrootsNostrConnectMethod::SignEvent,
    197             "130402"
    198         )
    199         .matches_sign_event_kind(30402)
    200     );
    201     assert!(
    202         !RadrootsNostrConnectPermission::with_parameter(
    203             RadrootsNostrConnectMethod::SignEvent,
    204             "not-a-kind"
    205         )
    206         .matches_request(
    207             &RadrootsNostrConnectMethod::SignEvent,
    208             Some("also-not-a-kind")
    209         )
    210     );
    211 
    212     let typed_permissions = RadrootsNostrConnectPermissions::from(vec![
    213         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
    214         kind_prefixed_sign_event,
    215     ]);
    216     assert!(typed_permissions.allows_request(&RadrootsNostrConnectMethod::Ping, None));
    217     assert!(typed_permissions.allows_sign_event_kind(30402));
    218     assert!(!typed_permissions.allows_sign_event_kind(0));
    219 }
    220 
    221 #[test]
    222 fn uri_surface_covers_rendering_ignored_queries_and_error_paths() {
    223     let bunker = RadrootsNostrConnectUri::parse(&format!(
    224         "bunker://{}?relay={}&foo=bar",
    225         FIXTURE_ALICE.public_key_hex,
    226         encode_uri_component(RELAY_PRIMARY_WSS),
    227     ))
    228     .expect("parse bunker");
    229     let bunker_rendered = bunker.to_string();
    230     assert!(bunker_rendered.contains(&format!(
    231         "relay={}",
    232         encode_uri_component(RELAY_PRIMARY_WSS)
    233     )));
    234     assert!(!bunker_rendered.contains("secret="));
    235 
    236     let minimal_client: RadrootsNostrConnectUri = format!(
    237         "nostrconnect://{}?relay={}&secret=shared",
    238         FIXTURE_ALICE.public_key_hex,
    239         encode_uri_component(RELAY_PRIMARY_WSS),
    240     )
    241     .parse()
    242     .expect("parse minimal client");
    243     let minimal_client_rendered = minimal_client.to_string();
    244     assert!(minimal_client_rendered.contains("secret=shared"));
    245     assert!(!minimal_client_rendered.contains("perms="));
    246     assert!(!minimal_client_rendered.contains("name="));
    247     assert!(!minimal_client_rendered.contains("url="));
    248     assert!(!minimal_client_rendered.contains("image="));
    249 
    250     let metadata_client = RadrootsNostrConnectUri::parse(&format!(
    251         "nostrconnect://{}?relay={}&secret=shared&perms=ping&name=myc&url={}&image={}&ignored=value",
    252         FIXTURE_ALICE.public_key_hex,
    253         encode_uri_component(RELAY_PRIMARY_WSS),
    254         encode_uri_component(APP_PRIMARY_HTTPS),
    255         encode_uri_component(&logo_url()),
    256     ))
    257     .expect("parse metadata client");
    258     let metadata_rendered = metadata_client.to_string();
    259     assert!(metadata_rendered.contains("perms=ping"));
    260     assert!(metadata_rendered.contains("name=myc"));
    261     assert!(metadata_rendered.contains(&format!(
    262         "url={}",
    263         encode_uri_component(&format!("{APP_PRIMARY_HTTPS}/"))
    264     )));
    265     assert!(metadata_rendered.contains(&format!("image={}", encode_uri_component(&logo_url()))));
    266 
    267     assert!(matches!(
    268         RadrootsNostrConnectUri::parse("not a uri"),
    269         Err(RadrootsNostrConnectError::InvalidUrl { .. })
    270     ));
    271     assert!(matches!(
    272         RadrootsNostrConnectUri::parse(
    273             "nostrconnect:///path?relay=wss%3A%2F%2Frelay.example.com&secret=abc"
    274         ),
    275         Err(RadrootsNostrConnectError::MissingPublicKey)
    276     ));
    277     assert!(matches!(
    278         RadrootsNostrConnectUri::parse(&format!("bunker://{}", FIXTURE_ALICE.public_key_hex)),
    279         Err(RadrootsNostrConnectError::MissingRelay)
    280     ));
    281     assert!(matches!(
    282         RadrootsNostrConnectUri::parse(&format!(
    283             "nostrconnect://{}?secret=abc",
    284             FIXTURE_ALICE.public_key_hex
    285         )),
    286         Err(RadrootsNostrConnectError::MissingRelay)
    287     ));
    288     assert!(matches!(
    289         RadrootsNostrConnectUri::parse(&format!(
    290             "nostrconnect://{}?relay={}",
    291             FIXTURE_ALICE.public_key_hex,
    292             encode_uri_component(RELAY_PRIMARY_WSS),
    293         )),
    294         Err(RadrootsNostrConnectError::MissingSecret)
    295     ));
    296     assert!(matches!(
    297         RadrootsNostrConnectUri::parse("https://example.com"),
    298         Err(RadrootsNostrConnectError::InvalidUriScheme(value)) if value == "https"
    299     ));
    300     assert!(matches!(
    301         RadrootsNostrConnectUri::parse(
    302             "nostrconnect://bad-key?relay=wss%3A%2F%2Frelay.example.com&secret=abc"
    303         ),
    304         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    305     ));
    306     assert!(matches!(
    307         RadrootsNostrConnectUri::parse(&format!(
    308             "nostrconnect://{}?relay=http%3A%2F%2Frelay.example.com&secret=abc",
    309             FIXTURE_ALICE.public_key_hex
    310         )),
    311         Err(RadrootsNostrConnectError::InvalidRelayUrl { .. })
    312     ));
    313     assert!(matches!(
    314         RadrootsNostrConnectUri::parse(&format!(
    315             "nostrconnect://{}?relay={}&secret=abc&url=not-a-url",
    316             FIXTURE_ALICE.public_key_hex,
    317             encode_uri_component(RELAY_PRIMARY_WSS),
    318         )),
    319         Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
    320     ));
    321     assert!(matches!(
    322         RadrootsNostrConnectUri::parse("bunker://bad-key?relay=wss%3A%2F%2Frelay.example.com"),
    323         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    324     ));
    325     assert!(matches!(
    326         RadrootsNostrConnectUri::parse(&format!(
    327             "bunker://{}?relay=http%3A%2F%2Frelay.example.com",
    328             FIXTURE_ALICE.public_key_hex
    329         )),
    330         Err(RadrootsNostrConnectError::InvalidRelayUrl { .. })
    331     ));
    332     assert!(matches!(
    333         RadrootsNostrConnectUri::parse(&format!(
    334             "nostrconnect://{}?relay={}&secret=abc&perms=sign_event%3A",
    335             FIXTURE_ALICE.public_key_hex,
    336             encode_uri_component(RELAY_PRIMARY_WSS),
    337         )),
    338         Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == "sign_event:"
    339     ));
    340     assert!(matches!(
    341         RadrootsNostrConnectUri::parse(&format!(
    342             "nostrconnect://{}?relay={}&secret=abc&image=not-a-url",
    343             FIXTURE_ALICE.public_key_hex,
    344             encode_uri_component(RELAY_PRIMARY_WSS),
    345         )),
    346         Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
    347     ));
    348 }
    349 
    350 #[test]
    351 fn request_surface_covers_variant_methods_serialization_and_validation() {
    352     let ping_permission =
    353         RadrootsNostrConnectPermissions::from(vec![RadrootsNostrConnectPermission::new(
    354             RadrootsNostrConnectMethod::Ping,
    355         )]);
    356 
    357     let requests = vec![
    358         (
    359             RadrootsNostrConnectRequest::Connect {
    360                 remote_signer_public_key: test_public_key(),
    361                 secret: None,
    362                 requested_permissions: RadrootsNostrConnectPermissions::default(),
    363             },
    364             RadrootsNostrConnectMethod::Connect,
    365             vec![test_public_key().to_hex()],
    366         ),
    367         (
    368             RadrootsNostrConnectRequest::Connect {
    369                 remote_signer_public_key: test_public_key(),
    370                 secret: None,
    371                 requested_permissions: ping_permission.clone(),
    372             },
    373             RadrootsNostrConnectMethod::Connect,
    374             vec![test_public_key().to_hex(), String::new(), "ping".to_owned()],
    375         ),
    376         (
    377             RadrootsNostrConnectRequest::GetPublicKey,
    378             RadrootsNostrConnectMethod::GetPublicKey,
    379             Vec::new(),
    380         ),
    381         (
    382             RadrootsNostrConnectRequest::GetSessionCapability,
    383             RadrootsNostrConnectMethod::GetSessionCapability,
    384             Vec::new(),
    385         ),
    386         (
    387             RadrootsNostrConnectRequest::SignEvent(unsigned_event()),
    388             RadrootsNostrConnectMethod::SignEvent,
    389             vec![serde_json::to_string(&unsigned_event()).expect("serialize unsigned event")],
    390         ),
    391         (
    392             RadrootsNostrConnectRequest::Nip04Encrypt {
    393                 public_key: test_public_key(),
    394                 plaintext: "hello".to_owned(),
    395             },
    396             RadrootsNostrConnectMethod::Nip04Encrypt,
    397             vec![test_public_key().to_hex(), "hello".to_owned()],
    398         ),
    399         (
    400             RadrootsNostrConnectRequest::Nip04Decrypt {
    401                 public_key: test_public_key(),
    402                 ciphertext: "cipher".to_owned(),
    403             },
    404             RadrootsNostrConnectMethod::Nip04Decrypt,
    405             vec![test_public_key().to_hex(), "cipher".to_owned()],
    406         ),
    407         (
    408             RadrootsNostrConnectRequest::Nip44Encrypt {
    409                 public_key: test_public_key(),
    410                 plaintext: "hello".to_owned(),
    411             },
    412             RadrootsNostrConnectMethod::Nip44Encrypt,
    413             vec![test_public_key().to_hex(), "hello".to_owned()],
    414         ),
    415         (
    416             RadrootsNostrConnectRequest::Nip44Decrypt {
    417                 public_key: test_public_key(),
    418                 ciphertext: "cipher".to_owned(),
    419             },
    420             RadrootsNostrConnectMethod::Nip44Decrypt,
    421             vec![test_public_key().to_hex(), "cipher".to_owned()],
    422         ),
    423         (
    424             RadrootsNostrConnectRequest::Ping,
    425             RadrootsNostrConnectMethod::Ping,
    426             Vec::new(),
    427         ),
    428         (
    429             RadrootsNostrConnectRequest::SwitchRelays,
    430             RadrootsNostrConnectMethod::SwitchRelays,
    431             Vec::new(),
    432         ),
    433         (
    434             RadrootsNostrConnectRequest::Custom {
    435                 method: RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
    436                 params: vec!["one".to_owned(), "two".to_owned()],
    437             },
    438             RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
    439             vec!["one".to_owned(), "two".to_owned()],
    440         ),
    441     ];
    442     for (request, method, params) in requests {
    443         assert_eq!(request.method(), method);
    444         assert_eq!(request.to_params(), params);
    445     }
    446 
    447     assert_eq!(
    448         RadrootsNostrConnectRequest::from_parts(
    449             RadrootsNostrConnectMethod::Connect,
    450             vec![test_public_key().to_hex()],
    451         )
    452         .expect("connect without secret or perms"),
    453         RadrootsNostrConnectRequest::Connect {
    454             remote_signer_public_key: test_public_key(),
    455             secret: None,
    456             requested_permissions: RadrootsNostrConnectPermissions::default(),
    457         }
    458     );
    459     assert_eq!(
    460         RadrootsNostrConnectRequest::from_parts(
    461             RadrootsNostrConnectMethod::Connect,
    462             vec![test_public_key().to_hex(), String::new(), "ping".to_owned()],
    463         )
    464         .expect("connect with empty secret"),
    465         RadrootsNostrConnectRequest::Connect {
    466             remote_signer_public_key: test_public_key(),
    467             secret: None,
    468             requested_permissions: RadrootsNostrConnectPermissions::from(vec![
    469                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
    470             ]),
    471         }
    472     );
    473     assert_eq!(
    474         RadrootsNostrConnectRequest::from_parts(
    475             RadrootsNostrConnectMethod::GetPublicKey,
    476             Vec::new(),
    477         )
    478         .expect("get_public_key from parts"),
    479         RadrootsNostrConnectRequest::GetPublicKey
    480     );
    481     assert_eq!(
    482         RadrootsNostrConnectRequest::from_parts(
    483             RadrootsNostrConnectMethod::GetSessionCapability,
    484             Vec::new(),
    485         )
    486         .expect("get_session_capability from parts"),
    487         RadrootsNostrConnectRequest::GetSessionCapability
    488     );
    489     assert_eq!(
    490         RadrootsNostrConnectRequest::from_parts(
    491             RadrootsNostrConnectMethod::Nip04Encrypt,
    492             vec![test_public_key().to_hex(), "hello".to_owned()],
    493         )
    494         .expect("nip04 encrypt from parts"),
    495         RadrootsNostrConnectRequest::Nip04Encrypt {
    496             public_key: test_public_key(),
    497             plaintext: "hello".to_owned(),
    498         }
    499     );
    500     assert_eq!(
    501         RadrootsNostrConnectRequest::from_parts(
    502             RadrootsNostrConnectMethod::Nip04Decrypt,
    503             vec![test_public_key().to_hex(), "cipher".to_owned()],
    504         )
    505         .expect("nip04 decrypt from parts"),
    506         RadrootsNostrConnectRequest::Nip04Decrypt {
    507             public_key: test_public_key(),
    508             ciphertext: "cipher".to_owned(),
    509         }
    510     );
    511     assert_eq!(
    512         RadrootsNostrConnectRequest::from_parts(
    513             RadrootsNostrConnectMethod::Nip44Encrypt,
    514             vec![test_public_key().to_hex(), "hello".to_owned()],
    515         )
    516         .expect("nip44 encrypt from parts"),
    517         RadrootsNostrConnectRequest::Nip44Encrypt {
    518             public_key: test_public_key(),
    519             plaintext: "hello".to_owned(),
    520         }
    521     );
    522     assert_eq!(
    523         RadrootsNostrConnectRequest::from_parts(
    524             RadrootsNostrConnectMethod::Nip44Decrypt,
    525             vec![test_public_key().to_hex(), "cipher".to_owned()],
    526         )
    527         .expect("nip44 decrypt from parts"),
    528         RadrootsNostrConnectRequest::Nip44Decrypt {
    529             public_key: test_public_key(),
    530             ciphertext: "cipher".to_owned(),
    531         }
    532     );
    533     assert_eq!(
    534         RadrootsNostrConnectRequest::from_parts(RadrootsNostrConnectMethod::Ping, Vec::new())
    535             .expect("ping from parts"),
    536         RadrootsNostrConnectRequest::Ping
    537     );
    538     assert_eq!(
    539         RadrootsNostrConnectRequest::from_parts(
    540             RadrootsNostrConnectMethod::SwitchRelays,
    541             Vec::new(),
    542         )
    543         .expect("switch relays from parts"),
    544         RadrootsNostrConnectRequest::SwitchRelays
    545     );
    546 
    547     for (method, params, expected_error) in [
    548         (
    549             RadrootsNostrConnectMethod::GetPublicKey,
    550             vec!["oops".to_owned()],
    551             "no params",
    552         ),
    553         (
    554             RadrootsNostrConnectMethod::GetSessionCapability,
    555             vec!["oops".to_owned()],
    556             "no params",
    557         ),
    558         (
    559             RadrootsNostrConnectMethod::SignEvent,
    560             Vec::new(),
    561             "exactly 1 param",
    562         ),
    563         (
    564             RadrootsNostrConnectMethod::Nip04Encrypt,
    565             vec!["only-one".to_owned()],
    566             "exactly 2 params",
    567         ),
    568         (
    569             RadrootsNostrConnectMethod::Nip04Decrypt,
    570             vec!["only-one".to_owned()],
    571             "exactly 2 params",
    572         ),
    573         (
    574             RadrootsNostrConnectMethod::Nip44Encrypt,
    575             vec!["only-one".to_owned()],
    576             "exactly 2 params",
    577         ),
    578         (
    579             RadrootsNostrConnectMethod::Nip44Decrypt,
    580             vec!["only-one".to_owned()],
    581             "exactly 2 params",
    582         ),
    583         (
    584             RadrootsNostrConnectMethod::Ping,
    585             vec!["oops".to_owned()],
    586             "no params",
    587         ),
    588         (
    589             RadrootsNostrConnectMethod::SwitchRelays,
    590             vec!["oops".to_owned()],
    591             "no params",
    592         ),
    593     ] {
    594         assert!(matches!(
    595             RadrootsNostrConnectRequest::from_parts(method, params),
    596             Err(RadrootsNostrConnectError::InvalidParams { expected, .. }) if expected == expected_error
    597         ));
    598     }
    599     assert!(matches!(
    600         RadrootsNostrConnectRequest::from_parts(RadrootsNostrConnectMethod::Connect, Vec::new()),
    601         Err(RadrootsNostrConnectError::InvalidParams { expected, received, .. })
    602             if expected == "1 to 3 params" && received == 0
    603     ));
    604     assert!(matches!(
    605         RadrootsNostrConnectRequest::from_parts(
    606             RadrootsNostrConnectMethod::Connect,
    607             vec!["bad-key".to_owned()],
    608         ),
    609         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    610     ));
    611     assert!(matches!(
    612         RadrootsNostrConnectRequest::from_parts(
    613             RadrootsNostrConnectMethod::Connect,
    614             vec![test_public_key().to_hex(), "secret".to_owned(), "sign_event:".to_owned()],
    615         ),
    616         Err(RadrootsNostrConnectError::InvalidPermission(value)) if value == "sign_event:"
    617     ));
    618     assert!(matches!(
    619         RadrootsNostrConnectRequest::from_parts(
    620             RadrootsNostrConnectMethod::Connect,
    621             vec![
    622                 test_public_key().to_hex(),
    623                 "secret".to_owned(),
    624                 "ping".to_owned(),
    625                 "extra".to_owned(),
    626             ],
    627         ),
    628         Err(RadrootsNostrConnectError::InvalidParams { expected, received, .. })
    629             if expected == "1 to 3 params" && received == 4
    630     ));
    631     assert!(matches!(
    632         RadrootsNostrConnectRequest::from_parts(
    633             RadrootsNostrConnectMethod::SignEvent,
    634             vec!["not-json".to_owned()],
    635         ),
    636         Err(RadrootsNostrConnectError::InvalidRequestPayload { .. })
    637     ));
    638     assert!(matches!(
    639         RadrootsNostrConnectRequest::from_parts(
    640             RadrootsNostrConnectMethod::Nip04Encrypt,
    641             vec!["bad-key".to_owned(), "hello".to_owned()],
    642         ),
    643         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    644     ));
    645     assert!(matches!(
    646         RadrootsNostrConnectRequest::from_parts(
    647             RadrootsNostrConnectMethod::Nip04Decrypt,
    648             vec!["bad-key".to_owned(), "cipher".to_owned()],
    649         ),
    650         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    651     ));
    652     assert!(matches!(
    653         RadrootsNostrConnectRequest::from_parts(
    654             RadrootsNostrConnectMethod::Nip44Encrypt,
    655             vec!["bad-key".to_owned(), "hello".to_owned()],
    656         ),
    657         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    658     ));
    659     assert!(matches!(
    660         RadrootsNostrConnectRequest::from_parts(
    661             RadrootsNostrConnectMethod::Nip44Decrypt,
    662             vec!["bad-key".to_owned(), "cipher".to_owned()],
    663         ),
    664         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
    665     ));
    666 
    667     let custom_message = RadrootsNostrConnectRequestMessage::new(
    668         "req-custom",
    669         RadrootsNostrConnectRequest::Custom {
    670             method: RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
    671             params: vec!["a".to_owned()],
    672         },
    673     );
    674     let encoded = serde_json::to_string(&custom_message).expect("serialize custom request");
    675     let decoded: RadrootsNostrConnectRequestMessage =
    676         serde_json::from_str(&encoded).expect("deserialize custom request");
    677     assert_eq!(decoded, custom_message);
    678     assert!(
    679         serde_json::from_str::<RadrootsNostrConnectRequestMessage>("{")
    680             .expect_err("invalid request message json")
    681             .to_string()
    682             .contains("EOF")
    683     );
    684     assert!(
    685         serde_json::from_str::<RadrootsNostrConnectRequestMessage>(
    686             "{\"id\":\"req\",\"method\":\"get_public_key\",\"params\":[\"oops\"]}",
    687         )
    688         .expect_err("invalid request params")
    689         .to_string()
    690         .contains("invalid parameter count")
    691     );
    692 }
    693 
    694 #[test]
    695 fn response_surface_covers_success_and_error_paths() {
    696     let event = signed_event();
    697     let remote_session_capability =
    698         radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
    699             user_public_key: test_public_key(),
    700             relays: vec![relay(RELAY_PRIMARY_WSS), relay(RELAY_SECONDARY_WSS)],
    701             permissions: RadrootsNostrConnectPermissions::from(vec![
    702                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
    703                 RadrootsNostrConnectPermission::with_parameter(
    704                     RadrootsNostrConnectMethod::SignEvent,
    705                     "kind:1",
    706                 ),
    707             ]),
    708         };
    709     let cases = vec![
    710         (
    711             RadrootsNostrConnectResponse::ConnectAcknowledged,
    712             RadrootsNostrConnectMethod::Connect,
    713             RadrootsNostrConnectResponse::ConnectAcknowledged,
    714         ),
    715         (
    716             RadrootsNostrConnectResponse::ConnectSecretEcho("secret".to_owned()),
    717             RadrootsNostrConnectMethod::Connect,
    718             RadrootsNostrConnectResponse::ConnectSecretEcho("secret".to_owned()),
    719         ),
    720         (
    721             RadrootsNostrConnectResponse::UserPublicKey(test_public_key()),
    722             RadrootsNostrConnectMethod::GetPublicKey,
    723             RadrootsNostrConnectResponse::UserPublicKey(test_public_key()),
    724         ),
    725         (
    726             RadrootsNostrConnectResponse::PendingConnection,
    727             RadrootsNostrConnectMethod::GetSessionCapability,
    728             RadrootsNostrConnectResponse::PendingConnection,
    729         ),
    730         (
    731             RadrootsNostrConnectResponse::RemoteSessionCapability(
    732                 remote_session_capability.clone(),
    733             ),
    734             RadrootsNostrConnectMethod::GetSessionCapability,
    735             RadrootsNostrConnectResponse::RemoteSessionCapability(
    736                 remote_session_capability.clone(),
    737             ),
    738         ),
    739         (
    740             RadrootsNostrConnectResponse::SignedEvent(event.clone()),
    741             RadrootsNostrConnectMethod::SignEvent,
    742             RadrootsNostrConnectResponse::SignedEvent(event.clone()),
    743         ),
    744         (
    745             RadrootsNostrConnectResponse::Pong,
    746             RadrootsNostrConnectMethod::Ping,
    747             RadrootsNostrConnectResponse::Pong,
    748         ),
    749         (
    750             RadrootsNostrConnectResponse::Nip04Encrypt("cipher".to_owned()),
    751             RadrootsNostrConnectMethod::Nip04Encrypt,
    752             RadrootsNostrConnectResponse::Nip04Encrypt("cipher".to_owned()),
    753         ),
    754         (
    755             RadrootsNostrConnectResponse::Nip04Decrypt("plain".to_owned()),
    756             RadrootsNostrConnectMethod::Nip04Decrypt,
    757             RadrootsNostrConnectResponse::Nip04Decrypt("plain".to_owned()),
    758         ),
    759         (
    760             RadrootsNostrConnectResponse::Nip44Encrypt("cipher".to_owned()),
    761             RadrootsNostrConnectMethod::Nip44Encrypt,
    762             RadrootsNostrConnectResponse::Nip44Encrypt("cipher".to_owned()),
    763         ),
    764         (
    765             RadrootsNostrConnectResponse::Nip44Decrypt("plain".to_owned()),
    766             RadrootsNostrConnectMethod::Nip44Decrypt,
    767             RadrootsNostrConnectResponse::Nip44Decrypt("plain".to_owned()),
    768         ),
    769         (
    770             RadrootsNostrConnectResponse::RelayList(vec![
    771                 relay(RELAY_SECONDARY_WSS),
    772                 relay(RELAY_TERTIARY_WSS),
    773             ]),
    774             RadrootsNostrConnectMethod::SwitchRelays,
    775             RadrootsNostrConnectResponse::RelayList(vec![
    776                 relay(RELAY_SECONDARY_WSS),
    777                 relay(RELAY_TERTIARY_WSS),
    778             ]),
    779         ),
    780         (
    781             RadrootsNostrConnectResponse::RelayListUnchanged,
    782             RadrootsNostrConnectMethod::SwitchRelays,
    783             RadrootsNostrConnectResponse::RelayListUnchanged,
    784         ),
    785     ];
    786     for (response, method, expected) in cases {
    787         let envelope = response.into_envelope("req").expect("serialize response");
    788         let parsed =
    789             RadrootsNostrConnectResponse::from_envelope(&method, envelope).expect("parse response");
    790         assert_eq!(parsed, expected);
    791     }
    792 
    793     let error_envelope = RadrootsNostrConnectResponse::Error {
    794         result: Some(json!("partial")),
    795         error: "denied".to_owned(),
    796     }
    797     .into_envelope("req-error")
    798     .expect("serialize error response");
    799     assert_eq!(error_envelope.error.as_deref(), Some("denied"));
    800 
    801     let custom_envelope = RadrootsNostrConnectResponse::Custom {
    802         result: Some(json!({"ok": true})),
    803         error: Some("warning".to_owned()),
    804     }
    805     .into_envelope("req-custom")
    806     .expect("serialize custom response");
    807     assert_eq!(custom_envelope.error.as_deref(), Some("warning"));
    808 
    809     let auth_envelope =
    810         RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/challenge".to_owned())
    811             .into_envelope("req-auth")
    812             .expect("serialize auth_url");
    813     assert_eq!(
    814         RadrootsNostrConnectResponse::from_envelope(
    815             &RadrootsNostrConnectMethod::SignEvent,
    816             auth_envelope,
    817         )
    818         .expect("parse auth_url"),
    819         RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/challenge".to_owned())
    820     );
    821 
    822     assert_eq!(
    823         RadrootsNostrConnectResponse::from_envelope(
    824             &RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
    825             RadrootsNostrConnectResponseEnvelope {
    826                 id: "req-custom".to_owned(),
    827                 result: Some(json!("ok")),
    828                 error: None,
    829             },
    830         )
    831         .expect("parse custom response without error"),
    832         RadrootsNostrConnectResponse::Custom {
    833             result: Some(json!("ok")),
    834             error: None,
    835         }
    836     );
    837     assert_eq!(
    838         RadrootsNostrConnectResponse::from_envelope(
    839             &RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
    840             RadrootsNostrConnectResponseEnvelope {
    841                 id: "req-custom".to_owned(),
    842                 result: Some(json!({"ok": true})),
    843                 error: Some("warning".to_owned()),
    844             },
    845         )
    846         .expect("parse custom response"),
    847         RadrootsNostrConnectResponse::Custom {
    848             result: Some(json!({"ok": true})),
    849             error: Some("warning".to_owned()),
    850         }
    851     );
    852     assert_eq!(
    853         RadrootsNostrConnectResponse::from_envelope(
    854             &RadrootsNostrConnectMethod::GetPublicKey,
    855             RadrootsNostrConnectResponseEnvelope {
    856                 id: "req-pending".to_owned(),
    857                 result: None,
    858                 error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()),
    859             },
    860         )
    861         .expect("parse typed pending response"),
    862         RadrootsNostrConnectResponse::PendingConnection
    863     );
    864     assert_eq!(
    865         RadrootsNostrConnectResponse::from_envelope(
    866             &RadrootsNostrConnectMethod::GetSessionCapability,
    867             RadrootsNostrConnectResponseEnvelope {
    868                 id: "req-pending-capability".to_owned(),
    869                 result: None,
    870                 error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()),
    871             },
    872         )
    873         .expect("parse typed pending capability response"),
    874         RadrootsNostrConnectResponse::PendingConnection
    875     );
    876     assert_eq!(
    877         RadrootsNostrConnectResponse::from_envelope(
    878             &RadrootsNostrConnectMethod::GetPublicKey,
    879             RadrootsNostrConnectResponseEnvelope {
    880                 id: "req-nonpending-public-key".to_owned(),
    881                 result: None,
    882                 error: Some("denied".to_owned()),
    883             },
    884         )
    885         .expect("parse non-pending public key error"),
    886         RadrootsNostrConnectResponse::Error {
    887             result: None,
    888             error: "denied".to_owned(),
    889         }
    890     );
    891     assert_eq!(
    892         RadrootsNostrConnectResponse::from_envelope(
    893             &RadrootsNostrConnectMethod::GetSessionCapability,
    894             RadrootsNostrConnectResponseEnvelope {
    895                 id: "req-capability-error-with-result".to_owned(),
    896                 result: Some(json!({"code": "retry"})),
    897                 error: Some("denied".to_owned()),
    898             },
    899         )
    900         .expect("parse capability error with result"),
    901         RadrootsNostrConnectResponse::Error {
    902             result: Some(json!({"code": "retry"})),
    903             error: "denied".to_owned(),
    904         }
    905     );
    906     assert!(matches!(
    907         RadrootsNostrConnectResponse::from_envelope(
    908             &RadrootsNostrConnectMethod::GetSessionCapability,
    909             RadrootsNostrConnectResponseEnvelope {
    910                 id: "req-capability-invalid-result".to_owned(),
    911                 result: Some(json!({"permissions": "ping"})),
    912                 error: None,
    913             },
    914         ),
    915         Err(RadrootsNostrConnectError::InvalidResponsePayload { method, .. })
    916             if method == "get_session_capability"
    917     ));
    918     assert_eq!(
    919         RadrootsNostrConnectResponse::from_envelope(
    920             &RadrootsNostrConnectMethod::GetSessionCapability,
    921             RadrootsNostrConnectResponseEnvelope {
    922                 id: "req-capability-string-result".to_owned(),
    923                 result: Some(json!(
    924                     serde_json::to_string(&remote_session_capability)
    925                         .expect("serialize remote session capability")
    926                 )),
    927                 error: None,
    928             },
    929         )
    930         .expect("parse stringified capability result"),
    931         RadrootsNostrConnectResponse::RemoteSessionCapability(remote_session_capability.clone(),)
    932     );
    933     assert!(matches!(
    934         RadrootsNostrConnectResponse::from_envelope(
    935             &RadrootsNostrConnectMethod::GetSessionCapability,
    936             RadrootsNostrConnectResponseEnvelope {
    937                 id: "req-capability-invalid-string".to_owned(),
    938                 result: Some(json!("{")),
    939                 error: None,
    940             },
    941         ),
    942         Err(RadrootsNostrConnectError::InvalidResponsePayload { method, .. })
    943             if method == "get_session_capability"
    944     ));
    945     assert_eq!(
    946         RadrootsNostrConnectResponse::from_envelope(
    947             &RadrootsNostrConnectMethod::Ping,
    948             RadrootsNostrConnectResponseEnvelope {
    949                 id: "req-error".to_owned(),
    950                 result: Some(json!("partial")),
    951                 error: Some("denied".to_owned()),
    952             },
    953         )
    954         .expect("parse error response"),
    955         RadrootsNostrConnectResponse::Error {
    956             result: Some(json!("partial")),
    957             error: "denied".to_owned(),
    958         }
    959     );
    960     assert_eq!(
    961         RadrootsNostrConnectResponse::from_envelope(
    962             &RadrootsNostrConnectMethod::SignEvent,
    963             RadrootsNostrConnectResponseEnvelope {
    964                 id: "req-event".to_owned(),
    965                 result: Some(serde_json::to_value(&event).expect("event value")),
    966                 error: None,
    967             },
    968         )
    969         .expect("parse object event"),
    970         RadrootsNostrConnectResponse::SignedEvent(event)
    971     );
    972     assert_eq!(
    973         RadrootsNostrConnectResponse::from_envelope(
    974             &RadrootsNostrConnectMethod::SwitchRelays,
    975             RadrootsNostrConnectResponseEnvelope {
    976                 id: "req-switch".to_owned(),
    977                 result: Some(json!("null")),
    978                 error: None,
    979             },
    980         )
    981         .expect("parse string null"),
    982         RadrootsNostrConnectResponse::RelayListUnchanged
    983     );
    984     assert_eq!(
    985         RadrootsNostrConnectResponse::from_envelope(
    986             &RadrootsNostrConnectMethod::SwitchRelays,
    987             RadrootsNostrConnectResponseEnvelope {
    988                 id: "req-switch".to_owned(),
    989                 result: Some(json!(format!("[\"{RELAY_SECONDARY_WSS}\"]"))),
    990                 error: None,
    991             },
    992         )
    993         .expect("parse stringified relay list"),
    994         RadrootsNostrConnectResponse::RelayList(vec![relay(RELAY_SECONDARY_WSS)])
    995     );
    996 
    997     assert!(matches!(
    998         RadrootsNostrConnectResponse::AuthUrl("not-a-url".to_owned()).into_envelope("req"),
    999         Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
   1000     ));
   1001     assert!(matches!(
   1002         RadrootsNostrConnectResponse::from_envelope(
   1003             &RadrootsNostrConnectMethod::SignEvent,
   1004             RadrootsNostrConnectResponseEnvelope {
   1005                 id: "req-auth".to_owned(),
   1006                 result: Some(json!("auth_url")),
   1007                 error: Some("not-a-url".to_owned()),
   1008             },
   1009         ),
   1010         Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
   1011     ));
   1012     assert!(matches!(
   1013         RadrootsNostrConnectResponse::from_envelope(
   1014             &RadrootsNostrConnectMethod::GetPublicKey,
   1015             RadrootsNostrConnectResponseEnvelope {
   1016                 id: "req-key".to_owned(),
   1017                 result: Some(json!("bad-key")),
   1018                 error: None,
   1019             },
   1020         ),
   1021         Err(RadrootsNostrConnectError::InvalidPublicKey { .. })
   1022     ));
   1023     assert!(matches!(
   1024         RadrootsNostrConnectResponse::from_envelope(
   1025             &RadrootsNostrConnectMethod::Connect,
   1026             RadrootsNostrConnectResponseEnvelope {
   1027                 id: "req-connect".to_owned(),
   1028                 result: None,
   1029                 error: None,
   1030             },
   1031         ),
   1032         Err(RadrootsNostrConnectError::MissingResult)
   1033     ));
   1034     assert!(matches!(
   1035         RadrootsNostrConnectResponse::from_envelope(
   1036             &RadrootsNostrConnectMethod::GetPublicKey,
   1037             RadrootsNostrConnectResponseEnvelope {
   1038                 id: "req-key".to_owned(),
   1039                 result: None,
   1040                 error: None,
   1041             },
   1042         ),
   1043         Err(RadrootsNostrConnectError::MissingResult)
   1044     ));
   1045     assert!(matches!(
   1046         RadrootsNostrConnectResponse::from_envelope(
   1047             &RadrootsNostrConnectMethod::Ping,
   1048             RadrootsNostrConnectResponseEnvelope {
   1049                 id: "req-ping".to_owned(),
   1050                 result: Some(json!("nope")),
   1051                 error: None,
   1052             },
   1053         ),
   1054         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1055     ));
   1056     assert!(matches!(
   1057         RadrootsNostrConnectResponse::from_envelope(
   1058             &RadrootsNostrConnectMethod::Ping,
   1059             RadrootsNostrConnectResponseEnvelope {
   1060                 id: "req-ping".to_owned(),
   1061                 result: None,
   1062                 error: None,
   1063             },
   1064         ),
   1065         Err(RadrootsNostrConnectError::MissingResult)
   1066     ));
   1067     assert!(matches!(
   1068         RadrootsNostrConnectResponse::from_envelope(
   1069             &RadrootsNostrConnectMethod::Nip04Encrypt,
   1070             RadrootsNostrConnectResponseEnvelope {
   1071                 id: "req-nip04".to_owned(),
   1072                 result: Some(json!(5)),
   1073                 error: None,
   1074             },
   1075         ),
   1076         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1077     ));
   1078     assert!(matches!(
   1079         RadrootsNostrConnectResponse::from_envelope(
   1080             &RadrootsNostrConnectMethod::Nip04Encrypt,
   1081             RadrootsNostrConnectResponseEnvelope {
   1082                 id: "req-nip04".to_owned(),
   1083                 result: None,
   1084                 error: None,
   1085             },
   1086         ),
   1087         Err(RadrootsNostrConnectError::MissingResult)
   1088     ));
   1089     assert!(matches!(
   1090         RadrootsNostrConnectResponse::from_envelope(
   1091             &RadrootsNostrConnectMethod::SignEvent,
   1092             RadrootsNostrConnectResponseEnvelope {
   1093                 id: "req-event".to_owned(),
   1094                 result: Some(json!("not-json")),
   1095                 error: None,
   1096             },
   1097         ),
   1098         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1099     ));
   1100     assert!(matches!(
   1101         RadrootsNostrConnectResponse::from_envelope(
   1102             &RadrootsNostrConnectMethod::SignEvent,
   1103             RadrootsNostrConnectResponseEnvelope {
   1104                 id: "req-event".to_owned(),
   1105                 result: Some(json!(5)),
   1106                 error: None,
   1107             },
   1108         ),
   1109         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1110     ));
   1111     assert!(matches!(
   1112         RadrootsNostrConnectResponse::from_envelope(
   1113             &RadrootsNostrConnectMethod::SignEvent,
   1114             RadrootsNostrConnectResponseEnvelope {
   1115                 id: "req-event".to_owned(),
   1116                 result: None,
   1117                 error: None,
   1118             },
   1119         ),
   1120         Err(RadrootsNostrConnectError::MissingResult)
   1121     ));
   1122     assert!(matches!(
   1123         RadrootsNostrConnectResponse::from_envelope(
   1124             &RadrootsNostrConnectMethod::Nip04Decrypt,
   1125             RadrootsNostrConnectResponseEnvelope {
   1126                 id: "req-nip04d".to_owned(),
   1127                 result: None,
   1128                 error: None,
   1129             },
   1130         ),
   1131         Err(RadrootsNostrConnectError::MissingResult)
   1132     ));
   1133     assert!(matches!(
   1134         RadrootsNostrConnectResponse::from_envelope(
   1135             &RadrootsNostrConnectMethod::Nip44Encrypt,
   1136             RadrootsNostrConnectResponseEnvelope {
   1137                 id: "req-nip44e".to_owned(),
   1138                 result: None,
   1139                 error: None,
   1140             },
   1141         ),
   1142         Err(RadrootsNostrConnectError::MissingResult)
   1143     ));
   1144     assert!(matches!(
   1145         RadrootsNostrConnectResponse::from_envelope(
   1146             &RadrootsNostrConnectMethod::Nip44Decrypt,
   1147             RadrootsNostrConnectResponseEnvelope {
   1148                 id: "req-nip44d".to_owned(),
   1149                 result: None,
   1150                 error: None,
   1151             },
   1152         ),
   1153         Err(RadrootsNostrConnectError::MissingResult)
   1154     ));
   1155     assert!(matches!(
   1156         RadrootsNostrConnectResponse::from_envelope(
   1157             &RadrootsNostrConnectMethod::SwitchRelays,
   1158             RadrootsNostrConnectResponseEnvelope {
   1159                 id: "req-switch".to_owned(),
   1160                 result: Some(json!("[invalid")),
   1161                 error: None,
   1162             },
   1163         ),
   1164         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1165     ));
   1166     assert!(matches!(
   1167         RadrootsNostrConnectResponse::from_envelope(
   1168             &RadrootsNostrConnectMethod::SwitchRelays,
   1169             RadrootsNostrConnectResponseEnvelope {
   1170                 id: "req-switch".to_owned(),
   1171                 result: Some(json!([1])),
   1172                 error: None,
   1173             },
   1174         ),
   1175         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1176     ));
   1177     assert!(matches!(
   1178         RadrootsNostrConnectResponse::from_envelope(
   1179             &RadrootsNostrConnectMethod::SwitchRelays,
   1180             RadrootsNostrConnectResponseEnvelope {
   1181                 id: "req-switch".to_owned(),
   1182                 result: Some(json!(["http://relay.example.com"])),
   1183                 error: None,
   1184             },
   1185         ),
   1186         Err(RadrootsNostrConnectError::InvalidRelayUrl { .. })
   1187     ));
   1188     assert!(matches!(
   1189         RadrootsNostrConnectResponse::from_envelope(
   1190             &RadrootsNostrConnectMethod::SwitchRelays,
   1191             RadrootsNostrConnectResponseEnvelope {
   1192                 id: "req-switch".to_owned(),
   1193                 result: Some(json!(5)),
   1194                 error: None,
   1195             },
   1196         ),
   1197         Err(RadrootsNostrConnectError::InvalidResponsePayload { .. })
   1198     ));
   1199 }
   1200 
   1201 #[test]
   1202 fn pending_connection_poll_outcome_uses_typed_variants() {
   1203     let remote_session_capability =
   1204         radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
   1205             user_public_key: test_public_key(),
   1206             relays: vec![relay(RELAY_PRIMARY_WSS), relay(RELAY_SECONDARY_WSS)],
   1207             permissions: RadrootsNostrConnectPermissions::from(vec![
   1208                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
   1209                 RadrootsNostrConnectPermission::with_parameter(
   1210                     RadrootsNostrConnectMethod::SignEvent,
   1211                     "kind:1",
   1212                 ),
   1213             ]),
   1214         };
   1215 
   1216     assert_eq!(
   1217         RadrootsNostrConnectResponse::PendingConnection.into_pending_connection_poll_outcome(),
   1218         RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
   1219     );
   1220 
   1221     assert_eq!(
   1222         RadrootsNostrConnectResponse::UserPublicKey(test_public_key())
   1223             .into_pending_connection_poll_outcome(),
   1224         RadrootsNostrConnectPendingConnectionPollOutcome::Approved(test_public_key())
   1225     );
   1226     assert_eq!(
   1227         RadrootsNostrConnectResponse::RemoteSessionCapability(remote_session_capability.clone())
   1228             .into_pending_connection_poll_outcome(),
   1229         RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(
   1230             remote_session_capability
   1231         )
   1232     );
   1233 
   1234     assert_eq!(
   1235         RadrootsNostrConnectResponse::Error {
   1236             result: Some(json!("partial")),
   1237             error: "rejected".to_owned(),
   1238         }
   1239         .into_pending_connection_poll_outcome(),
   1240         RadrootsNostrConnectPendingConnectionPollOutcome::Rejected {
   1241             message: "rejected".to_owned(),
   1242         }
   1243     );
   1244     assert_eq!(
   1245         RadrootsNostrConnectResponse::Error {
   1246             result: None,
   1247             error: RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned(),
   1248         }
   1249         .into_pending_connection_poll_outcome(),
   1250         RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
   1251     );
   1252 
   1253     assert_eq!(
   1254         RadrootsNostrConnectResponse::AuthUrl("https://auth.example.com/challenge".to_owned())
   1255             .into_pending_connection_poll_outcome(),
   1256         RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge {
   1257             url: "https://auth.example.com/challenge".to_owned(),
   1258         }
   1259     );
   1260 
   1261     assert!(matches!(
   1262         RadrootsNostrConnectResponse::Pong.into_pending_connection_poll_outcome(),
   1263         RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { response }
   1264             if response == "Pong"
   1265     ));
   1266 }