104 lines
3.8 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|