- teacher/admin course pages, content, gradebook, profile, downloads - courseNo -> UUID только на границе REST API - обновлены e2e, шаблоны, тесты и docs - LDD-UI-002; baseline 59 claims / 385 тестов
323 lines
20 KiB
Markdown
323 lines
20 KiB
Markdown
# 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. Автоматические тесты
|
||
|
||
```bash
|
||
./gradlew test --rerun-tasks
|
||
```
|
||
|
||
Результат: **385 тестов, 0 failures, 0 errors, 0 skipped**.
|
||
`ktlintCheck` и `detekt` — зелёные.
|
||
|
||
### 2.2. LDD gate
|
||
|
||
```bash
|
||
./gradlew evidenceCheck
|
||
```
|
||
|
||
Результат: **59/59 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-схемы
|
||
|
||
```bash
|
||
./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
|
||
|
||
```bash
|
||
./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
|
||
|
||
```bash
|
||
./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
|
||
|
||
```bash
|
||
./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-контур: **59/59 claims proven, P0 gaps = 0**,
|
||
**385 тестов**, 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. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.
|