- preflight занятого логина и неактивной целевой группы - applied=false с номером строки, без частичных студентов - LDD-IMPORT-003; baseline 58 claims / 368 тестов
859 lines
60 KiB
JSON
859 lines
60 KiB
JSON
{
|
||
"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-проверок всё ещё откатывают транзакцию без построчного отчёта."
|
||
}
|
||
]
|
||
} |