import { createServer } from "node:http"; import { readFileSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { academicStartYear, todayIso } from "../../shared/weeks.js"; // A stand-in for the university's Nextcloud that serves the fixtures as a small catalog, and the // real API server started against it with a temporary data folder. export const SHARE = "/public.php/dav/files/no5zKbMrns6j5jQ/"; export const fixture = (name) => readFileSync(new URL(`../fixtures/${name}`, import.meta.url)); const year = academicStartYear(todayIso()); const YEAR = `${year}-${year + 1}`; const FOLDER = `${SHARE}${YEAR}/Финансово-экономический факультет/Дневная форма/`; const MODIFIED = "Mon, 14 Sep 2026 13:05:15 GMT"; const files = { [`${SHARE}Расписание белых и зеленых недель.docx`]: fixture("weeks.docx"), [`${SHARE}Расписание звонков.docx`]: fixture("bells.docx"), [`${FOLDER}23-ЛГ_23-БА.docx`]: fixture("fef-23-ba.docx"), [`${FOLDER}24-ИТ.docx`]: fixture("fit-daily.docx"), // The university fails on one document and is too slow on another. [`${FOLDER}Сломанный.docx`]: 500, [`${FOLDER}Медленный.docx`]: "slow", }; const folders = [SHARE, `${SHARE}${YEAR}/`, `${SHARE}${YEAR}/Финансово-экономический факультет/`, FOLDER]; const encode = (path) => path.split("/").map(encodeURIComponent).join("/"); const listing = (folder) => { const children = [...folders.filter((f) => f !== folder && f.startsWith(folder) && !f.slice(folder.length, -1).includes("/")), ...Object.keys(files).filter((f) => f.startsWith(folder) && !f.slice(folder.length).includes("/"))]; const entry = (path) => { const folder = path.endsWith("/"); const size = Buffer.isBuffer(files[path]) ? files[path].length : 1000; return `${encode(path)}${ folder ? "" : `${size}` }${MODIFIED}HTTP/1.1 200 OK`; }; return `${[folder, ...children].map(entry).join("")}`; }; /** * Starts the stand-in and the API server; `env` adds settings such as lower limits. * Returns the server's base URL, request statistics of the stand-in and `stop()`. */ export async function startApi(env = {}) { const stats = { requests: 0, connections: 0 }; const source = createServer((req, res) => { stats.requests++; const path = decodeURIComponent(req.url); if (req.method === "PROPFIND" && folders.includes(path)) { res.writeHead(207, { "Content-Type": "application/xml" }); return res.end(listing(path)); } const body = files[path]; if (body === "slow") return setTimeout(() => res.end("late"), 2000); if (typeof body === "number" || !body) return res.writeHead(body || 404).end(); res.end(body); }); source.on("connection", () => stats.connections++); await new Promise((resolve) => source.listen(0, "127.0.0.1", resolve)); const data = mkdtempSync(join(tmpdir(), "psu-api-")); Object.assign(process.env, { PSU_SOURCE_ORIGIN: `http://127.0.0.1:${source.address().port}`, PSU_DATA_DIR: data, PSU_SOURCE_TIMEOUT_MS: "500", ...env, }); const { app } = await import("../../server/index.js"); const server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server.on("listening", resolve)); return { base: `http://127.0.0.1:${server.address().port}`, stats, stop() { server.close(); server.closeAllConnections(); source.closeAllConnections(); source.close(); rmSync(data, { recursive: true, force: true }); }, }; }