diff --git a/server/src/config/services.ts b/server/src/config/services.ts index c00f91c72..309f55242 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -228,7 +228,7 @@ export const initializeServices = async ({ const gameProvider = new GameProvider(logger, GameDig); const grpcProvider = new GrpcProvider(grpc, protoLoader); const webSocketProvider = new WebSocketProvider(WebSocket); - const dnsProvider = new DNSProvider(new Resolver()); + const dnsProvider = new DNSProvider(() => new Resolver()); const networkService = new NetworkService(axios, logger, [ pingProvider, diff --git a/server/src/service/infrastructure/network/DNSProvider.ts b/server/src/service/infrastructure/network/DNSProvider.ts index 4ad212e62..64e473e88 100644 --- a/server/src/service/infrastructure/network/DNSProvider.ts +++ b/server/src/service/infrastructure/network/DNSProvider.ts @@ -1,6 +1,6 @@ import { DNSStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; -import { Resolver } from "dns/promises"; +import type { Resolver } from "dns/promises"; import { Monitor, MonitorType } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; @@ -9,7 +9,7 @@ const SERVICE_NAME = "DNSProvider"; export class DNSProvider implements IStatusProvider { readonly type = "dns"; - constructor(private resolver: Resolver) {} + constructor(private createResolver: () => Resolver) {} supports(type: MonitorType) { return type === "dns"; } @@ -21,7 +21,8 @@ export class DNSProvider implements IStatusProvider { throw new Error("DNS server is required for DNS monitoring"); } - this.resolver.setServers([dnsServer]); + const resolver = this.createResolver(); + resolver.setServers([dnsServer]); const { response: results, @@ -30,19 +31,19 @@ export class DNSProvider implements IStatusProvider { } = await timeRequest(() => { switch (dnsRecordType.toUpperCase()) { case "A": - return this.resolver.resolve4(hostname); + return resolver.resolve4(hostname); case "AAAA": - return this.resolver.resolve6(hostname); + return resolver.resolve6(hostname); case "CNAME": - return this.resolver.resolveCname(hostname); + return resolver.resolveCname(hostname); case "MX": - return this.resolver.resolveMx(hostname); + return resolver.resolveMx(hostname); case "TXT": - return this.resolver.resolveTxt(hostname); + return resolver.resolveTxt(hostname); case "NS": - return this.resolver.resolveNs(hostname); + return resolver.resolveNs(hostname); default: - return this.resolver.resolve(hostname, dnsRecordType); + return resolver.resolve(hostname, dnsRecordType); } }); diff --git a/server/test/unit/providers/network/dnsProvider.test.ts b/server/test/unit/providers/network/dnsProvider.test.ts index ba38fa5d2..3fcea5113 100644 --- a/server/test/unit/providers/network/dnsProvider.test.ts +++ b/server/test/unit/providers/network/dnsProvider.test.ts @@ -2,6 +2,7 @@ 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"; +import { DNSProvider } from "../../../../src/service/infrastructure/network/DNSProvider.ts"; // ── Mocks ──────────────────────────────────────────────────────────────────── @@ -16,13 +17,7 @@ const mockResolverInstance = { 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"); +const createResolver = () => mockResolverInstance as any; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -38,7 +33,6 @@ const makeMonitor = (overrides?: Partial): Monitor => }) as Monitor; beforeEach(() => { - ResolverMock.mockClear(); mockResolverInstance.setServers.mockClear(); mockResolverInstance.resolve4.mockReset().mockResolvedValue(["1.2.3.4"]); mockResolverInstance.resolve6.mockReset().mockResolvedValue(["::1"]); @@ -52,7 +46,7 @@ beforeEach(() => { // ── Contract ───────────────────────────────────────────────────────────────── testStatusProviderContract("DNSProvider", { - create: () => new DNSProvider(mockResolverInstance as any), + create: () => new DNSProvider(createResolver), supportedType: "dns", unsupportedType: "ping", makeMonitor: () => makeMonitor(), @@ -63,7 +57,7 @@ testStatusProviderContract("DNSProvider", { describe("DNSProvider", () => { it("returns success with results and payload on resolve", async () => { mockResolverInstance.resolve4.mockResolvedValue(["1.2.3.4"]); - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); const result = await provider.handle(makeMonitor()); @@ -88,63 +82,63 @@ describe("DNSProvider", () => { }); it("calls setServers with the configured dnsServer", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); 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(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); 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(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await provider.handle(makeMonitor({ dnsRecordType: "AAAA" })); expect(mockResolverInstance.resolve6).toHaveBeenCalledWith("example.com"); }); it("dispatches CNAME records to resolveCname", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await provider.handle(makeMonitor({ dnsRecordType: "CNAME" })); expect(mockResolverInstance.resolveCname).toHaveBeenCalledWith("example.com"); }); it("dispatches MX records to resolveMx", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await provider.handle(makeMonitor({ dnsRecordType: "MX" })); expect(mockResolverInstance.resolveMx).toHaveBeenCalledWith("example.com"); }); it("dispatches TXT records to resolveTxt", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await provider.handle(makeMonitor({ dnsRecordType: "TXT" })); expect(mockResolverInstance.resolveTxt).toHaveBeenCalledWith("example.com"); }); it("dispatches NS records to resolveNs", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); 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(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await provider.handle(makeMonitor({ dnsRecordType: "SOA" })); expect(mockResolverInstance.resolve).toHaveBeenCalledWith("example.com", "SOA"); }); it("treats record type case-insensitively", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); 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(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); const result = await provider.handle(makeMonitor()); @@ -170,7 +164,7 @@ describe("DNSProvider", () => { it("stringifies non-Error rejections in failure response", async () => { mockResolverInstance.resolve4.mockRejectedValue("DNS bombed"); - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); const result = await provider.handle(makeMonitor()); @@ -179,7 +173,7 @@ describe("DNSProvider", () => { }); it("throws AppError when dnsServer is missing", async () => { - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await expect(provider.handle(makeMonitor({ dnsServer: undefined }))).rejects.toThrow("DNS server is required for DNS monitoring"); }); @@ -188,7 +182,7 @@ describe("DNSProvider", () => { mockResolverInstance.setServers.mockImplementationOnce(() => { throw new Error(""); }); - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing DNS check"); }); @@ -197,7 +191,7 @@ describe("DNSProvider", () => { mockResolverInstance.setServers.mockImplementationOnce(() => { throw 42; }); - const provider = new DNSProvider(mockResolverInstance as any); + const provider = new DNSProvider(createResolver); await expect(provider.handle(makeMonitor())).rejects.toThrow("42"); });