diff --git a/server/src/db/migration/timescaledb/0022_add_strategy_to_monitors.ts b/server/src/db/migration/timescaledb/0022_add_strategy_to_monitors.ts deleted file mode 100644 index 06d48cc75..000000000 --- a/server/src/db/migration/timescaledb/0022_add_strategy_to_monitors.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Pool } from "pg"; - -export const addStrategyToMonitors = async (pool: Pool) => { - await pool.query(` - ALTER TABLE monitors - ADD COLUMN IF NOT EXISTS strategy TEXT DEFAULT NULL; - `); -}; - -export const dropStrategyFromMonitors = async (pool: Pool) => { - await pool.query(` - ALTER TABLE monitors - DROP COLUMN IF EXISTS strategy; - `); -}; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 1889aa415..69d1c2c2a 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; -import { DefaultPageSpeedStrategy, DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js"; +import { DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -49,6 +49,16 @@ const refineDnsHostname = (body: { type?: string; url?: string }, ctx: z.Refinem } }; +const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.RefinementCtx) => { + if (body.strategy !== undefined && body.type !== undefined && body.type !== "pagespeed") { + ctx.addIssue({ + code: "custom", + path: ["strategy"], + message: "Strategy is only valid for pagespeed monitors", + }); + } +}; + export const createMonitorBodyValidation = z .object({ _id: z.string().optional(), @@ -83,7 +93,8 @@ export const createMonitorBodyValidation = z dnsServer: dnsServerValidation.optional(), dnsRecordType: z.enum(DnsRecordTypes).optional(), }) - .superRefine(refineDnsHostname); + .superRefine(refineDnsHostname) + .superRefine(refineStrategyType); export const editMonitorBodyValidation = z .object({ @@ -117,7 +128,8 @@ export const editMonitorBodyValidation = z dnsServer: dnsServerValidation.optional(), dnsRecordType: z.enum(DnsRecordTypes).optional(), }) - .superRefine(refineDnsHostname); + .superRefine(refineDnsHostname) + .superRefine(refineStrategyType); export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -175,7 +187,7 @@ const importedMonitorSchema = z selectedDisks: z.array(z.string()).default([]), gameId: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).default(""), - strategy: z.enum(PageSpeedStrategies).default(DefaultPageSpeedStrategy), + strategy: z.enum(PageSpeedStrategies).optional(), group: z.union([z.string().max(50).trim(), z.null()]).default(null), geoCheckEnabled: z.boolean().default(false), geoCheckLocations: z.array(z.enum(GeoContinents)).default([]), @@ -185,7 +197,8 @@ const importedMonitorSchema = z createdAt: z.string().optional(), updatedAt: z.string().optional(), }) - .superRefine(refineDnsHostname); + .superRefine(refineDnsHostname) + .superRefine(refineStrategyType); export const importMonitorsBodyValidation = z.object({ monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"), diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index 62fa91625..057ff6ae1 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -129,3 +129,96 @@ describe("monitorValidation — DNS fields", () => { }); }); }); + +describe("monitorValidation — strategy gating", () => { + describe("createMonitorBodyValidation", () => { + it("accepts strategy on pagespeed monitors", () => { + const parsed = createMonitorBodyValidation.parse({ + name: "PS check", + type: "pagespeed", + url: "https://example.com", + strategy: "mobile", + }); + expect(parsed.strategy).toBe("mobile"); + }); + + it("accepts pagespeed monitors without a strategy", () => { + const parsed = createMonitorBodyValidation.parse({ + name: "PS check", + type: "pagespeed", + url: "https://example.com", + }); + expect(parsed.strategy).toBeUndefined(); + }); + + it("rejects strategy on non-pagespeed monitor types", () => { + for (const type of ["http", "ping", "docker", "port", "game", "grpc", "websocket", "dns", "hardware"] as const) { + expect(() => + createMonitorBodyValidation.parse({ + name: "wrong-type check", + type, + url: type === "dns" ? "example.com" : "https://example.com", + strategy: "desktop", + }) + ).toThrow(); + } + }); + + it("allows non-pagespeed monitors without a strategy", () => { + const parsed = createMonitorBodyValidation.parse({ + name: "HTTP check", + type: "http", + url: "https://example.com", + }); + expect(parsed.strategy).toBeUndefined(); + }); + }); + + describe("editMonitorBodyValidation", () => { + it("rejects strategy when type is explicitly non-pagespeed", () => { + expect(() => + editMonitorBodyValidation.parse({ + type: "http", + strategy: "mobile", + }) + ).toThrow(); + }); + + it("allows strategy when type is omitted (partial edit)", () => { + const parsed = editMonitorBodyValidation.parse({ + strategy: "mobile", + }); + expect(parsed.strategy).toBe("mobile"); + }); + }); + + describe("importMonitorsBodyValidation", () => { + it("does not inject a default strategy on imported HTTP monitors", () => { + const parsed = importMonitorsBodyValidation.parse({ + monitors: [ + { + name: "Imported HTTP", + type: "http", + url: "https://example.com", + }, + ], + }); + expect(parsed.monitors[0].strategy).toBeUndefined(); + }); + + it("rejects strategy on imported non-pagespeed monitors", () => { + expect(() => + importMonitorsBodyValidation.parse({ + monitors: [ + { + name: "Imported HTTP", + type: "http", + url: "https://example.com", + strategy: "desktop", + }, + ], + }) + ).toThrow(); + }); + }); +});