From 71a4fddffdce3d334874d1ebd6cbfe18da2d9786 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Jun 2026 10:38:15 -0700 Subject: [PATCH] add tests --- .../providers/network/httpProvider.test.ts | 61 +++++++++++++++++++ .../unit/validation/monitorValidation.test.ts | 51 ++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts index 10b66a93a..7556f7595 100644 --- a/server/test/unit/providers/network/httpProvider.test.ts +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -433,4 +433,65 @@ describe("HttpProvider", () => { expect(result.extracted).toBe("down"); }); }); + + // ── HTTP method (GET / HEAD) ───────────────────────────────────────── + + describe("request method", () => { + it("passes GET to got when method is GET", async () => { + mockGot.mockResolvedValue(makeGotResponse()); + const { provider } = createProvider(); + + await provider.handle(makeMonitor({ method: "GET" })); + + expect(mockGot).toHaveBeenCalledWith("https://example.com", expect.objectContaining({ method: "GET" })); + }); + + it("passes HEAD to got when method is HEAD", async () => { + mockGot.mockResolvedValue(makeGotResponse({ body: "" })); + const { provider } = createProvider(); + + await provider.handle(makeMonitor({ method: "HEAD" })); + + expect(mockGot).toHaveBeenCalledWith("https://example.com", expect.objectContaining({ method: "HEAD" })); + }); + + it("does not mark a HEAD monitor down on an empty body even with advanced matching on", async () => { + mockGot.mockResolvedValue(makeGotResponse({ body: "", headers: {} })); + // A matcher that would fail — proving HEAD never reaches it + const matcher = createMockMatcher({ ok: false, message: "Extracted value is falsy" }); + const { provider, advancedMatcher } = createProvider(matcher); + + const result = await provider.handle(makeMonitor({ method: "HEAD", useAdvancedMatching: true, jsonPath: "status" })); + + expect(result.status).toBe(true); + expect(advancedMatcher.validate).not.toHaveBeenCalled(); + }); + + it("bases HEAD status on the status code (down when not an up code)", async () => { + const err = new HTTPError("Internal Server Error"); + (err as any).response = { statusCode: 500, body: "", headers: {} }; + (err as any).timings = { phases: { total: 40 } }; + mockGot.mockRejectedValue(err); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ method: "HEAD" })); + + expect(result.status).toBe(false); + expect(result.code).toBe(500); + }); + + it("respects customUpCodes for HEAD requests without consulting the matcher", async () => { + const err = new HTTPError("Unauthorized"); + (err as any).response = { statusCode: 401, body: "", headers: {} }; + (err as any).timings = { phases: { total: 30 } }; + mockGot.mockRejectedValue(err); + const { provider, advancedMatcher } = createProvider(); + + const result = await provider.handle(makeMonitor({ method: "HEAD", customUpCodes: [401], useAdvancedMatching: true })); + + expect(result.status).toBe(true); + expect(result.code).toBe(401); + expect(advancedMatcher.validate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index 08f516f99..d7a68b00b 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -227,6 +227,57 @@ describe("monitorValidation — strategy gating", () => { }); }); +describe("monitorValidation — HEAD method gating", () => { + const baseHead = { + name: "HEAD check", + type: "http" as const, + url: "https://example.com", + method: "HEAD" as const, + }; + + describe("createMonitorBodyValidation", () => { + it("accepts a HEAD monitor with no body matching", () => { + const parsed = createMonitorBodyValidation.parse(baseHead); + expect(parsed.method).toBe("HEAD"); + }); + + it("accepts GET together with advanced matching", () => { + const parsed = createMonitorBodyValidation.parse({ + ...baseHead, + method: "GET", + useAdvancedMatching: true, + jsonPath: "status", + }); + expect(parsed.method).toBe("GET"); + }); + + it("rejects HEAD combined with advanced matching", () => { + expect(() => createMonitorBodyValidation.parse({ ...baseHead, useAdvancedMatching: true })).toThrow(); + }); + + it("rejects HEAD combined with a non-empty jsonPath", () => { + expect(() => createMonitorBodyValidation.parse({ ...baseHead, jsonPath: "status" })).toThrow(); + }); + + it("allows HEAD with an empty-string jsonPath", () => { + const parsed = createMonitorBodyValidation.parse({ ...baseHead, jsonPath: "" }); + expect(parsed.method).toBe("HEAD"); + }); + }); + + describe("editMonitorBodyValidation", () => { + it("rejects HEAD combined with advanced matching", () => { + expect(() => editMonitorBodyValidation.parse({ method: "HEAD", useAdvancedMatching: true })).toThrow(); + }); + }); + + describe("importMonitorsBodyValidation", () => { + it("rejects HEAD combined with a jsonPath on imported monitors", () => { + expect(() => importMonitorsBodyValidation.parse({ monitors: [{ ...baseHead, jsonPath: "status" }] })).toThrow(); + }); + }); +}); + describe("monitorValidation — customUpCodes", () => { const baseHttpBody = { name: "HTTP check",