lib

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

commit e4e092e311e64f7f1e59da93fe4f5337572fbdd0
parent 4c5d294585e909cf5bccac8068abbee90ada2323
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 23:32:30 +0000

nostr-signer: cover NIP-46 handler gates

- add configurable NIP-46 signer and policy test fixtures
- cover codec RPC parsing, response building, crypto, and sign-event outcomes
- cover handler authorization, policy denial, auth challenge, and response helper branches
- verify radroots_nostr_signer against the 98 percent coverage gate

Diffstat:
Mcrates/nostr_signer/src/nip46.rs | 777++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 753 insertions(+), 24 deletions(-)

diff --git a/crates/nostr_signer/src/nip46.rs b/crates/nostr_signer/src/nip46.rs @@ -811,22 +811,34 @@ where } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::{ - RadrootsNostrSignerHandledRequest, RadrootsNostrSignerNip46ConnectDecision, - RadrootsNostrSignerNip46Handler, RadrootsNostrSignerNip46Policy, - RadrootsNostrSignerNip46Signer, + RadrootsNostrSignerHandledRequest, RadrootsNostrSignerHandledRequestOutcome, + RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler, + RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer, }; use crate::backend::{RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend}; use crate::error::RadrootsNostrSignerError; - use crate::model::{RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthState}; + use crate::evaluation::{ + RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint, + }; + use crate::model::{ + RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthChallenge, + RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionRecord, + RadrootsNostrSignerPendingRequest, + }; use crate::test_support::{fixture_alice_identity, fixture_carol_public_key, primary_relay}; - use nostr::UnsignedEvent; + use nostr::{Keys, Timestamp, UnsignedEvent}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; - use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrPublicKey}; + use radroots_nostr::prelude::{ + RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKind, RadrootsNostrPublicKey, + RadrootsNostrTagKind, + }; use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, - RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, + RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod, + RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, + RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, }; @@ -834,10 +846,28 @@ mod tests { struct TestSigner { signer_identity: RadrootsIdentity, user_identity: RadrootsIdentity, + sign_events: bool, + fail_crypto: bool, } #[derive(Clone)] - struct TestPolicy; + struct TestPolicy { + connect_decision: RadrootsNostrSignerNip46ConnectDecision, + rate_limit_reason: Option<&'static str>, + approval_requirement: Option<RadrootsNostrSignerApprovalRequirement>, + prepare_denial: Option<&'static str>, + } + + impl Default for TestPolicy { + fn default() -> Self { + Self { + connect_decision: RadrootsNostrSignerNip46ConnectDecision::Allow, + rate_limit_reason: None, + approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::NotRequired), + prepare_denial: None, + } + } + } impl RadrootsNostrSignerNip46Signer for TestSigner { fn signer_public_key_hex(&self) -> String { @@ -868,7 +898,11 @@ mod tests { &self, unsigned_event: UnsignedEvent, ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError> { - let _ = unsigned_event; + if self.sign_events { + return unsigned_event + .sign_with_keys(self.user_identity.keys()) + .map_err(|error| RadrootsNostrSignerError::Sign(error.to_string())); + } Err(RadrootsNostrSignerError::Sign( "test signer does not sign events".to_owned(), )) @@ -879,6 +913,11 @@ mod tests { _public_key: &RadrootsNostrPublicKey, plaintext: &str, ) -> Result<String, RadrootsNostrSignerError> { + if self.fail_crypto { + return Err(RadrootsNostrSignerError::Sign( + "test crypto failure".to_owned(), + )); + } Ok(plaintext.to_owned()) } @@ -887,6 +926,11 @@ mod tests { _public_key: &RadrootsNostrPublicKey, ciphertext: &str, ) -> Result<String, RadrootsNostrSignerError> { + if self.fail_crypto { + return Err(RadrootsNostrSignerError::Sign( + "test crypto failure".to_owned(), + )); + } Ok(ciphertext.to_owned()) } @@ -895,6 +939,11 @@ mod tests { _public_key: &RadrootsNostrPublicKey, plaintext: &str, ) -> Result<String, RadrootsNostrSignerError> { + if self.fail_crypto { + return Err(RadrootsNostrSignerError::Sign( + "test crypto failure".to_owned(), + )); + } Ok(plaintext.to_owned()) } @@ -903,6 +952,11 @@ mod tests { _public_key: &RadrootsNostrPublicKey, ciphertext: &str, ) -> Result<String, RadrootsNostrSignerError> { + if self.fail_crypto { + return Err(RadrootsNostrSignerError::Sign( + "test crypto failure".to_owned(), + )); + } Ok(ciphertext.to_owned()) } } @@ -912,21 +966,21 @@ mod tests { &self, _client_public_key: &RadrootsNostrPublicKey, ) -> RadrootsNostrSignerNip46ConnectDecision { - RadrootsNostrSignerNip46ConnectDecision::Allow + self.connect_decision } fn connect_rate_limit_denied_reason( &self, _client_public_key: &RadrootsNostrPublicKey, ) -> Option<String> { - None + self.rate_limit_reason.map(ToOwned::to_owned) } fn approval_requirement_for_client( &self, _client_public_key: &RadrootsNostrPublicKey, ) -> Option<RadrootsNostrSignerApprovalRequirement> { - Some(RadrootsNostrSignerApprovalRequirement::NotRequired) + self.approval_requirement } fn filtered_requested_permissions( @@ -949,11 +1003,15 @@ mod tests { _connection: &crate::model::RadrootsNostrSignerConnectionRecord, _request_message: &RadrootsNostrConnectRequestMessage, ) -> Result<Option<String>, RadrootsNostrSignerError> { - Ok(None) + Ok(self.prepare_denial.map(ToOwned::to_owned)) } } fn test_signer() -> TestSigner { + test_signer_with_options(false, false) + } + + fn test_signer_with_options(sign_events: bool, fail_crypto: bool) -> TestSigner { TestSigner { signer_identity: RadrootsIdentity::from_secret_key_str( "1111111111111111111111111111111111111111111111111111111111111111", @@ -963,6 +1021,8 @@ mod tests { "2222222222222222222222222222222222222222222222222222222222222222", ) .expect("user identity"), + sign_events, + fail_crypto, } } @@ -978,27 +1038,696 @@ mod tests { backend: RadrootsNostrEmbeddedSignerBackend, ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner> { - RadrootsNostrSignerNip46Handler::new( - backend, - TestPolicy, - vec![primary_relay()], - test_signer(), - ) + handler_with_policy(backend, TestPolicy::default()) + } + + fn handler_with_policy( + backend: RadrootsNostrEmbeddedSignerBackend, + policy: TestPolicy, + ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner> + { + RadrootsNostrSignerNip46Handler::new(backend, policy, vec![primary_relay()], test_signer()) } fn connect_request(secret: Option<&str>) -> RadrootsNostrConnectRequestMessage { + connect_request_with_permissions( + secret, + vec![RadrootsNostrConnectPermission::new( + RadrootsNostrConnectMethod::Nip04Encrypt, + )], + ) + } + + fn connect_request_with_permissions( + secret: Option<&str>, + permissions: Vec<RadrootsNostrConnectPermission>, + ) -> RadrootsNostrConnectRequestMessage { let signer_public_key = test_signer().signer_identity.public_key(); RadrootsNostrConnectRequestMessage::new( "req-connect", RadrootsNostrConnectRequest::Connect { remote_signer_public_key: signer_public_key, secret: secret.map(ToOwned::to_owned), - requested_permissions: vec![RadrootsNostrConnectPermission::new( - RadrootsNostrConnectMethod::Nip04Encrypt, - )] - .into(), + requested_permissions: permissions.into(), + }, + ) + } + + fn all_runtime_permissions() -> Vec<RadrootsNostrConnectPermission> { + vec![ + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SignEvent), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Decrypt), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Decrypt), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), + ] + } + + fn request_message( + id: &str, + request: RadrootsNostrConnectRequest, + ) -> RadrootsNostrConnectRequestMessage { + RadrootsNostrConnectRequestMessage::new(id, request) + } + + fn unsigned_user_event(kind: u16) -> UnsignedEvent { + serde_json::from_value(serde_json::json!({ + "pubkey": test_signer().user_identity.public_key().to_hex(), + "created_at": Timestamp::from(1).as_secs(), + "kind": kind, + "tags": [], + "content": "hello", + })) + .expect("unsigned event") + } + + fn registered_connection( + backend: &RadrootsNostrEmbeddedSignerBackend, + client_public_key: &RadrootsNostrPublicKey, + ) -> RadrootsNostrSignerConnectionRecord { + backend + .find_connections_by_client_public_key(client_public_key) + .expect("connections") + .into_iter() + .next() + .expect("connection") + } + + fn connect_with_permissions( + handler: &RadrootsNostrSignerNip46Handler< + RadrootsNostrEmbeddedSignerBackend, + TestPolicy, + TestSigner, + >, + client_public_key: RadrootsNostrPublicKey, + permissions: Vec<RadrootsNostrConnectPermission>, + ) { + let outcome = handler + .handle_request( + client_public_key, + connect_request_with_permissions(None, permissions), + ) + .expect("connect"); + assert!(matches!( + outcome.handled_request, + RadrootsNostrSignerHandledRequest::Respond { .. } + )); + } + + fn response_from_outcome( + outcome: RadrootsNostrSignerHandledRequestOutcome, + ) -> RadrootsNostrConnectResponse { + match outcome.handled_request { + RadrootsNostrSignerHandledRequest::Respond { response, .. } => *response, + other => panic!("unexpected handled request: {other:?}"), + } + } + + #[test] + fn codec_and_handler_facades_cover_rpc_event_surface() { + let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer()); + let _ = codec.filter().expect("codec filter"); + let client_public_key = fixture_carol_public_key(); + let request = request_message("req-parse", RadrootsNostrConnectRequest::Ping); + let raw = serde_json::to_string(&request).expect("serialize request"); + let event = RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + raw, + ) + .sign_with_keys(&Keys::generate()) + .expect("sign request event"); + + let parsed = codec.parse_request_event(&event).expect("parse request"); + assert_eq!(parsed, request); + + let response_builder = codec + .build_response_event( + client_public_key, + "req-parse", + RadrootsNostrConnectResponse::Pong, + ) + .expect("response builder"); + let response_event = response_builder.build(test_signer().signer_identity.public_key()); + assert_eq!( + response_event.kind, + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) + ); + assert!(response_event.tags.iter().any(|tag| { + tag.kind() == RadrootsNostrTagKind::p() + && tag.content() == Some(client_public_key.to_hex().as_str()) + })); + + let handler = handler_with_backend(embedded_backend()); + let _ = handler.filter().expect("handler filter"); + assert_eq!( + handler.parse_request_event(&event).expect("handler parse"), + request + ); + let handler_event = handler + .build_response_event( + client_public_key, + "req-handler", + RadrootsNostrConnectResponse::ConnectAcknowledged, + ) + .expect("handler response") + .build(test_signer().signer_identity.public_key()); + assert_eq!( + handler_event.kind, + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) + ); + } + + #[test] + fn codec_crypto_and_signing_responses_cover_method_matrix() { + let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer()); + let client_public_key = fixture_carol_public_key(); + + assert_eq!( + codec + .crypto_response(RadrootsNostrConnectRequest::Nip04Encrypt { + public_key: client_public_key, + plaintext: "plain".to_owned(), + }) + .expect("nip04 encrypt"), + RadrootsNostrConnectResponse::Nip04Encrypt("plain".to_owned()) + ); + assert_eq!( + codec + .crypto_response(RadrootsNostrConnectRequest::Nip04Decrypt { + public_key: client_public_key, + ciphertext: "cipher".to_owned(), + }) + .expect("nip04 decrypt"), + RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned()) + ); + assert_eq!( + codec + .crypto_response(RadrootsNostrConnectRequest::Nip44Encrypt { + public_key: client_public_key, + plaintext: "plain44".to_owned(), + }) + .expect("nip44 encrypt"), + RadrootsNostrConnectResponse::Nip44Encrypt("plain44".to_owned()) + ); + assert_eq!( + codec + .crypto_response(RadrootsNostrConnectRequest::Nip44Decrypt { + public_key: client_public_key, + ciphertext: "cipher44".to_owned(), + }) + .expect("nip44 decrypt"), + RadrootsNostrConnectResponse::Nip44Decrypt("cipher44".to_owned()) + ); + + let non_crypto = codec + .crypto_response(RadrootsNostrConnectRequest::Ping) + .expect("non crypto response"); + assert!(matches!( + non_crypto, + RadrootsNostrConnectResponse::Error { .. } + )); + + let failing_codec = + super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(false, true)); + for request in [ + RadrootsNostrConnectRequest::Nip04Encrypt { + public_key: client_public_key, + plaintext: "plain".to_owned(), + }, + RadrootsNostrConnectRequest::Nip04Decrypt { + public_key: client_public_key, + ciphertext: "cipher".to_owned(), + }, + RadrootsNostrConnectRequest::Nip44Encrypt { + public_key: client_public_key, + plaintext: "plain44".to_owned(), + }, + RadrootsNostrConnectRequest::Nip44Decrypt { + public_key: client_public_key, + ciphertext: "cipher44".to_owned(), + }, + ] { + assert!(matches!( + failing_codec + .crypto_response(request) + .expect("failing crypto response"), + RadrootsNostrConnectResponse::Error { .. } + )); + } + + let signing = codec + .sign_event_response(unsigned_user_event(1)) + .expect("signing response"); + match signing { + RadrootsNostrConnectResponse::Error { error, .. } => { + assert!(error.contains("failed to sign event")); + } + other => panic!("unexpected sign response: {other:?}"), + } + + let signed = + super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(true, false)) + .sign_event_response(unsigned_user_event(1)) + .expect("signed response"); + assert!(matches!( + signed, + RadrootsNostrConnectResponse::SignedEvent(_) + )); + } + + #[test] + fn handler_connect_policy_paths_cover_registration_branches() { + let client_public_key = fixture_carol_public_key(); + + let rate_limited = handler_with_policy( + embedded_backend(), + TestPolicy { + rate_limit_reason: Some("slow down"), + ..TestPolicy::default() + }, + ) + .handle_request(client_public_key, connect_request(None)) + .expect("rate limit outcome"); + assert_eq!( + response_from_outcome(rate_limited), + RadrootsNostrConnectResponse::Error { + result: None, + error: "slow down".to_owned(), + } + ); + + let denied_registration = handler_with_policy( + embedded_backend(), + TestPolicy { + approval_requirement: None, + ..TestPolicy::default() }, ) + .handle_request(client_public_key, connect_request(None)) + .expect("registration denial"); + assert_eq!( + response_from_outcome(denied_registration), + RadrootsNostrConnectResponse::Error { + result: None, + error: "client public key denied by policy".to_owned(), + } + ); + + let approval_backend = embedded_backend(); + let approval_handler = handler_with_policy( + approval_backend.clone(), + TestPolicy { + approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::ExplicitUser), + ..TestPolicy::default() + }, + ); + let _ = approval_handler + .handle_request(client_public_key, connect_request(None)) + .expect("approval connect"); + let approval_connection = registered_connection(&approval_backend, &client_public_key); + assert_eq!( + approval_connection.approval_requirement, + RadrootsNostrSignerApprovalRequirement::ExplicitUser + ); + } + + #[test] + fn handler_request_paths_cover_base_sign_crypto_denied_and_challenged() { + let backend = embedded_backend(); + let handler = handler_with_backend(backend.clone()); + let client_public_key = fixture_carol_public_key(); + connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); + + assert!(matches!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message("req-pubkey", RadrootsNostrConnectRequest::GetPublicKey), + ) + .expect("pubkey") + ), + RadrootsNostrConnectResponse::UserPublicKey(_) + )); + assert!(matches!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message( + "req-capability", + RadrootsNostrConnectRequest::GetSessionCapability, + ), + ) + .expect("capability") + ), + RadrootsNostrConnectResponse::RemoteSessionCapability(_) + )); + assert_eq!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message("req-relays", RadrootsNostrConnectRequest::SwitchRelays), + ) + .expect("relays") + ), + RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) + ); + assert!(matches!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message( + "req-sign", + RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), + ), + ) + .expect("sign") + ), + RadrootsNostrConnectResponse::Error { .. } + )); + assert_eq!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message( + "req-nip04-decrypt", + RadrootsNostrConnectRequest::Nip04Decrypt { + public_key: client_public_key, + ciphertext: "cipher".to_owned(), + }, + ), + ) + .expect("nip04 decrypt") + ), + RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned()) + ); + assert_eq!( + response_from_outcome( + handler + .handle_request( + client_public_key, + request_message( + "req-nip44-encrypt", + RadrootsNostrConnectRequest::Nip44Encrypt { + public_key: client_public_key, + plaintext: "plain".to_owned(), + }, + ), + ) + .expect("nip44 encrypt") + ), + RadrootsNostrConnectResponse::Nip44Encrypt("plain".to_owned()) + ); + + let unimplemented = handler + .handle_request( + client_public_key, + request_message( + "req-custom", + RadrootsNostrConnectRequest::Custom { + method: RadrootsNostrConnectMethod::Custom("publish_note".to_owned()), + params: vec![], + }, + ), + ) + .expect("custom"); + assert!(matches!( + response_from_outcome(unimplemented), + RadrootsNostrConnectResponse::Error { .. } + )); + + let limited_backend = embedded_backend(); + let limited_handler = handler_with_backend(limited_backend); + connect_with_permissions( + &limited_handler, + client_public_key, + vec![RadrootsNostrConnectPermission::new( + RadrootsNostrConnectMethod::Nip04Encrypt, + )], + ); + let denied_crypto = limited_handler + .handle_request( + client_public_key, + request_message( + "req-denied", + RadrootsNostrConnectRequest::Nip04Decrypt { + public_key: client_public_key, + ciphertext: "cipher".to_owned(), + }, + ), + ) + .expect("denied crypto"); + assert!(matches!( + response_from_outcome(denied_crypto), + RadrootsNostrConnectResponse::Error { .. } + )); + + let denied_backend = embedded_backend(); + let open_handler = handler_with_backend(denied_backend.clone()); + connect_with_permissions(&open_handler, client_public_key, all_runtime_permissions()); + let denying_handler = handler_with_policy( + denied_backend, + TestPolicy { + prepare_denial: Some("policy blocked"), + ..TestPolicy::default() + }, + ); + let denied_base = denying_handler + .handle_request( + client_public_key, + request_message("req-policy-denied", RadrootsNostrConnectRequest::Ping), + ) + .expect("policy denied"); + assert!(matches!( + response_from_outcome(denied_base), + RadrootsNostrConnectResponse::Error { .. } + )); + + let challenge_backend = embedded_backend(); + let challenge_handler = handler_with_backend(challenge_backend.clone()); + connect_with_permissions( + &challenge_handler, + client_public_key, + all_runtime_permissions(), + ); + let challenged = registered_connection(&challenge_backend, &client_public_key); + challenge_backend + .manager() + .require_auth_challenge(&challenged.connection_id, "https://example.test/auth") + .expect("require challenge"); + let auth_url = challenge_handler + .handle_request( + client_public_key, + request_message("req-challenge", RadrootsNostrConnectRequest::Ping), + ) + .expect("challenge"); + assert_eq!( + response_from_outcome(auth_url), + RadrootsNostrConnectResponse::AuthUrl("https://example.test/auth".to_owned()) + ); + } + + #[test] + fn handler_authorized_evaluation_facade_covers_request_variants() { + let backend = embedded_backend(); + let handler = handler_with_backend(backend.clone()); + let client_public_key = fixture_carol_public_key(); + connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); + let connection = registered_connection(&backend, &client_public_key); + + let base = request_message("req-eval-ping", RadrootsNostrConnectRequest::Ping); + let base_eval = backend + .evaluate_request(&connection.connection_id, base.clone()) + .expect("base evaluation"); + assert_eq!( + response_from_outcome( + handler + .handle_authorized_request_evaluation(base, base_eval) + .expect("base authorized") + ), + RadrootsNostrConnectResponse::Pong + ); + + let crypto = request_message( + "req-eval-crypto", + RadrootsNostrConnectRequest::Nip44Decrypt { + public_key: client_public_key, + ciphertext: "sealed".to_owned(), + }, + ); + let crypto_eval = backend + .evaluate_request(&connection.connection_id, crypto.clone()) + .expect("crypto evaluation"); + assert_eq!( + response_from_outcome( + handler + .handle_authorized_request_evaluation(crypto, crypto_eval) + .expect("crypto authorized") + ), + RadrootsNostrConnectResponse::Nip44Decrypt("sealed".to_owned()) + ); + + let sign = request_message( + "req-eval-sign", + RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)), + ); + let sign_eval = backend + .evaluate_request(&connection.connection_id, sign.clone()) + .expect("sign evaluation"); + assert!(matches!( + response_from_outcome( + handler + .handle_authorized_request_evaluation(sign, sign_eval) + .expect("sign authorized") + ), + RadrootsNostrConnectResponse::Error { .. } + )); + + let custom = request_message( + "req-eval-custom", + RadrootsNostrConnectRequest::Custom { + method: RadrootsNostrConnectMethod::Custom("do_work".to_owned()), + params: vec![], + }, + ); + let custom_eval = backend + .evaluate_request(&connection.connection_id, custom.clone()) + .expect("custom evaluation"); + assert!(matches!( + response_from_outcome( + handler + .handle_authorized_request_evaluation(custom, custom_eval) + .expect("custom authorized") + ), + RadrootsNostrConnectResponse::Error { .. } + )); + } + + #[test] + fn standalone_response_helpers_cover_publish_parts_and_hints() { + let backend = embedded_backend(); + let handler = handler_with_backend(backend.clone()); + let client_public_key = fixture_carol_public_key(); + connect_with_permissions(&handler, client_public_key, all_runtime_permissions()); + let connection = registered_connection(&backend, &client_public_key); + + let parts = super::connect_response_outcome(&connection, Some("secret".to_owned())) + .into_publish_parts() + .expect("publish parts"); + assert_eq!( + parts.0, + RadrootsNostrConnectResponse::ConnectSecretEcho("secret".to_owned()) + ); + assert_eq!(parts.1, Some(connection.connection_id.clone())); + assert_eq!(parts.2, Some(connection.connection_id.clone())); + assert!( + RadrootsNostrSignerHandledRequest::Ignore + .into_publish_parts() + .is_none() + ); + assert!( + RadrootsNostrSignerHandledRequest::respond(RadrootsNostrConnectResponse::Pong) + .into_publish_parts() + .is_some() + ); + assert_eq!( + response_from_outcome(RadrootsNostrSignerHandledRequestOutcome::respond( + RadrootsNostrConnectResponse::Pong, + )), + RadrootsNostrConnectResponse::Pong + ); + + assert_eq!( + super::response_from_hint( + &connection, + RadrootsNostrSignerRequestResponseHint::UserPublicKey(client_public_key), + ), + RadrootsNostrConnectResponse::UserPublicKey(client_public_key) + ); + let capability = RadrootsNostrConnectRemoteSessionCapability { + user_public_key: client_public_key, + relays: vec![primary_relay()], + permissions: all_runtime_permissions().into(), + }; + assert_eq!( + super::response_from_hint( + &connection, + RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability.clone(),), + ), + RadrootsNostrConnectResponse::RemoteSessionCapability(capability) + ); + assert_eq!( + super::response_from_hint( + &connection, + RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()]), + ), + RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) + ); + assert_eq!( + super::response_from_hint( + &connection, + RadrootsNostrSignerRequestResponseHint::RelayList(Vec::new()), + ), + RadrootsNostrConnectResponse::RelayList(vec![primary_relay()]) + ); + assert!(matches!( + super::response_from_hint(&connection, RadrootsNostrSignerRequestResponseHint::None), + RadrootsNostrConnectResponse::Error { .. } + )); + + let denied = super::handled_request_for_action( + &connection, + RadrootsNostrSignerRequestAction::Denied { + reason: "blocked".to_owned(), + }, + || Ok(RadrootsNostrConnectResponse::Pong), + ) + .expect("denied action"); + assert!(matches!( + denied, + RadrootsNostrSignerHandledRequest::Respond { .. } + )); + + let allowed = super::handled_request_for_action( + &connection, + RadrootsNostrSignerRequestAction::Allowed { + required_permission: None, + response_hint: RadrootsNostrSignerRequestResponseHint::Pong, + }, + || Ok(RadrootsNostrConnectResponse::Pong), + ) + .expect("allowed action"); + assert!(matches!( + allowed, + RadrootsNostrSignerHandledRequest::Respond { .. } + )); + + let challenged = super::handled_request_for_action( + &connection, + RadrootsNostrSignerRequestAction::Challenged { + auth_challenge: RadrootsNostrSignerAuthChallenge::new( + "https://example.test/auth", + 1, + ) + .expect("challenge"), + pending_request: RadrootsNostrSignerPendingRequest::new( + request_message("req-pending", RadrootsNostrConnectRequest::Ping), + 1, + ) + .expect("pending"), + }, + || Ok(RadrootsNostrConnectResponse::Pong), + ) + .expect("challenged action"); + assert!(matches!( + challenged, + RadrootsNostrSignerHandledRequest::Respond { .. } + )); } #[test]