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

859 lines
60 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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#anonymous 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 закрыты отдельными claims LDD-ASSET-002/003."
},
{
"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; публичный глобальный ALL-актив доступен без входа.",
"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#published global ALL asset stays public()"
],
"limitations": "Legacy-файлы без article_assets теперь закрыты отдельным claim LDD-ASSET-002."
},
{
"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()",
"ru.ysu.lms.identity.roster.TeacherIntegrationTest#student creation accepts omitted optional json fields()"
],
"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–V13, academic V6–V12, progress V13; V10 audit, V11 refresh, V12 absolute-expiry и V13 keyset-индексы создаются, 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 и нагрузочные тесты."
},
{
"id": "LDD-MUTATION-001",
"title": "Мутационная прочность матрицы доступа",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "PIT по CourseAccessService убивает не менее 70% покрытых мутаций.",
"oracle": "PIT mutations.xml: killed / covered",
"runtime": [
"mutation.course_access"
],
"limitations": "PIT 1.20 + JUnit4 harness; Kotlin/Spring-специфичные мутации и эквивалентные мутанты не входят в цель."
},
{
"id": "LDD-MUTATION-002",
"title": "Мутационная прочность планировщика повышения",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "PIT по PromotionPlanner убивает не менее 60% покрытых мутаций.",
"oracle": "PIT mutations.xml: killed / covered",
"runtime": [
"mutation.promotion_planner"
],
"limitations": "Проверяется планировщик, не транзакционный apply/rollback (они покрыты интеграционными тестами)."
},
{
"id": "LDD-BROWSER-001",
"title": "Браузерный E2E в Chromium, Firefox и WebKit",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "6 сценариев (админ, преподаватель, студент/аноним, сдача+mobile, cursor-аудит, инвентаризация активов) проходят в Chromium, Firefox и WebKit: 18/18 без unexpected.",
"oracle": "Playwright assertions на DOM/URL",
"runtime": [
"browser.e2e"
],
"limitations": "Нет визуальной регрессии и полного набора мобильных страниц; WebKit на Fedora запускается через Docker-образ Playwright."
},
{
"id": "LDD-RUNTIME-002",
"title": "Нагрузочный smoke чтения курса, лаб, профиля и аудита",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "100 параллельных чтений (10 workers) через gateway дают 0 ошибок и p95 < 2 c.",
"oracle": "HTTP status + latency percentiles",
"runtime": [
"runtime.load_gradebook_profile"
],
"limitations": "Smoke, не benchmark; одна машина и синтетические данные, без SLA production."
},
{
"id": "LDD-PROM-006",
"title": "Конкурентные apply/rollback одного запуска повышения",
"criticality": "P0",
"status": "active",
"requirement": "docs/07-groups-courses.md, §7",
"claim": "Два одновременных apply не дублируют историю; два одновременных rollback дают ровно один успех и один конфликт, состояние восстанавливается один раз.",
"oracle": "Результаты двух потоков, количество APPLY/ROLLBACK и history",
"evidence": [
"ru.ysu.lms.identity.promotion.PromotionConcurrencyIntegrationTest#concurrent apply of one run is idempotent and writes history once()",
"ru.ysu.lms.identity.promotion.PromotionConcurrencyIntegrationTest#concurrent rollback allows exactly one rollback and restores once()"
],
"limitations": "Два потока в одном JVM; распределённая нагрузка и несколько инстансов проверяются advisory lock-ом, но не chaos-тестом."
},
{
"id": "LDD-PROM-007",
"title": "Транзакционный apply/rollback на сгенерированных когортах",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §7",
"claim": "Для 10 сгенерированных когорт apply приводит группу к плану, а rollback возвращает точный before-state и активный статус студента.",
"oracle": "Сравнение состояния группы/студента до, после apply и после rollback",
"evidence": [
"ru.ysu.lms.identity.promotion.PromotionApplyRollbackPropertyTest#generated cohorts apply to plan and rollback to before-state()"
],
"limitations": "10 детерминированных сценариев в одной транзакции; не заменяет property-генератор по всем полям."
},
{
"id": "LDD-RUNTIME-003",
"title": "Масштабный load: 100 групп и 1000 студентов",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "После seed 100 групп/1000 студентов 125 параллельных чтений курса, лаб, профиля, групп и аудита дают 0 ошибок и p95 < 2 c.",
"oracle": "runtime.scale_seed + runtime.load_gradebook_profile",
"runtime": [
"runtime.scale_seed",
"runtime.load_gradebook_profile"
],
"limitations": "Синтетические данные в одноразовой БД; это smoke, не SLA production."
},
{
"id": "LDD-ASSET-002",
"title": "Файлы без привязки к статье не публичны",
"criticality": "P0",
"status": "active",
"requirement": "docs/15-stage5-course-materials.md",
"claim": "Аноним, студент и преподаватель получают 404 для legacy-актива без article_assets; админ получает файл; endpoint legacy-assets перечисляет такие файлы до привязки.",
"oracle": "HTTP 404/200 и список unlinkedAssetNames",
"evidence": [
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#legacy asset without article association is not public()",
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#unlinkedAssetNames lists stored file until it is linked()"
],
"limitations": "Старые внешние ссылки на бесхозные файлы перестают работать у обычных пользователей — осознанный security-компромисс."
},
{
"id": "LDD-AUTH-001",
"title": "Короткий access-токен и ротация refresh",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Login выдаёт access ≤15 минут и refresh; refresh ротируется, старый refresh отклоняется, повторное использование отзывает активные сессии.",
"oracle": "TTL JWT, ответы refresh, отзыв сессий",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#login returns short lived access token and refresh token()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#refresh rotates the token and old token is rejected()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#reuse of a rotated token revokes every active session()"
],
"limitations": "Refresh-токен хранится как SHA-256 отпечаток; очистка истёкших записей выполняется позже."
},
{
"id": "LDD-AUTH-002",
"title": "Отзыв refresh при logout/смене пароля и закрытом статусе",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Logout и смена пароля отзывают refresh; отчисленный/архивный студент, архивный аккаунт и закрытая группа не могут обновить сессию.",
"oracle": "401 на refresh после отзыва/закрытия",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#logout revokes the refresh token idempotently()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#password change revokes refresh tokens()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#expelled student cannot refresh()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#archived account cannot refresh()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#student in archived group cannot refresh()"
],
"limitations": "Access-токен живёт до 15 минут после отзыва refresh — это принятый компромисс."
},
{
"id": "LDD-AUTH-003",
"title": "Web-сессия 7 дней sliding и автообновление",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Web хранит refresh и expiry, обновляет access за 2 минуты до истечения, продлевает cookie на каждом ответе; logout отзывает refresh; identity outage не разлогинивает всех.",
"oracle": "SessionTokenService/AuthPageController/фильтр cookie",
"evidence": [
"ru.ysu.lms.web.session.SessionTokenServiceTest#token expiring soon is refreshed and session is updated()",
"ru.ysu.lms.web.session.SessionTokenServiceTest#identity outage fails open()",
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#refreshes cookie max age on every existing session()",
"ru.ysu.lms.web.auth.AuthPageControllerTest#logout revokes refresh token and invalidates session()"
],
"limitations": "Cookie и серверный timeout 7 дней; поверх них действует абсолютный потолок 30 дней (LDD-AUTH-006)."
},
{
"id": "LDD-AUDIT-004",
"title": "Пагинация журнала аудита",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Audit API поддерживает offset-страницы `page`/`limit` для совместимости; UI по умолчанию продолжает журнал keyset-курсором.",
"oracle": "Страницы AuditIntegrationTest и ссылки шаблона",
"evidence": [
"ru.ysu.lms.identity.audit.AuditIntegrationTest#pagination returns disjoint pages newest first()",
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#renders audit events with labels and filter()"
],
"limitations": "Offset-режим остаётся только для старых ссылок; основной путь — курсор (LDD-AUDIT-005)."
},
{
"id": "LDD-PERF-001",
"title": "Bulk-группы курса без N+1",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Страница преподавателя получает названия всех привязанных групп одним запросом; чужой преподаватель получает 403.",
"oracle": "Один вызов courseGroups и отсутствие courseGroupStudents на страницу",
"evidence": [
"ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#teacher gets attached groups with names in one call()",
"ru.ysu.lms.web.teacher.TeacherControllerCourseGroupsTest#loads group names with a single call instead of per-group requests()"
],
"limitations": "Детали лабораторных теперь пакетные (LDD-PERF-002); отдельные запросы на лабораторную в ведомости ещё остаются."
},
{
"id": "LDD-AUTH-004",
"title": "Gateway пропускает refresh/logout без access-токена",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "POST /api/v1/auth/refresh и /api/v1/auth/logout проходят через gateway без access-JWT, иначе автообновление и отзыв сессии невозможны.",
"oracle": "GatewayFilterChain вызван для public path; protected path без токена — 401",
"evidence": [
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#refresh and logout are public()",
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#protected route without bearer token is rejected()"
],
"limitations": "Проверена маршрутизация gateway; интеграционный E2E с истёкшим access-токеном добавлен через web-тесты."
},
{
"id": "LDD-AUTH-005",
"title": "Gateway пропускает refresh/logout без access-JWT",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Gateway не требует access-JWT для /api/v1/auth/refresh и /api/v1/auth/logout; защищённый маршрут без токена получает 401. Полный цикл refresh/logout через реальный gateway проверяется отдельным P1 runtime-claim.",
"oracle": "JwtAuthenticationFilterTest: public paths пропущены, protected path без токена — 401",
"runtime": [],
"limitations": "Unit-тест проверяет маршрутизацию gateway; полный обмен пары и отзыв refresh — LDD-AUTH-008.",
"evidence": [
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#refresh and logout are public()",
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#protected route without bearer token is rejected()"
]
},
{
"id": "LDD-AUTH-006",
"title": "Абсолютный потолок сессии 30 дней",
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Ротация refresh не продлевает absolute_expires_at; после 30 дней refresh отклоняется, а web-сессия принудительно завершается и cookie не живёт дольше остатка.",
"oracle": "absolute_expires_at в цепочке ротаций, 401 на refresh, redirect /login?expired=absolute, Max-Age cookie",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#refresh past the absolute session expiry is rejected with unauthorized()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#rotation keeps the absolute session expiry unchanged()",
"ru.ysu.lms.web.session.AuthInterceptorTest#absolute expired session redirects to absolute login without identity call()",
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#cookie max age is capped by remaining absolute lifetime()",
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#absolute expired session receives zero max age cookie()",
"ru.ysu.lms.web.auth.AuthPageControllerTest#login stores authenticated at timestamp()"
],
"limitations": "Срок настраивается LMS_SESSION_ABSOLUTE_TIMEOUT; абсолютный отсчёт начинается с логина, а не с создания web-сессии."
},
{
"id": "LDD-AUTH-007",
"title": "Очистка истёкших refresh-токенов",
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Ночная очистка удаляет токены, у которых sliding- или absolute-срок истёк раньше retention (30 дней), и сохраняет свежие/недавно отозванные токены для detection reuse.",
"oracle": "cleanupExpired(now) и количество удалённых записей",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#cleanup deletes only tokens expired beyond retention()"
],
"limitations": "Расписание задаётся LMS_REFRESH_TOKEN_CLEANUP_CRON; в тесте вызывается метод напрямую, без ожидания cron."
},
{
"id": "LDD-AUDIT-005",
"title": "Keyset-пагинация аудита",
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §2.7",
"claim": "Audit API принимает непрозрачный cursor (occurredAt|id), отдаёт строго следующие записи без пересечений и сохраняет фильтр по action; битый курсор — 400; UI ведёт «Показать ещё» по курсору.",
"oracle": "Страницы AuditIntegrationTest и cursor-ссылка шаблона",
"evidence": [
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor pagination walks all events newest first()",
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor pagination respects the action filter()",
"ru.ysu.lms.identity.audit.AuditIntegrationTest#malformed audit cursor is rejected with bad request()",
"ru.ysu.lms.identity.audit.AuditIntegrationTest#cursor from the newest event returns an empty page()",
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#cursor mode links next page with cursor and resets to start()"
],
"limitations": "Offset page остаётся для обратной совместимости; keyset-курсор не подписан, но не содержит секретов."
},
{
"id": "LDD-PERF-002",
"title": "Пакетные детали лабораторных без N+1",
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §2.7",
"claim": "Academic отдаёт все лабораторные курса с уровнями и вариантами одним запросом /api/v1/labs/details; progress делает ровно один HTTP-вызов вместо запроса на каждую лабораторную.",
"oracle": "LabDetailsIntegrationTest и счётчик запросов AcademicClientBatchLabsTest",
"evidence": [
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#teacher gets all course labs with levels and variants in one call()",
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#student of the course receives lab details()",
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#foreign teacher is forbidden for lab details()",
"ru.ysu.lms.progress.AcademicClientBatchLabsTest#getCourseLabs loads all labs with levels and variants in one request()"
],
"limitations": "Ведомость всё ещё запрашивает сдачи по выбранной лабораторной; это не N+1 по студентам."
},
{
"id": "LDD-ASSET-003",
"title": "Админская инвентаризация legacy-активов",
"criticality": "P1",
"status": "active",
"requirement": "docs/15-stage5-course-materials.md",
"claim": "Админ открывает /admin/legacy-assets, видит список файлов без статьи, ссылку на файл и пустое состояние; страница не показывает ошибку при пустом списке.",
"oracle": "LegacyAssetsTemplateTest и HTTP-страница",
"evidence": [
"ru.ysu.lms.web.admin.LegacyAssetsTemplateTest#renders orphan assets with links and admin-only explanation()",
"ru.ysu.lms.web.admin.LegacyAssetsTemplateTest#renders empty legacy assets state()"
],
"limitations": "Удаление файлов остаётся ручной операцией на стороне academic-service; UI только инвентаризирует."
},
{
"id": "LDD-AUTH-008",
"title": "Refresh/logout через реальный gateway",
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §2.3",
"claim": "Через gateway refresh без access-токена выдаёт новую пару, новый access работает, logout отзывает refresh, повторный refresh — 401.",
"oracle": "runtime.auth_refresh",
"runtime": [
"runtime.auth_refresh"
],
"limitations": "Проверено на одноразовом стеке; TTL не истекает в рамках smoke."
},
{
"id": "LDD-ACCESS-010",
"title": "Черновики лабораторных скрыты от студентов",
"criticality": "P0",
"status": "active",
"requirement": "docs/07-groups-courses.md, §5",
"claim": "Студент не видит черновик лабораторной ни в списке, ни в пакетных деталях, ни по прямому UUID; преподаватель и админ видят черновик.",
"oracle": "HTTP 200/404 и фильтрация списков",
"evidence": [
"ru.ysu.lms.academic.lab.LabDetailsIntegrationTest#student sees published labs but not drafts in list details or by id()",
"ru.ysu.lms.academic.course.CourseAccessServiceTest#student cannot access a draft lab of own course()"
],
"limitations": "Проверены academic API и список; отдельный тест прогресса на сдачу черновика не добавлялся, так как academic отдаёт 404."
},
{
"id": "LDD-LIFE-004",
"title": "Академический отпуск: вход только для чтения",
"criticality": "P0",
"status": "active",
"requirement": "docs/07-groups-courses.md, §0.3",
"claim": "Студент в ACADEMIC_LEAVE может войти и читать, но не может сдавать лабораторные и начинать/завершать защиту.",
"oracle": "readOnly в login/me, ForbiddenException на submit/start/finish",
"evidence": [
"ru.ysu.lms.identity.roster.StudentLifecycleIntegrationTest#academic leave login is read only()",
"ru.ysu.lms.progress.SubmissionServiceTest#academic leave student cannot submit()",
"ru.ysu.lms.progress.DefenseServiceAcademicLeaveTest#academic leave student cannot start or finish defense()"
],
"limitations": "Запрет проверяется на progress-service; UI скрывает кнопки, но не является единственной защитой."
},
{
"id": "LDD-LIFE-005",
"title": "Действие «Убрать из группы» удалено",
"criticality": "P1",
"status": "active",
"requirement": "docs/14-stage4-report.md, §3",
"claim": "В админском UI и API больше нет отдельного действия unenroll; доступны только перевод, академ, отчисление, архив и восстановление.",
"oracle": "Отсутствие маршрута/кнопки/метода и отрицательные проверки шаблона",
"evidence": [
"ru.ysu.lms.web.admin.AdminStage4TemplateTest#renders group lifecycle buttons by student status()",
"ru.ysu.lms.web.admin.TeacherGradebookTemplateTest#teacher gradebook has no admin-only controls()"
],
"limitations": "Удалённый API-маршрут не проверяется отдельным HTTP-тестом; отсутствие метода компилятор проверяет по коду."
},
{
"id": "LDD-IMPORT-002",
"title": "Импорт не воскрешает неактивных студентов без флага",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §9",
"claim": "Повторный импорт сохраняет EXPELLED/GRADUATED/ARCHIVED/ACADEMIC_LEAVE без явного флага restoreInactiveStatuses и сообщает строку в skipped; с флагом активирует и зачисляет.",
"oracle": "Статус/зачисление/ImportResult.skipped",
"evidence": [
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import does not resurrect expelled student without explicit flag()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import restores expelled student with explicit flag and records import history()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import preserves academic leave without explicit flag()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import still updates an active student()",
"ru.ysu.lms.web.admin.AdminStage4TemplateTest#group import form requires explicit restoration flag()",
"ru.ysu.lms.web.admin.AdminStage4TemplateTest#group import result shows skipped inactive rows()"
],
"limitations": "Флаг восстанавливает статус студента; архивный аккаунт может потребовать отдельного lifecycle-восстановления."
},
{
"id": "LDD-CONTENT-003",
"title": "Глобальные материалы STUDENTS/TEACHERS",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §6",
"claim": "Аноним видит только ALL; студент — ALL+STUDENTS; преподаватель — ALL+TEACHERS; прямой доступ к чужой аудитории даёт 404; админ может создать/сменить аудиторию.",
"oracle": "Фильтрация дерева/статей по JWT-роли и authoring audience",
"evidence": [
"ru.ysu.lms.academic.content.GlobalAudienceAccessIntegrationTest#global tree is filtered by role audience()",
"ru.ysu.lms.academic.content.GlobalAudienceAccessIntegrationTest#global article access follows audience and role()",
"ru.ysu.lms.academic.content.GlobalAudienceAccessIntegrationTest#authoring creates requested audience and rejects invalid()",
"ru.ysu.lms.academic.content.GlobalAudienceAccessIntegrationTest#changing audience immediately changes access()",
"ru.ysu.lms.academic.content.GlobalAudienceAccessIntegrationTest#course chapter never gets audience and cannot change it()"
],
"limitations": "Админ видит все аудитории через админское дерево; публичное дерево с админским JWT также не скрывает аудитории."
},
{
"id": "LDD-IMPORT-003",
"title": "Ошибки импорта報告 по строкам до записи",
"criticality": "P1",
"status": "active",
"requirement": "docs/07-groups-courses.md, §9",
"claim": "Занятый логин и неактивная целевая группа возвращают applied=false с номером строки и не создают частичных студентов/зачислений.",
"oracle": "ImportResult.errors[].row и отсутствие записей после ошибки",
"evidence": [
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import reports occupied login with row and writes nothing()",
"ru.ysu.lms.identity.roster.GroupServiceIntegrationTest#import reports inactive target group and writes nothing()"
],
"limitations": "Неожиданные ошибки БД вне известных preflight-проверок всё ещё откатывают транзакцию без построчного отчёта."
}
]
}