Итоги сессии системного тестирования: - 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>
14 KiB
Системное тестирование 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. Команды воспроизведения
# 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. Коммиты этой сессии
a06464b—fix(migrations): чиним P0-блокеры разворачивания на чистой БД(8 файлов, 7 миграционных багов).ab5c4c9—fix(security): SuperAdmin edit-mode override обходит [Authorize(Roles=Admin)](новый ClaimsTransformer).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 |