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/); });