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

80 lines
3.7 KiB
JavaScript

import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { setTimeout as sleep } from "node:timers/promises";
import { readFileSync, writeFileSync } from "node:fs";
import { startApi } from "./support/nextcloud.js";
// The university is asked politely: every request names the service and its owner, and while
// the university is down nothing reaches it until a pause that doubles with every failure.
let api, watch;
const health = async () =>
(await fetch(api.base + "/status/data.json", { headers: { Authorization: "Basic " + Buffer.from("admin:secret").toString("base64") } })).json();
before(async () => {
api = await startApi({
PSU_PUBLIC_URL: "https://psu.example",
PSU_CONTACT: "mailto:owner@example.com",
PSU_MIN_REFRESH_MS: "0",
// A short pause instead of five minutes: 300 ms, then 600 ms.
PSU_SOURCE_BACKOFF_MS: "300",
DASHBOARD_PASSWORD: "secret",
});
watch = await import("../server/watch.js");
await watch.watchOnce();
});
after(() => api?.stop());
test("Источник: запросы представляются сервисом и контактом владельца", () => {
assert.match(api.stats.userAgent, /^PSU-Schedule\/\d+\.\d+\.\d+ \(\+https:\/\/psu\.example; mailto:owner@example\.com\)$/);
});
test("Источник: пока университет не отвечает, проверки ждут всё дольше", async () => {
api.catalog.down(true);
let requests = api.stats.requests;
await watch.watchOnce();
assert.equal(api.stats.requests - requests, 1, "одна попытка, дальше обход не идёт");
let { catalog } = await health();
assert.equal(catalog.backoff.failures, 1);
assert.ok(Date.parse(catalog.backoff.retryAt) > Date.now());
// Within the pause neither the watcher nor a forced refresh reaches the university, and
// students still get the saved catalog.
requests = api.stats.requests;
await watch.watchOnce();
const refreshed = await fetch(api.base + "/api/catalog?refresh=1");
assert.equal(refreshed.status, 200);
assert.equal((await refreshed.json()).stale, true);
assert.equal(api.stats.requests, requests);
// After the pause: one more attempt, and the next pause is twice as long.
await sleep(350);
await watch.watchOnce();
assert.equal(api.stats.requests - requests, 1);
({ catalog } = await health());
assert.equal(catalog.backoff.failures, 2);
assert.ok(Date.parse(catalog.backoff.retryAt) - Date.now() > 350);
// Back again: the first attempt after the pause reads the catalog, and the pause is over.
api.catalog.down(false);
await sleep(650);
await watch.watchOnce();
({ catalog } = await health());
assert.equal(catalog.backoff, null);
assert.equal(catalog.failingSince, null);
assert.ok(!(await (await fetch(api.base + "/api/catalog")).json()).stale);
});
test("Источник: новый парсер перечитывает сохранённый документ, не обращаясь к университету", async () => {
const { files } = await (await fetch(api.base + "/api/catalog")).json();
const file = files.find((f) => f.name === "23-ЛГ_23-БА.docx");
assert.equal((await fetch(api.base + "/api/schedule/" + file.id)).status, 200);
// The cache as an older parser left it.
const cache = `${process.env.PSU_DATA_DIR}/parsed/${file.id}.json`;
writeFileSync(cache, JSON.stringify({ ...JSON.parse(readFileSync(cache, "utf8")), schemaVersion: 1 }));
const requests = api.stats.requests;
const schedule = await (await fetch(api.base + "/api/schedule/" + file.id)).json();
assert.equal(api.stats.requests, requests);
assert.notEqual(schedule.schemaVersion, 1);
assert.ok(schedule.lessons.length > 0);
});