diff --git a/extensions/nostr/src/nostr-bus.publish.test.ts b/extensions/nostr/src/nostr-bus.publish.test.ts new file mode 100644 index 000000000..191158c34 --- /dev/null +++ b/extensions/nostr/src/nostr-bus.publish.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it, vi } from "vitest"; + +const TEST_HEX_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const TEST_PUBKEY = "f".repeat(64); + +let lastPool: { + publish: ReturnType; + subscribeMany: ReturnType; +} | null = null; + +vi.mock("nostr-tools", async () => { + const actual = await vi.importActual("nostr-tools"); + class MockPool { + publish = vi.fn(); + subscribeMany = vi.fn(() => ({ close: vi.fn() })); + constructor() { + lastPool = this as unknown as { + publish: ReturnType; + subscribeMany: ReturnType; + }; + } + } + return { + ...actual, + SimplePool: MockPool, + }; +}); + +vi.mock("nostr-tools/nip04", async () => { + return { + encrypt: vi.fn(async () => "cipher"), + decrypt: vi.fn(async () => "plain"), + }; +}); + +vi.mock("./nostr-state-store.js", () => { + return { + readNostrBusState: vi.fn(async () => null), + writeNostrBusState: vi.fn(async () => undefined), + computeSinceTimestamp: vi.fn(() => 0), + readNostrProfileState: vi.fn(async () => null), + writeNostrProfileState: vi.fn(async () => undefined), + }; +}); + +describe("startNostrBus publish handling", () => { + it("awaits publish rejections for DMs without unhandled rejection", async () => { + const { startNostrBus } = await import("./nostr-bus.js"); + const onError = vi.fn(); + const bus = await startNostrBus({ + privateKey: TEST_HEX_KEY, + relays: ["wss://relay.test"], + onMessage: async () => {}, + onError, + }); + + expect(lastPool).not.toBeNull(); + lastPool?.publish.mockReturnValue([Promise.reject(new Error("rate-limited"))]); + + let unhandled: unknown; + process.once("unhandledRejection", (reason) => { + unhandled = reason; + }); + + await expect(bus.sendDm(TEST_PUBKEY, "hi")).rejects.toThrow("rate-limited"); + await new Promise((resolve) => setImmediate(resolve)); + + expect(unhandled).toBeUndefined(); + expect(onError).toHaveBeenCalled(); + + bus.close(); + }); + + it("does not throw on typing publish failures", async () => { + const { startNostrBus } = await import("./nostr-bus.js"); + const onError = vi.fn(); + const bus = await startNostrBus({ + privateKey: TEST_HEX_KEY, + relays: ["wss://relay.test"], + onMessage: async () => {}, + onError, + }); + + expect(lastPool).not.toBeNull(); + lastPool?.publish.mockReturnValue([Promise.reject(new Error("rate-limited"))]); + + let unhandled: unknown; + process.once("unhandledRejection", (reason) => { + unhandled = reason; + }); + + await expect(bus.sendTypingStart(TEST_PUBKEY)).resolves.toBeUndefined(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(unhandled).toBeUndefined(); + expect(onError).toHaveBeenCalled(); + + bus.close(); + }); +});