import { describe, expect, it } from "vitest"; import { validatePrivateKey, isValidPubkey, normalizePubkey } from "./nostr-bus.js"; import { createSeenTracker } from "./seen-tracker.js"; import { createMetrics, type MetricName } from "./metrics.js"; // ============================================================================ // Fuzz Tests for validatePrivateKey // ============================================================================ describe("validatePrivateKey fuzz", () => { describe("type confusion", () => { it("rejects null input", () => { expect(() => validatePrivateKey(null as unknown as string)).toThrow(); }); it("rejects undefined input", () => { expect(() => validatePrivateKey(undefined as unknown as string)).toThrow(); }); it("rejects number input", () => { expect(() => validatePrivateKey(123 as unknown as string)).toThrow(); }); it("rejects boolean input", () => { expect(() => validatePrivateKey(true as unknown as string)).toThrow(); }); it("rejects object input", () => { expect(() => validatePrivateKey({} as unknown as string)).toThrow(); }); it("rejects array input", () => { expect(() => validatePrivateKey([] as unknown as string)).toThrow(); }); it("rejects function input", () => { expect(() => validatePrivateKey((() => {}) as unknown as string)).toThrow(); }); }); describe("unicode attacks", () => { it("rejects unicode lookalike characters", () => { // Using zero-width characters const withZeroWidth = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u200Bf"; expect(() => validatePrivateKey(withZeroWidth)).toThrow(); }); it("rejects RTL override", () => { const withRtl = "\u202E0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(() => validatePrivateKey(withRtl)).toThrow(); }); it("rejects homoglyph 'a' (Cyrillic а)", () => { // Using Cyrillic 'а' (U+0430) instead of Latin 'a' const withCyrillicA = "0123456789\u0430bcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(() => validatePrivateKey(withCyrillicA)).toThrow(); }); it("rejects emoji", () => { const withEmoji = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab😀"; expect(() => validatePrivateKey(withEmoji)).toThrow(); }); it("rejects combining characters", () => { // 'a' followed by combining acute accent const withCombining = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u0301"; expect(() => validatePrivateKey(withCombining)).toThrow(); }); }); describe("injection attempts", () => { it("rejects null byte injection", () => { const withNullByte = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\x00f"; expect(() => validatePrivateKey(withNullByte)).toThrow(); }); it("rejects newline injection", () => { const withNewline = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\nf"; expect(() => validatePrivateKey(withNewline)).toThrow(); }); it("rejects carriage return injection", () => { const withCR = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\rf"; expect(() => validatePrivateKey(withCR)).toThrow(); }); it("rejects tab injection", () => { const withTab = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\tf"; expect(() => validatePrivateKey(withTab)).toThrow(); }); it("rejects form feed injection", () => { const withFormFeed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\ff"; expect(() => validatePrivateKey(withFormFeed)).toThrow(); }); }); describe("edge cases", () => { it("rejects very long string", () => { const veryLong = "a".repeat(10000); expect(() => validatePrivateKey(veryLong)).toThrow(); }); it("rejects string of spaces matching length", () => { const spaces = " ".repeat(64); expect(() => validatePrivateKey(spaces)).toThrow(); }); it("rejects hex with spaces between characters", () => { const withSpaces = "01 23 45 67 89 ab cd ef 01 23 45 67 89 ab cd ef 01 23 45 67 89 ab cd ef 01 23 45 67 89 ab cd ef"; expect(() => validatePrivateKey(withSpaces)).toThrow(); }); }); describe("nsec format edge cases", () => { it("rejects nsec with invalid bech32 characters", () => { // 'b', 'i', 'o' are not valid bech32 characters const invalidBech32 = "nsec1qypqxpq9qtpqscx7peytbfwtdjmcv0mrz5rjpej8vjppfkqfqy8skqfv3l"; expect(() => validatePrivateKey(invalidBech32)).toThrow(); }); it("rejects nsec with wrong prefix", () => { expect(() => validatePrivateKey("nsec0aaaa")).toThrow(); }); it("rejects partial nsec", () => { expect(() => validatePrivateKey("nsec1")).toThrow(); }); }); }); // ============================================================================ // Fuzz Tests for isValidPubkey // ============================================================================ describe("isValidPubkey fuzz", () => { describe("type confusion", () => { it("handles null gracefully", () => { expect(isValidPubkey(null as unknown as string)).toBe(false); }); it("handles undefined gracefully", () => { expect(isValidPubkey(undefined as unknown as string)).toBe(false); }); it("handles number gracefully", () => { expect(isValidPubkey(123 as unknown as string)).toBe(false); }); it("handles object gracefully", () => { expect(isValidPubkey({} as unknown as string)).toBe(false); }); }); describe("malicious inputs", () => { it("rejects __proto__ key", () => { expect(isValidPubkey("__proto__")).toBe(false); }); it("rejects constructor key", () => { expect(isValidPubkey("constructor")).toBe(false); }); it("rejects toString key", () => { expect(isValidPubkey("toString")).toBe(false); }); }); }); // ============================================================================ // Fuzz Tests for normalizePubkey // ============================================================================ describe("normalizePubkey fuzz", () => { describe("prototype pollution attempts", () => { it("throws for __proto__", () => { expect(() => normalizePubkey("__proto__")).toThrow(); }); it("throws for constructor", () => { expect(() => normalizePubkey("constructor")).toThrow(); }); it("throws for prototype", () => { expect(() => normalizePubkey("prototype")).toThrow(); }); }); describe("case sensitivity", () => { it("normalizes uppercase to lowercase", () => { const upper = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; const lower = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(normalizePubkey(upper)).toBe(lower); }); it("normalizes mixed case to lowercase", () => { const mixed = "0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf"; const lower = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(normalizePubkey(mixed)).toBe(lower); }); }); }); // ============================================================================ // Fuzz Tests for SeenTracker // ============================================================================ describe("SeenTracker fuzz", () => { describe("malformed IDs", () => { it("handles empty string IDs", () => { const tracker = createSeenTracker({ maxEntries: 100 }); expect(() => tracker.add("")).not.toThrow(); expect(tracker.peek("")).toBe(true); tracker.stop(); }); it("handles very long IDs", () => { const tracker = createSeenTracker({ maxEntries: 100 }); const longId = "a".repeat(100000); expect(() => tracker.add(longId)).not.toThrow(); expect(tracker.peek(longId)).toBe(true); tracker.stop(); }); it("handles unicode IDs", () => { const tracker = createSeenTracker({ maxEntries: 100 }); const unicodeId = "事件ID_🎉_тест"; expect(() => tracker.add(unicodeId)).not.toThrow(); expect(tracker.peek(unicodeId)).toBe(true); tracker.stop(); }); it("handles IDs with null bytes", () => { const tracker = createSeenTracker({ maxEntries: 100 }); const idWithNull = "event\x00id"; expect(() => tracker.add(idWithNull)).not.toThrow(); expect(tracker.peek(idWithNull)).toBe(true); tracker.stop(); }); it("handles prototype property names as IDs", () => { const tracker = createSeenTracker({ maxEntries: 100 }); // These should not affect the tracker's internal operation expect(() => tracker.add("__proto__")).not.toThrow(); expect(() => tracker.add("constructor")).not.toThrow(); expect(() => tracker.add("toString")).not.toThrow(); expect(() => tracker.add("hasOwnProperty")).not.toThrow(); expect(tracker.peek("__proto__")).toBe(true); expect(tracker.peek("constructor")).toBe(true); expect(tracker.peek("toString")).toBe(true); expect(tracker.peek("hasOwnProperty")).toBe(true); tracker.stop(); }); }); describe("rapid operations", () => { it("handles rapid add/check cycles", () => { const tracker = createSeenTracker({ maxEntries: 1000 }); for (let i = 0; i < 10000; i++) { const id = `event-${i}`; tracker.add(id); // Recently added should be findable if (i < 1000) { tracker.peek(id); } } // Size should be capped at maxEntries expect(tracker.size()).toBeLessThanOrEqual(1000); tracker.stop(); }); it("handles concurrent-style operations", () => { const tracker = createSeenTracker({ maxEntries: 100 }); // Simulate interleaved operations for (let i = 0; i < 100; i++) { tracker.add(`add-${i}`); tracker.peek(`peek-${i}`); tracker.has(`has-${i}`); if (i % 10 === 0) { tracker.delete(`add-${i - 5}`); } } expect(() => tracker.size()).not.toThrow(); tracker.stop(); }); }); describe("seed edge cases", () => { it("handles empty seed array", () => { const tracker = createSeenTracker({ maxEntries: 100 }); expect(() => tracker.seed([])).not.toThrow(); expect(tracker.size()).toBe(0); tracker.stop(); }); it("handles seed with duplicate IDs", () => { const tracker = createSeenTracker({ maxEntries: 100 }); tracker.seed(["id1", "id1", "id1", "id2", "id2"]); expect(tracker.size()).toBe(2); tracker.stop(); }); it("handles seed larger than maxEntries", () => { const tracker = createSeenTracker({ maxEntries: 5 }); const ids = Array.from({ length: 100 }, (_, i) => `id-${i}`); tracker.seed(ids); expect(tracker.size()).toBeLessThanOrEqual(5); tracker.stop(); }); }); }); // ============================================================================ // Fuzz Tests for Metrics // ============================================================================ describe("Metrics fuzz", () => { describe("invalid metric names", () => { it("handles unknown metric names gracefully", () => { const metrics = createMetrics(); // Cast to bypass type checking - testing runtime behavior expect(() => { metrics.emit("invalid.metric.name" as MetricName); }).not.toThrow(); }); }); describe("invalid label values", () => { it("handles null relay label", () => { const metrics = createMetrics(); expect(() => { metrics.emit("relay.connect", 1, { relay: null as unknown as string }); }).not.toThrow(); }); it("handles undefined relay label", () => { const metrics = createMetrics(); expect(() => { metrics.emit("relay.connect", 1, { relay: undefined as unknown as string }); }).not.toThrow(); }); it("handles very long relay URL", () => { const metrics = createMetrics(); const longUrl = "wss://" + "a".repeat(10000) + ".com"; expect(() => { metrics.emit("relay.connect", 1, { relay: longUrl }); }).not.toThrow(); const snapshot = metrics.getSnapshot(); expect(snapshot.relays[longUrl]).toBeDefined(); }); }); describe("extreme values", () => { it("handles NaN value", () => { const metrics = createMetrics(); expect(() => metrics.emit("event.received", NaN)).not.toThrow(); const snapshot = metrics.getSnapshot(); expect(isNaN(snapshot.eventsReceived)).toBe(true); }); it("handles Infinity value", () => { const metrics = createMetrics(); expect(() => metrics.emit("event.received", Infinity)).not.toThrow(); const snapshot = metrics.getSnapshot(); expect(snapshot.eventsReceived).toBe(Infinity); }); it("handles negative value", () => { const metrics = createMetrics(); metrics.emit("event.received", -1); const snapshot = metrics.getSnapshot(); expect(snapshot.eventsReceived).toBe(-1); }); it("handles very large value", () => { const metrics = createMetrics(); metrics.emit("event.received", Number.MAX_SAFE_INTEGER); const snapshot = metrics.getSnapshot(); expect(snapshot.eventsReceived).toBe(Number.MAX_SAFE_INTEGER); }); }); describe("rapid emissions", () => { it("handles many rapid emissions", () => { const events: unknown[] = []; const metrics = createMetrics((e) => events.push(e)); for (let i = 0; i < 10000; i++) { metrics.emit("event.received"); } expect(events).toHaveLength(10000); const snapshot = metrics.getSnapshot(); expect(snapshot.eventsReceived).toBe(10000); }); }); describe("reset during operation", () => { it("handles reset mid-operation safely", () => { const metrics = createMetrics(); metrics.emit("event.received"); metrics.emit("event.received"); metrics.reset(); metrics.emit("event.received"); const snapshot = metrics.getSnapshot(); expect(snapshot.eventsReceived).toBe(1); }); }); }); // ============================================================================ // Event Shape Validation (simulating malformed events) // ============================================================================ describe("Event shape validation", () => { describe("malformed event structures", () => { // These test what happens if malformed data somehow gets through it("identifies missing required fields", () => { const malformedEvents = [ {}, // empty { id: "abc" }, // missing pubkey, created_at, etc. { id: null, pubkey: null }, // null values { id: 123, pubkey: 456 }, // wrong types { tags: "not-an-array" }, // wrong type for tags { tags: [[1, 2, 3]] }, // wrong type for tag elements ]; for (const event of malformedEvents) { // These should be caught by shape validation before processing const hasId = typeof event?.id === "string"; const hasPubkey = typeof (event as { pubkey?: unknown })?.pubkey === "string"; const hasTags = Array.isArray((event as { tags?: unknown })?.tags); // At least one should be invalid expect(hasId && hasPubkey && hasTags).toBe(false); } }); }); describe("timestamp edge cases", () => { const testTimestamps = [ { value: NaN, desc: "NaN" }, { value: Infinity, desc: "Infinity" }, { value: -Infinity, desc: "-Infinity" }, { value: -1, desc: "negative" }, { value: 0, desc: "zero" }, { value: 253402300800, desc: "year 10000" }, // Far future { value: -62135596800, desc: "year 0001" }, // Far past { value: 1.5, desc: "float" }, ]; for (const { value, desc } of testTimestamps) { it(`handles ${desc} timestamp`, () => { const isValidTimestamp = typeof value === "number" && !isNaN(value) && isFinite(value) && value >= 0 && Number.isInteger(value); // Timestamps should be validated as positive integers if (["NaN", "Infinity", "-Infinity", "negative", "float"].includes(desc)) { expect(isValidTimestamp).toBe(false); } }); } }); }); // ============================================================================ // JSON parsing edge cases (simulating relay responses) // ============================================================================ describe("JSON parsing edge cases", () => { const malformedJsonCases = [ { input: "", desc: "empty string" }, { input: "null", desc: "null literal" }, { input: "undefined", desc: "undefined literal" }, { input: "{", desc: "incomplete object" }, { input: "[", desc: "incomplete array" }, { input: '{"key": undefined}', desc: "undefined value" }, { input: "{'key': 'value'}", desc: "single quotes" }, { input: '{"key": NaN}', desc: "NaN value" }, { input: '{"key": Infinity}', desc: "Infinity value" }, { input: "\x00", desc: "null byte" }, { input: "abc", desc: "plain string" }, { input: "123", desc: "plain number" }, ]; for (const { input, desc } of malformedJsonCases) { it(`handles malformed JSON: ${desc}`, () => { let parsed: unknown; let parseError = false; try { parsed = JSON.parse(input); } catch { parseError = true; } // Either it throws or produces something that needs validation if (!parseError) { // If it parsed, we need to validate the structure const isValidRelayMessage = Array.isArray(parsed) && parsed.length >= 2 && typeof parsed[0] === "string"; // Most malformed cases won't produce valid relay messages if (["null literal", "plain number", "plain string"].includes(desc)) { expect(isValidRelayMessage).toBe(false); } } }); } });