Merge pull request #3690 from harsh-aghara/feat/accept-status-codes-backend
Add customUpCodes support for HTTP monitors (Backend)
This commit is contained in:
13 files changed
+346
-60
No files matched your search
@@ -291,6 +291,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
ref: "Tag",
|
||||
},
|
||||
],
|
||||
customUpCodes: {
|
||||
type: [Number],
|
||||
default: [],
|
||||
},
|
||||
secret: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
@@ -548,6 +548,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
tags: tagIds,
|
||||
customUpCodes: doc.customUpCodes ?? [],
|
||||
secret: doc.secret ?? undefined,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
@@ -612,6 +613,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
tags: tagIds,
|
||||
customUpCodes: doc.customUpCodes ?? [],
|
||||
secret: doc.secret ?? undefined,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
|
||||
@@ -89,7 +89,7 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
}
|
||||
|
||||
// Step 2: Poll for results
|
||||
const results = await this.globalPingService.pollForResults(measurementId);
|
||||
const results = await this.globalPingService.pollForResults(measurementId, undefined, monitor.customUpCodes ?? []);
|
||||
|
||||
if (results.length === 0) {
|
||||
// No successful results (all locations timed out or failed)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
import { supportsGeoCheck, type HttpStatusCode } from "@/types/monitor.js";
|
||||
import { MonitorType } from "@/types/index.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
import got from "got";
|
||||
import { isStatusUp } from "@/service/infrastructure/network/utils.js";
|
||||
|
||||
const SERVICE_NAME = "GlobalPingService";
|
||||
const GLOBAL_PING_API_BASE = "https://api.globalping.io/v1";
|
||||
@@ -62,7 +63,7 @@ interface GlobalPingProbeResult {
|
||||
export interface IGlobalPingService {
|
||||
readonly serviceName: string;
|
||||
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise<GeoCheckResult[]>;
|
||||
}
|
||||
|
||||
export class GlobalPingService implements IGlobalPingService {
|
||||
@@ -119,7 +120,11 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
}
|
||||
}
|
||||
|
||||
async pollForResults(measurementId: string, timeoutMs: number = MAX_POLL_TIMEOUT_MS): Promise<GeoCheckResult[]> {
|
||||
async pollForResults(
|
||||
measurementId: string,
|
||||
timeoutMs: number = MAX_POLL_TIMEOUT_MS,
|
||||
customUpCodes: HttpStatusCode[] = []
|
||||
): Promise<GeoCheckResult[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
@@ -132,7 +137,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
const measurement = response.body;
|
||||
|
||||
if (measurement.status === "finished") {
|
||||
const results = this.transformResults(measurement.results || []);
|
||||
const results = this.transformResults(measurement.results || [], customUpCodes);
|
||||
this.logger.debug({
|
||||
message: `GlobalPing measurement completed: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
@@ -174,7 +179,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
return [];
|
||||
}
|
||||
|
||||
private transformResults(probeResults: GlobalPingProbeResult[]): GeoCheckResult[] {
|
||||
private transformResults(probeResults: GlobalPingProbeResult[], customUpCodes: HttpStatusCode[] = []): GeoCheckResult[] {
|
||||
const successfulResults: GeoCheckResult[] = [];
|
||||
|
||||
for (const probeResult of probeResults) {
|
||||
@@ -205,7 +210,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
|
||||
successfulResults.push({
|
||||
location,
|
||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||
status: isStatusUp(probeResult.result.statusCode, customUpCodes),
|
||||
statusCode: probeResult.result.statusCode,
|
||||
timings,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MonitorStatusResponse } from "@/types/network.js";
|
||||
import { Agent as HttpsAgent } from "https";
|
||||
import { Agent as HttpAgent } from "http";
|
||||
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||
import { NETWORK_ERROR, isStatusUp } from "@/service/infrastructure/network/utils.js";
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
|
||||
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
@@ -40,21 +40,93 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
return type === "http";
|
||||
}
|
||||
|
||||
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
|
||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
||||
private buildResponse<T>(
|
||||
monitor: Monitor,
|
||||
opts: {
|
||||
body: string;
|
||||
contentType: string;
|
||||
statusCode: number;
|
||||
statusUp: boolean;
|
||||
message: string;
|
||||
responseTime: number;
|
||||
timings?: MonitorStatusResponse<T>["timings"];
|
||||
}
|
||||
): MonitorStatusResponse<T> {
|
||||
const { body, contentType, statusCode, statusUp, message, responseTime, timings } = opts;
|
||||
const isJson = contentType.includes("application/json");
|
||||
|
||||
if (monitor.jsonPath && !isJson) {
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: error.response?.statusCode ?? NETWORK_ERROR,
|
||||
message: error.message,
|
||||
responseTime: error.timings?.phases?.firstByte ?? error.timings?.phases?.total ?? 0,
|
||||
timings: error.timings,
|
||||
payload: null as T,
|
||||
code: statusCode,
|
||||
message: "Response is not JSON",
|
||||
responseTime,
|
||||
timings,
|
||||
payload: body,
|
||||
};
|
||||
}
|
||||
|
||||
let payload: T | string;
|
||||
if (isJson) {
|
||||
try {
|
||||
payload = JSON.parse(body) as T;
|
||||
} catch {
|
||||
payload = body;
|
||||
}
|
||||
} else {
|
||||
payload = body;
|
||||
}
|
||||
|
||||
const matchResult = this.advancedMatcher.validate<T | string>(payload, monitor);
|
||||
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: statusUp && matchResult.ok,
|
||||
code: statusCode,
|
||||
message: matchResult.ok ? message : matchResult.message,
|
||||
responseTime,
|
||||
timings,
|
||||
payload,
|
||||
extracted: matchResult.extracted,
|
||||
};
|
||||
}
|
||||
|
||||
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
|
||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
||||
const statusCode = error.response?.statusCode;
|
||||
const statusUp = isStatusUp(statusCode, monitor.customUpCodes);
|
||||
const responseTime = error.timings?.phases?.firstByte ?? error.timings?.phases?.total ?? 0;
|
||||
|
||||
if (!statusUp) {
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: statusCode ?? NETWORK_ERROR,
|
||||
message: error.message,
|
||||
responseTime,
|
||||
timings: error.timings,
|
||||
payload: null as T,
|
||||
};
|
||||
}
|
||||
|
||||
return this.buildResponse<T>(monitor, {
|
||||
body: (error.response?.body ?? "") as string,
|
||||
contentType: error.response?.headers?.["content-type"] || "",
|
||||
statusCode: statusCode ?? NETWORK_ERROR,
|
||||
statusUp,
|
||||
message: error.message,
|
||||
responseTime,
|
||||
timings: error.timings,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
@@ -68,7 +140,7 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
}
|
||||
|
||||
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
||||
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
|
||||
const { url, secret, ignoreTlsErrors } = monitor;
|
||||
|
||||
if (!url) {
|
||||
throw new Error("URL is required for HTTP monitor");
|
||||
@@ -85,47 +157,17 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
|
||||
try {
|
||||
const response = await this.got<string>(url, options);
|
||||
const contentType = response.headers["content-type"] || "";
|
||||
const isJson = contentType.includes("application/json");
|
||||
const statusUp = isStatusUp(response.statusCode, monitor.customUpCodes);
|
||||
|
||||
if (jsonPath && !isJson) {
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: response.statusCode,
|
||||
message: "Response is not JSON",
|
||||
responseTime: response.timings.phases.firstByte ?? 0,
|
||||
timings: response.timings,
|
||||
payload: response.body as unknown as T,
|
||||
};
|
||||
}
|
||||
|
||||
let payload: T;
|
||||
if (isJson) {
|
||||
try {
|
||||
payload = JSON.parse(response.body) as T;
|
||||
} catch {
|
||||
payload = response.body as unknown as T;
|
||||
}
|
||||
} else {
|
||||
payload = response.body as unknown as T;
|
||||
}
|
||||
|
||||
const matchResult = this.advancedMatcher.validate<T>(payload, monitor);
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: response.ok && matchResult.ok,
|
||||
code: response.statusCode,
|
||||
message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message,
|
||||
return this.buildResponse<T>(monitor, {
|
||||
body: response.body,
|
||||
contentType: response.headers["content-type"] || "",
|
||||
statusCode: response.statusCode,
|
||||
statusUp,
|
||||
message: response.statusMessage ?? "OK",
|
||||
responseTime: response.timings.phases.firstByte ?? 0,
|
||||
timings: response.timings,
|
||||
payload,
|
||||
extracted: matchResult.extracted,
|
||||
};
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return this.handleHttpError(error, monitor);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { HttpStatusCode } from "@/types/monitor.js";
|
||||
|
||||
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
|
||||
const start = process.hrtime.bigint();
|
||||
try {
|
||||
@@ -12,3 +14,8 @@ export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ res
|
||||
|
||||
export const NETWORK_ERROR = 5000;
|
||||
export const PING_ERROR = 5001;
|
||||
|
||||
export const isStatusUp = (statusCode: number | undefined, customUpCodes: HttpStatusCode[] = []): boolean => {
|
||||
if (statusCode === undefined) return false;
|
||||
return (statusCode >= 200 && statusCode < 300) || customUpCodes.includes(statusCode);
|
||||
};
|
||||
@@ -186,7 +186,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
||||
const breaches: ThresholdBreach[] = [];
|
||||
|
||||
// Check if this is a hardware monitor with threshold data
|
||||
if (monitor.type !== "hardware" || !monitorStatusResponse.payload) {
|
||||
if (monitor.type !== "hardware" || !monitorStatusResponse.payload || typeof monitorStatusResponse.payload === "string") {
|
||||
return breaches;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { CheckSnapshot } from "@/types/check.js";
|
||||
export type { CheckSnapshot } from "@/types/check.js";
|
||||
import type { GeoContinent, GroupedGeoCheck } from "@/types/geoCheck.js";
|
||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
import http from "node:http";
|
||||
|
||||
export const HttpStatusCodes = Object.keys(http.STATUS_CODES).map(Number);
|
||||
export const HttpStatusCodeSet = new Set(HttpStatusCodes);
|
||||
export type HttpStatusCode = number;
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
@@ -60,6 +65,7 @@ export interface Monitor {
|
||||
uptimePercentage?: number;
|
||||
notifications: string[];
|
||||
tags: string[];
|
||||
customUpCodes: HttpStatusCode[];
|
||||
secret?: string;
|
||||
cpuAlertThreshold: number;
|
||||
cpuAlertCounter: number;
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface MonitorStatusResponse<
|
||||
code: number;
|
||||
message: string;
|
||||
responseTime?: number;
|
||||
payload?: T | null;
|
||||
payload?: T | string | null;
|
||||
timings?: GotTimings;
|
||||
first_byte_took?: number;
|
||||
body_read_took?: number;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js";
|
||||
import { DnsRecordTypes, HttpStatusCodeSet, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js";
|
||||
|
||||
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
|
||||
|
||||
export const getMonitorByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
@@ -81,6 +83,7 @@ export const createMonitorBodyValidation = z
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -110,6 +113,7 @@ export const editMonitorBodyValidation = z
|
||||
interval: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
@@ -180,6 +184,7 @@ const importedMonitorSchema = z
|
||||
uptimePercentage: z.number().optional(),
|
||||
notifications: z.array(z.string()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().default(100),
|
||||
cpuAlertCounter: z.number().default(5),
|
||||
@@ -241,6 +246,7 @@ export const monitorResponseSchema = z
|
||||
matchMethod: z.enum(MonitorMatchMethods).optional(),
|
||||
notifications: z.array(z.string()),
|
||||
tags: z.array(z.string()),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number(),
|
||||
memoryAlertThreshold: z.number(),
|
||||
|
||||
@@ -56,6 +56,7 @@ const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
useAdvancedMatching: false,
|
||||
matchMethod: undefined,
|
||||
expectedValue: undefined,
|
||||
customUpCodes: [],
|
||||
...overrides,
|
||||
}) as Monitor;
|
||||
|
||||
@@ -253,14 +254,13 @@ describe("HttpProvider", () => {
|
||||
expect(result.extracted).toBe("value");
|
||||
});
|
||||
|
||||
it("sets status to false when response.ok is false even if matcher passes", async () => {
|
||||
it("sets status to false when status code is non-2xx even if matcher passes", async () => {
|
||||
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301 }));
|
||||
const matcher = createMockMatcher({ ok: true, message: "Success" });
|
||||
const { provider } = createProvider(matcher);
|
||||
|
||||
const result = await provider.handle(makeMonitor());
|
||||
|
||||
// status = response.ok && matchResult.ok
|
||||
expect(result.status).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -345,4 +345,92 @@ describe("HttpProvider", () => {
|
||||
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for HTTP monitor");
|
||||
});
|
||||
});
|
||||
|
||||
// ── customUpCodes ────────────────────────────────────────────────────
|
||||
|
||||
describe("customUpCodes", () => {
|
||||
it("returns status true when HTTPError status code is in customUpCodes", async () => {
|
||||
const err = new HTTPError("Unauthorized");
|
||||
(err as any).response = { statusCode: 401 };
|
||||
(err as any).timings = { phases: { total: 30 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [401] }));
|
||||
|
||||
expect(result.status).toBe(true);
|
||||
expect(result.code).toBe(401);
|
||||
});
|
||||
|
||||
it("returns status false when HTTPError status code is not in customUpCodes", async () => {
|
||||
const err = new HTTPError("Internal Server Error");
|
||||
(err as any).response = { statusCode: 500 };
|
||||
(err as any).timings = { phases: { total: 40 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [401, 403] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.code).toBe(500);
|
||||
});
|
||||
|
||||
it("returns status false when customUpCodes is empty", async () => {
|
||||
const err = new HTTPError("Unauthorized");
|
||||
(err as any).response = { statusCode: 401 };
|
||||
(err as any).timings = { phases: { total: 25 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.code).toBe(401);
|
||||
});
|
||||
|
||||
it("treats non-2xx success response as up when status code is in customUpCodes", async () => {
|
||||
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301, statusMessage: "Moved Permanently" }));
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [301] }));
|
||||
|
||||
expect(result.status).toBe(true);
|
||||
expect(result.code).toBe(301);
|
||||
});
|
||||
|
||||
it("does not override matcher failure even when status code is in customUpCodes", async () => {
|
||||
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301 }));
|
||||
const matcher = createMockMatcher({ ok: false, message: "Body mismatch" });
|
||||
const { provider } = createProvider(matcher);
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [301] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.message).toBe("Body mismatch");
|
||||
});
|
||||
|
||||
it("does not override matcher failure on HTTPError when status code is in customUpCodes", async () => {
|
||||
const err = new HTTPError("Unauthorized");
|
||||
(err as any).response = {
|
||||
statusCode: 401,
|
||||
body: '{"status":"down"}',
|
||||
headers: { "content-type": "application/json" },
|
||||
};
|
||||
(err as any).timings = { phases: { total: 30 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const matcher = createMockMatcher({
|
||||
ok: false,
|
||||
message: "Body mismatch",
|
||||
extracted: "down",
|
||||
});
|
||||
const { provider } = createProvider(matcher);
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [401], useAdvancedMatching: true }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.message).toBe("Body mismatch");
|
||||
expect(result.payload).toEqual({ status: "down" });
|
||||
expect(result.extracted).toBe("down");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { timeRequest, NETWORK_ERROR, PING_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
|
||||
import { timeRequest, NETWORK_ERROR, PING_ERROR, isStatusUp } from "../../../../src/service/infrastructure/network/utils.ts";
|
||||
|
||||
describe("network utils", () => {
|
||||
describe("timeRequest", () => {
|
||||
@@ -41,4 +41,40 @@ describe("network utils", () => {
|
||||
expect(PING_ERROR).toBe(5001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isStatusUp", () => {
|
||||
it("returns true for 2xx status codes", () => {
|
||||
expect(isStatusUp(200)).toBe(true);
|
||||
expect(isStatusUp(201)).toBe(true);
|
||||
expect(isStatusUp(204)).toBe(true);
|
||||
expect(isStatusUp(299)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-2xx status codes without customUpCodes", () => {
|
||||
expect(isStatusUp(100)).toBe(false);
|
||||
expect(isStatusUp(301)).toBe(false);
|
||||
expect(isStatusUp(404)).toBe(false);
|
||||
expect(isStatusUp(500)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when status code is in customUpCodes", () => {
|
||||
expect(isStatusUp(301, [301])).toBe(true);
|
||||
expect(isStatusUp(404, [200, 404])).toBe(true);
|
||||
expect(isStatusUp(401, [401, 403])).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when status code is not in customUpCodes", () => {
|
||||
expect(isStatusUp(500, [401, 403])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when statusCode is undefined", () => {
|
||||
expect(isStatusUp(undefined)).toBe(false);
|
||||
expect(isStatusUp(undefined, [200])).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults customUpCodes to empty array", () => {
|
||||
expect(isStatusUp(200)).toBe(true);
|
||||
expect(isStatusUp(404)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -222,3 +222,93 @@ describe("monitorValidation — strategy gating", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("monitorValidation — customUpCodes", () => {
|
||||
const baseHttpBody = {
|
||||
name: "HTTP check",
|
||||
type: "http" as const,
|
||||
url: "https://example.com",
|
||||
};
|
||||
|
||||
describe("createMonitorBodyValidation", () => {
|
||||
it("accepts valid HTTP status codes", () => {
|
||||
const parsed = createMonitorBodyValidation.parse({
|
||||
...baseHttpBody,
|
||||
customUpCodes: [200, 301, 404, 503],
|
||||
});
|
||||
expect(parsed.customUpCodes).toEqual([200, 301, 404, 503]);
|
||||
});
|
||||
|
||||
it("rejects invalid HTTP status codes", () => {
|
||||
expect(() =>
|
||||
createMonitorBodyValidation.parse({
|
||||
...baseHttpBody,
|
||||
customUpCodes: [5000],
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
expect(() =>
|
||||
createMonitorBodyValidation.parse({
|
||||
...baseHttpBody,
|
||||
customUpCodes: [-1],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("defaults to an empty array when not provided", () => {
|
||||
const parsed = createMonitorBodyValidation.parse(baseHttpBody);
|
||||
expect(parsed.customUpCodes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editMonitorBodyValidation", () => {
|
||||
it("accepts valid HTTP status codes on edits", () => {
|
||||
const parsed = editMonitorBodyValidation.parse({
|
||||
customUpCodes: [200, 201],
|
||||
});
|
||||
expect(parsed.customUpCodes).toEqual([200, 201]);
|
||||
});
|
||||
|
||||
it("rejects invalid HTTP status codes on edits", () => {
|
||||
expect(() =>
|
||||
editMonitorBodyValidation.parse({
|
||||
customUpCodes: [9999],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("importMonitorsBodyValidation", () => {
|
||||
it("retains valid HTTP status codes on imported HTTP monitors", () => {
|
||||
const parsed = importMonitorsBodyValidation.parse({
|
||||
monitors: [
|
||||
{
|
||||
...baseHttpBody,
|
||||
customUpCodes: [404],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(parsed.monitors[0].customUpCodes).toEqual([404]);
|
||||
});
|
||||
|
||||
it("rejects invalid HTTP status codes on imported HTTP monitors", () => {
|
||||
expect(() =>
|
||||
importMonitorsBodyValidation.parse({
|
||||
monitors: [
|
||||
{
|
||||
...baseHttpBody,
|
||||
customUpCodes: [5000],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("defaults to an empty array when not provided on import", () => {
|
||||
const parsed = importMonitorsBodyValidation.parse({
|
||||
monitors: [baseHttpBody],
|
||||
});
|
||||
expect(parsed.monitors[0].customUpCodes).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user