- академический отпуск: readOnly, запрет submit/start/finish защиты - черновики лабораторных скрыты от студентов в списке/деталях/по UUID - удалено действие «Убрать из группы» из UI и API - импорт: restoreInactiveStatuses, skipped-отчёт, разблокировка архивного аккаунта - глобальные материалы STUDENTS/TEACHERS с JWT-фильтрацией и сменой аудитории - LDD-AUTH-005 переведён на unit-evidence, clean check проходит по P0 - backup: безопасный парсинг .env, verify-latest, кавычки cron - robots/noindex, комментарии без автора, профиль без черновиков - docs/18-goal-audit.md; baseline 57 claims / 366 тестов / release PASS
20 KiB
LDD baseline: 2026-10-27/28
Три LDD-цикла проверки текущей функциональности LMS; последний выполнен
2026-10-28 после закрытия продуктовых рисков. Добавлены реестр claims, gate
evidenceCheck, runtime/migration/mutation/smoke-скрипты и браузерный E2E на
Playwright. В ходе проверки найден и исправлен реальный дефект Jackson 3.
1. Что проверялось
Реестр: docs/evidence/claims.json, 51 утверждение.
| Критичность | Claims | Proven | Gaps |
|---|---|---|---|
| P0 | 25 | 25 | 0 |
| P1 | 26 | 26 | 0 |
| Итого | 51 | 51 | 0 |
Домены:
- доступы: аноним/студент/преподаватель/админ, чужие курс/лаба/студент/группа, внутренние API identity;
- повышение: dry-run, apply, идемпотентность, rollback, cancel;
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
- профиль студента и скачивание сдач;
- материалы и активы, включая secure-by-default legacy-файлы;
- миграции V11–V13, полный upgrade с master-схемы;
- аудит: токен, admin-only, сохранность после удаления, representative-хуки, keyset-пагинация;
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия, абсолютный потолок 30 дней и очистка истёкших токенов;
- группы курса и детали лабораторных без N+1, авторство материалов, ведомость, UI/контракты, импорт, копирование лабораторных;
- runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke.
2. Команды и результаты
2.1. Автоматические тесты
./gradlew test --rerun-tasks
Результат: 366 тестов, 0 failures, 0 errors, 0 skipped.
ktlintCheck и detekt — зелёные.
2.2. LDD gate
./gradlew evidenceCheck
Результат: 57/57 claims proven, P0 gaps = 0.
Отчёт: build/reports/evidence/index.md.
Gate устроен так:
- каждый claim ссылается на конкретный JUnit-тест
Class#method; - P0 без зелёного теста валит сборку;
- P1-пробел показывается в отчёте;
- runtime-проверки читаются из
build/evidence/*.jsonи тоже должны бытьpassed, если claim на них ссылается.
2.3. Upgrade с master-схемы
./scripts/ldd-migration-upgrade.sh
Сценарий: одноразовая PostgreSQL 18, master identity V1–V3, academic
V1–V5, progress V1–V9,V11,V12, синтетические данные, затем ветка:
identity V4–V13, academic V6–V12, progress V13.
Проверено:
identity.audit_events(V10),identity.refresh_tokens(V11),absolute_expires_at(V12) и keyset-индексaudit_events_occurred_id_idx(V13) созданы;students,groups,membership_historyсохранились;labs.course_id,chapters.course_id,article_assetsсозданы;grade_comments.author_account_id,lab_grades.updated_by_account_idсозданы;- оценки и комментарии сохранились;
- повторное применение
V13идемпотентно.
Результат: PASS, evidence: build/evidence/migration.json.
2.4. Runtime-smoke полного стека и CI smoke
./scripts/ldd-runtime.sh
./scripts/ldd-smoke.sh # то же, что CI workflow smoke
Сценарий: одноразовая БД, bootJar, запуск identity/academic/progress/gateway/web,
реальные HTTP-вызовы через gateway.
Проверено:
- health всех пяти сервисов =
UP; adminExists=falseдо setup, создание админа,/auth/me=ADMIN;- создание группы, курса, преподавателя, привязка группы, создание лабы;
- преподаватель входит и видит свой курс, лабу и состав группы;
- чужой преподаватель получает
403на курс, лабу и аудит; - админ видит audit-события
COURSE_CREATE,TEACHER_CREATE,COURSE_TEACHER_ASSIGN,COURSE_GROUP_ATTACH,LAB_CREATE; - web
/loginотдаёт200, публичное дерево материалов доступно, курсовые материалы анонимно —401.
ldd-smoke.sh выполняет тот же полный стек, health, вход админа и один
доменный сценарий; это обязательный workflow .woodpecker/smoke.yaml на
каждом push/MR, поэтому базовый runtime-контур проверяется до merge, а не
только перед релизом.
Результат: PASS, evidence: build/evidence/runtime.json и
build/evidence/smoke.json.
2.5. Legacy-активы: secure-by-default
Проверено автоматическими тестами доступа к активам:
- файл без строки в
article_assetsотдаёт404анониму, студенту и преподавателю (существование не раскрывается); - админ может скачать такой файл;
GET /api/v1/content/legacy-assetsзащищёнROLE_ADMINи перечисляет несвязанные файлы; админская страница/admin/legacy-assetsиscripts/legacy-assets.shдают тот же список для привязки или удаления;- связанный актив по-прежнему наследует видимость статьи → раздела → главы:
публичная
ALL— без входа, курсовая — только участникам курса, скрытая для студента —404.
Старые внешние ссылки на несвязанные файлы у обычных пользователей перестают работать. Это осознанное изменение поведения: безопасность важнее совместимости с «осиротевшими» активами.
Результат: PASS, claims LDD-ASSET-001, LDD-ASSET-002.
2.6. Аутентификация, refresh и web-сессия
- identity
V11__refresh_tokens.sqlхранит только SHA-256-хеш refresh-токена, срок, отзыв и цепочку замен; - access JWT — 15 минут (
LMS_ACCESS_TOKEN_TTL), refresh — 7 дней (LMS_REFRESH_TOKEN_TTL); POST /api/v1/auth/refreshротирует пару; повторное использование старого токена отзывает все сессии аккаунта;- logout и смена пароля отзывают refresh; отчисленный, выпущенный, архивный студент, архивный аккаунт и закрытая группа не могут обновить сессию;
V12__refresh_absolute.sqlзадаётabsolute_expires_at; ротация не продлевает абсолютный срок, после 30 дней refresh отклоняется;- web-сессия — 7 дней sliding (
LMS_SESSION_TIMEOUT), но не дольше 30 дней (LMS_SESSION_ABSOLUTE_TIMEOUT); cookieLMS_SESSION(HttpOnly,SameSite=Lax,Max-Ageперевыпускается на каждом ответе и ограничен остатком абсолютного срока); - access обновляется автоматически за 2 минуты до истечения, поэтому активная вкладка не требует входа каждые 15 минут; logout отзывает refresh;
- истёкшие refresh-токены старше retention (30 дней) удаляет ночная задача
(
LMS_REFRESH_TOKEN_CLEANUP_CRON), свежие отозванные токены сохраняются для detection reuse.
Прямой API-клиент должен сам использовать POST /api/v1/auth/refresh;
автоматического обновления вне web BFF нет.
Результат: PASS, claims LDD-AUTH-001, LDD-AUTH-002, LDD-AUTH-003,
LDD-AUTH-006, LDD-AUTH-007.
2.7. Пагинация, N+1 и масштаб
GET /api/v1/auditпринимает keyset-курсорcursor(opaqueoccurredAt|id),limit(максимум 500) иaction; offsetpageостался для совместимости. Курсорные страницы не пересекаются, битый курсор —400; в UI «Показать ещё» идёт по курсору, «В начало» возвращает к первой странице.GET /api/v1/labs/detailsотдаёт все лабораторные курса с уровнями и вариантами одним запросом; progress делает ровно один HTTP-вызов вместо запроса на каждую лабораторную.GET /api/v1/courses/{id}/groupsвозвращает привязанные группы с именами одним вызовом; страница преподавателя не делает N+1 по именам групп.- Полный load использует seed на 100 групп / 1000 студентов (SQL, только release-контур) и 125 параллельных чтений курса, лаб, профиля, групп и аудита: 0 ошибок, p95 < 1 с (порог p95 < 2 с).
Остаётся запрос сдач по выбранной лабораторной в ведомости — это не N+1 по студентам и не продуктовый риск доступа.
Результат: PASS, claims LDD-AUDIT-004, LDD-AUDIT-005, LDD-PERF-001,
LDD-PERF-002, LDD-RUNTIME-003.
2.8. Mutation testing
./scripts/ldd-mutation.sh
PIT запускается через CLI с JUnit 4 harness: JUnit5-плагин PIT несовместим с JUnit Platform 6 из Spring Boot 4.
| Компонент | Мутаций | Убито | Test strength | Порог |
|---|---|---|---|---|
CourseAccessService |
56 | 42 | 75% | 70% |
PromotionPlanner |
162 | 109 | 69% | 60% |
Результат: PASS, evidence: build/evidence/mutation.json.
Остаток — эквивалентные мутации, Kotlin-интринсики и ветки, не входящие в
текущий P0-контур.
2.9. Браузерный E2E
./scripts/ldd-browser.sh
Playwright в Chromium, Firefox и WebKit на полном стеке, одноразовая БД:
- admin: вход,
/admin/coursesс курсом,/admin/auditс событием; - teacher: вход, курс, ведомость (группа/студент, без админских кнопок), профиль студента, создание главы курса;
- anonymous:
/admin/audit→/login; - student: вход,
/admin/audit→/student,/learn/course/{no}; - admin: cursor-пагинация аудита («Показать ещё» → «Продолжение» → «В начало»);
- admin: страница
/admin/legacy-assetsсо списком или пустым состоянием.
Результат: 18/18 passed (6 сценариев × 3 браузера), evidence:
build/evidence/browser.json. Дополнительно проверены сдача файла студентом и
mobile-размер teacher-страницы. На Fedora WebKit запускается через Docker-образ
Playwright, потому что системных Ubuntu-библиотек нет.
2.10. Конкурентные apply/rollback
PromotionConcurrencyIntegrationTest (без @Transactional, два потока,
advisory lock): два одновременных apply идемпотентны и пишут history один раз;
два одновременных rollback дают ровно один успех и один ConflictException,
состояние восстанавливается один раз.
2.11. Release gate и CI
./scripts/ldd-release.sh последовательно запускает тесты/линтеры, upgrade,
mutation, runtime + scale и browser E2E, затем финальный evidenceCheck.
В .woodpecker/ (Gitea + Woodpecker 3.x):
check—test ktlintCheck detekt evidenceCheckна push/MR;smoke—ldd-smoke.sh(полный стек, health, вход админа, один курс) на каждом push/MR, послеcheck;release— полныйldd-release.shс Chromium/Firefox/WebKit и Node: обязателен для тегов, для веток запускается вручную.- workflows требуют метку
isolation=dedicated-vm; агент работает на отдельной VM, шаги — вlms-ci:1с nested dockerd и без хост-Docker сокета. - smoke/release начинаются с
scripts/ci-preflight.sh, который проверяет Java 21, Docker Compose v2, Python 3, Node и Playwright. - Первый реальный прогон на Woodpecker-агенте ещё не подтверждён; локальные
скрипты и конфигурация проверены, установка —
deploy/WOODPECKER.md.
2.12. Найденный дефект
Первый browser E2E упал на создании студента: API вернул 400, потому что
Jackson 3 не применял Kotlin-модуль, и DTO с default-значениями требовали все
поля JSON. Исправлено: jackson-module-kotlin для Jackson 3 +
JsonMapperBuilderCustomizer в identity/academic/progress/web. Добавлен
регрессионный тест TeacherIntegrationTest.student creation accepts omitted optional json fields.
3. Что это доказывает
- Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля, материалов, миграции, аудита и аутентификации имеют зелёные автоматические свидетельства.
- Приложение собирается, мигрирует с master-схемы и поднимается полным стеком на чистой БД.
- Ключевой доменный поток «админ → группа → курс → преподаватель → лаба» проходит через реальные HTTP/gateway/БД и браузер.
- Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке.
- Legacy-актив без привязки не утекает обычным пользователям, а админ может провести инвентаризацию.
- Refresh-токены ротируются и отзываются; закрытый статус блокирует продление сессии, абсолютный потолок 30 дней ограничивает сессию, а web-сессия переживает 15-минутный access-токен.
- Аудит листается keyset-курсором, детали лабораторных и группы курса отдаются пакетно без N+1.
- Аудит пишется и читается; upgrade не теряет синтетические данные; истёкшие refresh-токены очищаются.
- Мутационная прочность критичных компонентов измерена, а не заявлена.
- Масштабный load на 100 группах / 1000 студентов проходит без ошибок.
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
4. Чего это не доказывает
- Mutation testing не 100%. Остаются эквивалентные мутанты и Kotlin/Spring-специфичные мутации; PIT harness — JUnit 4, а не JUnit 5.
- Property-based тесты покрывают не все ветки. Есть генераторы для матрицы доступа, планировщика и 10 транзакционных apply/rollback-кейсов, но не для всех сочетаний полей и распределённой нагрузки.
- Браузерный E2E — 3 браузера, 6 сценариев. Нет визуальной регрессии и полного мобильного набора страниц; WebKit на Fedora требует Docker-образа.
- Нагрузочный smoke — не benchmark. Одна машина, синтетические данные, без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
- Сдачи по выбранной лабораторной в ведомости запрашиваются отдельно; это не N+1 по студентам, но и не пакетная выборка.
- Инвентаризация legacy-активов на production не выполнена. UI, API и скрипт готовы, но реальные файлы нужно привязать или удалить вручную.
- Первый CI-прогон не подтверждён. Конфигурация
.woodpecker/и preflight готовы, но Woodpecker-сервер и Docker-backend агент на отдельной VM ещё нужно развернуть и проверить на первом push-MR и теге. - Полный release не на каждом коммите. На push/MR идёт smoke, а mutation/browser/scale — для тегов или вручную.
- Интеграция «Контингент» исключена из плана — без нового ТЗ и доступа к API она не проверялась и не реализуется.
- Номер группы числовой. Отдельного требования к ведущим нулям нет;
01и1— одно значение, это не открытый риск.
5. Вывод
Текущая функциональность прошла LDD-контур: 57/57 claims proven, P0 gaps = 0, 366 тестов, upgrade, mutation baseline, 18/18 browser E2E, smoke и масштабный load зелёные. Это достаточное основание продолжать доработки, но не «формальное доказательство всей системы».
Следующий цикл LDD:
- прогнать инвентаризацию legacy-активов на production/staging через
/admin/legacy-assetsилиscripts/legacy-assets.shи привязать/удалить найденные файлы; - развернуть Woodpecker и подтвердить первый push-MR pipeline и tag-job;
- при необходимости расширить E2E на визуальную регрессию и остальные мобильные страницы;
- пакетно отдавать сдачи в ведомости, если появятся медленные курсы.