Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-10-19 19:36:19 +03:00
parent 170a44c377
commit 5f7bffd6e4
17 changed files with 454 additions and 85 deletions

27
test/errors.test.js Normal file
View File

@@ -0,0 +1,27 @@
import test from "node:test";
import assert from "node:assert/strict";
import { apiError, describe, sourceError } from "../server/errors.js";
test("Ошибки API: код и статус для клиентов", () => {
assert.deepEqual(describe(apiError(404, "group_not_found", "В документе нет группы 1-А.")), {
status: 404,
code: "group_not_found",
message: "В документе нет группы 1-А.",
});
assert.deepEqual(describe({ code: "LIMIT_FILE_SIZE" }).code, "file_too_large");
// Faults of the server itself never show their technical text.
const internal = describe(new TypeError("Cannot read properties of undefined"));
assert.equal(internal.status, 500);
assert.equal(internal.code, "internal");
assert.doesNotMatch(internal.message, /undefined/);
});
test("Ошибки источника: таймаут curl отдельно от недоступности", () => {
assert.equal(sourceError({ code: 28 }).code, "source_timeout");
assert.equal(sourceError({ code: 28 }).status, 504);
assert.equal(sourceError({ killed: true, signal: "SIGTERM" }).code, "source_timeout");
assert.equal(sourceError({ code: 6 }).code, "source_unavailable");
assert.equal(sourceError({ code: 22 }).status, 502);
const own = apiError(502, "source_invalid", "Таблица недель не распознана.");
assert.equal(sourceError(own), own);
});

BIN
test/fixtures/isf-credits-26-arh.docx vendored Normal file

Binary file not shown.

BIN
test/fixtures/itzs-summer-2026.docx vendored Normal file

Binary file not shown.

BIN
test/fixtures/itzs-winter-2024.docx vendored Normal file

Binary file not shown.

View File

@@ -14,7 +14,7 @@ test("Импорт: не больше двух разборов сразу, ли
active--;
});
const running = [task(), task(), task()];
await assert.rejects(task(), (e) => e.status === 429 && e.message === "Занято");
await assert.rejects(task(), (e) => e.status === 429 && e.code === "busy" && e.message === "Занято");
// Each finished task hands its slot to the one waiting.
while (release.length) {
release.shift()();
@@ -47,6 +47,10 @@ test("Импорт: лимит запросов считается по адре
};
assert.ok(call("10.0.0.1").passed);
assert.ok(call("10.0.0.1").passed);
assert.deepEqual(call("10.0.0.1"), { status: 429, body: { error: "Подождите" }, passed: false });
assert.deepEqual(call("10.0.0.1"), {
status: 429,
body: { error: "Подождите", code: "too_many_requests" },
passed: false,
});
assert.ok(call("10.0.0.2").passed);
});

View File

@@ -160,3 +160,79 @@ test("DAV: метаданные вложенных документов и то
assert.equal(file.course, "4 курс");
assert.equal(file.name, "23-ЛГ_23-БА.docx");
});
// Laboratory-examination sessions of correspondence students (23-ИТзс): weeks side by side,
// the group named only in the title, a date on every day.
const itzs = (name) => parseDocx(fixture(name), { name: `${name}.docx` });
const at = (s, date, start) =>
s.lessons
.filter((l) => l.date === date && l.start === start)
.map((l) => [l.type, l.subject, l.teacher, l.room, l.subgroups.map((g) => g.label).join()]);
test("Заочная сессия: группа из заголовка, недели рядом, даты из Word", () => {
const s = itzs("itzs-winter-2024");
assert.deepEqual(s.groups, ["23-ИТзс"]);
assert.equal(s.period, "Лабораторно-экзаменационная сессия");
// Every filled discipline cell of the three weeks is one lesson.
assert.equal(s.lessons.length, 36);
assert.ok(s.lessons.every((l) => l.date >= "2024-01-08" && l.date <= "2024-01-28"));
// Two columns under one heading are the subgroups' laboratory classes.
assert.deepEqual(at(s, "2024-01-08", "17:00"), [
["Лабораторная", "Основы информационных технологий и компьютерной графики", "Михнович О.В.", "217Д", "Подгруппа 1"],
["Лабораторная", "Основы алгоритмизации и программирования", "Конаплева Г.Ф.", "218Д", "Подгруппа 2"],
]);
assert.deepEqual(at(s, "2024-01-15", "17:00"), [
["Установочная лекция", "Математический анализ", "Забелендик О.Н.", "220Д", ""],
]);
// "Экзамен по учебной дисциплине «…»": the subject from the quotes, a room with a name.
assert.deepEqual(at(s, "2024-01-18", "08:30"), [
["Экзамен", "Основы алгоритмизации и программирования", "Чертков В.М.", "Медиа", ""],
]);
assert.deepEqual(at(s, "2024-01-16", "13:40"), [
["Защита курсовой работы", "Основы алгоритмизации и программирования", "Чертков В.М.", "254В", ""],
]);
});
test("Заочная сессия: неровное деление столбцов и экзамен без конца", () => {
const s = itzs("itzs-summer-2026");
assert.deepEqual(s.groups, ["23-ИТзс"]);
assert.equal(s.lessons.length, 58);
// The third week's heading spans three columns, split 2+1 or 1+2: left and right subgroup.
assert.deepEqual(at(s, "2026-06-24", "08:30").map((l) => [l[1], l[4]]), [
["Методы и алгоритмы принятия", "Подгруппа 1"],
["Распределенные вычисления", "Подгруппа 2"],
]);
// "8.30" alone: the exam starts then; its end comes from the bell table.
const exam = s.lessons.find((l) => l.date === "2026-06-18" && l.type === "Экзамен");
assert.deepEqual([exam.start, exam.end, exam.subject], ["08:30", null, "Базы данных"]);
assert.equal(s.lessons.filter((l) => l.type === "Консультация").length, 4);
});
test("Расписание зачетов: дата перед днём недели, одно время, зачёты по умолчанию", () => {
const s = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" });
assert.deepEqual(s.groups, ["26-АРХ"]);
assert.equal(s.period, "Зачёты");
assert.equal(s.lessons.length, 9);
const row = (l) => [l.date, l.start, l.end, l.type, l.subject, l.subgroups.map((g) => g.label).join()];
const sorted = [...s.lessons].sort((a, b) => (a.date + a.start).localeCompare(b.date + b.start));
assert.deepEqual(sorted.slice(0, 2).map(row), [
["2026-10-27", "17:00", null, "Консультация", "Введение в архитектурное проектирование", ""],
["2026-10-31", "08:30", null, "Зачёт", "Введение в архитектурное проектирование", ""],
]);
// An empty date cell continues the day above: the second consultation on 18 December.
assert.deepEqual(
s.lessons.filter((l) => l.date === "2026-12-18").map((l) => [l.start, l.subject]),
[
["08:30", "Проектирование небольшого архитектурного сооружения"],
["15:20", "Рисунок"],
],
);
// Two columns under the group: each subgroup takes its credit with its own teacher.
assert.deepEqual(
s.lessons.filter((l) => l.date === "2026-12-22").map((l) => [l.teacher, l.room, l.subgroups[0]?.label]),
[
["Кузякова О.Д.", "361", "Подгруппа 1"],
["Пискун А.А.", "370", "Подгруппа 2"],
],
);
});

View File

@@ -58,3 +58,28 @@ test("range=semester: все дни текущего семестра от пе
assert.equal(t.days.at(-1).date, t.semester.end);
assert.ok(t.days.length > 62, "больше обычного предела в 62 дня");
});
test("Сессия с датами в Word не зависит от таблицы недель", () => {
// June 2026 lies outside the 2026/27 week table, yet every lesson has its own date.
const session = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" });
const t = timetable({
file: { id: "itzs", name: "23-ИТзс.docx" },
schedule: session,
calendar,
group: "23-ИТзс",
from: "2026-10-19",
range: "semester",
today: "2026-10-19",
});
assert.deepEqual(t.semester, {
id: "session-2026-06-08",
label: "Лабораторно-экзаменационная сессия",
start: "2026-06-08",
end: "2026-06-27",
});
assert.equal(t.note, null);
assert.equal(t.days.length, 20);
assert.equal(t.days.reduce((n, d) => n + d.lessons.length, 0), 58);
const exam = t.days.find((d) => d.date === "2026-06-18").lessons.find((l) => l.type === "Экзамен");
assert.deepEqual([exam.start, exam.end, exam.pair], ["08:30", "09:55", 1]);
});