Examples
Practical usage examples for tsunagiya.
Table of Contents
- Basic REQ/EVENT Testing
- Event Publishing Testing
- Multiple Relay Testing
- Filter Matching Testing
- Custom REQ Handlers
- Error Handling Testing
- NIP-42 AUTH Testing
- Large Volume Event Testing
- Real-time Stream Testing
- Thread and Reaction Testing
- Invalid Data and Logging Testing
- Snapshot-based Testing
- Early-Capture Library Support
Basic REQ/EVENT Testing
Basic REQ/EVENT testing:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
// ❌ 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 MockWebSocketThe 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:
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.
// 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.
Related Documentation
- API Reference — Full API details
- Test Patterns — Test pattern collection
- Best Practices — Test design best practices