Tests for 22601be4 (tcp zombie)

This commit is contained in:
Dagur Valberg Johannsson 2026-04-14 08:42:14 +02:00
parent ef56fb198c
commit c8e0a2d7cc
No known key found for this signature in database
GPG key ID: FD701804AEE88107

View file

@ -0,0 +1,220 @@
import { describe, it, expect, vi, afterEach, type Mock } from "vitest";
import { generateRandomBytes, secp256k1 } from "@bitauth/libauth";
// Mock nostr-tools and isomorphic-ws before importing RelayClient
vi.mock("nostr-tools/nip59", () => ({
wrapEvent: vi.fn(() => ({ kind: 1059, content: "wrapped" })),
unwrapEvent: vi.fn((_event: any, _key: any) => ({
kind: 14,
content: '{"action":"dapp_ready","time":9999999999}',
pubkey: "aa".repeat(32),
})),
}));
vi.mock("nostr-tools/pool", () => ({
SimplePool: vi.fn(),
useWebSocketImplementation: vi.fn(),
}));
vi.mock("isomorphic-ws", () => ({ default: vi.fn() }));
import { RelayClient } from "./relay-client.js";
import type { SimplePool, SubCloser } from "nostr-tools/pool";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makePrivateKey(): Uint8Array {
let key: Uint8Array;
do {
key = generateRandomBytes(32);
} while (typeof secp256k1.derivePublicKeyCompressed(key) === "string");
return key;
}
interface MockPoolHandles {
pool: SimplePool;
triggerEose: () => void;
triggerClose: (reasons: string[]) => void;
publishMock: Mock;
}
function makeMockPool(): MockPoolHandles {
let onEose: (() => void) | null = null;
let onClose: ((reasons: string[]) => void) | null = null;
const closeFn = vi.fn();
const publishMock = vi.fn(() => [Promise.resolve("")]);
const pool = {
subscribeMany: vi.fn(
(
_urls: string[],
_filter: any,
callbacks: {
onevent: (event: any) => void;
oneose: () => void;
onclose: (reasons: string[]) => void;
},
) => {
onEose = callbacks.oneose;
onClose = callbacks.onclose;
return { close: closeFn } as SubCloser;
},
),
publish: publishMock,
close: vi.fn(),
} as unknown as SimplePool;
return {
pool,
triggerEose: () => onEose?.(),
triggerClose: (reasons: string[]) => onClose?.(reasons),
publishMock,
};
}
function makeClient(pool: SimplePool) {
const privateKey = makePrivateKey();
const pairedKey = makePrivateKey();
return new RelayClient(
{
explicitRelayUrls: ["wss://test.relay:443"],
signerPrivateKey: privateKey,
pairedPublicKey: pairedKey,
logNetworkActivity: false,
},
pool,
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("RelayClient — publish failure triggers disconnect", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("emits disconnect on publish failure", async () => {
const { pool, triggerEose, publishMock } = makeMockPool();
const client = makeClient(pool);
const disconnects: Error[] = [];
client.on("disconnect", (err: Error) => disconnects.push(err));
await client.connect();
triggerEose();
publishMock.mockReturnValueOnce([Promise.reject(new Error("send failed"))]);
await expect(
client.relay({
action: "dapp_ready" as any,
time: Math.floor(Date.now() / 1000),
}),
).rejects.toThrow(); // Promise.any wraps in AggregateError
expect(disconnects).toHaveLength(1);
});
it("still throws the error to the caller", async () => {
const { pool, triggerEose, publishMock } = makeMockPool();
const client = makeClient(pool);
client.on("disconnect", () => {}); // prevent unhandled
await client.connect();
triggerEose();
publishMock.mockReturnValueOnce([Promise.reject(new Error("relay down"))]);
await expect(
client.relay({
action: "dapp_ready" as any,
time: Math.floor(Date.now() / 1000),
}),
).rejects.toThrow();
});
it("does not double-emit disconnect from publish failure + onclose race", async () => {
const { pool, triggerEose, triggerClose, publishMock } = makeMockPool();
const client = makeClient(pool);
const disconnects: Error[] = [];
client.on("disconnect", (err: Error) => disconnects.push(err));
await client.connect();
triggerEose();
publishMock.mockReturnValueOnce([Promise.reject(new Error("send failed"))]);
await expect(
client.relay({
action: "dapp_ready" as any,
time: Math.floor(Date.now() / 1000),
}),
).rejects.toThrow();
// Subscription onclose also fires (race condition)
triggerClose(["relay gone"]);
expect(disconnects).toHaveLength(1);
});
it("reconnect resets the disconnect guard so future failures emit again", async () => {
const { pool, triggerEose, publishMock } = makeMockPool();
const client = makeClient(pool);
const disconnects: Error[] = [];
client.on("disconnect", (err: Error) => disconnects.push(err));
await client.connect();
triggerEose();
// First publish failure
publishMock.mockReturnValueOnce([Promise.reject(new Error("fail 1"))]);
await client
.relay({ action: "dapp_ready" as any, time: 1 })
.catch(() => {});
expect(disconnects).toHaveLength(1);
// Reconnect
await client.connect();
triggerEose();
// Second publish failure — should emit again
publishMock.mockReturnValueOnce([Promise.reject(new Error("fail 2"))]);
await client
.relay({ action: "dapp_ready" as any, time: 2 })
.catch(() => {});
expect(disconnects).toHaveLength(2);
});
});
describe("RelayClient — isConnected", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("returns false before connect", () => {
const { pool } = makeMockPool();
const client = makeClient(pool);
expect(client.isConnected()).toBe(false);
});
it("returns true after connect", async () => {
const { pool } = makeMockPool();
const client = makeClient(pool);
await client.connect();
expect(client.isConnected()).toBe(true);
});
it("returns false after disconnect", async () => {
const { pool } = makeMockPool();
const client = makeClient(pool);
await client.connect();
await client.disconnect();
expect(client.isConnected()).toBe(false);
});
});