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 = `${SHARE}2027-2028/HTTP/1.1 200 OKhttps://evil.example/file.docx`; 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-БА")); });