Files
psu-schedule/test/metrics.test.js

82 lines
3.9 KiB
JavaScript

import { test, before, after, mock } from "node:test";
import assert from "node:assert/strict";
import { anonymize, endpointOf, percentile } from "../server/metrics.js";
import { startApi } from "./support/nextcloud.js";
test("Метрики: адрес без последней части, адреса документов в одном счётчике", () => {
assert.equal(anonymize("203.0.113.57"), "203.0.113.0");
assert.equal(anonymize("::ffff:192.168.1.20"), "192.168.1.0");
assert.equal(anonymize("2001:db8:85a3:8d3:1319:8a2e:370:7348"), "2001:db8:85a3::");
assert.equal(endpointOf("/api/original/0fcb72d4cc36c729aa"), "/api/original/:id");
assert.equal(endpointOf("/api/timetable"), "/api/timetable");
// Bounds 25, 50, 100, …: half the answers took up to 50 ms, 95% up to 1000 ms.
const histogram = [3, 3, 2, 1, 0, 1, 0, 0, 0, 0];
assert.equal(percentile(histogram, 0.5), 50);
assert.equal(percentile(histogram, 0.95), 1000);
assert.equal(percentile(Array(10).fill(0), 0.5), null);
});
// The API with its log on, a low request limit and the log lines caught.
let api;
const lines = [];
before(async () => {
mock.method(console, "log", (line) => lines.push(line));
api = await startApi({ PSU_REQUEST_LOG: "on", PSU_API_RATE_LIMIT: "6" });
});
after(() => {
mock.restoreAll();
api?.stop();
});
const get = (path, headers) => fetch(api.base + path, { headers });
test("Метрики: строка журнала на запрос и сводка в /api/health", async () => {
const groups = await get("/api/groups?" + new URLSearchParams({ q: "23-БА" }));
const doc = (await groups.json()).results.find((g) => g.group === "23-БА").fileId;
const path = "/api/timetable?" + new URLSearchParams({ doc, group: "23-БА", from: "2026-10-19", days: 7 });
const etag = (await get(path)).headers.get("etag");
assert.equal((await get(path, { "If-None-Match": etag, "Cache-Control": "max-age=0" })).status, 304);
assert.equal((await get("/api/nope")).status, 404);
// Health checks are neither logged nor counted.
await get("/api/health");
await get("/api/ready");
const logged = lines.map((l) => JSON.parse(l));
assert.equal(logged.length, 4);
assert.deepEqual(logged.map((l) => l.status), [200, 200, 304, 404]);
const notFound = logged.at(-1);
assert.equal(notFound.method, "GET");
assert.equal(notFound.path, "/api/nope");
assert.equal(notFound.code, "unknown_endpoint");
assert.equal(notFound.ip, "127.0.0.0");
assert.equal(typeof notFound.ms, "number");
assert.ok(!logged.some((l) => l.path.startsWith("/api/health")));
const { traffic } = await (await get("/api/health")).json();
const hour = traffic.lastHour;
assert.equal(hour.requests, 4);
assert.equal(hour.notModified, 1);
assert.equal(hour.notModifiedShare, 0.25);
assert.equal(hour.clientErrors, 1);
assert.equal(hour.serverErrors, 0);
assert.equal(hour.clients, 1);
assert.deepEqual(hour.busiestClients, [{ client: "127.0.0.0", requests: 4 }]);
assert.equal(hour.endpoints["/api/timetable"].requests, 2);
assert.ok(hour.p50Ms !== null && hour.p95Ms !== null);
assert.equal(traffic.total.requests, 4);
});
test("Метрики: лимит запросов и сбои университета считаются отдельно", async () => {
const catalog = await (await get("/api/catalog")).json();
const broken = catalog.files.find((f) => f.name === "Сломанный.docx");
assert.equal((await get(`/api/original/${broken.id}`)).status, 502);
// The limit of 6 is reached: 4 earlier requests, the catalog and the original above.
assert.equal((await get("/api/nope")).status, 429);
const { traffic } = await (await get("/api/health")).json();
const hour = traffic.lastHour;
assert.equal(hour.limited, 1);
assert.equal(hour.sourceErrors, 1);
assert.ok(hour.sourceFailures >= 1);
assert.equal(hour.serverErrors, 1);
assert.equal(hour.endpoints["/api/original/:id"].errors, 1);
});