From fda9d9edfe2f4a6c58305beb80d0af7b6fb3d533 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 8 May 2026 08:59:31 -0700 Subject: [PATCH] add tests --- .../providers/network/dnsProvider.test.ts | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 server/test/unit/providers/network/dnsProvider.test.ts diff --git a/server/test/unit/providers/network/dnsProvider.test.ts b/server/test/unit/providers/network/dnsProvider.test.ts new file mode 100644 index 000000000..92c874836 --- /dev/null +++ b/server/test/unit/providers/network/dnsProvider.test.ts @@ -0,0 +1,204 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +const mockResolverInstance = { + setServers: jest.fn(), + resolve4: jest.fn<() => Promise>(), + resolve6: jest.fn<() => Promise>(), + resolveCname: jest.fn<() => Promise>(), + resolveMx: jest.fn<() => Promise>(), + resolveTxt: jest.fn<() => Promise>(), + resolveNs: jest.fn<() => Promise>(), + resolve: jest.fn<() => Promise>(), +}; + +const ResolverMock = jest.fn().mockImplementation(() => mockResolverInstance); + +jest.unstable_mockModule("dns/promises", () => ({ + Resolver: ResolverMock, +})); + +const { DNSProvider } = await import("../../../../src/service/infrastructure/network/DNSProvider.ts"); + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "dns", + url: "example.com", + dnsServer: "8.8.8.8", + dnsRecordType: "A", + ...overrides, + }) as Monitor; + +beforeEach(() => { + ResolverMock.mockClear(); + mockResolverInstance.setServers.mockClear(); + mockResolverInstance.resolve4.mockReset().mockResolvedValue(["1.2.3.4"]); + mockResolverInstance.resolve6.mockReset().mockResolvedValue(["::1"]); + mockResolverInstance.resolveCname.mockReset().mockResolvedValue(["alias.example.com"]); + mockResolverInstance.resolveMx.mockReset().mockResolvedValue([{ exchange: "mx.example.com", priority: 10 }]); + mockResolverInstance.resolveTxt.mockReset().mockResolvedValue([["v=spf1"]]); + mockResolverInstance.resolveNs.mockReset().mockResolvedValue(["ns.example.com"]); + mockResolverInstance.resolve.mockReset().mockResolvedValue(["something"]); +}); + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("DNSProvider", { + create: () => new DNSProvider({} as any), + supportedType: "dns", + unsupportedType: "ping", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("DNSProvider", () => { + it("returns success with results and payload on resolve", async () => { + mockResolverInstance.resolve4.mockResolvedValue(["1.2.3.4"]); + const provider = new DNSProvider({} as any); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "dns", + status: true, + code: 200, + message: "Success", + payload: { + hostname: "example.com", + dnsServer: "8.8.8.8", + recordType: "A", + resolved: true, + results: ["1.2.3.4"], + }, + }) + ); + expect(typeof result.responseTime).toBe("number"); + }); + + it("calls setServers with the configured dnsServer", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsServer: "1.1.1.1" })); + expect(mockResolverInstance.setServers).toHaveBeenCalledWith(["1.1.1.1"]); + }); + + it("defaults to A record when dnsRecordType is not provided", async () => { + const provider = new DNSProvider({} as any); + const result = await provider.handle(makeMonitor({ dnsRecordType: undefined })); + expect(mockResolverInstance.resolve4).toHaveBeenCalledWith("example.com"); + expect(result.payload?.recordType).toBe("A"); + }); + + it("dispatches AAAA records to resolve6", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "AAAA" })); + expect(mockResolverInstance.resolve6).toHaveBeenCalledWith("example.com"); + }); + + it("dispatches CNAME records to resolveCname", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "CNAME" })); + expect(mockResolverInstance.resolveCname).toHaveBeenCalledWith("example.com"); + }); + + it("dispatches MX records to resolveMx", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "MX" })); + expect(mockResolverInstance.resolveMx).toHaveBeenCalledWith("example.com"); + }); + + it("dispatches TXT records to resolveTxt", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "TXT" })); + expect(mockResolverInstance.resolveTxt).toHaveBeenCalledWith("example.com"); + }); + + it("dispatches NS records to resolveNs", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "NS" })); + expect(mockResolverInstance.resolveNs).toHaveBeenCalledWith("example.com"); + }); + + it("falls back to generic resolve for unrecognized record types", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "SOA" })); + expect(mockResolverInstance.resolve).toHaveBeenCalledWith("example.com", "SOA"); + }); + + it("treats record type case-insensitively", async () => { + const provider = new DNSProvider({} as any); + await provider.handle(makeMonitor({ dnsRecordType: "mx" })); + expect(mockResolverInstance.resolveMx).toHaveBeenCalledWith("example.com"); + }); + + it("returns failure response with NETWORK_ERROR when resolver rejects", async () => { + mockResolverInstance.resolve4.mockRejectedValue(new Error("ENOTFOUND")); + const provider = new DNSProvider({} as any); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "dns", + status: false, + code: NETWORK_ERROR, + message: "ENOTFOUND", + payload: expect.objectContaining({ + hostname: "example.com", + dnsServer: "8.8.8.8", + recordType: "A", + resolved: false, + results: null, + }), + }) + ); + expect(typeof result.responseTime).toBe("number"); + }); + + it("stringifies non-Error rejections in failure response", async () => { + mockResolverInstance.resolve4.mockRejectedValue("DNS bombed"); + const provider = new DNSProvider({} as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.message).toBe("DNS bombed"); + }); + + it("throws AppError when dnsServer is missing", async () => { + const provider = new DNSProvider({} as any); + + await expect(provider.handle(makeMonitor({ dnsServer: undefined }))).rejects.toThrow("DNS server is required for DNS monitoring"); + }); + + it("falls back to default message when underlying error has empty message", async () => { + mockResolverInstance.setServers.mockImplementationOnce(() => { + throw new Error(""); + }); + const provider = new DNSProvider({} as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing DNS check"); + }); + + it("stringifies non-Error throws from outside timeRequest", async () => { + mockResolverInstance.setServers.mockImplementationOnce(() => { + throw 42; + }); + const provider = new DNSProvider({} as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("42"); + }); +});