Skip to content

Examples

Practical usage examples for tsunagiya.

Table of Contents

  1. Basic REQ/EVENT Testing
  2. Event Publishing Testing
  3. Multiple Relay Testing
  4. Filter Matching Testing
  5. Custom REQ Handlers
  6. Error Handling Testing
  7. NIP-42 AUTH Testing
  8. Large Volume Event Testing
  9. Real-time Stream Testing
  10. Thread and Reaction Testing
  11. Invalid Data and Logging Testing
  12. Snapshot-based Testing
  13. Early-Capture Library Support

Basic REQ/EVENT Testing

Basic REQ/EVENT testing:

typescript
import { MockPool } from "@ikuradon/tsunagiya";
import { assertReceivedREQ, EventBuilder } from "@ikuradon/tsunagiya/testing";
import { assertEquals } from "@std/assert";

Deno.test("fetch kind:1 events", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  relay.store(EventBuilder.kind1().content("hello").build());
  relay.store(EventBuilder.kind(0).content('{"name":"Alice"}').build());

  pool.install();
  try {
    const events: string[] = [];
    const ws = new WebSocket("wss://relay.example.com");

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["REQ", "sub1", { kinds: [1] }]));
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") events.push(msg[2].content);
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    assertEquals(events, ["hello"]); // kind:0 is filtered out
    assertReceivedREQ(relay, { kinds: [1] });
  } finally {
    pool.uninstall();
  }
});

Event publishing test:

typescript
Deno.test("publish an event and receive OK", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  pool.install();
  try {
    const event = EventBuilder.kind1().content("my post").build();
    let okReceived = false;

    const ws = new WebSocket("wss://relay.example.com");
    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["EVENT", event]));
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "OK" && msg[1] === event.id && msg[2] === true) {
          okReceived = true;
          ws.close();
        }
      };
      ws.onclose = () => resolve();
    });

    assertEquals(okReceived, true);
    assertEquals(relay.hasEvent(event.id), true);
  } finally {
    pool.uninstall();
  }
});

Multiple relay testing:

typescript
Deno.test("aggregate events from 3 relays", async () => {
  const pool = new MockPool();
  const urls = [
    "wss://relay1.example.com",
    "wss://relay2.example.com",
    "wss://relay3.example.com",
  ];

  urls.forEach((url, i) => {
    pool.relay(url).store(
      EventBuilder.kind1().content(`event from relay ${i + 1}`).build(),
    );
  });

  pool.install();
  try {
    const allEvents: string[] = [];
    let done = 0;

    await new Promise<void>((resolve) => {
      for (const url of urls) {
        const ws = new WebSocket(url);
        ws.onopen = () => ws.send(JSON.stringify(["REQ", "s", { kinds: [1] }]));
        ws.onmessage = (e) => {
          const msg = JSON.parse(e.data);
          if (msg[0] === "EVENT") allEvents.push(msg[2].content);
          if (msg[0] === "EOSE") ws.close();
        };
        ws.onclose = () => {
          if (++done === 3) resolve();
        };
      }
    });

    assertEquals(allEvents.length, 3);
  } finally {
    pool.uninstall();
  }
});

Filter matching testing:

typescript
import { filterEvents, matchFilter } from "@ikuradon/tsunagiya";

Deno.test("filter matching", () => {
  const event = EventBuilder.kind1()
    .pubkey("alice")
    .createdAt(1700000000)
    .tag("t", "nostr")
    .build();

  // kind match
  assertEquals(matchFilter(event, { kinds: [1] }), true);
  assertEquals(matchFilter(event, { kinds: [0] }), false);

  // author match (prefix)
  assertEquals(matchFilter(event, { authors: ["alice"] }), true);

  // time range
  assertEquals(matchFilter(event, { since: 1699999999 }), true);
  assertEquals(matchFilter(event, { since: 1700000001 }), false);

  // tag filter
  assertEquals(matchFilter(event, { "#t": ["nostr"] }), true);
  assertEquals(matchFilter(event, { "#t": ["bitcoin"] }), false);
});

Deno.test("apply limit with filterEvents", () => {
  const events = EventBuilder.timeline(100, { kind: 1 });
  const result = filterEvents(events, { kinds: [1], limit: 10 });
  assertEquals(result.length, 10);
});

Custom REQ Handlers, Error Handling, and AUTH

Custom REQ handler:

typescript
Deno.test("return dynamic events with custom REQ handler", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  relay.onREQ((subId, filters) => {
    // dynamically generate events based on filter
    const kind = filters[0]?.kinds?.[0] ?? 1;
    return [
      EventBuilder.kind(kind).content(`dynamic event for ${subId}`).build(),
    ];
  });

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    let content = "";

    await new Promise<void>((resolve) => {
      ws.onopen = () =>
        ws.send(JSON.stringify(["REQ", "my-sub", { kinds: [1] }]));
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") content = msg[2].content;
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    assertEquals(content, "dynamic event for my-sub");
  } finally {
    pool.uninstall();
  }
});

Error handling testing:

typescript
Deno.test("connection to unregistered URL fails", async () => {
  const pool = new MockPool();
  pool.relay("wss://known.relay.test"); // register only a different URL

  pool.install();
  try {
    const ws = new WebSocket("wss://unknown.relay.test");
    let errorFired = false;

    const code = await new Promise<number>((resolve) => {
      ws.onerror = () => {
        errorFired = true;
      };
      ws.onclose = (e) => resolve(e.code);
    });

    assertEquals(errorFired, true);
    assertEquals(code, 1006);
  } finally {
    pool.uninstall();
  }
});

Deno.test("EVENT rejection case", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  relay.onEVENT((event) => {
    return ["OK", event.id, false, "blocked: content policy violation"];
  });

  pool.install();
  try {
    const event = EventBuilder.kind1().content("spam").build();
    const ws = new WebSocket("wss://relay.example.com");

    const result = await new Promise<[boolean, string]>((resolve) => {
      ws.onopen = () => ws.send(JSON.stringify(["EVENT", event]));
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "OK") {
          resolve([msg[2], msg[3]]);
          ws.close();
        }
      };
    });

    assertEquals(result[0], false);
    assertEquals(result[1], "blocked: content policy violation");
  } finally {
    pool.uninstall();
  }
});

Deno.test("receive NOTICE message", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    let notice = "";

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        relay.sendNotice("rate-limited: slow down");
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "NOTICE") {
          notice = msg[1];
          ws.close();
        }
      };
      ws.onclose = () => resolve();
    });

    assertEquals(notice, "rate-limited: slow down");
  } finally {
    pool.uninstall();
  }
});

NIP-42 AUTH testing:

typescript
Deno.test("AUTH challenge/response", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://auth.relay.test", { requiresAuth: true });

  relay.requireAuth((authEvent, context) => {
    return authEvent.tags.some(
      (t) => t[0] === "relay" && t[1] === context.relayUrl,
    );
  });

  pool.install();
  try {
    const ws = new WebSocket("wss://auth.relay.test");
    let authResult = false;

    await new Promise<void>((resolve) => {
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "AUTH") {
          const challenge = msg[1];
          const authEvent = EventBuilder.kind(22242)
            .tag("relay", "wss://auth.relay.test")
            .tag("challenge", challenge)
            .build();
          ws.send(JSON.stringify(["AUTH", authEvent]));
        }
        if (msg[0] === "OK") {
          authResult = msg[2];
          ws.close();
        }
      };
      ws.onclose = () => resolve();
    });

    assertEquals(authResult, true);
  } finally {
    pool.uninstall();
  }
});

Large volume event testing:

typescript
Deno.test("process 1000 events", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  const events = EventBuilder.bulk(1000, { kind: 1 });
  for (const e of events) relay.store(e);

  pool.install();
  try {
    const received: unknown[] = [];
    const ws = new WebSocket("wss://relay.example.com");

    await new Promise<void>((resolve) => {
      ws.onopen = () => ws.send(JSON.stringify(["REQ", "s", { kinds: [1] }]));
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") received.push(msg[2]);
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    assertEquals(received.length, 1000);
  } finally {
    pool.uninstall();
  }
});

Streams, Threads, Reactions, and Snapshots

Real-time stream testing:

typescript
import { startStream, streamEvents } from "@ikuradon/tsunagiya/testing";

Deno.test("events delivered with time delay", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  const events = EventBuilder.bulk(5, { kind: 1 });

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    const received: unknown[] = [];

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["REQ", "stream", { kinds: [1] }]));

        const handle = streamEvents(relay, events, { interval: 100 });

        setTimeout(() => {
          handle.stop();
          ws.close();
        }, 700);
      };

      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") received.push(msg[2]);
      };

      ws.onclose = () => resolve();
    });

    assertEquals(received.length >= 3, true);
  } finally {
    pool.uninstall();
  }
});

Deno.test("continuous stream", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    const received: unknown[] = [];

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["REQ", "live", { kinds: [1] }]));

        const stream = startStream(relay, {
          eventGenerator: () => EventBuilder.random({ kind: 1 }),
          interval: 50,
          count: 5,
        });

        setTimeout(() => {
          stream.stop();
          ws.close();
        }, 500);
      };

      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") received.push(msg[2]);
      };

      ws.onclose = () => resolve();
    });

    assertEquals(received.length, 5);
  } finally {
    pool.uninstall();
  }
});

Thread and reaction testing:

typescript
Deno.test("fetch thread", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  const thread = EventBuilder.thread(5);
  for (const e of thread) relay.store(e);

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    const replies: unknown[] = [];

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["REQ", "thread", { "#e": [thread[0].id] }]));
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") replies.push(msg[2]);
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    assertEquals(replies.length, 4);
  } finally {
    pool.uninstall();
  }
});

Deno.test("fetch reactions", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  const [post, reactions] = EventBuilder.withReactions(10);
  relay.store(post);
  for (const r of reactions) relay.store(r);

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    const received: unknown[] = [];

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(
          JSON.stringify(["REQ", "reactions", { kinds: [7], "#e": [post.id] }]),
        );
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EVENT") received.push(msg[2]);
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    assertEquals(received.length, 10);
  } finally {
    pool.uninstall();
  }
});

Invalid data and logging testing:

typescript
Deno.test("receive invalid JSON", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    const messages: string[] = [];

    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        relay.sendRaw("this is not json");
        relay.sendRaw('{"also": "not a nostr message"}');
        setTimeout(() => ws.close(), 100);
      };
      ws.onmessage = (e) => messages.push(e.data);
      ws.onclose = () => resolve();
    });

    assertEquals(messages.length, 2);
  } finally {
    pool.uninstall();
  }
});

Deno.test("custom log handler", async () => {
  const pool = new MockPool();
  const logs: LogEntry[] = [];

  pool.relay("wss://relay.example.com", {
    logging: (entry) => logs.push(entry),
  });

  pool.install();
  try {
    const ws = new WebSocket("wss://relay.example.com");
    await new Promise<void>((resolve) => {
      ws.onopen = () => {
        ws.send(JSON.stringify(["REQ", "s", { kinds: [1] }]));
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg[0] === "EOSE") ws.close();
      };
      ws.onclose = () => resolve();
    });

    const receives = logs.filter((l) => l.direction === "receive");
    const sends = logs.filter((l) => l.direction === "send");
    assertEquals(receives.length >= 1, true);
    assertEquals(sends.length >= 1, true);
  } finally {
    pool.uninstall();
  }
});

Snapshot-based testing:

typescript
import { restore, snapshot } from "@ikuradon/tsunagiya/testing";

Deno.test("efficiently run multiple test cases with snapshot", () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  const baseEvents = EventBuilder.bulk(10, { kind: 1 });
  for (const e of baseEvents) relay.store(e);

  const baseline = snapshot(relay);

  relay.store(EventBuilder.kind1().content("extra").build());
  // ... assertions ...

  restore(relay, baseline);

  relay.store(EventBuilder.kind(7).content("+").build());
  // ... assertions ...

  restore(relay, baseline);
});

Early-Capture Library Support

Some Nostr client libraries (such as NDK) capture a reference to globalThis.WebSocket at module load time. This means that even if you call pool.install(), those libraries may not use the MockWebSocket.

For libraries with this "early-capture" behavior, use the bootstrap pattern.

Why the Normal Approach Doesn't Work

typescript
// ❌ This won't work — NDK already captured WebSocket at module load time
import NDK from "@nostr-dev-kit/ndk";

const pool = new MockPool();
pool.install();
// NDK already holds a reference to the real WebSocket and won't use MockWebSocket

The Bootstrap Pattern

Install pool.install() first, then use a dynamic import for the library. This allows the library to capture MockWebSocket during its module load.

NDK bootstrap pattern:

typescript
import { MockPool } from "@ikuradon/tsunagiya";

// 1. Install MockPool first
const bootstrap = new MockPool();
bootstrap.relay("wss://bootstrap");
bootstrap.install();

// 2. Dynamic import NDK (captures MockWebSocket at this point)
const { default: NDK } = await import("@nostr-dev-kit/ndk");

// 3. Uninstall the bootstrap MockPool
bootstrap.uninstall();

// Write tests as usual from here
Deno.test("fetch events with NDK", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  relay.store(EventBuilder.kind1().content("hello from NDK").build());

  pool.install();
  try {
    const ndk = new NDK({ explicitRelayUrls: ["wss://relay.example.com"] });
    await ndk.connect();

    const events = await ndk.fetchEvents({ kinds: [1] });
    assertEquals([...events].length, 1);
  } finally {
    pool.uninstall();
  }
});

Applying It in Real Test Files

In a real project, run the bootstrap at the top level of the test file, then use a regular MockPool inside each test function.

typescript
// test_file.ts

import { MockPool } from "@ikuradon/tsunagiya";

// Bootstrap at the file top level (runs only once)
const _bootstrap = new MockPool();
_bootstrap.relay("wss://bootstrap");
_bootstrap.install();

// Dynamic import NDK (captures MockWebSocket)
const client = await import("./client.ts"); // module that imports NDK

_bootstrap.uninstall();

// Use a regular MockPool in each test as usual
Deno.test("fetch timeline", async () => {
  const pool = new MockPool();
  const relay = pool.relay("wss://relay.example.com");

  relay.store(EventBuilder.kind1().content("hello").build());

  pool.install();
  try {
    const events = await client.timeline(["wss://relay.example.com"]);
    assertEquals(events.length, 1);
  } finally {
    pool.uninstall();
  }
});

Note: Run the bootstrap only once at the top level of the test file. Do not repeat it inside individual test functions.


MIT License