Files
Checkmate/server/test/integration/checksRepositoryGroupedChecks.test.ts
2026-07-30 11:54:19 -07:00

104 lines
3.8 KiB
TypeScript

import { describe, expect, it, beforeAll, afterAll, beforeEach } from "@jest/globals";
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";
import MongoChecksRepository from "../../src/domain/checks/check.repository.mongo.ts";
import { CheckModel } from "../../src/domain/checks/check.model.ts";
import type { UptimeChecksResult } from "../../src/domain/checks/check.type.ts";
import type { ILogger } from "../../src/utils/logger.ts";
import { createMockLogger } from "../helpers/createMockLogger.ts";
// ── Real-Mongo harness ─────────────────────────────────────────────────────────
// The guarantee under test — every phase average uses the full bucket as its
// denominator — IS the $ifNull-inside-$avg behavior of the groupedChecks facet, so it
// can only be exercised against a live aggregation engine. Reused keep-alive sockets
// produce checks with no timings subdocument at all; without the coercion, $avg would
// skip them and average dns/tcp/tls over fresh-connection checks only.
let mongod: MongoMemoryServer;
beforeAll(async () => {
mongod = await MongoMemoryServer.create();
await mongoose.connect(mongod.getUri());
await CheckModel.createCollection(); // timeseries collections must exist before insert
}, 120_000);
afterAll(async () => {
await mongoose.disconnect();
await mongod.stop();
});
beforeEach(async () => {
await CheckModel.deleteMany({});
});
// ── Helpers ──────────────────────────────────────────────────────────────────
const MONITOR_ID = new mongoose.Types.ObjectId();
const TEAM_ID = new mongoose.Types.ObjectId();
// One shared createdAt puts every seeded check in the same hourly bucket of the "day"
// range, keeping the facet's group stage deterministic.
const BUCKET_TIME = new Date(Date.now() - 10 * 60 * 1000);
const seedCheck = (overrides: Record<string, unknown> = {}) =>
CheckModel.create({
metadata: { monitorId: MONITOR_ID, teamId: TEAM_ID, type: "http" },
status: true,
responseTime: 100,
createdAt: BUCKET_TIME,
...overrides,
});
const findGrouped = async (repo: MongoChecksRepository) =>
(await repo.findByDateRangeAndMonitorId(MONITOR_ID.toString(), "day", { type: "http" })) as UptimeChecksResult;
describe("MongoChecksRepository groupedChecks facet", () => {
let repo: MongoChecksRepository;
beforeEach(() => {
repo = new MongoChecksRepository(createMockLogger() as unknown as ILogger);
});
it("averages each phase over the full bucket, counting checks without timings as 0", async () => {
await seedCheck({
responseTime: 100,
timings: { phases: { dns: 10, tcp: 20, tls: 30, request: 8, firstByte: 40, download: 6 } },
});
await seedCheck({ responseTime: 200 }); // reused socket: no timings subdocument at all
const result = await findGrouped(repo);
expect(result.groupedChecks).toHaveLength(1);
// Plain $avg would skip the timings-less check and report 10/20/30/8/40/6 here.
expect(result.groupedChecks[0]).toMatchObject({
totalChecks: 2,
avgDns: 5,
avgTcp: 10,
avgTls: 15,
avgRequest: 4,
avgFirstByte: 20,
avgDownload: 3,
avgResponseTime: 150,
});
});
it("yields 0 (not null) for every phase in a bucket of only timings-less checks", async () => {
await seedCheck();
await seedCheck();
const result = await findGrouped(repo);
expect(result.groupedChecks).toHaveLength(1);
// The client sums these fields for the stacked chart; null would poison the math.
expect(result.groupedChecks[0]).toMatchObject({
totalChecks: 2,
avgDns: 0,
avgTcp: 0,
avgTls: 0,
avgRequest: 0,
avgFirstByte: 0,
avgDownload: 0,
});
});
});