update tests

This commit is contained in:
Alex Holliday committed 2026-09-03 12:56:44 -07:00
1 parent 6587ddf389
commit f7e1793bac
8 files changed
+392 -11

No files matched your search

+12
View File
@@ -24,6 +24,8 @@ import {
dockerContainerDetailsResponseSchema,
monitorResponseSchema,
uptimeDetailsResponseSchema,
getDockerContainerLogsQueryValidation,
dockerContainerLogsResponseSchema,
} from "@/api/validation/monitorValidation.js";
import { updateNotificationsValidation } from "@/api/validation/notificationValidation.js";
@@ -131,6 +133,16 @@ registry.registerPath({
responses: { "200": okJson(dockerContainerDetailsResponseSchema), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/docker/details/{monitorId}/containers/{containerName}/logs",
tags,
summary: "Get stored log lines for one container on a Docker host monitor, newest first",
security: bearer,
request: { params: getDockerContainerNameParamValidation, query: getDockerContainerLogsQueryValidation },
responses: { "200": okJson(dockerContainerLogsResponseSchema), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/pagespeed/details/{monitorId}",
@@ -5,6 +5,7 @@ import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/types/network.ts";
import { AppError } from "../../../../src/utils/AppError.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts";
import { DOCKER_LOG_TAIL_LINES } from "../../../../src/domain/docker/docker.type.ts";
// ── Fixtures ─────────────────────────────────────────────────────────────────
@@ -53,10 +54,19 @@ const makeStats = (overrides?: Record<string, any>) => ({
...overrides,
});
const setup = (overrides: Partial<{ ping: any; listContainers: any; getContainer: any; inspect: any; stats: any }> = {}) => {
const makeLogFrame = (streamType: number, text: string): Buffer => {
const payload = Buffer.from(text);
const header = Buffer.alloc(8);
header.writeUInt8(streamType, 0);
header.writeUInt32BE(payload.length, 4);
return Buffer.concat([header, payload]);
};
const setup = (overrides: Partial<{ ping: any; listContainers: any; getContainer: any; inspect: any; stats: any; logs: any }> = {}) => {
const inspect = overrides.inspect ?? jest.fn().mockResolvedValue(makeInspect());
const stats = overrides.stats ?? jest.fn().mockResolvedValue(makeStats());
const getContainer = overrides.getContainer ?? jest.fn().mockReturnValue({ inspect, stats });
const logs = overrides.logs ?? jest.fn().mockResolvedValue(Buffer.alloc(0));
const getContainer = overrides.getContainer ?? jest.fn().mockReturnValue({ inspect, stats, logs });
const instance = {
ping: overrides.ping ?? jest.fn().mockResolvedValue("OK"),
listContainers: overrides.listContainers ?? jest.fn().mockResolvedValue([makeContainer()]),
@@ -65,7 +75,7 @@ const setup = (overrides: Partial<{ ping: any; listContainers: any; getContainer
const DockerLib = jest.fn().mockReturnValue(instance) as any;
const logger = createMockLogger();
const provider = new DockerProvider(logger as any, DockerLib);
return { provider, logger, DockerLib, ping: instance.ping, listContainers: instance.listContainers, getContainer, inspect, stats };
return { provider, logger, DockerLib, ping: instance.ping, listContainers: instance.listContainers, getContainer, inspect, stats, logs };
};
// ── Contract ─────────────────────────────────────────────────────────────────
@@ -278,6 +288,65 @@ describe("DockerProvider", () => {
});
});
describe("container logs", () => {
it("parses multiplexed stdout and stderr with normalized timestamps", async () => {
const logs = jest
.fn()
.mockResolvedValue(
Buffer.concat([makeLogFrame(1, "2026-01-01T00:00:37.1Z ready\n"), makeLogFrame(2, "2026-01-01T00:00:38.123Z failed\r\n")])
);
const { provider } = setup({ logs });
const result = await provider.handle(makeMonitor());
expect(result.payload?.logs?.[0]?.lines).toEqual([
{ ts: "2026-01-01T00:00:37.100000000Z", stream: "stdout", text: "ready" },
{ ts: "2026-01-01T00:00:38.123000000Z", stream: "stderr", text: "failed" },
]);
});
it("treats raw TTY output as stdout and drops lines without timestamps", async () => {
const logs = jest.fn().mockResolvedValue(Buffer.from("noise\n2026-01-01T00:00:00Z first\n2026-01-01T00:00:01.12Z second\n"));
const { provider } = setup({ logs });
const result = await provider.handle(makeMonitor());
expect(result.payload?.logs?.[0]?.lines).toEqual([
{ ts: "2026-01-01T00:00:00.000000000Z", stream: "stdout", text: "first" },
{ ts: "2026-01-01T00:00:01.120000000Z", stream: "stdout", text: "second" },
]);
});
it("truncates oversized UTF-8 lines and appends the marker", async () => {
const logs = jest.fn().mockResolvedValue(Buffer.from(`2026-01-01T00:00:00Z ${"x".repeat(5000)}\n`));
const { provider } = setup({ logs });
const result = await provider.handle(makeMonitor());
const text = result.payload?.logs?.[0]?.lines[0]?.text;
expect(text).toEqual(expect.stringMatching(/ …\[truncated\]$/));
expect(Buffer.byteLength(text?.replace(" …[truncated]", "") ?? "", "utf8")).toBeLessThanOrEqual(4096);
});
it("isolates a logs failure and warns", async () => {
const { provider, logger } = setup({ logs: jest.fn().mockRejectedValue(new Error("logs failed")) });
const result = await provider.handle(makeMonitor());
expect(result.payload?.logs).toEqual([]);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to read logs for container my-container" }));
});
it("requests a bounded timestamped tail for every container state", async () => {
const containers = [makeContainer({ Id: "a".repeat(64) }), makeContainer({ Id: "b".repeat(64), State: "exited" })];
const { provider, logs } = setup({ listContainers: jest.fn().mockResolvedValue(containers) });
await provider.handle(makeMonitor());
expect(logs).toHaveBeenCalledTimes(2);
expect(logs).toHaveBeenCalledWith({ follow: false, stdout: true, stderr: true, timestamps: true, tail: DOCKER_LOG_TAIL_LINES });
});
});
// ── Enrichment fault isolation ───────────────────────────────────────
describe("enrichment fault isolation", () => {
+106 -7
View File
@@ -7,6 +7,8 @@ import type { ISettingsService } from "../../../src/domain/app-settings/app-sett
import type { IJobsRepository } from "../../../src/domain/jobs/job.repository.interface.ts";
import type { Check } from "../../../src/domain/checks/check.type.ts";
import type { GeoCheck } from "../../../src/domain/geo-checks/geo-check.type.ts";
import type { IDockerLogsService } from "../../../src/domain/docker/docker-log.service.ts";
import type { DockerLog } from "../../../src/domain/docker/docker-log.type.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -20,6 +22,11 @@ const createMockGeoChecksService = () =>
createGeoChecks: jest.fn().mockResolvedValue([]),
}) as unknown as jest.Mocked<IGeoChecksService>;
const createMockDockerLogsService = () =>
({
createDockerLogs: jest.fn().mockResolvedValue(0),
}) as unknown as jest.Mocked<IDockerLogsService>;
const createMockSettingsService = (nodeEnv: string = "development") =>
({
getSettings: jest.fn().mockReturnValue({ nodeEnv }),
@@ -48,14 +55,28 @@ const makeGeoCheck = (overrides?: Partial<GeoCheck>): GeoCheck =>
...overrides,
}) as GeoCheck;
const makeDockerLog = (overrides?: Partial<DockerLog>): DockerLog =>
({
id: "docker-log-1",
metadata: { monitorId: "mon-1", teamId: "team-1", containerId: "container-1", containerName: "web" },
lines: [{ ts: "2026-01-01T00:00:00.000000000Z", stream: "stdout", text: "ready" }],
gap: false,
checkedAt: "2026-01-01T00:00:01.000Z",
expiry: "2026-01-08T00:00:01.000Z",
createdAt: "2026-01-01T00:00:01.000Z",
updatedAt: "2026-01-01T00:00:01.000Z",
...overrides,
}) as DockerLog;
const createService = (nodeEnv: string = "development") => {
const logger = createMockLogger();
const checkService = createMockCheckService();
const geoChecksService = createMockGeoChecksService();
const dockerLogsService = createMockDockerLogsService();
const settingsService = createMockSettingsService(nodeEnv);
const jobsRepository = createMockJobsRepository();
const service = new BufferService(logger as any, checkService, geoChecksService, settingsService, jobsRepository);
return { service, logger, checkService, geoChecksService, jobsRepository };
const service = new BufferService(logger as any, checkService, geoChecksService, dockerLogsService, settingsService, jobsRepository);
return { service, logger, checkService, geoChecksService, dockerLogsService, jobsRepository };
};
// ── Tests ────────────────────────────────────────────────────────────────────
@@ -209,6 +230,27 @@ describe("BufferService", () => {
});
});
describe("addDockerLogToBuffer", () => {
it("adds a docker log to the buffer", async () => {
const { service, dockerLogsService } = createService();
const dockerLog = makeDockerLog();
service.addDockerLogToBuffer(dockerLog);
await service.flushDockerLogsBuffer();
expect(dockerLogsService.createDockerLogs).toHaveBeenCalledWith([dockerLog]);
});
it("logs an error if push throws", () => {
const { service, logger } = createService();
Object.defineProperty(service, "dockerLogBuffer", { value: Object.freeze([]) });
service.addDockerLogToBuffer(makeDockerLog());
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ method: "addDockerLogToBuffer" }));
});
});
// ── scheduleNextFlush ────────────────────────────────────────────────
describe("scheduleNextFlush", () => {
@@ -223,15 +265,17 @@ describe("BufferService", () => {
expect(jest.getTimerCount()).toBe(initialTimerCount);
});
it("flushes buffer and geo buffer when timer fires", async () => {
const { service, checkService, geoChecksService } = createService();
it("flushes all buffers when timer fires", async () => {
const { service, checkService, geoChecksService, dockerLogsService } = createService();
service.addToBuffer(makeCheck());
service.addGeoCheckToBuffer(makeGeoCheck());
service.addDockerLogToBuffer(makeDockerLog());
await jest.advanceTimersByTimeAsync(1000);
expect(checkService.createChecks).toHaveBeenCalled();
expect(geoChecksService.createGeoChecks).toHaveBeenCalled();
expect(dockerLogsService.createDockerLogs).toHaveBeenCalled();
});
it("reschedules after flush completes", async () => {
@@ -472,18 +516,72 @@ describe("BufferService", () => {
});
});
describe("flushDockerLogsBuffer", () => {
it("does nothing when the docker log buffer is empty", async () => {
const { service, dockerLogsService } = createService();
await service.flushDockerLogsBuffer();
expect(dockerLogsService.createDockerLogs).not.toHaveBeenCalled();
});
it("flushes the batch and clears it", async () => {
const { service, dockerLogsService } = createService();
const dockerLog = makeDockerLog();
service.addDockerLogToBuffer(dockerLog);
await service.flushDockerLogsBuffer();
await service.flushDockerLogsBuffer();
expect(dockerLogsService.createDockerLogs).toHaveBeenCalledWith([dockerLog]);
expect(dockerLogsService.createDockerLogs).toHaveBeenCalledTimes(1);
});
it("does not drop logs added while a flush is in flight", async () => {
const { service, dockerLogsService } = createService();
let resolveWrite!: (value: number) => void;
(dockerLogsService.createDockerLogs as jest.Mock).mockImplementationOnce(() => new Promise<number>((resolve) => (resolveWrite = resolve)));
const first = makeDockerLog({ id: "docker-log-1" });
const second = makeDockerLog({ id: "docker-log-2" });
service.addDockerLogToBuffer(first);
const flush = service.flushDockerLogsBuffer();
service.addDockerLogToBuffer(second);
resolveWrite(1);
await flush;
await service.flushDockerLogsBuffer();
expect(dockerLogsService.createDockerLogs).toHaveBeenNthCalledWith(1, [first]);
expect(dockerLogsService.createDockerLogs).toHaveBeenNthCalledWith(2, [second]);
});
it("drops a failed batch and logs the error", async () => {
const { service, dockerLogsService, logger } = createService();
(dockerLogsService.createDockerLogs as jest.Mock).mockRejectedValueOnce(new Error("DB error"));
service.addDockerLogToBuffer(makeDockerLog());
await service.flushDockerLogsBuffer();
await service.flushDockerLogsBuffer();
expect(dockerLogsService.createDockerLogs).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "DB error", method: "flushDockerLogsBuffer" }));
});
});
// ── shutdown ─────────────────────────────────────────────────────────────
describe("shutdown", () => {
it("stops the flush timer and flushes both buffers", async () => {
const { service, checkService, geoChecksService } = createService();
it("stops the flush timer and flushes all buffers", async () => {
const { service, checkService, geoChecksService, dockerLogsService } = createService();
service.addToBuffer(makeCheck());
service.addGeoCheckToBuffer(makeGeoCheck());
service.addDockerLogToBuffer(makeDockerLog());
await service.shutdown();
expect(checkService.createChecks).toHaveBeenCalledTimes(1);
expect(geoChecksService.createGeoChecks).toHaveBeenCalledTimes(1);
expect(dockerLogsService.createDockerLogs).toHaveBeenCalledTimes(1);
// Timer is cleared: advancing past the flush interval triggers no further flush.
await jest.advanceTimersByTimeAsync(60 * 1000);
@@ -491,12 +589,13 @@ describe("BufferService", () => {
});
it("is safe to call with empty buffers", async () => {
const { service, checkService, geoChecksService } = createService();
const { service, checkService, geoChecksService, dockerLogsService } = createService();
await service.shutdown();
expect(checkService.createChecks).not.toHaveBeenCalled();
expect(geoChecksService.createGeoChecks).not.toHaveBeenCalled();
expect(dockerLogsService.createDockerLogs).not.toHaveBeenCalled();
});
});
});
@@ -28,6 +28,7 @@ const createProducer = (overrides?: Record<string, any>) => {
networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) },
proxyResolver: { resolve: jest.fn().mockResolvedValue(undefined) },
buffer: { addToBuffer: jest.fn() },
dockerLogsService: { buildDockerLogs: jest.fn().mockResolvedValue([]) },
...overrides,
};
const producer = new CheckProducer(
@@ -37,6 +38,7 @@ const createProducer = (overrides?: Record<string, any>) => {
defaults.networkService as any,
defaults.proxyResolver as any,
defaults.buffer as any,
defaults.dockerLogsService as any,
defaults.logger as any
);
return { producer, defaults };
@@ -142,4 +144,28 @@ describe("CheckProducer", () => {
expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith(check);
expect(result).toEqual({ status, check });
});
it("builds and buffers docker logs for a docker status", async () => {
const status = { type: "docker", monitorId: "m1", teamId: "team", status: true, code: 200, message: "OK", payload: {} };
const dockerLogs = [{ id: "docker-log-1" }, { id: "docker-log-2" }];
const { producer, defaults } = createProducer({
networkService: { requestStatus: jest.fn().mockResolvedValue(status) },
buffer: { addToBuffer: jest.fn(), addDockerLogToBuffer: jest.fn() },
dockerLogsService: { buildDockerLogs: jest.fn().mockResolvedValue(dockerLogs) },
});
await producer.produce(makeMonitor({ type: "docker" }));
expect(defaults.dockerLogsService.buildDockerLogs).toHaveBeenCalledWith(status);
expect(defaults.buffer.addDockerLogToBuffer).toHaveBeenNthCalledWith(1, dockerLogs[0]);
expect(defaults.buffer.addDockerLogToBuffer).toHaveBeenNthCalledWith(2, dockerLogs[1]);
});
it("does not build docker logs for a non-docker status", async () => {
const { producer, defaults } = createProducer();
await producer.produce(makeMonitor());
expect(defaults.dockerLogsService.buildDockerLogs).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,87 @@
import { describe, expect, it, jest } from "@jest/globals";
import { DockerLogsService } from "../../../src/domain/docker/docker-log.service.ts";
import { DOCKER_LOG_TAIL_LINES, type DockerLogLine } from "../../../src/domain/docker/docker.type.ts";
import type { MonitorStatusResponse, DockerStatusPayload } from "../../../src/types/network.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
const line = (second: number): DockerLogLine => ({
ts: `2026-01-01T00:00:${String(second).padStart(2, "0")}.000000000Z`,
stream: "stdout",
text: `line ${second}`,
});
const status = (logs?: DockerStatusPayload["logs"]): MonitorStatusResponse<DockerStatusPayload> => ({
monitorId: "monitor-1",
teamId: "team-1",
type: "docker",
status: true,
code: 200,
message: "OK",
payload: { containers: [], summary: { total: 0, running: 0, stopped: 0, unhealthy: 0 }, logs },
});
const createService = () => {
const repository = {
findLastLineTimestamp: jest.fn().mockResolvedValue(null),
createDockerLogs: jest.fn().mockResolvedValue(0),
};
const logger = createMockLogger();
return { service: new DockerLogsService({ logger: logger as any, dockerLogsRepository: repository as any }), repository, logger };
};
describe("DockerLogsService", () => {
it("returns no documents for missing or empty logs", async () => {
const { service, repository } = createService();
expect(await service.buildDockerLogs(status())).toEqual([]);
expect(await service.buildDockerLogs(status([]))).toEqual([]);
expect(repository.findLastLineTimestamp).not.toHaveBeenCalled();
});
it("seeds the cursor, drops old lines, and sets seven-day expiry", async () => {
const { service, repository } = createService();
repository.findLastLineTimestamp.mockResolvedValue(line(1).ts);
const result = await service.buildDockerLogs(status([{ containerId: "container-1", containerName: "web", lines: [line(1), line(2)] }]));
expect(repository.findLastLineTimestamp).toHaveBeenCalledWith("monitor-1", "container-1");
expect(result[0]?.lines).toEqual([line(2)]);
expect(new Date(result[0]!.expiry).getTime() - new Date(result[0]!.checkedAt).getTime()).toBe(7 * 24 * 60 * 60 * 1000);
});
it("advances the cursor in memory and does not query again", async () => {
const { service, repository } = createService();
const logs = [{ containerId: "container-1", containerName: "web", lines: [line(1)] }];
expect(await service.buildDockerLogs(status(logs))).toHaveLength(1);
expect(await service.buildDockerLogs(status(logs))).toEqual([]);
expect(repository.findLastLineTimestamp).toHaveBeenCalledTimes(1);
});
it("sets gap only when every tail line is fresh", async () => {
const { service } = createService();
const lines = Array.from({ length: DOCKER_LOG_TAIL_LINES }, (_, index) => ({
ts: `2026-01-01T00:00:00.${String(index).padStart(9, "0")}Z`,
stream: "stdout" as const,
text: String(index),
}));
const [result] = await service.buildDockerLogs(status([{ containerId: "container-1", containerName: "web", lines }]));
expect(result?.gap).toBe(true);
});
it("skips a failed container and continues with the others", async () => {
const { service, repository, logger } = createService();
repository.findLastLineTimestamp.mockRejectedValueOnce(new Error("DB down")).mockResolvedValueOnce(null);
const result = await service.buildDockerLogs(
status([
{ containerId: "bad", containerName: "bad-container", lines: [line(1)] },
{ containerId: "good", containerName: "good-container", lines: [line(2)] },
])
);
expect(result).toHaveLength(1);
expect(result[0]?.metadata.containerId).toBe("good");
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build docker logs for container bad-container" }));
});
});
@@ -2,6 +2,7 @@ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { MonitorService } from "../../../src/domain/monitors/monitor.service.ts";
import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts";
import type { IGeoChecksRepository } from "../../../src/domain/geo-checks/geo-check.repository.interface.ts";
import type { IDockerLogsRepository } from "../../../src/domain/docker/docker-log.repository.interface.ts";
import type { IIncidentsRepository } from "../../../src/domain/incidents/incident.repository.interface.ts";
import type { IMonitorStatsRepository } from "../../../src/domain/monitor-stats/monitor-stats.repository.interface.ts";
import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts";
@@ -50,6 +51,12 @@ const createGeoChecksRepositoryMock = () =>
deleteByMonitorId: jest.fn(),
}) as unknown as IGeoChecksRepository;
const createDockerLogsRepositoryMock = () =>
({
findByContainerName: jest.fn(),
deleteByMonitorId: jest.fn().mockResolvedValue(0),
}) as unknown as IDockerLogsRepository;
const createIncidentsRepositoryMock = () =>
({
deleteByMonitorId: jest.fn(),
@@ -89,6 +96,7 @@ const createService = (
monitorStatsRepository?: IMonitorStatsRepository;
statusPagesRepository?: IStatusPagesRepository;
geoChecksRepository?: IGeoChecksRepository;
dockerLogsRepository?: IDockerLogsRepository;
incidentsRepository?: IIncidentsRepository;
jobQueue?: ReturnType<typeof createJobQueueMock>;
logger?: ReturnType<typeof createMockLogger>;
@@ -104,6 +112,7 @@ const createService = (
monitorsRepository: overrides.monitorsRepository ?? createMonitorsRepositoryMock(),
checksRepository: overrides.checksRepository ?? createChecksRepositoryMock(),
geoChecksRepository: overrides.geoChecksRepository ?? createGeoChecksRepositoryMock(),
dockerLogsRepository: overrides.dockerLogsRepository ?? createDockerLogsRepositoryMock(),
monitorStatsRepository: overrides.monitorStatsRepository ?? createMonitorStatsRepositoryMock(),
statusPagesRepository: overrides.statusPagesRepository ?? createStatusPagesRepositoryMock(),
incidentsRepository: overrides.incidentsRepository ?? createIncidentsRepositoryMock(),
@@ -594,6 +603,61 @@ describe("MonitorService", () => {
});
});
describe("getDockerContainerLogs", () => {
it("throws 404 when the monitor is not found", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(null);
const { service } = createService({ monitorsRepository });
await expect(service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: "missing", containerName: "web", limit: 20 })).rejects.toThrow(
"Monitor with ID missing not found"
);
});
it("throws 400 for a non-docker monitor", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "http" }));
const { service } = createService({ monitorsRepository });
await expect(service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", limit: 20 })).rejects.toThrow(
"monitors are not supported for docker container logs"
);
});
it("passes pagination through and returns the last checkedAt as a full-page cursor", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const dockerLogsRepository = createDockerLogsRepositoryMock();
const before = new Date("2026-01-02T00:00:00.000Z");
const logs = [{ checkedAt: "2026-01-01T02:00:00.000Z" }, { checkedAt: "2026-01-01T01:00:00.000Z" }];
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
(dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue(logs);
const { service } = createService({ monitorsRepository, dockerLogsRepository });
const result = await service.getDockerContainerLogs({
teamId: TEAM_ID,
monitorId: MONITOR_ID,
containerName: "web",
before,
limit: 2,
});
expect(dockerLogsRepository.findByContainerName).toHaveBeenCalledWith({ monitorId: MONITOR_ID, containerName: "web", before, limit: 2 });
expect(result.nextCursor).toBe("2026-01-01T01:00:00.000Z");
});
it("returns a null cursor for a short page", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const dockerLogsRepository = createDockerLogsRepositoryMock();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
(dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue([{ checkedAt: "2026-01-01T01:00:00.000Z" }]);
const { service } = createService({ monitorsRepository, dockerLogsRepository });
const result = await service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", limit: 2 });
expect(result.nextCursor).toBeNull();
});
});
describe("getPageSpeedDetailsById", () => {
it("returns pagespeed details for a pagespeed monitor", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
@@ -1802,6 +1866,7 @@ describe("MonitorService", () => {
monitorsRepository,
checksRepository,
geoChecksRepository,
dockerLogsRepository: createDockerLogsRepositoryMock(),
monitorStatsRepository,
statusPagesRepository,
incidentsRepository,
@@ -32,6 +32,9 @@ const createHelper = (overrides?: Record<string, unknown>) => {
const geoChecksRepository = {
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
};
const dockerLogsRepository = {
deleteByMonitorIdsNotIn: jest.fn().mockResolvedValue(0),
};
const settingsService = {
getDBSettings: jest.fn().mockResolvedValue({ checkTTL: 30 }),
};
@@ -50,6 +53,7 @@ const createHelper = (overrides?: Record<string, unknown>) => {
checksRepository,
incidentsRepository,
geoChecksRepository,
dockerLogsRepository,
...overrides,
};
@@ -63,7 +67,8 @@ const createHelper = (overrides?: Record<string, unknown>) => {
defaults.monitorStatsRepository as any,
defaults.checksRepository as any,
defaults.incidentsRepository as any,
defaults.geoChecksRepository as any
defaults.geoChecksRepository as any,
defaults.dockerLogsRepository as any
);
return { helper, defaults };
};
@@ -81,6 +86,7 @@ describe("WorkerHelper", () => {
(defaults.checksRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(4);
(defaults.incidentsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(1);
(defaults.geoChecksRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(5);
(defaults.dockerLogsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(7);
(defaults.jobsRepository.deleteByMonitorIdsNotIn as jest.Mock).mockResolvedValue(6);
const job = helper.getCleanupOrphanedJob();
@@ -93,12 +99,14 @@ describe("WorkerHelper", () => {
expect(defaults.checksRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
expect(defaults.incidentsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
expect(defaults.geoChecksRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
expect(defaults.dockerLogsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
expect(defaults.jobsRepository.deleteByMonitorIdsNotIn).toHaveBeenCalledWith(["m1"]);
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("2 orphaned monitors") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("3 orphaned monitor stats") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("4 orphaned checks") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("1 orphaned incidents") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("5 orphaned geo checks") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("7 orphaned docker logs") }));
expect(defaults.logger.info).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("6 orphaned jobs") }));
});
@@ -3,8 +3,23 @@ import {
createMonitorBodyValidation,
editMonitorBodyValidation,
importMonitorsBodyValidation,
getDockerContainerLogsQueryValidation,
} from "../../../src/api/validation/monitorValidation.ts";
describe("getDockerContainerLogsQueryValidation", () => {
it("defaults limit to 20", () => {
expect(getDockerContainerLogsQueryValidation.parse({})).toEqual({ limit: 20 });
});
it("rejects a limit above 50", () => {
expect(() => getDockerContainerLogsQueryValidation.parse({ limit: 51 })).toThrow();
});
it("rejects a non-ISO before cursor", () => {
expect(() => getDockerContainerLogsQueryValidation.parse({ before: "yesterday" })).toThrow();
});
});
const baseDnsBody = {
name: "DNS check",
type: "dns" as const,