diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index eeb5c138e..1c5486d18 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -39,6 +39,7 @@ export const useMonitorForm = ({ url: data?.url || "", ignoreTlsErrors: data?.ignoreTlsErrors || false, useAdvancedMatching: data?.useAdvancedMatching || false, + acceptedStatusCodes: data?.acceptedStatusCodes || "", matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", @@ -130,6 +131,7 @@ export const useMonitorForm = ({ url: "", ignoreTlsErrors: false, useAdvancedMatching: false, + acceptedStatusCodes: "", matchMethod: "", expectedValue: "", jsonPath: "", diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 3aae2e5e1..ba69c91c7 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -1033,6 +1033,28 @@ const CreateMonitorPage = () => { subtitle={t("pages.createMonitor.form.advanced.description")} rightContent={ + ( + + )} + /> !value || /^\s*[1-5]\d{2}(?:\s*,\s*[1-5]\d{2})*\s*$/.test(value), + "Enter comma-separated HTTP status codes between 100 and 599" + ); // Common base schema for all monitor types const baseSchema = z.object({ @@ -37,6 +44,7 @@ const httpSchema = baseSchema.extend({ url: urlSchema, ignoreTlsErrors: z.boolean(), useAdvancedMatching: z.boolean(), + acceptedStatusCodes: acceptedStatusCodesSchema, matchMethod: z.enum(["equal", "include", "regex", ""]).optional(), expectedValue: z.string().optional(), jsonPath: z.string().optional(), diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index c11543cad..dc98fc4f1 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -20,7 +20,7 @@ const nameSchema = joi .string() .max(50) .trim() - .pattern(/^[\p{L}\p{M}''()\-\. ]+$/u) + .pattern(/^[\p{L}\p{M}''(). -]+$/u) .messages({ "string.empty": "auth.common.inputs.firstName.errors.empty", "string.max": "auth.common.inputs.firstName.errors.length", @@ -31,7 +31,7 @@ const lastnameSchema = joi .string() .max(50) .trim() - .pattern(/^[\p{L}\p{M}''()\-\. ]+$/u) + .pattern(/^[\p{L}\p{M}''(). -]+$/u) .messages({ "string.empty": "auth.common.inputs.lastName.errors.empty", "string.max": "auth.common.inputs.lastName.errors.length", @@ -235,6 +235,14 @@ const monitorValidation = joi.object({ expectedValue: joi.string().allow(null, ""), jsonPath: joi.string().allow(null, ""), matchMethod: joi.string().allow(null, ""), + acceptedStatusCodes: joi + .string() + .allow(null, "") + .pattern(/^\s*[1-5]\d{2}(?:\s*,\s*[1-5]\d{2})*\s*$/) + .messages({ + "string.pattern.base": + "Enter comma-separated HTTP status codes between 100 and 599", + }), gameId: joi.when("type", { is: "game", then: joi.string().required().messages({ @@ -414,7 +422,7 @@ const infrastructureMonitorValidation = joi.object({ .trim() .custom((value, helpers) => { const urlRegex = - /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i; + /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z.-]+)(\.[a-z.]{2,6})?(:(\d+))?([/\w .-]*)*\/?$/i; if (!urlRegex.test(value)) { return helpers.error("string.invalidUrl"); diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index 504fa5b49..d629f2f78 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -501,6 +501,10 @@ "equal": "يساوي", "include": "يحتوي", "regex": "تعبير عادي" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "إعدادات متقدمة" diff --git a/client/src/locales/ca.json b/client/src/locales/ca.json index 9760e7157..41448757b 100644 --- a/client/src/locales/ca.json +++ b/client/src/locales/ca.json @@ -501,6 +501,10 @@ "equal": "Igual", "include": "Inclou", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Configuració avançada" diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 9f7ba63d5..8146d21a0 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -501,6 +501,10 @@ "equal": "Rovná se", "include": "Obsahuje", "regex": "Regulární výraz" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Pokročilá nastavení" diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 2c6a33ae4..779bd575e 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -501,6 +501,10 @@ "equal": "Gleich", "include": "Enthält", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Erweiterte Einstellungen" diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 42b4bf34f..e630d1761 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -491,6 +491,10 @@ "advancedMatching": { "label": "Use advanced matching" }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" + }, "expectedValue": { "label": "Expected value" }, diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 70dd557d0..69f0f5344 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -501,6 +501,10 @@ "equal": "Igual", "include": "Contiene", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Configuración avanzada" diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index 3c34577a2..51bb41c5b 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -501,6 +501,10 @@ "equal": "Yhtä suuri", "include": "Sisältää", "regex": "Säännöllinen lauseke" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Lisäasetukset" diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 49e7e94bc..04a84b312 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -501,6 +501,10 @@ "equal": "Égal", "include": "Inclure", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Paramètres avancés" diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index eb97dc105..ec103ccb9 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -501,6 +501,10 @@ "equal": "等しい", "include": "含む", "regex": "正規表現" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "詳細設定" diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index 39cbf4577..6bbfd7cf3 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -501,6 +501,10 @@ "equal": "Igual", "include": "Inclui", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Configurações avançadas" diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index 9bfd20481..269018b39 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -501,6 +501,10 @@ "equal": "Равный", "include": "Включить", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Расширенные настройки" diff --git a/client/src/locales/th.json b/client/src/locales/th.json index 5873b65d4..9c37eb1a5 100644 --- a/client/src/locales/th.json +++ b/client/src/locales/th.json @@ -501,6 +501,10 @@ "equal": "เท่ากับ", "include": "รวม", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "ตั้งค่าขั้นสูง" diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 2d6deaa97..1bd4aa02e 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -501,6 +501,10 @@ "equal": "Eşit", "include": "İçerir", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Gelişmiş ayarlar" diff --git a/client/src/locales/uk.json b/client/src/locales/uk.json index de40bbf05..3039568f0 100644 --- a/client/src/locales/uk.json +++ b/client/src/locales/uk.json @@ -501,6 +501,10 @@ "equal": "Дорівнює", "include": "Містить", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Розширені налаштування" diff --git a/client/src/locales/vi.json b/client/src/locales/vi.json index 13907f21b..13d85bfd0 100644 --- a/client/src/locales/vi.json +++ b/client/src/locales/vi.json @@ -501,6 +501,10 @@ "equal": "Bằng", "include": "Bao gồm", "regex": "Regex" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "Cài đặt nâng cao" diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index a7166e6c2..3fe4b26d0 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -501,6 +501,10 @@ "equal": "相等", "include": "包含", "regex": "正则表达式" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "高级设置" diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json index ba599bb4f..e716b69dd 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -501,6 +501,10 @@ "equal": "等於", "include": "包含", "regex": "正規表達式" + }, + "acceptedStatusCodes": { + "description": "Comma-separated HTTP status codes that should count as up. Leave empty to use the default 2xx response check.", + "label": "Accepted status codes" } }, "title": "進階設定" diff --git a/server/openapi.json b/server/openapi.json index 26c58fa29..69994b3ec 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -376,6 +376,9 @@ "useAdvancedMatching": { "type": "boolean" }, + "acceptedStatusCodes": { + "type": "string" + }, "jsonPath": { "type": "string" }, @@ -392,6 +395,12 @@ "type": "string" } }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "secret": { "type": "string" }, @@ -469,6 +478,7 @@ "ignoreTlsErrors", "useAdvancedMatching", "notifications", + "tags", "cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", @@ -2362,6 +2372,16 @@ "profilePicture": { "type": "string" }, + "password": { + "type": "string", + "minLength": 8, + "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\\-_=+[\\]{};:'\",.~`|\\\\/])[A-Za-z0-9!?@#$%^&*()\\-_=+[\\]{};:'\",.~`|\\\\/]+$" + }, + "newPassword": { + "type": "string", + "minLength": 8, + "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\\-_=+[\\]{};:'\",.~`|\\\\/])[A-Za-z0-9!?@#$%^&*()\\-_=+[\\]{};:'\",.~`|\\\\/]+$" + }, "profileImage": { "type": "string", "format": "binary" @@ -5207,7 +5227,8 @@ "type": "array", "items": { "type": "string" - } + }, + "minItems": 1 }, "duration": { "type": "number" @@ -5478,6 +5499,24 @@ "required": false, "name": "filter", "in": "query" + }, + { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "required": false, + "name": "tags", + "in": "query" } ], "responses": { @@ -5672,6 +5711,24 @@ "name": "type", "in": "query" }, + { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "required": false, + "name": "tags", + "in": "query" + }, { "schema": { "type": "boolean" @@ -6897,6 +6954,17 @@ "type": "boolean", "default": false }, + "acceptedStatusCodes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "enum": [""] + } + ] + }, "port": { "type": "number" }, @@ -6924,6 +6992,12 @@ "type": "string" } }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "secret": { "type": "string" }, @@ -7527,6 +7601,17 @@ "type": "boolean", "default": false }, + "acceptedStatusCodes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "enum": [""] + } + ] + }, "jsonPath": { "anyOf": [ { @@ -7586,6 +7671,13 @@ }, "default": [] }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, "secret": { "type": "string" }, @@ -8104,6 +8196,12 @@ "type": "string" } }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "secret": { "type": "string" }, @@ -8113,6 +8211,17 @@ "useAdvancedMatching": { "type": "boolean" }, + "acceptedStatusCodes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "enum": [""] + } + ] + }, "jsonPath": { "anyOf": [ { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index e6bffddd8..878539487 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -250,6 +250,9 @@ const MonitorSchema = new Schema( type: Boolean, default: false, }, + acceptedStatusCodes: { + type: String, + }, jsonPath: { type: String, }, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 3d1f9c3a5..896563e77 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -446,6 +446,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, useAdvancedMatching: doc.useAdvancedMatching ?? false, + acceptedStatusCodes: doc.acceptedStatusCodes ?? undefined, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, @@ -510,6 +511,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, useAdvancedMatching: doc.useAdvancedMatching ?? false, + acceptedStatusCodes: doc.acceptedStatusCodes ?? undefined, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index bbc0934f8..57bb76bb7 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -8,6 +8,14 @@ import { Monitor, MonitorType } from "@/types/monitor.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; import CacheableLookup from "cacheable-lookup"; +const parseAcceptedStatusCodes = (value?: string): number[] => + (value ?? "") + .split(",") + .map((code) => Number(code.trim())) + .filter((code) => Number.isInteger(code) && code >= 100 && code <= 599); + +const formatAcceptedStatusCodes = (codes: number[]): string => codes.join(", "); + export class HttpProvider implements IStatusProvider { readonly type = "http"; @@ -63,8 +71,10 @@ export class HttpProvider implements IStatusProvider { throw new Error("URL is required for HTTP monitor"); } + const acceptedStatusCodes = parseAcceptedStatusCodes(monitor.acceptedStatusCodes); const options: Record = { headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined, + throwHttpErrors: acceptedStatusCodes.length > 0 ? false : undefined, }; options.agent = { @@ -102,13 +112,18 @@ export class HttpProvider implements IStatusProvider { } const matchResult = this.advancedMatcher.validate(payload, monitor); + const statusCodeMatches = acceptedStatusCodes.length > 0 ? acceptedStatusCodes.includes(response.statusCode) : response.ok; + const statusCodeMessage = + acceptedStatusCodes.length > 0 + ? `Expected status code ${formatAcceptedStatusCodes(acceptedStatusCodes)} but received ${response.statusCode}` + : (response.statusMessage ?? "HTTP status check failed"); return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, - status: response.ok && matchResult.ok, + status: statusCodeMatches && matchResult.ok, code: response.statusCode, - message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message, + message: !statusCodeMatches ? statusCodeMessage : matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message, responseTime: response.timings.phases.total ?? 0, timings: response.timings, payload, diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 0708297f3..4a47142c5 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -50,6 +50,7 @@ export interface Monitor { type: MonitorType; ignoreTlsErrors: boolean; useAdvancedMatching: boolean; + acceptedStatusCodes?: string; jsonPath?: string; expectedValue?: string; matchMethod?: MonitorMatchMethod; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index d5164b8ef..a72cd58ee 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -61,6 +61,12 @@ const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.R } }; +const acceptedStatusCodesValidation = z + .union([z.string(), z.literal("")]) + .refine((value) => value === "" || /^\s*[1-5]\d{2}(?:\s*,\s*[1-5]\d{2})*\s*$/.test(value), { + message: "Enter comma-separated HTTP status codes between 100 and 599", + }); + export const createMonitorBodyValidation = z .object({ _id: z.string().optional(), @@ -72,6 +78,7 @@ export const createMonitorBodyValidation = z url: z.string().min(1, "URL is required"), ignoreTlsErrors: z.boolean().default(false), useAdvancedMatching: z.boolean().default(false), + acceptedStatusCodes: acceptedStatusCodesValidation.optional(), port: z.number().optional(), isActive: z.boolean().optional(), interval: z.number().optional(), @@ -113,6 +120,7 @@ export const editMonitorBodyValidation = z secret: z.string().optional(), ignoreTlsErrors: z.boolean().optional(), useAdvancedMatching: z.boolean().optional(), + acceptedStatusCodes: acceptedStatusCodesValidation.optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), @@ -170,6 +178,7 @@ const importedMonitorSchema = z type: z.enum(MonitorTypes, "Invalid monitor type"), ignoreTlsErrors: z.boolean().default(false), useAdvancedMatching: z.boolean().default(false), + acceptedStatusCodes: acceptedStatusCodesValidation.optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), @@ -236,6 +245,7 @@ export const monitorResponseSchema = z statusWindowThreshold: z.number(), ignoreTlsErrors: z.boolean(), useAdvancedMatching: z.boolean(), + acceptedStatusCodes: z.string().optional(), jsonPath: z.string().optional(), expectedValue: z.string().optional(), matchMethod: z.enum(MonitorMatchMethods).optional(), diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts index 1b814d7b2..3052c26ab 100644 --- a/server/test/unit/providers/network/httpProvider.test.ts +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -181,6 +181,42 @@ describe("HttpProvider", () => { ); }); + it("counts configured accepted status codes as successful", async () => { + mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 401, statusMessage: "Unauthorized" })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ acceptedStatusCodes: "200, 401" })); + + expect(mockGot).toHaveBeenCalledWith( + "https://example.com", + expect.objectContaining({ + throwHttpErrors: false, + }) + ); + expect(result).toEqual( + expect.objectContaining({ + status: true, + code: 401, + message: "Unauthorized", + }) + ); + }); + + it("fails when a response is outside configured accepted status codes", async () => { + mockGot.mockResolvedValue(makeGotResponse({ ok: true, statusCode: 204, statusMessage: "No Content" })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ acceptedStatusCodes: "200, 201" })); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: 204, + message: "Expected status code 200, 201 but received 204", + }) + ); + }); + it("defaults responseTime to 0 when timings.phases.total is undefined", async () => { mockGot.mockResolvedValue(makeGotResponse({ timings: { phases: { total: undefined } } })); const { provider } = createProvider(); diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index 057ff6ae1..c9731b5cb 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -222,3 +222,37 @@ describe("monitorValidation — strategy gating", () => { }); }); }); + +describe("monitorValidation — accepted status codes", () => { + it("retains comma-separated HTTP status codes on create", () => { + const parsed = createMonitorBodyValidation.parse({ + name: "HTTP check", + type: "http", + url: "https://example.com", + acceptedStatusCodes: "200, 204, 401", + }); + + expect(parsed.acceptedStatusCodes).toBe("200, 204, 401"); + }); + + it("retains accepted status codes on edit", () => { + const parsed = editMonitorBodyValidation.parse({ + acceptedStatusCodes: "200,403", + }); + + expect(parsed.acceptedStatusCodes).toBe("200,403"); + }); + + it("rejects invalid accepted status codes", () => { + for (const acceptedStatusCodes of ["99", "600", "200, nope", "200-204"]) { + expect(() => + createMonitorBodyValidation.parse({ + name: "HTTP check", + type: "http", + url: "https://example.com", + acceptedStatusCodes, + }) + ).toThrow(); + } + }); +});