Files
LMS/docs/evidence/baseline-2026-10-27.md
Vladislav Kopnin 1570196cc7 fix(import): построчные ошибки до записи
- preflight занятого логина и неактивной целевой группы
- applied=false с номером строки, без частичных студентов
- LDD-IMPORT-003; baseline 58 claims / 368 тестов
2026-10-29 19:20:16 +03:00

20 KiB
Raw Blame History

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

Результат: 368 тестов, 0 failures, 0 errors, 0 skipped. ktlintCheck и detekt — зелёные.

2.2. LDD gate

./gradlew evidenceCheck

Результат: 58/58 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); cookie LMS_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 (opaque occurredAt|id), limit (максимум 500) и action; offset page остался для совместимости. Курсорные страницы не пересекаются, битый курсор — 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. Чего это не доказывает

  1. Mutation testing не 100%. Остаются эквивалентные мутанты и Kotlin/Spring-специфичные мутации; PIT harness — JUnit 4, а не JUnit 5.
  2. Property-based тесты покрывают не все ветки. Есть генераторы для матрицы доступа, планировщика и 10 транзакционных apply/rollback-кейсов, но не для всех сочетаний полей и распределённой нагрузки.
  3. Браузерный E2E — 3 браузера, 6 сценариев. Нет визуальной регрессии и полного мобильного набора страниц; WebKit на Fedora требует Docker-образа.
  4. Нагрузочный smoke — не benchmark. Одна машина, синтетические данные, без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
  5. Сдачи по выбранной лабораторной в ведомости запрашиваются отдельно; это не N+1 по студентам, но и не пакетная выборка.
  6. Инвентаризация legacy-активов на production не выполнена. UI, API и скрипт готовы, но реальные файлы нужно привязать или удалить вручную.
  7. Первый CI-прогон не подтверждён. Конфигурация .woodpecker/ и preflight готовы, но Woodpecker-сервер и Docker-backend агент на отдельной VM ещё нужно развернуть и проверить на первом push-MR и теге.
  8. Полный release не на каждом коммите. На push/MR идёт smoke, а mutation/browser/scale — для тегов или вручную.
  9. Интеграция «Контингент» исключена из плана — без нового ТЗ и доступа к API она не проверялась и не реализуется.
  10. Номер группы числовой. Отдельного требования к ведущим нулям нет; 01 и 1 — одно значение, это не открытый риск.

5. Вывод

Текущая функциональность прошла LDD-контур: 58/58 claims proven, P0 gaps = 0, 368 тестов, upgrade, mutation baseline, 18/18 browser E2E, smoke и масштабный load зелёные. Это достаточное основание продолжать доработки, но не «формальное доказательство всей системы».

Следующий цикл LDD:

  1. прогнать инвентаризацию legacy-активов на production/staging через /admin/legacy-assets или scripts/legacy-assets.sh и привязать/удалить найденные файлы;
  2. развернуть Woodpecker и подтвердить первый push-MR pipeline и tag-job;
  3. при необходимости расширить E2E на визуальную регрессию и остальные мобильные страницы;
  4. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.