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

75 lines
3.8 KiB
JavaScript
Raw Permalink 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 { existsSync, readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { startApi } from "./support/nextcloud.js";
// Protection of a server open to the internet, with the limits lowered so the tests reach them:
// five API requests a minute and an 800 ms time limit.
let api, base;
let sent = 0;
before(async () => {
api = await startApi({ PSU_API_RATE_LIMIT: "5", PSU_REQUEST_TIMEOUT_MS: "800", PSU_SOURCE_TIMEOUT_MS: "5000" });
base = api.base;
});
after(() => api?.stop());
const apiGet = (path) => {
sent++;
return fetch(base + path);
};
test("Защита: заголовки безопасности у страницы и API", async () => {
for (const path of ["/", "/api/health"]) {
const response = await fetch(base + path);
await response.arrayBuffer();
const csp = response.headers.get("content-security-policy");
assert.match(csp, /default-src 'self'/, path);
assert.match(csp, /frame-ancestors 'none'/, path);
assert.match(csp, /object-src 'none'/, path);
assert.equal(response.headers.get("x-content-type-options"), "nosniff", path);
assert.equal(response.headers.get("x-frame-options"), "DENY", path);
assert.ok(response.headers.get("referrer-policy"), path);
assert.ok(response.headers.get("permissions-policy"), path);
// Plain HTTP: no HSTS.
assert.equal(response.headers.get("strict-transport-security"), null, path);
}
});
test("Защита: CSP разрешает ровно inline-скрипт темы из собранной страницы", { skip: !existsSync(new URL("../dist/index.html", import.meta.url)) && "нет dist/, сначала npm run build" }, async () => {
const html = readFileSync(new URL("../dist/index.html", import.meta.url), "utf8");
const inline = html.match(/<script>([\s\S]*?)<\/script>/)[1];
const hash = createHash("sha256").update(inline).digest("base64");
const csp = (await fetch(base + "/")).headers.get("content-security-policy");
assert.ok(csp.includes(`'sha256-${hash}'`), csp);
assert.doesNotMatch(csp, /unsafe-inline|unsafe-eval/);
});
test("Защита: запрос, на который сервер не успел ответить, получает 504 timeout", async () => {
const catalog = await (await apiGet("/api/catalog")).json();
const slow = catalog.files.find((f) => f.name === "Медленный.docx");
const started = Date.now();
const response = await apiGet(`/api/original/${slow.id}`);
assert.equal(response.status, 504);
assert.equal((await response.json()).code, "timeout");
assert.equal(response.headers.get("retry-after"), "30");
assert.ok(Date.now() - started < 1800, "ответ не ждёт университет");
// The late answer of the university does not break the server.
await new Promise((resolve) => setTimeout(resolve, 2200));
assert.equal((await fetch(base + "/api/health")).status, 200);
});
test("Защита: лимит запросов к API с одного адреса, проверки здоровья не считаются", async () => {
for (let i = 0; i < 10; i++) {
assert.equal((await fetch(base + "/api/health")).status, 200);
assert.equal((await fetch(base + "/api/ready")).status === 429, false);
}
while (sent < 5) assert.notEqual((await apiGet("/api/nope")).status, 429);
const limited = await apiGet("/api/nope");
assert.equal(limited.status, 429);
assert.equal((await limited.json()).code, "too_many_requests");
assert.ok(Number(limited.headers.get("retry-after")) > 0);
// The page and its files are not API requests and stay available.
assert.equal((await fetch(base + "/")).status, 200);
assert.equal((await fetch(base + "/api/health")).status, 200);
});