feat(ldd): реестр утверждений, evidence gate и runtime-проверки
This commit is contained in:
76
docs/evidence/README.md
Normal file
76
docs/evidence/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# LDD: доказательное тестирование
|
||||
|
||||
Этот каталог — реестр утверждений о системе и свидетельств того, что они
|
||||
выполняются. LDD здесь означает не «тесты вместо требований», а
|
||||
**риск-ориентированную проверку**: требование → проверяемое утверждение →
|
||||
независимый оракул → воспроизводимое свидетельство → допуск.
|
||||
|
||||
## Уровни
|
||||
|
||||
- **P0** — утверждение о безопасности, сохранности данных, доступах или
|
||||
ключевом доменном правиле. P0 без зелёного свидетельства валит
|
||||
`./gradlew evidenceCheck`.
|
||||
- **P1** — важное поведение/UX/контракт. Пробел показывается в отчёте, но не
|
||||
блокирует сборку.
|
||||
- **runtime** — проверка на реально поднятом стеке. Выполняется отдельным
|
||||
скриптом; в unit-CI может отсутствовать, но обязательна перед релизом.
|
||||
|
||||
## Реестр
|
||||
|
||||
`claims.json` содержит:
|
||||
|
||||
- `id` — стабильный идентификатор (`LDD-ACCESS-001`);
|
||||
- `title` — короткая формулировка;
|
||||
- `criticality` — `P0`/`P1`;
|
||||
- `status` — `active` или `gap`;
|
||||
- `requirement` — ссылка на ТЗ/отчёт;
|
||||
- `claim` — проверяемое утверждение (что именно истинно);
|
||||
- `oracle` — как это проверяется;
|
||||
- `evidence` — список `fully.qualified.Class#test name()` из JUnit XML;
|
||||
- `runtime` — идентификаторы проверок из `build/evidence/runtime.json`;
|
||||
- `limitations` — что утверждение **не** покрывает.
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
# 1. Все тесты и линтеры
|
||||
./gradlew test ktlintCheck detekt
|
||||
|
||||
# 2. Гейт свидетельств (P0 без evidence = fail)
|
||||
./gradlew evidenceCheck
|
||||
|
||||
# 3. Полный runtime-smoke на одноразовой БД
|
||||
./scripts/ldd-runtime.sh
|
||||
|
||||
# 4. Проверка upgrade с master-схемы
|
||||
./scripts/ldd-migration-upgrade.sh
|
||||
```
|
||||
|
||||
Отчёт: `build/reports/evidence/index.md`.
|
||||
|
||||
## Правила
|
||||
|
||||
1. Новое P0-требование без свидетельства не мержится.
|
||||
2. Свидетельство должно быть воспроизводимым в CI, а не «проверил руками».
|
||||
3. Тест должен падать при поломке соответствующей логики; для критичных
|
||||
пакетов позже добавим mutation testing.
|
||||
4. Один тест может подтверждать несколько claims, но claim не должен
|
||||
опираться на тест, который проверяет только реализацию.
|
||||
5. `limitations` обязателен: LDD не должен создавать иллюзию полного
|
||||
доказательства.
|
||||
6. При переименовании теста обновляется `claims.json`; устаревшая ссылка —
|
||||
это gap, а не «забыли».
|
||||
7. Runtime-свидетельства обязательны перед релизом; CI проверяет unit/
|
||||
integration-слой.
|
||||
|
||||
## Текущий baseline
|
||||
|
||||
Первый прогон LDD выполнен 2026-10-27:
|
||||
|
||||
- 267 автоматических тестов, 0 failures/errors;
|
||||
- 33 claims: P0 — доступы, повышение, жизненный цикл, профиль/скачивание,
|
||||
материалы/активы, миграция V13, аудит; P1 — группы, авторство, UI,
|
||||
контракты, импорт, копирование лаб, runtime.
|
||||
- Runtime-smoke и master-upgrade вынесены в отдельные скрипты.
|
||||
|
||||
Результаты и найденные пробелы — в `baseline-2026-10-27.md`.
|
||||
142
docs/evidence/baseline-2026-10-27.md
Normal file
142
docs/evidence/baseline-2026-10-27.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# LDD baseline: 2026-10-27
|
||||
|
||||
Первый прогон доказательной проверки текущей функциональности LMS.
|
||||
Код приложения в этом прогоне не менялся; добавлены реестр claims, gate
|
||||
`evidenceCheck`, runtime- и migration-скрипты.
|
||||
|
||||
## 1. Что проверялось
|
||||
|
||||
Реестр: `docs/evidence/claims.json`, 31 утверждение.
|
||||
|
||||
| Критичность | Claims | Proven | Gaps |
|
||||
|---|---:|---:|---:|
|
||||
| P0 | 19 | 19 | 0 |
|
||||
| P1 | 12 | 12 | 0 |
|
||||
| **Итого** | **31** | **31** | **0** |
|
||||
|
||||
Домены:
|
||||
|
||||
- доступы: аноним/студент/преподаватель/админ, чужие курс/лаба/студент/группа,
|
||||
внутренние API identity;
|
||||
- повышение: dry-run, apply, идемпотентность, rollback, cancel;
|
||||
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
|
||||
- профиль студента и скачивание сдач;
|
||||
- материалы и активы;
|
||||
- миграция V13 и полный upgrade с master-схемы;
|
||||
- аудит: токен, admin-only, сохранность после удаления, representative-хуки;
|
||||
- группы курса, авторство материалов, ведомость, UI/контракты, импорт, копирование лаб;
|
||||
- runtime-smoke полного стека.
|
||||
|
||||
## 2. Команды и результаты
|
||||
|
||||
### 2.1. Автоматические тесты
|
||||
|
||||
```bash
|
||||
./gradlew test --rerun-tasks
|
||||
```
|
||||
|
||||
Результат: **267 тестов, 0 failures, 0 errors, 0 skipped**.
|
||||
`ktlintCheck` и `detekt` — зелёные.
|
||||
|
||||
### 2.2. LDD gate
|
||||
|
||||
```bash
|
||||
./gradlew evidenceCheck
|
||||
```
|
||||
|
||||
Результат: **31/31 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–V10`, academic `V6–V12`, progress `V13`.
|
||||
|
||||
Проверено:
|
||||
|
||||
- `identity.audit_events` создана;
|
||||
- `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 полного стека
|
||||
|
||||
```bash
|
||||
./scripts/ldd-runtime.sh
|
||||
```
|
||||
|
||||
Сценарий: одноразовая БД, `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`.
|
||||
|
||||
Результат: **PASS**, evidence: `build/evidence/runtime.json`.
|
||||
Стек и одноразовая БД после прогона остановлены/удалены.
|
||||
|
||||
## 3. Что это доказывает
|
||||
|
||||
- Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля,
|
||||
материалов, миграции и аудита имеют зелёные автоматические свидетельства.
|
||||
- Приложение собирается, мигрируется с master-схемы и поднимается полным
|
||||
стеком на чистой БД.
|
||||
- Ключевой доменный поток «админ → группа → курс → преподаватель → лаба»
|
||||
проходит через реальные HTTP/gateway/БД, а не только через mock.
|
||||
- Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке.
|
||||
- Аудит пишется и читается; upgrade не теряет синтетические данные.
|
||||
|
||||
## 4. Чего это не доказывает
|
||||
|
||||
1. **Нет браузерного E2E.** Thymeleaf-тесты проверяют HTML, но не реальные
|
||||
клики/JS/CSRF/редиректы в браузере.
|
||||
2. **Нет нагрузочных и конкурентных испытаний.** Advisory locks и гонки
|
||||
проверены кодом и unit/integration-тестами, но не под нагрузкой.
|
||||
3. **Нет mutation testing.** Сила тестов измерена косвенно; следующий шаг —
|
||||
PIT на `CourseAccessService`, `PromotionService`, `StudentProfileService`.
|
||||
4. **Нет property-based/model-based проверки повышения.** Один сценарный тест
|
||||
не покрывает все комбинации курсов, степеней и академ-переходов.
|
||||
5. **Legacy-активы без `article_assets` остаются публичными.**
|
||||
6. **JWT живёт до 30 дней.** Web перепроверяет статус, прямой API — нет.
|
||||
7. **Ведущие нули номера группы нормализуются** (`01` → `1`).
|
||||
8. **Нет пагинации/keyset** для части списков и N+1 в отдельных экранах.
|
||||
9. **Runtime-smoke не входит в unit-CI**; он запускается отдельным скриптом
|
||||
перед релизом.
|
||||
|
||||
## 5. Вывод
|
||||
|
||||
Текущая функциональность прошла первый LDD-контур: **P0 gaps = 0**,
|
||||
полный стек и upgrade работают. Это достаточное основание продолжать
|
||||
доработки, но не «формальное доказательство всей системы». Следующий цикл
|
||||
LDD должен добавить:
|
||||
|
||||
1. mutation testing для критичных пакетов;
|
||||
2. property-based тесты повышения и матрицы доступа;
|
||||
3. браузерный E2E (Playwright) для ведомости, профиля и материалов;
|
||||
4. нагрузочный smoke на чтение ведомости/профиля;
|
||||
5. автоматический runtime-smoke в release-пайплайне.
|
||||
463
docs/evidence/claims.json
Normal file
463
docs/evidence/claims.json
Normal file
@@ -0,0 +1,463 @@
|
||||
{
|
||||
"version": 1,
|
||||
"description": "LDD claims для текущей функциональности LMS. P0 без зелёного свидетельства валит ./gradlew evidenceCheck.",
|
||||
"claims": [
|
||||
{
|
||||
"id": "LDD-ACCESS-001",
|
||||
"title": "Преподаватель не получает доступ к чужому курсу, лабе, студенту или группе",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "Для любого преподавателя T и ресурса R чужого курса сервис возвращает 403/404 и не отдаёт данные R.",
|
||||
"oracle": "HTTP-статус отказа и отсутствие данных в ответе",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.course.CourseAccessServiceTest#teacher cannot access a foreign course()",
|
||||
"ru.ysu.lms.academic.course.CourseRosterServiceTest#foreign teacher is forbidden()",
|
||||
"ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#teacher is forbidden to grade foreign lab()",
|
||||
"ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#teacher is forbidden to grade student from another course()"
|
||||
],
|
||||
"limitations": "Проверены основные API-точки; UI-навигация не является границей безопасности."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-002",
|
||||
"title": "Студент видит только свои курсы, группу и опубликованные данные",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "Студент чужой группы не получает доступ к курсу, а в своём профиле видит только опубликованные оценки и комментарии.",
|
||||
"oracle": "403 для чужого курса; отсутствие неопубликованных данных в ответе",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.course.CourseAccessServiceTest#student cannot access course of a foreign group()",
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#student sees only published grade and comments of own profile()",
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher cannot read or download student from group outside course but admin can()"
|
||||
],
|
||||
"limitations": "Студенческий BFF-профиль не является отдельной страницей; проверен профильный API."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-003",
|
||||
"title": "Анонимный доступ ограничен глобальными материалами ALL",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §6",
|
||||
"claim": "Аноним не читает курсовые и приватные материалы/активы; публичное дерево содержит только глобальные главы audience=ALL.",
|
||||
"oracle": "401/404 для приватного; 200 только для ALL; состав публичного дерева",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#anonymous cannot read course content()",
|
||||
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#public tree contains only global chapters with ALL audience()",
|
||||
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#course asset requires authentication()",
|
||||
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#published global ALL asset stays public()"
|
||||
],
|
||||
"limitations": "Legacy-активы без article_assets остаются публичными и вынесены в отдельный риск."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-004",
|
||||
"title": "Администратор сохраняет полный доступ",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "Администратор не ограничен курсовой областью и может читать/писать любой курс и профиль.",
|
||||
"oracle": "Успешный доступ к чужому курсу/лабе/студенту для ADMIN",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.course.CourseAccessServiceTest#admin bypasses checks and has no course restriction()",
|
||||
"ru.ysu.lms.academic.course.CourseAccessServiceTest#admin my courses contains every course()",
|
||||
"ru.ysu.lms.progress.TeacherWriteAccessIntegrationTest#admin keeps access without course scope checks()"
|
||||
],
|
||||
"limitations": "Проверены сервисные проверки, не все административные UI-маршруты."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-005",
|
||||
"title": "Внутренние API identity защищены сервисным токеном и не раскрывают учётные данные",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/13-stage3-report.md, §3",
|
||||
"claim": "Без корректного X-Internal-Token внутренние API возвращают 403; ответы не содержат паролей, tempPassword и лишних данных.",
|
||||
"oracle": "403 на неверный/пустой токен; отсутствие чувствительных полей в JSON",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.internal.InternalControllerIntegrationTest#invalid token is forbidden()",
|
||||
"ru.ysu.lms.identity.internal.InternalControllerIntegrationTest#groups response exposes no credentials or students()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#invalid or missing internal token is forbidden and writes nothing()"
|
||||
],
|
||||
"limitations": "Проверены контекст студента, состав группы, список групп и аудит."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-006",
|
||||
"title": "Преподаватель управляет лабораторными только своего курса",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "TEACHER может создавать и изменять лабы/уровни/варианты своего курса, но не чужого; STUDENT не может писать.",
|
||||
"oracle": "200 для своего курса; 403 для чужого; 403 для студента",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#teacher cannot update lab of foreign course()",
|
||||
"ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#teacher can create lab in own course()",
|
||||
"ru.ysu.lms.academic.lab.LabTeacherAccessIntegrationTest#student cannot update or create labs()"
|
||||
],
|
||||
"limitations": "Импорт ZIP проверен отдельными тестами парсера/копирования."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROM-001",
|
||||
"title": "Dry-run повышения не меняет данные и дедуплицируется по году",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §7",
|
||||
"claim": "Построение dry-run создаёт только план; данные групп/студентов не меняются; повторный запуск за год не создаёт дубль.",
|
||||
"oracle": "Снимок данных до/после и количество запусков",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#dry run only plans and does not change data()",
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#scheduled dry run guard respects date and deduplicates by year()"
|
||||
],
|
||||
"limitations": "Проверено на синтетических данных Testcontainers."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROM-002",
|
||||
"title": "Применение повышения переводит группы и студентов по правилам",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §7",
|
||||
"claim": "Apply повышает курс групп, выпускает на пределе и переводит академических студентов в целевую группу.",
|
||||
"oracle": "Группы, статусы, зачисления и membership_history после apply",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#apply promotes groups graduates and moves academic students()"
|
||||
],
|
||||
"limitations": "Проверен один сценарий; граничные сочетания требуют property-based тестов."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROM-003",
|
||||
"title": "Повторное применение повышения идемпотентно",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §7",
|
||||
"claim": "Повторный apply того же run не создаёт повторных изменений и истории.",
|
||||
"oracle": "Состояние и количество history-записей после первого и второго apply",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#repeated apply is idempotent()"
|
||||
],
|
||||
"limitations": "Конкурентный apply проверяется advisory lock, но отдельного нагрузочного теста нет."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROM-004",
|
||||
"title": "Откат повышения восстанавливает состояние и обнаруживает конфликт",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §7",
|
||||
"claim": "Rollback возвращает before-state; при ручной правке после apply сообщает конфликт и не затирает данные.",
|
||||
"oracle": "Снимок before/after и 409 при конфликте",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#rollback restores data and removes created target group()",
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#rollback reports manual conflict()"
|
||||
],
|
||||
"limitations": "Проверены группы и студенты; rollback файлов/аудита не применим."
|
||||
},
|
||||
{
|
||||
"id": "LDD-LIFE-001",
|
||||
"title": "Жизненный цикл студента пишет историю и переводит зачисление",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §8",
|
||||
"claim": "Академ/возврат/отчисление/архив/восстановление меняют статусы, зачисление и membership_history по правилам.",
|
||||
"oracle": "Статус student/account, группа зачисления, membership_history",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#academic leave creates target cohort group and records history()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#return from academic leave moves to requested active group()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#expel blocks login and keeps current enrollment for history()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#archive blocks login and restore returns student to active group()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#restore returns an expelled student to active group()"
|
||||
],
|
||||
"limitations": "Проверены основные переходы; массовые операции не автоматизированы."
|
||||
},
|
||||
{
|
||||
"id": "LDD-LIFE-002",
|
||||
"title": "Вход закрыт для отчисленных, архивных и закрытых групп",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §1.3",
|
||||
"claim": "EXPELLED/GRADUATED/ARCHIVED студенты и архивные аккаунты не проходят login; активный студент проходит.",
|
||||
"oracle": "Unauthorized/403 при login и active=false в /auth/me",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#expelled student cannot login()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#student in archived group cannot login()",
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#student in active group can login()",
|
||||
"ru.ysu.lms.identity.AuthServiceMeActiveTest#me is inactive for archived account()"
|
||||
],
|
||||
"limitations": "JWT с прежним сроком жизни остаётся остаточным риском."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROFILE-001",
|
||||
"title": "Профиль студента курса различает staff/student и публикацию",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5.1",
|
||||
"claim": "Преподаватель своего курса видит все лабы и неопубликованные данные; студент — только опубликованное; чужой преподаватель получает 403.",
|
||||
"oracle": "Состав DTO профиля и HTTP-статус",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher sees unpublished grade, authors and all submission attempts()",
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#student sees only published grade and comments of own profile()",
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#teacher of another course gets forbidden()"
|
||||
],
|
||||
"limitations": "Проверен API; UI-шаблон проверяется отдельно."
|
||||
},
|
||||
{
|
||||
"id": "LDD-DOWNLOAD-001",
|
||||
"title": "Скачивание сдачи ограничено курсом и студентом",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5.1",
|
||||
"claim": "Преподаватель скачивает только сдачи своего курса/своих студентов; сдача лабы другого курса отклоняется.",
|
||||
"oracle": "ZIP для разрешённой сдачи; 403 для чужой лабы/студента",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.progress.StudentProfileIntegrationTest#download rejects submission from another course lab and allows current course lab()",
|
||||
"ru.ysu.lms.progress.TeacherLabPageAccessIntegrationTest#teacher downloads submission of own lab()",
|
||||
"ru.ysu.lms.progress.TeacherLabPageAccessIntegrationTest#teacher is forbidden for foreign lab()"
|
||||
],
|
||||
"limitations": "Аудит скачивания проверяется отдельным claim."
|
||||
},
|
||||
{
|
||||
"id": "LDD-CONTENT-001",
|
||||
"title": "Курсовые материалы не утекают в публичное дерево",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §6",
|
||||
"claim": "Курсовая глава недоступна через публичный article endpoint; студент видит только published; преподаватель видит hidden.",
|
||||
"oracle": "404/отсутствие курсовой главы в public tree; состав courseTree",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#course chapter is not exposed through public article endpoint()",
|
||||
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#student of course sees only published materials()",
|
||||
"ru.ysu.lms.academic.content.CourseContentIntegrationTest#teacher of course sees published and hidden materials()"
|
||||
],
|
||||
"limitations": "Импорт и авторство проверяются отдельными тестами."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ASSET-001",
|
||||
"title": "Видимость активов наследуется от статьи",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §6",
|
||||
"claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; legacy без связи публичен.",
|
||||
"oracle": "HTTP-статус и содержимое файла",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#hidden course asset is not served to student but is served to teacher()",
|
||||
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#foreign teacher and student are forbidden for course asset()",
|
||||
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#legacy asset without article association stays public()"
|
||||
],
|
||||
"limitations": "Legacy-файлы без article_assets остаются публичными — отдельный остаточный риск."
|
||||
},
|
||||
{
|
||||
"id": "LDD-MIG-001",
|
||||
"title": "Миграция V13 идемпотентна",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.2",
|
||||
"claim": "Повторное применение V13 не меняет схему и не падает.",
|
||||
"oracle": "Повторный Flyway migrate и проверка колонок/индекса",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.progress.migration.ProfileAuthorsMigrationTest#V13 profile authors migration is idempotent()"
|
||||
],
|
||||
"limitations": "Полный upgrade с master-схемы проверяется runtime-скриптом."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUDIT-001",
|
||||
"title": "Аудит защищён и доступен только администратору",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.3",
|
||||
"claim": "Внутренняя запись требует X-Internal-Token; чтение /api/v1/audit требует ADMIN.",
|
||||
"oracle": "403 без токена/для не-админа; 200 для админа",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#admin list requires admin role()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#invalid or missing internal token is forbidden and writes nothing()"
|
||||
],
|
||||
"limitations": "Проверены API; UI-страница проверяется отдельным шаблонным тестом."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUDIT-002",
|
||||
"title": "Аудит переживает удаление сущности и сохраняет поля",
|
||||
"criticality": "P0",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5.2",
|
||||
"claim": "Событие аудита сохраняется после удаления студента/преподавателя и корректно отдаётся в списке.",
|
||||
"oracle": "Наличие строки после delete; поля id/action/entity/occurredAt",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#identity hooks record teacher create and student delete that survives deletion()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#internal write is listed newest first with all fields()"
|
||||
],
|
||||
"limitations": "Проверены identity-хуки; cross-service хуки проверяются отдельными тестами."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-007",
|
||||
"title": "Преподаватель управляет группами своего курса",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "TEACHER привязывает/отвязывает активные группы только своего курса; чужой преподаватель и студент получают 403.",
|
||||
"oracle": "200 и history-запись для своего курса; 403 для чужого",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#teacher sees available groups, attaches and detaches on own course()",
|
||||
"ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#foreign teacher cannot list attach or detach groups()"
|
||||
],
|
||||
"limitations": "Проверено API; UI-форма — шаблонным тестом."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-008",
|
||||
"title": "Преподаватель авторствует материалы только своего курса",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5",
|
||||
"claim": "TEACHER создаёт/меняет/публикует главы, разделы и статьи своего курса; глобальные материалы — только ADMIN; чужой курс — 403.",
|
||||
"oracle": "200 для своего курса; 403 для чужого; global routes только ADMIN",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#teacher creates chapter section and article in own course()",
|
||||
"ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#foreign teacher cannot author course content()",
|
||||
"ru.ysu.lms.academic.content.CourseContentAuthoringIntegrationTest#global chapter authoring stays admin only()"
|
||||
],
|
||||
"limitations": "Импорт ZIP остаётся админским; это осознанное ограничение."
|
||||
},
|
||||
{
|
||||
"id": "LDD-ACCESS-009",
|
||||
"title": "Ведомость преподавателя не показывает админские операции",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/13-stage3-report.md, §4",
|
||||
"claim": "В teacher-view скрыты пароли, аттестации и жизненный цикл; админский вид не меняется; админские URL закрыты интерсептором.",
|
||||
"oracle": "Состав HTML и redirect для teacher",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.web.admin.TeacherGradebookTemplateTest#teacher gradebook has no admin-only controls()",
|
||||
"ru.ysu.lms.web.admin.TeacherGradebookTemplateTest#admin view is used when teacherView attribute is missing()",
|
||||
"ru.ysu.lms.web.session.AdminInterceptorTest#teacher is redirected from admin-only group operations()"
|
||||
],
|
||||
"limitations": "Это UI-защита; серверные проверки — в LDD-ACCESS-001."
|
||||
},
|
||||
{
|
||||
"id": "LDD-LIFE-003",
|
||||
"title": "Удаление требует усиленного подтверждения",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §11",
|
||||
"claim": "DELETE студента без совпадения confirm с login/ФИО отклоняется; корректный confirm удаляет.",
|
||||
"oracle": "400 при неверном confirm; успешное удаление при верном",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#delete requires confirm equal to login or full name()"
|
||||
],
|
||||
"limitations": "Проверен сервис; UI-диалог — шаблонным тестом."
|
||||
},
|
||||
{
|
||||
"id": "LDD-PROM-005",
|
||||
"title": "Отмена устаревшего dry-run разрешает перестроить план",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.3",
|
||||
"claim": "Cancel переводит PLANNED dry-run в FAILED/CANCELLED; после этого можно построить новый план за год.",
|
||||
"oracle": "Статус run и создание нового dry-run",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.promotion.PromotionServiceIntegrationTest#cancel marks planned dry run and allows rebuild()"
|
||||
],
|
||||
"limitations": "Конкурентный cancel/apply защищён advisory lock, отдельного теста нет."
|
||||
},
|
||||
{
|
||||
"id": "LDD-IMPORT-001",
|
||||
"title": "Импорт групп валидирует файл и не применяется частично",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §9",
|
||||
"claim": "Ошибочные строки блокируют импорт целиком; корректный шаблон и дубли № распознаются.",
|
||||
"oracle": "applied=false и отсутствие изменений при ошибках; отчёт парсера",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#does not apply import when there are row errors()",
|
||||
"ru.ysu.lms.identity.roster.StudentImportParserTest#parses new template and splits full name()",
|
||||
"ru.ysu.lms.identity.roster.StudentImportParserTest#reports duplicate student number()"
|
||||
],
|
||||
"limitations": "Старый формат не поддерживается; ведущие нули нормализуются."
|
||||
},
|
||||
{
|
||||
"id": "LDD-LAB-002",
|
||||
"title": "Копирование лабораторной не переносит прогресс и требует другой курс",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §10",
|
||||
"claim": "Копия создаёт новые уровни/варианты в целевом курсе, не копирует оценки/сдачи; тот же/архивный курс отклоняется.",
|
||||
"oracle": "Состав копии и отсутствие progress-строк; 400 для same/archived",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copies lab with levels and variants into another course()",
|
||||
"ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copy rejects the source course as target()",
|
||||
"ru.ysu.lms.academic.lab.LabCopyIntegrationTest#copy rejects an archived target course()"
|
||||
],
|
||||
"limitations": "Проверено на Testcontainers; UI-копирование — шаблонным тестом."
|
||||
},
|
||||
{
|
||||
"id": "LDD-CONTRACT-001",
|
||||
"title": "DTO-контракты web и сервисов совпадают",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/09-teacher-student-profile.md",
|
||||
"claim": "Ключевые JSON-контракты профиля, состава группы, аудита и повышения десериализуются web-слоем без потери полей.",
|
||||
"oracle": "JsonIgnoreProperties + значения полей после десериализации",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.web.profile.ProfileDtoJsonTest#deserializes profile contract()",
|
||||
"ru.ysu.lms.web.api.CourseGroupRosterDtoTest#deserializes course group roster contract()",
|
||||
"ru.ysu.lms.web.api.AuditDtoTest#deserializes audit event list with nulls()",
|
||||
"ru.ysu.lms.web.api.PromotionDtoTest#deserializes promotion detail with item states and result()"
|
||||
],
|
||||
"limitations": "Это smoke-контракты, не consumer-driven contracts."
|
||||
},
|
||||
{
|
||||
"id": "LDD-UI-001",
|
||||
"title": "Ключевые страницы содержат нужные действия и не раскрывают лишнего",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §3.3",
|
||||
"claim": "Шаблоны ведомости, материалов и аудита рендерят ожидаемые формы/ссылки и экранируют данные.",
|
||||
"oracle": "Содержимое HTML и отсутствие запрещённых элементов",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.web.teacher.TeacherCourseContentTemplateTest#content page renders create forms and full tree()",
|
||||
"ru.ysu.lms.web.profile.StudentProfileTemplateTest#renders student profile with labs, comments and downloads()",
|
||||
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#renders audit events with labels and filter()"
|
||||
],
|
||||
"limitations": "Thymeleaf-тесты не заменяют браузерный E2E."
|
||||
},
|
||||
{
|
||||
"id": "LDD-AUDIT-003",
|
||||
"title": "Критичные действия progress пишут аудит",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/07-groups-courses.md, §5.2",
|
||||
"claim": "Оценка и публикация оценки создают audit-событие с actor из JWT и корректными идентификаторами.",
|
||||
"oracle": "Вызов AuditClient с ожидаемыми action/entity/student/course",
|
||||
"evidence": [
|
||||
"ru.ysu.lms.progress.ProgressControllerAuditTest#grade write records audit entry for actor from jwt()",
|
||||
"ru.ysu.lms.progress.ProgressControllerAuditTest#publication records student count()",
|
||||
"ru.ysu.lms.identity.audit.AuditIntegrationTest#action filter and limit are applied()"
|
||||
],
|
||||
"limitations": "Проверены representative-хуки; остальные actions проверяются кодом и smoke."
|
||||
},
|
||||
{
|
||||
"id": "LDD-MIG-002",
|
||||
"title": "Upgrade с master-схемы применяет миграции ветки",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §2.2",
|
||||
"claim": "Схема master + синтетические данные → миграции identity V4–V10, academic V6–V12, progress V13; V10 audit создаётся, V13 идемпотентна.",
|
||||
"oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов",
|
||||
"runtime": [
|
||||
"migration.master_upgrade"
|
||||
],
|
||||
"limitations": "Запускается отдельным скриптом на одноразовой БД; не входит в unit-CI."
|
||||
},
|
||||
{
|
||||
"id": "LDD-RUNTIME-001",
|
||||
"title": "Полный стек поднимается и проходит E2E smoke",
|
||||
"criticality": "P1",
|
||||
"status": "active",
|
||||
"requirement": "docs/16-stage6-report.md, §3.3",
|
||||
"claim": "На чистой БД стартуют все сервисы, health=UP, admin setup/login работает, создаются группа/курс/преподаватель/лаба, чужой преподаватель получает 403, аудит доступен.",
|
||||
"oracle": "HTTP health/status/JSON на реальном стеке",
|
||||
"runtime": [
|
||||
"runtime.stack_health",
|
||||
"runtime.admin_setup_login",
|
||||
"runtime.course_flow",
|
||||
"runtime.foreign_teacher_denied",
|
||||
"runtime.audit_visible",
|
||||
"runtime.web_login_page"
|
||||
],
|
||||
"limitations": "Smoke не заменяет полный UI E2E и нагрузочные тесты."
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user