Files
psu-schedule/test/watch.test.js
Zakhar Timoshenko c0487e4c2a feat: enhance user-agent identification and implement backoff strategy for university source requests
- Added PSU_CONTACT environment variable to docker-compose for owner contact in User-Agent.
- Updated source.js to include user-agent construction with service version and contact information.
- Implemented exponential backoff strategy for failed requests to the university, with retry logic based on HTTP response status.
- Modified status.js to display backoff information in the status response.
- Adjusted watch.js to reduce request frequency during university downtime.
- Enhanced test coverage with new backoff.test.js to validate backoff behavior and user-agent format.
2026-10-22 01:17:54 +03:00

126 lines
5.8 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, even while it is down
// (the pause after failures has its own test, backoff.test.js).
PSU_MIN_REFRESH_MS: "0",
PSU_SOURCE_BACKOFF_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/);
});