initial commit

This commit is contained in:
2026-10-16 22:08:45 +03:00
commit 19ee41411b
97 changed files with 15141 additions and 0 deletions

183
test/sources.test.js Normal file
View File

@@ -0,0 +1,183 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import {
pickYearFolder,
pickReferences,
isWeeksTable,
isBellSchedule,
mergeWeekTables,
fileMetadata,
decodeListing,
} from "../server/source.js";
import { parseWeeksDocx } from "../server/calendar.js";
import { parseDocx } from "../server/parser.js";
import {
addDays,
semesterFor,
semesterView,
lessonDates,
academicStartYear,
} from "../shared/weeks.js";
import { scheduleUpdate, namesGroup } from "../shared/updates.js";
const fixture = (name) =>
readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url));
const autumn = parseWeeksDocx(fixture("weeks"));
// A spring table in the same format: numbering restarts at 1 on Monday 8 February.
const spring = Array.from({ length: 17 }, (_, i) => ({
n: i + 1,
start: addDays("2027-02-08", i * 7),
end: addDays("2027-02-08", i * 7 + 6),
color: i % 2 ? "green" : "white",
}));
const bothSemesters = { ...mergeWeekTables([spring, autumn]), bells: [], source: {} };
const SHARE = "/public.php/dav/files/no5zKbMrns6j5jQ/";
test("Учебный год: папка меняется 1 сентября, без новой папки остаётся прошлая", () => {
const folders = ["2025-2026", "2026-2027", "Майноры", "2026-2028"];
assert.equal(academicStartYear("2026-08-31"), 2025);
assert.equal(pickYearFolder(folders, "2026-08-31").name, "2025-2026");
assert.deepEqual(pickYearFolder(folders, "2026-09-01"), {
name: "2026-2027",
label: "2026–2027",
current: true,
expected: "2026-2027",
});
const late = pickYearFolder(folders, "2027-09-01");
assert.equal(late.name, "2026-2027");
assert.equal(late.current, false);
assert.equal(late.expected, "2027-2028");
// Only next year's folder published early: still better than nothing.
assert.equal(pickYearFolder(["2027-2028"], "2027-08-20").name, "2027-2028");
assert.equal(pickYearFolder(["Майноры"], "2026-10-16"), null);
});
test("Справочные файлы находятся по смыслу имени, папка года важнее корня", () => {
assert.ok(isWeeksTable("Расписание белых и зеленых недель.docx"));
assert.ok(isWeeksTable("Расписание белых и зелёных недель 2027.docx"));
assert.ok(!isWeeksTable("Расписание белых и зеленых недель.doc"));
assert.ok(!isWeeksTable("23-ЛГ_23-БА.docx"));
assert.ok(isBellSchedule("Расписание звонков (новое).docx"));
const entry = (path, modified, folder = false) => ({ path, modified, folder, size: 1 });
const refs = pickReferences(
[
entry(SHARE + encodeURIComponent("Расписание белых и зеленых недель.docx"), "Wed, 26 Aug 2026 10:00:00 GMT"),
entry(SHARE + encodeURIComponent("Расписание звонков.docx"), "Wed, 12 Feb 2025 07:55:59 GMT"),
entry(SHARE + "2026-2027/", null, true),
],
[
entry(SHARE + "2026-2027/" + encodeURIComponent("Белые и зелёные недели, весна.docx"), "Mon, 25 Jan 2027 09:00:00 GMT"),
],
);
assert.deepEqual(
refs.weeks.map((f) => [f.name, f.scope]),
[
["Белые и зелёные недели, весна.docx", "year"],
["Расписание белых и зеленых недель.docx", "root"],
],
);
assert.equal(refs.bells.name, "Расписание звонков.docx");
});
test("DAV: год берётся из пути документа, чужие ссылки отбрасываются", () => {
const xml = `<d:multistatus xmlns:d="DAV:"><d:response><d:href>${SHARE}2027-2028/</d:href><d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>https://evil.example/file.docx</d:href></d:response></d:multistatus>`;
assert.equal(decodeListing(xml).length, 1);
const metadata = JSON.parse(
readFileSync(new URL("./fixtures/fef-23-ba.docx.json", import.meta.url)),
);
const file = fileMetadata(metadata);
assert.equal(file.year, "2026–2027");
assert.equal(file.faculty, "Финансово-экономический факультет");
});
test("Таблица с двумя семестрами делится по сбросу нумерации", () => {
const { semesters, weeks } = bothSemesters;
assert.deepEqual(
semesters.map((s) => [s.id, s.label, s.start, s.end]),
[
["2026-autumn", "Осенний семестр 2026/27", "2026-09-01", "2027-01-03"],
["2026-spring", "Весенний семестр 2026/27", "2027-02-08", "2027-06-06"],
],
);
assert.equal(weeks.filter((w) => w.semester === "2026-spring").length, 17);
// The same table read twice (root and year folder) does not duplicate weeks.
assert.equal(mergeWeekTables([autumn, autumn]).weeks.length, 18);
});
test("Расписание получает недели своего семестра, а не первого попавшегося", () => {
const fef = parseDocx(fixture("fef-23-ba"), { name: "23-ЛГ_23-БА.docx" });
assert.equal(semesterFor(bothSemesters, fef, "2027-03-01").id, "2026-autumn");
const springSchedule = { ...fef, period: "Весенний семестр" };
const view = semesterView(bothSemesters, springSchedule, "2026-10-16");
assert.equal(view.semester.id, "2026-spring");
const lesson = { day: 1, date: null, condition: "1-15н", variant: "upper" };
assert.equal(lessonDates(lesson, view)[0], "2027-02-08");
assert.equal(lessonDates(lesson, semesterView(bothSemesters, fef, "2026-10-16"))[0], "2026-09-14");
// Spring table not published yet: no guessed dates.
const onlyAutumn = { ...mergeWeekTables([autumn]), bells: [], source: {} };
assert.equal(semesterFor(onlyAutumn, springSchedule, "2027-01-20"), null);
// Next year's autumn schedule against this year's table: no match either.
assert.equal(semesterFor(onlyAutumn, { ...fef, academicYear: "2027" }, "2027-09-10"), null);
// Unknown period: the semester that contains today, none between semesters.
const plain = { lessons: [{ date: null }], period: "Расписание из Word" };
assert.equal(semesterFor(bothSemesters, plain, "2026-11-02").id, "2026-autumn");
assert.equal(semesterFor(bothSemesters, plain, "2027-01-20"), null);
// A session schedule follows its own dates.
const session = { lessons: [{ date: "2027-02-10" }, { date: "2027-02-11" }], period: "Установочная сессия" };
assert.equal(semesterFor(bothSemesters, session, "2026-12-01").id, "2026-spring");
// A calendar cached before semesters existed still works.
assert.equal(
semesterView({ weeks: autumn, bells: [], source: {} }, fef, "2026-10-16").weeks.length,
18,
);
});
test("Обновления: тот же файл перечитывается, новый документ группы предлагается", () => {
const saved = {
schemaVersion: 5,
source: {
fileId: "a",
modified: "Mon, 12 Oct 2026 11:45:13 GMT",
importedAt: "2026-10-16T10:00:00Z",
faculty: "ФЭФ",
},
};
const file = (id, name, modified, faculty = "ФЭФ") => ({ id, name, modified, faculty });
const base = file("a", "23-ЛГ_23-БА.docx", saved.source.modified);
const options = { schemaVersion: 5 };
assert.equal(scheduleUpdate(saved, { files: [base] }, "23-БА", options), null);
const republished = { ...base, modified: "Tue, 20 Oct 2026 08:00:00 GMT" };
assert.equal(
scheduleUpdate(saved, { files: [republished] }, "23-БА", options).changed,
republished,
);
assert.equal(
scheduleUpdate({ ...saved, schemaVersion: 4 }, { files: [base] }, "23-БА", options).changed,
base,
);
const spring = file("b", "23-БА весна.docx", "Mon, 25 Jan 2027 09:00:00 GMT");
const others = [
file("c", "23-БАз.docx", "Mon, 25 Jan 2027 09:00:00 GMT"),
file("d", "23-БА.docx", "Mon, 25 Jan 2027 09:00:00 GMT", "ФИТ"),
file("e", "23-БА старое.docx", "Mon, 01 Sep 2026 09:00:00 GMT"),
];
const found = scheduleUpdate(saved, { files: [base, spring, ...others] }, "23-БА", options);
assert.equal(found.newer, spring);
assert.equal(found.changed, null);
assert.equal(
scheduleUpdate(saved, { files: [base, spring] }, "23-БА", { ...options, dismissed: ["b"] }),
null,
);
// New academic year: the old path is gone.
assert.deepEqual(scheduleUpdate(saved, { files: [] }, "23-БА", options), {
changed: null,
newer: null,
missing: true,
});
// An offline copy of the catalog proves nothing.
assert.equal(scheduleUpdate(saved, { files: [], stale: true }, "23-БА", options), null);
assert.ok(namesGroup("23-ЛГ_23-БА.docx", "23-БА"));
assert.ok(!namesGroup("123-БА.docx", "23-БА"));
assert.ok(!namesGroup("23-БАз.docx", "23-БА"));
});