//! Integration tests for the nostr channel using a fake `NostrFetcher`. //! The real fetcher uses websockets to live relays; tests substitute //! canned events so they're hermetic. use std::sync::Arc; use async_trait::async_trait; use chrono::Utc; use kez_channels::nostr::{KEZ_NOSTR_KIND, NostrChannel, NostrEvent, NostrFetcher, NostrFilter}; use kez_channels::{Channel, ChannelError, ChannelResult}; use kez_core::{ClaimPayload, Identity, NostrSecret, SignedClaim, nostr_pubkey_hex}; struct CapturingFetcher { events: Vec, expected_authors: Vec, expected_kinds: Vec, } #[async_trait] impl NostrFetcher for CapturingFetcher { async fn fetch_events(&self, filter: &NostrFilter) -> ChannelResult> { assert_eq!(filter.authors, self.expected_authors, "wrong authors filter"); assert_eq!(filter.kinds, self.expected_kinds, "wrong kinds filter"); Ok(self.events.clone()) } } struct FailingFetcher; #[async_trait] impl NostrFetcher for FailingFetcher { async fn fetch_events(&self, _filter: &NostrFilter) -> ChannelResult> { Err(ChannelError::Unreachable("all relays down".into())) } } fn make_event(pubkey_hex: &str, content: String) -> NostrEvent { NostrEvent { id: "0".repeat(64), pubkey: pubkey_hex.to_owned(), created_at: Utc::now().timestamp(), kind: KEZ_NOSTR_KIND, tags: vec![vec!["d".to_owned(), "kez".to_owned()]], content, sig: "f".repeat(128), } } fn sign_for_self() -> (NostrSecret, Identity, SignedClaim) { let secret = NostrSecret::generate(); let identity = Identity::parse(format!("nostr:{}", secret.npub())).unwrap(); let signed = SignedClaim::sign( ClaimPayload::new(identity.clone(), identity.clone(), Utc::now()), &secret, ) .unwrap(); (secret, identity, signed) } #[tokio::test] async fn verifies_self_published_proof_from_relay() { let (_secret, identity, signed) = sign_for_self(); let pubkey_hex = nostr_pubkey_hex(&identity).unwrap(); let compact = signed.to_compact().unwrap(); let fetcher = CapturingFetcher { events: vec![make_event(&pubkey_hex, compact)], expected_authors: vec![pubkey_hex.clone()], expected_kinds: vec![KEZ_NOSTR_KIND], }; let channel = NostrChannel::with_fetcher(Arc::new(fetcher)); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn skips_events_whose_pubkey_field_mismatches() { // Relay returns an event with a forged content-pubkey discrepancy: // event.pubkey claims to be someone else even though the filter asked // for `identity`. We must not trust the content in that case. let (_secret_a, identity_a, signed_a) = sign_for_self(); let (_secret_b, identity_b, _signed_b) = sign_for_self(); let pubkey_b_hex = nostr_pubkey_hex(&identity_b).unwrap(); let compact_a = signed_a.to_compact().unwrap(); let fetcher = CapturingFetcher { events: vec![make_event(&pubkey_b_hex, compact_a)], expected_authors: vec![nostr_pubkey_hex(&identity_a).unwrap()], expected_kinds: vec![KEZ_NOSTR_KIND], }; let channel = NostrChannel::with_fetcher(Arc::new(fetcher)); let err = channel.fetch_and_verify(&identity_a).await.unwrap_err(); assert!( matches!(err, ChannelError::NotFound(_)), "expected NotFound (all events rejected by author check), got {err:?}" ); } #[tokio::test] async fn rejects_proof_signed_for_different_subject() { // The event is correctly authored, but the embedded claim is for a // different identity. The subject-mismatch check must fire. let (secret_a, identity_a, _signed_self_a) = sign_for_self(); let (_secret_b, identity_b, _signed_self_b) = sign_for_self(); let pubkey_a_hex = nostr_pubkey_hex(&identity_a).unwrap(); // A signs a claim with subject == B (legitimate proof but for B, not A). let claim_for_b = SignedClaim::sign( ClaimPayload::new(identity_b.clone(), identity_a.clone(), Utc::now()), &secret_a, ) .unwrap(); let compact = claim_for_b.to_compact().unwrap(); let fetcher = CapturingFetcher { events: vec![make_event(&pubkey_a_hex, compact)], expected_authors: vec![pubkey_a_hex.clone()], expected_kinds: vec![KEZ_NOSTR_KIND], }; let channel = NostrChannel::with_fetcher(Arc::new(fetcher)); let err = channel.fetch_and_verify(&identity_a).await.unwrap_err(); assert!( matches!(err, ChannelError::SubjectMismatch { .. }), "expected SubjectMismatch, got {err:?}" ); } #[tokio::test] async fn no_events_yields_not_found() { let (_s, identity, _signed) = sign_for_self(); let fetcher = CapturingFetcher { events: vec![], expected_authors: vec![nostr_pubkey_hex(&identity).unwrap()], expected_kinds: vec![KEZ_NOSTR_KIND], }; let channel = NostrChannel::with_fetcher(Arc::new(fetcher)); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!(matches!(err, ChannelError::NotFound(_))); } #[tokio::test] async fn fetcher_failure_surfaces_as_unreachable() { let (_s, identity, _signed) = sign_for_self(); let channel = NostrChannel::with_fetcher(Arc::new(FailingFetcher)); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!(matches!(err, ChannelError::Unreachable(_))); }