Сводный отчёт systemic-2026-05-26.md + зелёные прогоны всех сценариев (82 шага, 0 падений). За сессию исправлено: refresh-rotation (TokenId + zero reuse-leeway), сериализуемое проведение приёмки против lost update, MoySklad BaseUrl в конфиг. Покрыты впервые: конкурентность приёмок, дашбордная выручка, импорт MoySklad (идемпотентность/маппинг). Зафиксированы gap'ы по нереализованным отчётам (профит/ABC/экспорт, ТЗ 2.12). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.3 KiB
Системное тестирование Food Market — 2026-05-26
Инициировано Opus 4.7 по плану из
docs/TZ-тестирование.md(продолжение сессии 2026-05-23, см.systemic-2026-05-23.md). Среда: dockerfood-market-postgres(postgres:16-alpine, 127.0.0.1:5434) + dotnet 8 API локально на :5081 + E2E через axios/psql. Запуск:E2E_ADMIN_URL=http://127.0.0.1:5081 ./tests/e2e/run.sh <scenario> --api-only.
0. TL;DR
| Сценарий | Результат |
|---|---|
| full-cycle (signup → bootstrap → supply → sale) | 12/12 ✓ |
| multi-tenant-isolation (Alpha/Beta + SuperAdmin override) | 12/12 ✓ |
| documents-edge (защита денег и инварианта на posting) | 10/10 ✓ |
| auth-edge (refresh-rotation, подделка JWT, архив-орг, signup) | 10/10 ✓ |
| catalog-edge (валидация, дубли, удаление с зависимостями) | 12/12 ✓ |
| stock-invariant-deep (Stock == Σ Movement, post/unpost/repost) | 10/10 ✓ |
| stock-concurrency (конкурентное проведение приёмок) | 4/4 ✓ |
| reports-stats (дашбордная выручка + tenant-изоляция) | 5/5 ✓ |
| moysklad-import (импорт, идемпотентность, маппинг) | 7/7 ✓ |
Итого 9 сценариев, 82 шага — все зелёные. Багов нет.
Найдено и исправлено в этой сессии: 3 бага (1 critical, 1 high, + связка из 2 правок по безопасности refresh-токенов).
1. Найденные баги и исправления
BUG #1 — Старый refresh-token остаётся валидным после ротации (commit 32729e7)
auth-edge step03. Две причины, обе закрыты:
AuthorizationController.Exchange(refresh-ветка) строил новый principal с нуля и прокидывал толькоAuthorizationId, но неTokenId. Handler OpenIddictRedeemTokenEntryчитаетTokenIdиз подписываемого principal — без него старый refresh не помечалсяRedeemed.- Даже после починки редемпшна OpenIddict по умолчанию даёт 30-секундный reuse-leeway — погашенный refresh ещё принимается в этом окне. Для розничной админки это дыра: утёкший refresh живёт 30с после ротации.
Severity: high (одна утечка refresh → продлеваемый доступ).
Fix: прокидываем TokenId старого refresh в новый principal + SetRefreshTokenReuseLeeway(TimeSpan.Zero) в Program.cs. Проверено в БД: старый токен переходит в redeemed и немедленно отвергается (4xx).
BUG #2 — Конкурентное проведение приёмки ломает инвариант остатков (commit 15f27fd)
stock-concurrency step03. Supply.Post шёл на дефолтной изоляции (Read Committed), а StockService.ApplyMovementAsync делает read-modify-write по Stock.Quantity без RowVersion. Под гонкой:
- двойное проведение ОДНОЙ приёмки (оба запроса читают
Status=Draftдо коммита соседа) применяло остаток дважды — 2StockMovement, ноStockрос на одну партию →Stock=32,Σ Movement=39; - две разные приёмки одного товара могли потерять обновление остатка и посчитать скользящее среднее
Costот устаревшегоcurrentQty.
Severity: critical (нарушение главного учётного инварианта Stock == Σ StockMovement).
Fix: проведение переведено на IsolationLevel.Serializable (как RetailSale.Post), конфликт сериализации (SQLSTATE 40001/40P01) перехватывается → 409 (клиент повторяет, а не получает 500). После фикса: Stock=32, Σ=32, statuses 204+409.
Доработка для тестируемости — базовый URL MoySklad из конфига (commit e78e921)
MoySkladClient.BaseUrl был константой api.moysklad.ru, импорт нельзя было прогнать без боевого токена. Вынесли BaseAddress в MoySklad:BaseUrl (дефолт — прежний боевой URL); e2e наводит клиент на mock-сервер lib/moysklad-mock.ts. Прод-поведение не меняется.
2. Что покрыто впервые в этой сессии
- Конкурентность приёмок (
stock-concurrency) — раньше под Serializable был толькоRetailSale.Post; теперь иSupply.Post. - Дашбордная выручка (
reports-stats) — только Posted-чеки, непрерывная серия по дням, параметрdays, строгая tenant-изоляция/stats. - Импорт MoySklad (
moysklad-import) — сохранение/маскирование токена, test-connection, фоновый job, идемпотентность повторного импорта (overwrite=false → Skipped), обновление по ключу (overwrite=true → Updated), маппинг полей в БД (BIN/тип/адрес контрагента; артикул/НДС/упаковка/цена/штрихкод/группа/страна товара) — поля сверены сMoySkladDtos/remap 1.2.
3. Logic gaps (не баги — нереализованный функционал по ТЗ 2.12)
- Отчёт «прибыль» (выручка − себестоимость) не реализован:
RetailSaleLineне хранит снимок себестоимости,/statsотдаёт только валовую выручку. - ABC-анализ, «остатки на дату» (
SUM(Movement) до даты), экспорт CSV/XLSX — отдельногоReportsControllerнет. Supply.Unpostиспользует те же read-modify-write поStockбез транзакции — под одновременным unpost теоретически уязвим к lost update (вне фокуса этой сессии; проведениеPostзакрыто).
4. Замечание по окружению
- На dev-vm установлен только SDK 8.0.126; в
global.jsonрепозитория остаётся8.0.417. Локальный даунгрейдglobal.jsonиспользован только для сборки и в коммиты не включён. admin.food-market.kz— отдельный деплой с другой БД; e2e обязательно гонять против локального API, подключённого к контейнеруfood-market-postgres(иначе DB-проверки черезdocker execнекогерентны).