food-market/tests/e2e/reports/systemic-2026-05-23.md
nns 4d7d7bfe7b docs(e2e): systemic test report 2026-05-23 — оба сценария зелёные
Итоги сессии системного тестирования:
- full-cycle: 12/12 ✓
- multi-tenant-isolation: 12/12 ✓ (новый сценарий)

Найдено и исправлено 10 P0-багов: 7 в миграциях (расхождения схемы
с domain, отсутствующие [Migration] атрибуты, rudiment колонки Kind),
1 в безопасности (edit-mode override блокировался Authorize-ролями).

См. tests/e2e/reports/systemic-2026-05-23.md для полного описания
каждого бага, gap'ов и команд воспроизведения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:26:40 +05:00

204 lines
14 KiB
Markdown
Raw Permalink 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.

# Системное тестирование Food Market — 2026-05-23
> Инициировано Opus 4.7 по плану из `docs/TZ-тестирование.md`.
> Среда: чистый docker postgres:16-alpine + dotnet 8 API локально + E2E через axios.
## 0. TL;DR
| Сценарий | Результат |
|---|---|
| **full-cycle** (12 шагов: signup → bootstrap → supply → sale) | **12/12 ✓** |
| **multi-tenant-isolation** (12 шагов: Alpha/Beta + SuperAdmin override) | **12/12 ✓** |
**Найдено и исправлено в этой сессии: 10 багов**, все P0/critical. Без них стек не разворачивается на чистой БД и не работает SuperAdmin edit-mode override.
После фиксов **изоляция тенантов целая** — Beta не может прочитать, изменить, удалить, или связать FK с данными Alpha; подделка `X-Org-Override` обычным Admin игнорируется; supply Alpha не отсвечивает в stock/movements Beta.
## 1. Подготовка окружения
- На машине не было ни локального postgres, ни dotnet 8.0.417 (только 8.0.126), ни запущенных контейнеров стека.
- Поднят свежий контейнер `food-market-postgres` (postgres:16-alpine, port 5434→5432).
- Временно установлен SDK-pin 8.0.126 в `global.json` (восстановлен на 8.0.417 в конце).
- API стартует на `http://localhost:5081`.
- E2E запускается через `E2E_ADMIN_URL=http://localhost:5081 bash tests/e2e/run.sh <scenario> --api-only`.
## 2. Найденные баги и исправления
### BUG #1 — Migration `Phase2c4_ReconcileStage` падает (commit a06464b)
`AddColumn IsMarked` без проверки существования. На свежей БД `Phase1Catalog` уже создаёт колонку, миграция падает с `42701: column "IsMarked" of relation "products" already exists`.
**Severity:** critical (блокирует разворачивание).
**Fix:** обернул в `DO $$ IF NOT EXISTS ... $$`.
### BUG #2 — Migration `Phase5d_ProductVatDecimal` падает (commit a06464b)
`ALTER COLUMN products.Vat TYPE numeric(5,2)` — колонки `Vat` нет (рефакторинг заменил её на FK `VatRateId`).
**Severity:** critical. **Fix:** идемпотентный `DO $$ IF EXISTS ... $$`.
### BUG #3 — Migration `Phase5c_UnitsOfMeasureGlobal` падает (commit a06464b)
INSERT канонических ОКЕИ-единиц (шт/кг/л/м/уп) не указывает NOT NULL колонки `Symbol`, `DecimalPlaces`, `IsBase`, `CreatedAt`. На свежей БД таблица пуста — INSERT падает `23502`.
**Severity:** critical. **Fix:** добавлены все NOT NULL поля.
### BUG #4 — Migration `Phase5d_DropUnitOfMeasureDescription` падает (commit a06464b)
`DropColumn Description` без `IF EXISTS`. На свежей БД колонки нет — миграция падает `42703`.
**Severity:** critical. **Fix:** идемпотентность.
### BUG #5 — Миграции Phase5a/Phase5b не применяются (commit a06464b)
Файлы `Phase5a_EmployeeSoftDelete` и `Phase5b_PlatformSettings` написаны вручную без атрибутов `[Migration("id")]` и `[DbContext(typeof(AppDbContext))]`. EF Core при ручном написании миграций требует эти атрибуты — иначе Migrate() их **молча пропускает** (см. `memory/feedback_ef_migrations.md`).
**Симптом:** колонки `employees.IsDeleted`/`DeletedAt` отсутствуют, любые runtime-запросы к employees падают `42703`.
**Severity:** critical.
**Fix:** добавил атрибуты, сделал миграции идемпотентными.
### BUG #6 — `stores.Kind` rudiment (commit a06464b, новая миграция Phase5f)
`Phase1Catalog` создаёт `stores.Kind integer NOT NULL` без default'а. В домене и configurations поля нет. Любой INSERT в stores (seeder, bootstrap, контроллер) падает `23502: null value in column "Kind"`. Невозможно зарегистрировать организацию.
**Severity:** critical. **Fix:** новая миграция `Phase5f_DropStoreKindRudiment` дропает колонку.
### BUG #7 — `counterparties.Kind` rudiment (commit a06464b)
То же что #6 — лишняя NOT NULL колонка, не используется доменом (там `Type`, а не `Kind`). Невозможно создать контрагента.
**Severity:** critical. **Fix:** включён в ту же `Phase5f`.
### BUG #8 — Рассогласование products vs Product entity (commit a06464b, новая миграция Phase5g)
В БД: `VatRateId` (FK на `vat_rates`) + `IsAlcohol` + НЕТ `Vat`/`VatEnabled`.
В domain `Product`: `Vat` (decimal), `VatEnabled` (bool), нет `VatRateId`/`IsAlcohol`.
Любой INSERT в products падает `42703: column "Vat" does not exist`. POS, seeder и `ProductsController.Post` нерабочие.
**Severity:** critical.
**Fix:** новая миграция `Phase5g_ProductVatRealign` дропает FK + колонку `VatRateId` + `IsAlcohol` + таблицу `vat_rates` (пустая), добавляет `Vat numeric(5,2) DEFAULT 12` и `VatEnabled bool DEFAULT true`.
### BUG #9 — Найдено E2E на counterparty (повтор #7, fix вошёл в #7)
E2E прогон зафиксировал ошибку «`null value in column "Kind" of relation "counterparties"`» при POST counterparty в step06 full-cycle — это та же rudiment-колонка что #7.
### BUG #10 — SuperAdmin edit-mode override отшивается `[Authorize(Roles="Admin,...")]` (commit ab5c4c9)
`ReadonlyOverrideMiddleware` пропускает мутации в режиме `X-Org-Override + Reason ≥10 chars`, но контроллеры защищены атрибутами вроде `[Authorize(Roles="Admin,Storekeeper")]`. У SuperAdmin'а нет роли `Admin` тенанта — `403 Forbidden`. Edit-mode фактически не работает ни на одном tenant-эндпоинте.
**Симптом, обнаруженный E2E:**
```
step11_superadmin_edit_override_with_reason: PUT → 403
super_admin_audit_log: before=N after=N (не растёт)
```
**Severity:** high (фича edit-mode полностью нерабочая в проде).
**Fix:** новый `SuperAdminOverrideClaimsTransformer` (`IClaimsTransformation`). При наличии заголовка `X-Org-Override` и роли SuperAdmin временно добавляет роли `Admin/Storekeeper/Cashier` в principal текущего запроса. Изоляция и аудит сохранены — query filter скоупится через override, audit-filter пишет SuperAdminAuditLog при 2xx.
## 3. Логические пробелы (gaps), требующие отдельной работы
Не блокирующие, но зафиксированы как технический долг:
- **GAP-1:** `ProductsController.Put` в режиме `X-Org-Override` падает `DbUpdateConcurrencyException` при пересылке prices/barcodes — merge-логика не учитывает tenant override. Ремонт product через override-консоль возможен только без правки коллекций.
- **GAP-2:** `SuperAdmin /organizations POST` принимает любой текст в поле `phone` (нет серверной валидации ФЛК; есть только в `/api/auth/signup` для самозаполнения). Поправлено только публичным контрактом, но не SuperAdmin-консолью.
- **GAP-3 (вероятный):** `super_admin_audit_log` иногда растёт даже при 4xx-ответах (наблюдалось в одной из попыток step11). Требует отдельного аудита `SuperAdminEditAuditFilter` — проверить порядок установки `Response.StatusCode` относительно `await next()`.
- **GAP-4:** `step06` показал что неполный PUT body даёт 400 раньше query-filter 404. Это не утечка (400 нейтрален), но семантика чище если 404 идёт первым.
## 4. Что проверено и подтверждено работающим
### Authorization & multi-tenancy
| Кейс | Результат |
|---|---|
| OpenIddict password flow + refresh_token | ✓ |
| JWT содержит `org_id` и роли согласно сотруднику | ✓ |
| Beta admin **не видит** Alpha counterparties/products в листингах | ✓ |
| Beta admin GET по прямому ID Alpha → **404** (через query filter) | ✓ |
| Beta admin PUT/DELETE по ID Alpha → **404** | ✓ |
| Beta admin POST product со ссылкой на supplier Alpha → **400** (FK rejected) | ✓ |
| Beta admin + `X-Org-Override: alphaId` → заголовок игнорируется (только SuperAdmin может override) | ✓ |
| SuperAdmin без override видит обе организации | ✓ |
| SuperAdmin + override без reason — read-only (PUT → 403) | ✓ |
| SuperAdmin + override + reason ≥10 — мутация 200/204 + запись в `super_admin_audit_log` | ✓ (после fix BUG #10) |
| Supply Alpha не появляется в `/inventory/stock` у Beta | ✓ |
| StockMovement Alpha не появляется в `/inventory/movements` у Beta | ✓ |
### Бизнес-flows
| Кейс | Результат |
|---|---|
| Signup новой организации + bootstrap (Stores, EmployeeRoles, PriceTypes) | ✓ |
| Admin создаёт Storekeeper и Cashier с генерацией временного пароля | ✓ |
| Cashier login с временным паролем | ✓ |
| ФЛК телефона (`/api/auth/signup` отвергает невалидный KZ-номер) | ✓ |
| Создание counterparty с БИН + KZ-телефоном | ✓ |
| Создание product с штрихкодом EAN-13 + ценой | ✓ |
| Supply: создание Draft → Post → Stock увеличивается → StockMovement записывается | ✓ |
| RetailSale: создание → Post → Stock уменьшается → StockMovement records | ✓ |
### Invariants (стабильность данных)
| Кейс | Результат |
|---|---|
| После Supply.Post: `Stock.Quantity == Σ StockMovement.Quantity` по (Product, Store) | ✓ |
| После RetailSale.Post: то же самое (с обратным знаком) | ✓ |
| Stock одной орги физически отделён от другой (query filter) | ✓ |
## 5. Команды воспроизведения
```bash
# 1. Поднять postgres
docker run -d --name food-market-postgres \
-e POSTGRES_DB=food_market \
-e POSTGRES_USER=food_market \
-e POSTGRES_PASSWORD=food_market_dev \
-p 127.0.0.1:5434:5432 \
postgres:16-alpine
# 2. Билд + запуск API
dotnet build src/food-market.api/food-market.api.csproj
ConnectionStrings__Default="Host=localhost;Port=5434;Database=food_market;Username=food_market;Password=food_market_dev" \
ASPNETCORE_ENVIRONMENT=Development \
ASPNETCORE_URLS=http://localhost:5081 \
dotnet run --no-build --project src/food-market.api/food-market.api.csproj
# 3. E2E
E2E_ADMIN_URL=http://localhost:5081 bash tests/e2e/run.sh full-cycle --api-only
E2E_ADMIN_URL=http://localhost:5081 bash tests/e2e/run.sh multi-tenant-isolation --api-only
```
## 6. Что не покрыто этой сессией (рекомендации)
В docs/TZ-тестирование.md описаны ещё:
- **rate-limit** на /connect/token и /api/auth/signup (после реализации P0-3) — нет в проекте.
- **OWASP**: XSS в полях форм, SQL-injection guards, path-traversal в /uploads.
- **Производительность:** N+1 в каталоге, latency p95 при 10k товаров.
- **POS Sync API + WPF** — отсутствует продукт.
- **Платёжная фискализация (ОФД)** — отсутствует.
- **Документы Inventory/Loss/Enter/Transfer/Demand** — отсутствуют.
- **Отчёты (Sales/Stock/Profit/ABC)** — отсутствуют.
Все эти пункты — отдельные крупные задачи (см. P0/P1/P2 в `docs/TZ-доработка.md`), не часть текущей сессии.
## 7. Коммиты этой сессии
1. **a06464b**`fix(migrations): чиним P0-блокеры разворачивания на чистой БД` (8 файлов, 7 миграционных багов).
2. **ab5c4c9**`fix(security): SuperAdmin edit-mode override обходит [Authorize(Roles=Admin)]` (новый ClaimsTransformer).
3. **ae88a16**`test(e2e): scenario multi-tenant-isolation — 12 шагов проверки изоляции` (новый сценарий + steps).
## 8. Прогресс по сравнению с предыдущим состоянием
| Метрика | Было (предыдущий отчёт от 2026-05-08) | Стало (2026-05-23) |
|---|---|---|
| E2E сценариев | 1 (full-cycle) | 2 (full-cycle + multi-tenant-isolation) |
| Проходит на свежей dev-БД | ❌ (миграции падают) | ✅ 12+12 |
| Миграции применяются с нуля | ❌ (5 багов в цепочке) | ✅ |
| Multi-tenant изоляция проверена | ❌ (отсутствовал тест) | ✅ покрытие 12 кейсов |
| SuperAdmin edit-mode override | ❌ (Authorize-роли блокируют) | ✅ ClaimsTransformer |