Files
psu-schedule/test/watch.test.js
Zakhar Timoshenko 3ae7b38fca feat: implement dashboard with authentication and event logging
- Added dashboard functionality at /status with Basic Auth using DASHBOARD_PASSWORD.
- Created server-side event logging to track catalog and university status.
- Implemented traffic metrics tracking and display on the dashboard.
- Enhanced API tests to cover new dashboard features and authentication.
- Introduced CSS and JS for dashboard styling and functionality.
- Added tests for dashboard access control and data retrieval.
2026-10-21 00:04:33 +03:00

124 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import { startApi, fixture } from "./support/nextcloud.js";
// The server's own checks of the catalog: listings only of folders whose ETag changed, the index
// rebuilt only for a changed document, and messages to the owner in Telegram (a stand-in here).
let api, telegram, watch;
const messages = [];
// What the dashboard shows (/status, behind its password).
const health = async () => (await fetch(api.base + "/status/data.json", { headers: { Authorization: "Basic " + Buffer.from("admin:secret").toString("base64") } })).json();
const lastMessage = () => messages.at(-1)?.text || "";
before(async () => {
telegram = createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
messages.push({ path: req.url, ...JSON.parse(body) });
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
});
});
await new Promise((resolve) => telegram.listen(0, "127.0.0.1", resolve));
api = await startApi({
TELEGRAM_BOT_TOKEN: "test-token",
TELEGRAM_CHAT_ID: "42",
TELEGRAM_API: `http://127.0.0.1:${telegram.address().port}`,
PSU_PUBLIC_URL: "https://psu.example",
PSU_ALERT_SOURCE_DOWN_MS: "0",
// Every check reaches the stand-in, however soon after the last one.
PSU_MIN_REFRESH_MS: "0",
DASHBOARD_PASSWORD: "secret",
});
watch = await import("../server/watch.js");
});
after(() => {
api?.stop();
telegram?.close();
});
test("Наблюдение: первая проверка обходит весь каталог и включает оповещения", async () => {
await watch.watchOnce();
const { catalog, directory, telegram: tg, watch: status } = await health();
assert.equal(catalog.lastCrawl.full, true);
assert.equal(directory.documents, 2);
assert.equal(tg.enabled, true);
assert.equal(status.lastError, null);
assert.equal(messages.length, 1);
assert.equal(messages[0].path, "/bottest-token/sendMessage");
assert.equal(messages[0].chat_id, "42");
assert.match(lastMessage(), /Оповещения расписания ПГУ включены/);
assert.match(lastMessage(), /2 документов, не читаются: 2/);
});
test("Наблюдение: неизменный каталог — два листинга, индекс и ETag расписаний те же", async () => {
const before = await health();
const listings = api.stats.listings;
await watch.watchOnce();
const after = await health();
assert.equal(api.stats.listings - listings, 2);
assert.equal(after.catalog.lastCrawl.requests, 2);
assert.equal(after.catalog.lastCrawl.full, false);
assert.ok(after.catalog.lastCrawl.reused >= 1);
assert.equal(after.directory.builtAt, before.directory.builtAt);
assert.equal(messages.length, 1);
});
test("Наблюдение: новый документ — листинг только изменившихся папок и сообщение", async () => {
const listings = api.stats.listings;
api.catalog.put("26-АРХ.docx", fixture("isf-credits-26-arh.docx"));
await watch.watchOnce();
const { catalog, directory } = await health();
// The share's root, the year folder, the faculty folder and the day-time folder.
assert.equal(api.stats.listings - listings, 4);
assert.equal(catalog.lastCrawl.requests, 4);
assert.equal(directory.documents, 3);
assert.equal(messages.length, 2);
assert.match(lastMessage(), /Новый документ: 26-АРХ\.docx/);
assert.match(lastMessage(), /Группы: 26-АРХ/);
const groups = await (await fetch(api.base + "/api/groups?" + new URLSearchParams({ q: "26-АРХ" }))).json();
assert.ok(groups.results.some((g) => g.group === "26-АРХ"));
});
test("Наблюдение: перезалитый документ с новыми подозрительными ячейками", async () => {
// 23-БА's "2,6-12-16н" lands in a document that had no such cell.
api.catalog.put("24-ИТ.docx", fixture("fef-23-ba.docx"));
await watch.watchOnce();
assert.equal(messages.length, 3);
assert.match(lastMessage(), /Перезалит с новыми подозрительными ячейками: 24-ИТ\.docx/);
assert.match(lastMessage(), /2,6-12-16н/);
assert.match(lastMessage(), /https:\/\/psu\.example\/api\/diagnostics\?doc=/);
// Republished again as it is: nothing new to say.
api.catalog.put("24-ИТ.docx", fixture("fef-23-ba.docx"));
await watch.watchOnce();
assert.equal(messages.length, 3);
});
test("Наблюдение: университет не отвечает и снова отвечает", async () => {
api.catalog.down(true);
await watch.watchOnce();
const { watch: status, catalog } = await health();
assert.ok(status.lastError);
assert.ok(catalog.failingSince);
assert.equal(messages.length, 4);
assert.match(lastMessage(), /Каталог университета не отвечает/);
// Still down: one message per outage.
await watch.watchOnce();
assert.equal(messages.length, 4);
api.catalog.down(false);
await watch.watchOnce();
assert.equal(messages.length, 5);
assert.match(lastMessage(), /снова отвечает/);
const { watch: after, events } = await health();
assert.equal(after.lastError, null);
// The dashboard's feed, newest first: everything above, republished-as-is included.
assert.deepEqual(
events.map((e) => e.kind),
["source_up", "source_down", "republished", "republished", "new"],
);
assert.equal(events[2].text, "без новых подозрительных ячеек");
assert.match(events[3].text, /новых подозрительных ячеек: 1/);
});