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

14 KiB
Raw Blame History

Системное тестирование 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. Коммиты этой сессии

  1. a06464bfix(migrations): чиним P0-блокеры разворачивания на чистой БД (8 файлов, 7 миграционных багов).
  2. ab5c4c9fix(security): SuperAdmin edit-mode override обходит [Authorize(Roles=Admin)] (новый ClaimsTransformer).
  3. ae88a16test(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