openapi docs, tests

This commit is contained in:
Alex Holliday committed 2026-09-01 10:09:37 -07:00
1 parent 8ccc7cbe9a
commit f85ed902e4
4 files changed
+566

No files matched your search

+419
View File
@@ -7467,6 +7467,425 @@
}
}
},
"/monitors/docker/details/{monitorId}/containers/{containerName}": {
"get": {
"tags": ["monitors"],
"summary": "Get details for one container on a Docker host monitor",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"schema": {
"type": "string",
"minLength": 1
},
"required": true,
"name": "monitorId",
"in": "path"
},
{
"schema": {
"type": "string",
"minLength": 1
},
"required": true,
"name": "containerName",
"in": "path"
},
{
"schema": {
"type": "string",
"enum": ["recent", "hour", "day", "week", "month", "all"]
},
"required": false,
"name": "dateRange",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"enum": [true]
},
"msg": {
"type": "string"
},
"data": {
"type": "object",
"properties": {
"monitor": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"url": {
"type": "string"
},
"port": {
"type": "number"
},
"isActive": {
"type": "boolean"
},
"interval": {
"type": "number"
},
"status": {
"type": "string",
"enum": ["up", "down", "paused", "initializing", "maintenance", "breached"]
},
"statusWindowSize": {
"type": "number"
},
"statusWindowThreshold": {
"type": "number"
},
"ignoreTlsErrors": {
"type": "boolean"
},
"proxyMode": {
"type": "string",
"enum": ["inherit", "none", "custom"]
},
"proxyId": {
"type": "string"
},
"useAdvancedMatching": {
"type": "boolean"
},
"jsonPath": {
"type": "string"
},
"expectedValue": {
"type": "string"
},
"matchMethod": {
"type": "string",
"enum": ["equal", "include", "regex"]
},
"method": {
"type": "string",
"enum": ["GET", "HEAD"]
},
"notifications": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"customUpCodes": {
"type": "array",
"items": {
"type": "number"
}
},
"secret": {
"type": "string"
},
"sshPrivateKey": {
"type": "string"
},
"cpuAlertThreshold": {
"type": "number"
},
"memoryAlertThreshold": {
"type": "number"
},
"diskAlertThreshold": {
"type": "number"
},
"tempAlertThreshold": {
"type": "number"
},
"selectedDisks": {
"type": "array",
"items": {
"type": "string"
}
},
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string"
},
"group": {
"type": "string",
"nullable": true
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array",
"items": {
"type": "string",
"enum": ["EU", "NA", "AS", "SA", "AF", "OC"]
}
},
"geoCheckInterval": {
"type": "number"
},
"dnsServer": {
"type": "string"
},
"dnsRecordType": {
"type": "string",
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
},
"teamId": {
"type": "string"
},
"userId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"lastEvaluatedAt": {
"type": "number"
},
"id": {
"type": "string"
}
},
"required": [
"name",
"type",
"url",
"isActive",
"interval",
"status",
"statusWindowSize",
"statusWindowThreshold",
"ignoreTlsErrors",
"proxyMode",
"useAdvancedMatching",
"method",
"notifications",
"tags",
"cpuAlertThreshold",
"memoryAlertThreshold",
"diskAlertThreshold",
"tempAlertThreshold",
"selectedDisks",
"geoCheckEnabled",
"geoCheckLocations",
"geoCheckInterval",
"teamId",
"userId",
"createdAt",
"updatedAt",
"lastEvaluatedAt",
"id"
],
"additionalProperties": {
"nullable": true
}
},
"stats": {
"type": "object",
"properties": {
"aggregate": {
"type": "array",
"items": {
"type": "object",
"properties": {
"_id": {
"type": "string"
},
"avgCpuPct": {
"type": "number",
"nullable": true
},
"avgMemoryUsedBytes": {
"type": "number",
"nullable": true
},
"avgMemoryPct": {
"type": "number",
"nullable": true
},
"minRestartCount": {
"type": "number",
"nullable": true
},
"maxRestartCount": {
"type": "number",
"nullable": true
}
},
"required": ["_id", "avgCpuPct", "avgMemoryUsedBytes", "avgMemoryPct", "minRestartCount", "maxRestartCount"]
}
},
"restartsInRange": {
"type": "number"
},
"latest": {
"type": "object",
"nullable": true,
"properties": {
"container": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"image": {
"type": "string"
},
"state": {
"type": "string",
"enum": ["created", "running", "paused", "restarting", "removing", "exited", "dead"]
},
"status": {
"type": "string"
},
"health": {
"type": "string",
"enum": ["healthy", "unhealthy", "starting", "none"]
},
"cpuPct": {
"type": "number"
},
"memoryUsedBytes": {
"type": "number"
},
"memoryLimitBytes": {
"type": "number"
},
"memoryPct": {
"type": "number"
},
"restartCount": {
"type": "number"
},
"startedAt": {
"type": "string"
}
},
"required": ["id", "name", "image", "state", "status", "health"]
},
"checkedAt": {
"type": "string"
}
},
"required": ["container", "checkedAt"]
}
},
"required": ["aggregate", "restartsInRange", "latest"]
}
},
"required": ["monitor", "stats"]
}
},
"required": ["success", "msg", "data"]
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"enum": [false]
},
"msg": {
"type": "string"
},
"data": {
"nullable": true
}
},
"required": ["success", "msg"]
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"enum": [false]
},
"msg": {
"type": "string"
},
"data": {
"nullable": true
}
},
"required": ["success", "msg"]
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"enum": [false]
},
"msg": {
"type": "string"
},
"data": {
"nullable": true
}
},
"required": ["success", "msg"]
}
}
}
}
}
}
},
"/monitors/pagespeed/details/{monitorId}": {
"get": {
"tags": ["monitors"],
+13
View File
@@ -18,7 +18,10 @@ import {
getHardwareDetailsByIdQueryValidation,
getDockerDetailsByIdParamValidation,
getDockerDetailsByIdQueryValidation,
getDockerContainerNameParamValidation,
getDockerContainerByNameQueryValidation,
dockerDetailsResponseSchema,
dockerContainerDetailsResponseSchema,
monitorResponseSchema,
uptimeDetailsResponseSchema,
} from "@/api/validation/monitorValidation.js";
@@ -118,6 +121,16 @@ registry.registerPath({
responses: { "200": okJson(dockerDetailsResponseSchema), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/docker/details/{monitorId}/containers/{containerName}",
tags,
summary: "Get details for one container on a Docker host monitor",
security: bearer,
request: { params: getDockerContainerNameParamValidation, query: getDockerContainerByNameQueryValidation },
responses: { "200": okJson(dockerContainerDetailsResponseSchema), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/pagespeed/details/{monitorId}",
@@ -484,3 +484,29 @@ export const dockerDetailsResponseSchema = z.object({
}),
monitorStats: monitorStatsResponseSchema.nullable(),
});
// Keep aligned with DockerContainerStatsBucket in domain/checks/check.type.ts.
export const dockerContainerStatsBucketResponseSchema = z.object({
_id: z.string(),
avgCpuPct: z.number().nullable(),
avgMemoryUsedBytes: z.number().nullable(),
avgMemoryPct: z.number().nullable(),
minRestartCount: z.number().nullable(),
maxRestartCount: z.number().nullable(),
});
// Response of GET /monitors/docker/details/{monitorId}/containers/{containerName}. Keep
// aligned with DockerContainerDetailsResult in domain/monitors/monitor.type.ts.
export const dockerContainerDetailsResponseSchema = z.object({
monitor: monitorResponseSchema.omit({ _id: true }).extend({ id: z.string() }),
stats: z.object({
aggregate: z.array(dockerContainerStatsBucketResponseSchema),
restartsInRange: z.number(),
latest: z
.object({
container: dockerContainerResponseSchema,
checkedAt: z.string(),
})
.nullable(),
}),
});
@@ -29,6 +29,7 @@ const createMonitorsRepositoryMock = () =>
const createChecksRepositoryMock = () =>
({
findByDateRangeAndMonitorId: jest.fn(),
findDockerContainerChecks: jest.fn(),
deleteByMonitorId: jest.fn(),
}) as unknown as IChecksRepository;
@@ -486,6 +487,113 @@ describe("MonitorService", () => {
});
});
describe("getDockerContainerByName", () => {
const makeBucket = (overrides: Partial<Record<string, number | string | null>> = {}) => ({
_id: "2024-01-01T00:00:00Z",
avgCpuPct: 12.5,
avgMemoryUsedBytes: 1024,
avgMemoryPct: 50,
minRestartCount: 0,
maxRestartCount: 0,
...overrides,
});
it("returns container details for a docker monitor", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const checksRepository = createChecksRepositoryMock();
const monitor = makeMonitor({ type: "docker" });
const container = { id: "abc", name: "checkmate-mongo", image: "mongo:8.0", state: "running", status: "Up 3 hours", health: "healthy" };
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
aggregate: [makeBucket()],
latest: { container, checkedAt: "2024-01-01T00:00:00Z" },
});
const { service } = createService({ monitorsRepository, checksRepository });
const result = await service.getDockerContainerByName({
teamId: TEAM_ID,
monitorId: MONITOR_ID,
containerName: "checkmate-mongo",
dateRange: "recent",
});
expect(checksRepository.findDockerContainerChecks).toHaveBeenCalledWith(MONITOR_ID, "checkmate-mongo", "recent");
expect(result.monitor).toMatchObject({ id: MONITOR_ID });
expect(result.stats.latest?.container).toMatchObject({ name: "checkmate-mongo" });
expect(result.stats.restartsInRange).toBe(0);
});
it("sums restarts within and between buckets", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const checksRepository = createChecksRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
aggregate: [
makeBucket({ _id: "01", minRestartCount: 0, maxRestartCount: 2 }),
makeBucket({ _id: "02", minRestartCount: 3, maxRestartCount: 3 }),
makeBucket({ _id: "03", minRestartCount: null, maxRestartCount: null }),
makeBucket({ _id: "04", minRestartCount: 3, maxRestartCount: 4 }),
],
latest: null,
});
const { service } = createService({ monitorsRepository, checksRepository });
const result = await service.getDockerContainerByName({
teamId: TEAM_ID,
monitorId: MONITOR_ID,
containerName: "checkmate-mongo",
dateRange: "day",
});
// 2 within bucket 01, +1 between 01 and 02, +1 within 04 (null bucket skipped)
expect(result.stats.restartsInRange).toBe(4);
});
it("counts a restart-count drop as a recreate", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const checksRepository = createChecksRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
aggregate: [
makeBucket({ _id: "01", minRestartCount: 5, maxRestartCount: 5 }),
makeBucket({ _id: "02", minRestartCount: 1, maxRestartCount: 1 }),
],
latest: null,
});
const { service } = createService({ monitorsRepository, checksRepository });
const result = await service.getDockerContainerByName({
teamId: TEAM_ID,
monitorId: MONITOR_ID,
containerName: "checkmate-mongo",
dateRange: "day",
});
// counter dropped 5 -> 1: the recreate itself plus the 1 restart the new counter shows
expect(result.stats.restartsInRange).toBe(2);
});
it("throws 404 when monitor not found", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(null);
const { service } = createService({ monitorsRepository });
await expect(
service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: "missing", containerName: "checkmate-mongo", dateRange: "recent" })
).rejects.toThrow("Monitor with ID missing not found.");
});
it("throws 400 when monitor type is not docker", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "http" }));
const { service } = createService({ monitorsRepository });
await expect(
service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "recent" })
).rejects.toThrow("monitors are not supported for docker container details");
});
});
describe("getPageSpeedDetailsById", () => {
it("returns pagespeed details for a pagespeed monitor", async () => {
const monitorsRepository = createMonitorsRepositoryMock();