test: add realistic DNS coverage for http proxy

This commit is contained in:
shamoon
2026-02-04 12:42:22 -08:00
parent 9661dbf5c8
commit 07f676139a

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { state, cache, logger } = vi.hoisted(() => ({
const { state, cache, logger, dns, net } = vi.hoisted(() => ({
state: {
response: {
statusCode: 200,
@@ -8,6 +8,7 @@ const { state, cache, logger } = vi.hoisted(() => ({
body: Buffer.from(""),
},
error: null,
lastAgentOptions: null,
},
cache: {
get: vi.fn(),
@@ -17,6 +18,22 @@ const { state, cache, logger } = vi.hoisted(() => ({
debug: vi.fn(),
error: vi.fn(),
},
dns: {
lookup: vi.fn(),
resolve4: vi.fn(),
resolve6: vi.fn(),
},
net: {
isIP: vi.fn(),
},
}));
vi.mock("node:dns", () => ({
default: dns,
}));
vi.mock("node:net", () => ({
default: net,
}));
vi.mock("follow-redirects", async () => {
@@ -32,6 +49,7 @@ vi.mock("follow-redirects", async () => {
const req = new EventEmitter();
req.write = vi.fn();
req.end = vi.fn(() => {
state.lastAgentOptions = params?.agent?.opts ?? null;
if (state.error) {
req.emit("error", state.error);
return;
@@ -74,6 +92,7 @@ describe("utils/proxy/http cachedRequest", () => {
headers: { "content-type": "application/json" },
body: Buffer.from(""),
};
state.lastAgentOptions = null;
vi.resetModules();
});
@@ -110,3 +129,157 @@ describe("utils/proxy/http cachedRequest", () => {
expect(logger.debug).toHaveBeenCalled();
});
});
describe("utils/proxy/http homepageDNSLookupFn", () => {
const getLookupFn = async () => {
const httpMod = await import("./http");
await httpMod.httpProxy("http://example.com");
expect(state.lastAgentOptions?.lookup).toEqual(expect.any(Function));
return state.lastAgentOptions.lookup;
};
beforeEach(() => {
vi.clearAllMocks();
state.error = null;
state.lastAgentOptions = null;
net.isIP.mockReturnValue(0);
dns.lookup.mockImplementation((hostname, options, cb) => cb(null, "127.0.0.1", 4));
dns.resolve4.mockImplementation((hostname, cb) => cb(null, ["127.0.0.1"]));
dns.resolve6.mockImplementation((hostname, cb) => cb(null, ["::1"]));
vi.resetModules();
});
it("short-circuits when hostname is already an IP (all=false)", async () => {
const lookup = await getLookupFn();
net.isIP.mockReturnValueOnce(4);
const cb = vi.fn();
lookup("1.2.3.4", cb);
expect(dns.lookup).not.toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(null, "1.2.3.4", 4);
});
it("short-circuits when hostname is already an IP (all=true)", async () => {
const lookup = await getLookupFn();
net.isIP.mockReturnValueOnce(6);
const cb = vi.fn();
lookup("::1", { all: true }, cb);
expect(dns.lookup).not.toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(null, [{ address: "::1", family: 6 }]);
});
it("uses dns.lookup when it succeeds (2-argument form)", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(null, "10.0.0.1", 4));
lookup("example.com", cb);
expect(dns.lookup).toHaveBeenCalledWith("example.com", {}, expect.any(Function));
expect(dns.resolve4).not.toHaveBeenCalled();
expect(dns.resolve6).not.toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(null, "10.0.0.1", 4);
});
it("does not fall back for non-ENOTFOUND/EAI_NONAME lookup errors", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const err = Object.assign(new Error("temporary"), { code: "EAI_AGAIN" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(err));
lookup("example.com", { all: true }, cb);
expect(dns.resolve4).not.toHaveBeenCalled();
expect(dns.resolve6).not.toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(err);
});
it("falls back to resolve4 when lookup fails with ENOTFOUND and family=4", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const lookupErr = Object.assign(new Error("not found"), { code: "ENOTFOUND" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(lookupErr));
dns.resolve4.mockImplementationOnce((hostname, resolveCb) => resolveCb(null, ["1.1.1.1"]));
lookup("example.com", { family: 4, all: true }, cb);
expect(dns.resolve4).toHaveBeenCalledWith("example.com", expect.any(Function));
expect(dns.resolve6).not.toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(null, [{ address: "1.1.1.1", family: 4 }]);
expect(logger.debug).toHaveBeenCalledWith("DNS fallback to c-ares resolver succeeded for %s", "example.com");
});
it("falls back to resolve6 when lookup fails with ENOTFOUND and family=6", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const lookupErr = Object.assign(new Error("not found"), { code: "ENOTFOUND" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(lookupErr));
dns.resolve6.mockImplementationOnce((hostname, resolveCb) => resolveCb(null, ["::1"]));
lookup("example.com", 6, cb);
expect(dns.lookup).toHaveBeenCalledWith("example.com", { family: 6 }, expect.any(Function));
expect(dns.resolve4).not.toHaveBeenCalled();
expect(dns.resolve6).toHaveBeenCalledWith("example.com", expect.any(Function));
expect(cb).toHaveBeenCalledWith(null, ["::1"], 6);
});
it("tries resolve4 then resolve6 when lookup fails and no family is specified", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const lookupErr = Object.assign(new Error("not found"), { code: "ENOTFOUND" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(lookupErr));
dns.resolve4.mockImplementationOnce((hostname, resolveCb) =>
resolveCb(Object.assign(new Error("v4 failed"), { code: "EAI_FAIL" })),
);
dns.resolve6.mockImplementationOnce((hostname, resolveCb) => resolveCb(null, ["::1"]));
lookup("example.com", { all: true }, cb);
expect(dns.resolve4).toHaveBeenCalledWith("example.com", expect.any(Function));
expect(dns.resolve6).toHaveBeenCalledWith("example.com", expect.any(Function));
expect(cb).toHaveBeenCalledWith(null, [{ address: "::1", family: 6 }]);
});
it("returns ENOTFOUND when fallback resolver returns no addresses", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const lookupErr = Object.assign(new Error("not found"), { code: "ENOTFOUND" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(lookupErr));
dns.resolve4.mockImplementationOnce((hostname, resolveCb) => resolveCb(null, []));
lookup("example.com", { family: 4, all: true }, cb);
const err = cb.mock.calls[0][0];
expect(err).toBeInstanceOf(Error);
expect(err.code).toBe("ENOTFOUND");
expect(dns.resolve6).not.toHaveBeenCalled();
});
it("returns resolve error when fallback resolver fails", async () => {
const lookup = await getLookupFn();
const cb = vi.fn();
const lookupErr = Object.assign(new Error("not found"), { code: "ENOTFOUND" });
const resolveErr = Object.assign(new Error("resolver down"), { code: "EAI_FAIL" });
dns.lookup.mockImplementationOnce((hostname, options, lookupCb) => lookupCb(lookupErr));
dns.resolve4.mockImplementationOnce((hostname, resolveCb) => resolveCb(resolveErr));
lookup("example.com", { family: 4, all: true }, cb);
expect(cb).toHaveBeenCalledWith(resolveErr);
expect(logger.debug).toHaveBeenCalledWith(
"DNS fallback failed for %s: lookup error=%s, resolve error=%s",
"example.com",
"ENOTFOUND",
"EAI_FAIL",
);
});
});