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

142
test/parser.test.js Normal file
View File

@@ -0,0 +1,142 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { parseDocx, parseWord } from "../server/parser.js";
import { decodeListing, fileMetadata } from "../server/source.js";
const fixture = (name) =>
readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url));
const fef = () => parseDocx(fixture("fef-23-ba"), { name: "23-ЛГ_23-БА.docx" });
test("ФЭФ: 23-БА не получает дисциплины 23-ЛГ", () => {
const s = fef(),
ba = s.lessons.filter((l) => l.groups.includes("23-БА"));
assert.deepEqual(s.groups, ["23-ЛГ", "23-БА"]);
assert.equal(ba.length, 33);
assert.ok(ba.some((l) => l.subject === "Бизнес-анализ"));
assert.ok(
!ba.some((l) =>
/логистик|Экспедирование|Управление запасами/i.test(l.subject),
),
);
assert.ok(
s.lessons
.filter((l) => l.groups.includes("23-ЛГ"))
.some((l) => /Финансовая логистика/.test(l.subject)),
);
});
test("ФЭФ: общая ячейка принадлежит обеим группам", () => {
const shared = fef().lessons.filter((l) =>
l.subject.startsWith("Единый день информирования"),
);
assert.equal(shared.length, 1);
assert.deepEqual(shared[0].groups, ["23-ЛГ", "23-БА"]);
assert.match(shared[0].raw, /15 октября/);
assert.equal(shared[0].start, "13:40");
});
test("ФЭФ: варианты недель, цвет, преподаватель и аудитория сохранены", () => {
const s = fef();
const lessons = s.lessons.filter(
(l) =>
l.day === 4 &&
l.groups.includes("23-БА") &&
l.subject === "Бизнес-анализ",
);
assert.deepEqual(
lessons.map((l) => [l.condition, l.variant, l.room, l.teacher]),
[
["7,9,11н.", "upper", "430", "Войтенок Ж.Е."],
["4-12н", "lower", "430", "Войтенок Ж.Е."],
],
);
assert.equal(lessons[1].sourceColor, "CCFFCC");
assert.equal(lessons[0].start, "10:05");
assert.equal(lessons[0].end, "11:30");
const plan = s.lessons.find(
(l) =>
l.subject === "Бизнес-планирование" && l.start === "11:45" && l.day === 4,
);
assert.equal(plan.condition, "1-8,10-17");
assert.ok(s.warnings.length);
});
test("ФЭФ: кураторский час в 16.50 не становится невозможной датой", () => {
const l = fef().lessons.find(
(l) =>
l.groups.includes("23-БА") &&
l.day === 1 &&
l.subject.includes("кураторский"),
);
assert.equal(l.start, "16:50");
assert.equal(l.end, null);
assert.equal(l.date, null);
assert.equal(l.condition, "");
for (const lesson of fef().lessons)
if (lesson.date)
assert.ok(Number.isFinite(new Date(lesson.date).getTime()));
});
test("ФЭФ: объединённое занятие не дублируется на зелёной строке", () => {
const s = fef();
const l = s.lessons.filter(
(l) =>
l.day === 2 &&
l.groups.includes("23-БА") &&
l.subject === "Управление проектами" &&
l.start === "15:20",
);
assert.equal(l.length, 1);
assert.equal(l[0].variant, "all");
});
test("Дневные таблицы четырёх факультетов распознаются со своими группами", () => {
for (const [name, groups] of [
["fit-daily", ["24-ИТ-1", "24-ИТ-2"]],
["fkne-daily", ["24-КБ", "24-КФ"]],
["isf-daily", ["26-ПГС-1", "26-ПГС-2"]],
["gf-daily", ["25-РГФ-1", "25-РГФ-2"]],
]) {
const s = parseDocx(fixture(name), { name });
assert.deepEqual(s.groups, groups);
assert.ok(s.lessons.length > 20);
assert.ok(
s.lessons.every(
(l) => l.day >= 1 && l.day <= 7 && /^\d{2}:\d{2}$/.test(l.start),
),
);
assert.ok(
s.lessons.every((l) => l.groups.every((g) => groups.includes(g))),
);
}
});
test("Сессии: отдельные колонки времени, аудитории и преподавателя", () => {
const s = parseDocx(fixture("fit"), { name: "26-ИТзс.docx" }),
l = s.lessons[0];
assert.deepEqual(s.groups, ["26-ИТзс"]);
assert.equal(l.date, "2026-09-17");
assert.equal(l.start, "11:00");
assert.equal(l.end, "11:30");
assert.equal(l.teacher, "Петрович О.Н.");
assert.equal(l.room, "220Д");
assert.deepEqual(l.subgroups, []);
assert.match(l.raw, /Петрович/);
assert.equal(s.period, "Установочная сессия");
const f = parseDocx(fixture("fkne"), {});
assert.deepEqual(f.groups, ["26-ЭЭТзс"]);
assert.ok(f.lessons.every((l) => l.date));
});
test("Импорт отклоняет повреждённые и посторонние файлы", async () => {
assert.throws(() => parseDocx(Buffer.from("broken")), /повреждён/);
await assert.rejects(
() => parseWord(Buffer.from("not a word file")),
/настоящий файл Word/,
);
});
test("DAV: метаданные вложенных документов и только доверенный каталог", () => {
const xml = `<d:multistatus xmlns:d="DAV:"><d:response><d:href>/public.php/dav/files/no5zKbMrns6j5jQ/2026-2027/</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>`;
const entries = decodeListing(xml);
assert.equal(entries.length, 1);
assert.equal(entries[0].folder, true);
const metadata = JSON.parse(
readFileSync(new URL("./fixtures/fef-23-ba.docx.json", import.meta.url)),
);
const file = fileMetadata(metadata);
assert.equal(file.faculty, "Финансово-экономический факультет");
assert.equal(file.course, "4 курс");
assert.equal(file.name, "23-ЛГ_23-БА.docx");
});