Tests for 22601be4 (tcp zombie)
This commit is contained in:
parent
ef56fb198c
commit
c8e0a2d7cc
1 changed files with 220 additions and 0 deletions
220
packages/core/src/relay-client.test.ts
Normal file
220
packages/core/src/relay-client.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue