nostr-sdk 0.45.0

A full-featured SDK for building high-performance and reliable nostr applications.
Documentation
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

//! Nostr Relay Builder and Mock Relay for tests

mod builder;
mod local;
mod mock;

pub use self::builder::*;
pub use self::local::*;
pub use self::mock::*;

#[cfg(test)]
mod tests {
    use std::future::Future;
    use std::pin::Pin;

    use nostr::event::{EventBuilder, FinalizeEvent, Kind, Tag};
    use nostr::filter::Filter;
    use nostr::key::{Keys, PublicKey};
    use nostr::nips::nip17::PrivateDirectMessageBuilder;
    use nostr_memory::MemoryDatabase;

    use super::*;
    use crate::client::Client;

    const UPDATE_TAG: &str = "updated";

    #[derive(Debug)]
    struct UpdateFilterPlugin;

    impl QueryPolicy for UpdateFilterPlugin {
        fn admit_query<'a>(
            &'a self,
            query: &'a mut Filter,
            _addr: &'a std::net::SocketAddr,
        ) -> Pin<Box<dyn Future<Output = QueryPolicyResult> + Send + 'a>> {
            Box::pin(async move {
                *query = query.clone().hashtag(UPDATE_TAG);
                QueryPolicyResult::Accept
            })
        }
    }

    #[tokio::test]
    async fn update_filter() {
        let relay = LocalRelay::builder()
            .database(MemoryDatabase::unbounded())
            .query_policy(UpdateFilterPlugin)
            .build();
        relay.run().await.unwrap();

        let keys = Keys::generate();
        let client = Client::default();

        client
            .add_relay(relay.url().await)
            .and_connect()
            .await
            .unwrap();

        // Event with our target tag
        let event = EventBuilder::new(Kind::TextNote, ":)")
            .tag(Tag::hashtag(UPDATE_TAG))
            .finalize(&keys)
            .unwrap();
        client.send_event(&event).await.unwrap();

        // This event has a random tag and should be filtered out in the REQ.
        // It would only appear if the filter had not been updated correctly.
        let event = EventBuilder::new(Kind::TextNote, ":)")
            .tag(Tag::hashtag("TEST"))
            .finalize(&keys)
            .unwrap();
        client.send_event(&event).await.unwrap();

        // Empty filter to get all events. It should be updated to have `UPDATE_TAG`
        let events = client.fetch_events(Filter::new()).await.unwrap();

        assert!(!events.is_empty(), "Should not be empty");
        assert!(
            events
                .iter()
                .all(|e| { e.tags.hashtags().all(|hashtag| hashtag == UPDATE_TAG) }),
            "All tags should have the updated filter tag"
        );
    }

    #[tokio::test]
    async fn kind_blacklist() {
        let relay = LocalRelay::builder()
            .database(MemoryDatabase::unbounded())
            .blacklist_kinds(&[Kind::TextNote])
            .build();
        relay.run().await.unwrap();

        let keys = Keys::generate();
        let client = Client::default();

        client
            .add_relay(relay.url().await)
            .and_connect()
            .await
            .unwrap();
        let event = EventBuilder::new(Kind::TextNote, ":)")
            .finalize(&keys)
            .unwrap();
        let output = client.send_event(&event).await.unwrap();

        assert_eq!(
            "blocked: kind `1` is not accepted by this relay",
            output.failed.values().next().unwrap()
        )
    }

    #[tokio::test]
    async fn invalid_gift_wrap() {
        let relay = LocalRelay::builder()
            .database(MemoryDatabase::unbounded())
            .blacklist_kinds(&[Kind::TextNote])
            .build();
        relay.run().await.unwrap();

        let keys = Keys::generate();
        let client = Client::default();

        client
            .add_relay(relay.url().await)
            .and_connect()
            .await
            .unwrap();
        let event = PrivateDirectMessageBuilder::new(keys.public_key(), "Hey")
            .extra_tags([Tag::public_key(PublicKey::from_slice(&[0; 32]).unwrap())])
            .finalize(&keys)
            .unwrap();
        let output = client.send_event(&event).await.unwrap();

        assert_eq!(
            "blocked: GiftWrap must contain exactly one recipient public key",
            output.failed.values().next().unwrap()
        );

        let event = EventBuilder::new(Kind::GiftWrap, "Hey")
            .finalize(&keys)
            .unwrap();
        let output = client.send_event(&event).await.unwrap();

        assert_eq!(
            "blocked: GiftWrap must contain exactly one recipient public key",
            output.failed.values().next().unwrap()
        );
    }

    #[tokio::test]
    async fn event_size() {
        const MAX_SIZE: usize = 500;

        let relay = LocalRelay::builder()
            .max_event_size(MAX_SIZE)
            .database(MemoryDatabase::unbounded())
            .build();
        relay.run().await.unwrap();

        let keys = Keys::generate();
        let client = Client::default();

        client
            .add_relay(relay.url().await)
            .and_connect()
            .await
            .unwrap();

        let base_event_size = EventBuilder::new(Kind::TextNote, "")
            .finalize(&keys)
            .unwrap()
            .as_json()
            .len();

        let equal_max_size =
            EventBuilder::new(Kind::TextNote, ".".repeat(MAX_SIZE - base_event_size))
                .finalize(&keys)
                .unwrap();
        let greater_max_size =
            EventBuilder::new(Kind::TextNote, ".".repeat((MAX_SIZE - base_event_size) + 1))
                .finalize(&keys)
                .unwrap();

        let output = client.send_event(&equal_max_size).await.unwrap();
        dbg!(&output);
        assert!(!output.success.is_empty());

        let output = client.send_event(&greater_max_size).await.unwrap();
        assert_eq!(
            "blocked: event size (501 bytes) exceeds maximum allowed size (500 bytes)",
            output.failed.values().next().unwrap()
        );
    }
}