- 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.
126 lines
5.8 KiB
JavaScript
126 lines
5.8 KiB
JavaScript
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/);
|
||
});
|