docs(e2e): итоговый отчёт 2026-05-26 — 15 сценариев зелёные (124 шага)

Полная регрессия всех сценариев + 6 новых областей этой сессии (employees,
roles, superadmin-console, platform-smtp, auth-password, security-edge).
За день исправлено 4 бага: уволенный сотрудник логинится (P0), конкурентное
проведение приёмки ломает инвариант (critical), refresh не гасится после
ротации (high), change-owner принимал короткий reason (medium). Нереализованный
по ТЗ функционал (отчёты/склад-документы/POS/permission-authz/login-ratelimit)
зафиксирован как Logic gaps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-26 12:05:23 +05:00
parent 888c8c28f0
commit 6098c03e1a
16 changed files with 678 additions and 175 deletions

View file

@ -1,13 +1,13 @@
# E2E report: auth-edge
Запущен: 2026-05-26T06:28:24.035Z
Запущен: 2026-05-26T07:02:20.238Z
Длительность: 3.9с
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
## ✓ Step step01_bootstrap_admin: SuperAdmin создаёт орг + админа, получаем access+refresh
Длительность: 1083мс
Длительность: 1168мс
| Тип | Проверка | Результат |
|---|---|---|
@ -16,7 +16,7 @@
## ✓ Step step02_refresh_token_works: Refresh: старый access обменивается на новые access+refresh
Длительность: 348мс
Длительность: 331мс
| Тип | Проверка | Результат |
|---|---|---|
@ -27,7 +27,7 @@
## ✓ Step step03_refresh_token_rotates: После refresh — старый refresh-token больше не работает (rotation)
Длительность: 124мс
Длительность: 127мс
| Тип | Проверка | Результат |
|---|---|---|
@ -35,7 +35,7 @@
## ✓ Step step04_invalid_refresh_rejected: Невалидный refresh-token возвращает 400 invalid_grant
Длительность: 81мс
Длительность: 79мс
| Тип | Проверка | Результат |
|---|---|---|
@ -43,7 +43,7 @@
## ✓ Step step05_tampered_jwt_rejected: JWT с подделанным org_id (изменён без переподписи) отшивается 401
Длительность: 19мс
Длительность: 24мс
| Тип | Проверка | Результат |
|---|---|---|
@ -59,7 +59,7 @@
## ✓ Step step07_deactivated_user_blocked: Деактивация User.IsActive=false: повторный login и refresh возвращают 400
Длительность: 520мс
Длительность: 528мс
| Тип | Проверка | Результат |
|---|---|---|
@ -68,7 +68,7 @@
## ✓ Step step08_archived_org_blocks_login: Архивная организация: login существующего админа возвращает 400 invalid_grant
Длительность: 391мс
Длительность: 373мс
| Тип | Проверка | Результат |
|---|---|---|
@ -77,7 +77,7 @@
## ✓ Step step09_duplicate_signup_blocked: Повторный signup с тем же email живой орги отвергается 400
Длительность: 4с
Длительность: 3с
| Тип | Проверка | Результат |
|---|---|---|
@ -85,12 +85,12 @@
## ✓ Step step10_orphan_signup_reactivates: Signup с email orphan-юзера (его org удалена) — реактивирует с новой org
Длительность: 1273мс
Длительность: 1179мс
| Тип | Проверка | Результат |
|---|---|---|
| api | First signup → 200/201 | ✓ status=200 {"organizationId":"55892550-bc32-4284-8841-643b709a7b62","email":"orphan-1779776904034@example.kz"} |
| api | Re-signup orphan email → 200/201 (реактивация) | ✓ status=200 {"organizationId":"4a4b198c-0441-4576-a013-bfb583c5d95d","email":"orphan-1779776904034@example.kz"} |
| api | First signup → 200/201 | ✓ status=200 {"organizationId":"c41c2728-ce20-4205-840e-d465bbee537e","email":"orphan-1779778940238@example.kz"} |
| api | Re-signup orphan email → 200/201 (реактивация) | ✓ status=200 {"organizationId":"14bd0fa0-fcc2-404a-948b-b6980972ffa3","email":"orphan-1779778940238@example.kz"} |
| api | Login после реактивации → 200 | ✓ status=200 |
## Summary

View file

@ -0,0 +1,65 @@
# E2E report: auth-password
Запущен: 2026-05-26T07:02:27.683Z
Длительность: 1.0с
**Итог:** 6 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 6)
## ✓ Step step01_bootstrap: Создать орг → известный email активного пользователя
Длительность: 804мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг + известный email готовы | ✓ pwd-1779778947683@example.kz |
## ✓ Step step02_forgot_unknown_200: forgot-password несуществующего email → 200 (анти-энумерация)
Длительность: 41мс
| Тип | Проверка | Результат |
|---|---|---|
| api | forgot несуществующего → 200 (анти-энумерация) | ✓ status=200 |
## ✓ Step step03_forgot_known_200: forgot-password существующего email → 200
Длительность: 70мс
| Тип | Проверка | Результат |
|---|---|---|
| api | forgot существующего → 200 | ✓ status=200 |
## ✓ Step step04_reset_bad_token: reset-password с битым токеном → 400
Длительность: 36мс
| Тип | Проверка | Результат |
|---|---|---|
| api | reset с битым токеном → 400 | ✓ status=400 |
## ✓ Step step05_reset_short_password: reset-password со слишком коротким паролем → 400
Длительность: 11мс
| Тип | Проверка | Результат |
|---|---|---|
| api | reset со слишком коротким паролем → 400 | ✓ status=400 |
## ✓ Step step06_forgot_rate_limited: Серия forgot с одного IP → появляется 429 (рейт-лимит)
Длительность: 46мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Серия forgot упирается в 429 (рейт-лимит) | ✓ statuses=200,429,429,429,429,429 |
## Summary
- Passed: 6
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.

View file

@ -1,13 +1,13 @@
# E2E report: catalog-edge
Запущен: 2026-05-26T06:28:32.030Z
Длительность: 1.6с
Запущен: 2026-05-26T07:02:32.669Z
Длительность: 1.7с
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
## ✓ Step step01_bootstrap: Орг + admin + lookups
Длительность: 1128мс
Длительность: 1251мс
| Тип | Проверка | Результат |
|---|---|---|
@ -15,7 +15,7 @@
## ✓ Step step02_empty_product_name_rejected: POST product с пустым name → 400
Длительность: 10мс
Длительность: 12мс
| Тип | Проверка | Результат |
|---|---|---|
@ -23,7 +23,7 @@
## ✓ Step step03_negative_price_rejected: POST product с отрицательной ценой amount=-100 → 400
Длительность: 8мс
Длительность: 12мс
| Тип | Проверка | Результат |
|---|---|---|
@ -31,7 +31,7 @@
## ✓ Step step04_oversized_name_truncated_or_rejected: POST product с name > 500 символов → 400 (превышение maxLength)
Длительность: 10мс
Длительность: 9мс
| Тип | Проверка | Результат |
|---|---|---|
@ -39,7 +39,7 @@
## ✓ Step step05_duplicate_product_article: POST второго product с тем же article → 4xx (если уникальный) или OK + проверка БД
Длительность: 77мс
Длительность: 76мс
| Тип | Проверка | Результат |
|---|---|---|
@ -48,7 +48,7 @@
## ✓ Step step06_self_parent_group_rejected: POST product-group с parentId=собственный id (цикл) → 400
Длительность: 48мс
Длительность: 49мс
| Тип | Проверка | Результат |
|---|---|---|
@ -56,7 +56,7 @@
## ✓ Step step07_delete_group_with_children: DELETE group у которой есть подгруппы → 409
Длительность: 73мс
Длительность: 68мс
| Тип | Проверка | Результат |
|---|---|---|
@ -64,7 +64,7 @@
## ✓ Step step08_delete_group_with_products: DELETE group в которой есть продукты → 409
Длительность: 10мс
Длительность: 11мс
| Тип | Проверка | Результат |
|---|---|---|
@ -72,7 +72,7 @@
## ✓ Step step09_delete_unit_with_products: DELETE enable у unit, на которую ссылаются продукты → 409
Длительность: 29мс
Длительность: 36мс
| Тип | Проверка | Результат |
|---|---|---|
@ -80,7 +80,7 @@
## ✓ Step step10_delete_system_price_type: DELETE PriceType.IsSystem=true → 409
Длительность: 3с
Длительность: 4с
| Тип | Проверка | Результат |
|---|---|---|
@ -88,7 +88,7 @@
## ✓ Step step11_second_retail_price_type: POST PriceType с IsRetail=true когда уже есть Retail → 409
Длительность: 52мс
Длительность: 47мс
| Тип | Проверка | Результат |
|---|---|---|
@ -97,7 +97,7 @@
## ✓ Step step12_delete_counterparty_with_supply: DELETE counterparty который использован в Supply → 409
Длительность: 109мс
Длительность: 104мс
| Тип | Проверка | Результат |
|---|---|---|

View file

@ -1,24 +1,24 @@
# E2E report: documents-edge
Запущен: 2026-05-26T06:28:16.561Z
Длительность: 2.9с
Запущен: 2026-05-26T07:02:13.560Z
Длительность: 3.1с
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
## ✓ Step step01_bootstrap: SuperAdmin создаёт орг Test + admin, делаем product + supply (10 шт по 100 KZT)
Длительность: 1265мс
Длительность: 1211мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг + админ созданы | ✓ org=cdbad68a-b0fd-47ea-85dc-32272d39c2d8 |
| api | Орг + админ созданы | ✓ org=ddb8db28-039e-4f65-8f0d-62da5ce9c431 |
| api | Counterparty создан | ✓ |
| api | Product создан | ✓ 08c989ff-3ad7-4e14-beef-e6e744959e07 |
| api | Supply Draft создана | ✓ 0c635259-abec-40fa-8087-a73054d8009a |
| api | Product создан | ✓ 6d1eee5c-06aa-48a9-8690-b7dfbb7b6c3f |
| api | Supply Draft создана | ✓ e143e4b0-1278-41af-9f25-7a68630727b7 |
## ✓ Step step02_post_supply_stock_10: Supply провести: stock=10, ReferencePrice=100, Cost=100
Длительность: 88мс
Длительность: 86мс
| Тип | Проверка | Результат |
|---|---|---|
@ -27,16 +27,16 @@
## ✓ Step step03_oversell_blocked: RetailSale qty=15 (больше остатка 10), POST /post возвращает 409
Длительность: 122мс
Длительность: 96мс
| Тип | Проверка | Результат |
|---|---|---|
| api | POST RetailSale Draft (qty=15) | ✓ actual=201 {"id":"85112d55-fd90-4a7d-9cb9-c4cb4509d3d3","number":"ПР-2026-000001","date":"2026-05-26T06:28:19.547Z","status":0,"sto |
| api | POST /post → 409 (oversell) | ✓ actual=409 {"error":"Недостаточно остатка для проведения чека.","lines":[{"productId":"08c989ff-3ad7-4e14-beef-e6e744959e07","produ |
| api | POST RetailSale Draft (qty=15) | ✓ actual=201 {"id":"5d3aad81-df91-459b-8a1c-bab9d7f1cc64","number":"ПР-2026-000001","date":"2026-05-26T07:02:16.056Z","status":0,"sto |
| api | POST /post → 409 (oversell) | ✓ actual=409 {"error":"Недостаточно остатка для проведения чека.","lines":[{"productId":"6d1eee5c-06aa-48a9-8690-b7dfbb7b6c3f","produ |
## ✓ Step step04_oversell_stock_unchanged: После заблокированного post stock остался 10, StockMovement не добавлен
Длительность: 260мс
Длительность: 267мс
| Тип | Проверка | Результат |
|---|---|---|
@ -45,7 +45,7 @@
## ✓ Step step05_payment_mismatch_blocked: RetailSale с PaidCash+PaidCard не равной Total отвергается на post
Длительность: 4с
Длительность: 5с
| Тип | Проверка | Результат |
|---|---|---|
@ -53,7 +53,7 @@
## ✓ Step step06_edit_posted_supply_blocked: PUT проведённой Supply (Posted) возвращает 409
Длительность: 49мс
Длительность: 65мс
| Тип | Проверка | Результат |
|---|---|---|
@ -61,7 +61,7 @@
## ✓ Step step07_delete_posted_supply_blocked: DELETE проведённой Supply возвращает 409
Длительность: 26мс
Длительность: 34мс
| Тип | Проверка | Результат |
|---|---|---|
@ -69,12 +69,12 @@
## ✓ Step step08_unpost_negative_blocked: После Sale qty=5 unpost Supply qty=10 возвращает 409 (stock минус)
Длительность: 98мс
Длительность: 153мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Sale qty=5 проведён | ✓ actual=204 |
| api | Unpost Supply при stock<unpost-qty 409 | actual=409 {"error":"Нельзя отменить проведение: остаток уйдёт в минус (часть товара уже расходована).","lines":[{"productId":"08c9 |
| api | Unpost Supply при stock<unpost-qty 409 | actual=409 {"error":"Нельзя отменить проведение: остаток уйдёт в минус (часть товара уже расходована).","lines":[{"productId":"6d1e |
## ✓ Step step09_barcode_unique_within_org: Дубль штрихкода в одной орге, POST второго product отвергается
@ -82,15 +82,15 @@
| Тип | Проверка | Результат |
|---|---|---|
| api | POST product с тем же barcode → 4xx | ✓ actual=400 {"error":"Штрихкод 206899370undefined3 уже используется товаром «Edge Product 1779776896561»."} |
| api | POST product с тем же barcode → 4xx | ✓ actual=400 {"error":"Штрихкод 208935890undefined5 уже используется товаром «Edge Product 1779778933560»."} |
## ✓ Step step10_barcode_per_tenant: Тот же штрихкод в другой орге допустим (per-tenant unique)
Длительность: 972мс
Длительность: 1085мс
| Тип | Проверка | Результат |
|---|---|---|
| api | POST product с тем же barcode в другой орге → 201 | ✓ actual=201 {"id":"4fe7e2aa-cef3-458b-ba34-984ce7af8f2e","name":"Tenant-2 product (same barcode)","article":"1","description":null," |
| api | POST product с тем же barcode в другой орге → 201 | ✓ actual=201 {"id":"4bd355b5-11d0-4170-ab38-2ab2d885c725","name":"Tenant-2 product (same barcode)","article":"1","description":null," |
| db | В product_barcodes 2 записи с этим Code (одна на орг) | ✓ count=2 |
## Summary

View file

@ -0,0 +1,110 @@
# E2E report: employees
Запущен: 2026-05-26T07:03:19.659Z
Длительность: 3.6с
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
## ✓ Step step01_bootstrap: Орг A + логин админа + выбор не-админской роли
Длительность: 1197мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг A + не-админская роль найдены | ✓ role=Кладовщик |
## ✓ Step step02_create_without_account: Создание сотрудника без учётки (createAccount=false) → UserId=null
Длительность: 27мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Создан без учётки → 200/201 | ✓ status=200 |
| api | UserId = null | ✓ userId=null |
| api | generatedPassword отсутствует | ✓ |
## ✓ Step step03_create_with_account: Создание сотрудника с учёткой → temp password в ответе, User создан
Длительность: 351мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Создан с учёткой → 200/201 | ✓ status=200 |
| api | Temp password возвращён один раз | ✓ |
| api | UserId присвоен | ✓ userId=75ef2110-7770-4256-9b7d-33be90b832de |
## ✓ Step step04_email_required: createAccount=true без email → 400
Длительность: 13мс
| Тип | Проверка | Результат |
|---|---|---|
| api | createAccount=true без email → 400 | ✓ status=400 |
## ✓ Step step05_duplicate_email: Дубль email при createAccount → 4xx
Длительность: 17мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Дубль email → 4xx (rejected) | ✓ status=400 |
## ✓ Step step06_account_can_login: Новый сотрудник логинится временным паролем → 200
Длительность: 282мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Логин новым сотрудником → 200 | ✓ status=200 |
## ✓ Step step07_fire_blocks_login: Увольнение (DELETE) гасит логин: повторный login и refresh → 4xx
Длительность: 581мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Увольнение (DELETE) → 204 | ✓ status=204 |
| db | Employee.IsActive=false (уволен) | ✓ IsActive=f |
| db | User.IsActive=false (логин погашен) | ✓ User.IsActive=f |
| api | Повторный login уволенного → 4xx | ✓ status=400 |
| api | Refresh уволенного → 4xx | ✓ status=400 |
## ✓ Step step08_two_step_delete: Второй DELETE → soft-delete (IsDeleted), третий → 409
Длительность: 262мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Второй DELETE → 204 (soft-delete) | ✓ status=204 |
| db | Employee.IsDeleted=true | ✓ IsDeleted=t |
| api | Третий DELETE → 409 (уже удалён) | ✓ status=409 |
## ✓ Step step09_owner_self_protected: DELETE главного администратора (он же self) → 403
Длительность: 101мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Главный администратор найден в списке | ✓ owner=1c6f6673-4a67-464e-bf3e-00f71cdc8977 |
| api | DELETE главного администратора (он же self) → 403 | ✓ status=403 |
## ✓ Step step10_tenant_isolation: Админ орг A не видит и не удаляет сотрудника орг B → 404
Длительность: 769мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Сотрудник в орг B создан | ✓ |
| api | GET чужого сотрудника (орг B) из орг A → 404 | ✓ status=404 |
| api | DELETE чужого сотрудника → 404 | ✓ status=404 |
## Summary
- Passed: 10
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.

View file

@ -1,23 +1,23 @@
# E2E report: full-cycle
Запущен: 2026-05-26T06:27:56.114Z
Длительность: 8.6с
Запущен: 2026-05-26T07:01:50.662Z
Длительность: 10.6с
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
## ✓ Step step01_create_organization: SuperAdmin создаёт «Test Shop {timestamp}» (KZ, KZT, ФЛК телефона)
Длительность: 84с
Длительность: 170с
| Тип | Проверка | Результат |
|---|---|---|
| api | POST /api/super-admin/organizations → 200 | ✓ org=Test Shop 1779776876113 |
| api | POST /api/super-admin/organizations → 200 | ✓ org=Test Shop 1779778910662 |
| api | GET /api/super-admin/organizations включает созданную org | ✓ |
| api | Невалидный phone отвергается | ✓ 400 |
## ✓ Step step02_create_first_admin: SuperAdmin создаёт первого Admin сотрудника организации (Employee + AppUser)
Длительность: 603мс
Длительность: 664мс
| Тип | Проверка | Результат |
|---|---|---|
@ -27,17 +27,17 @@
## ✓ Step step03_login_as_admin: Логин под admin (не SuperAdmin override) — JWT с org_id и role=Admin
Длительность: 235мс
Длительность: 416мс
| Тип | Проверка | Результат |
|---|---|---|
| api | /connect/token password-grant выдал токен | ✓ |
| api | /api/me содержит role=Admin | ✓ Admin |
| api | /api/me содержит правильный orgId | ✓ b43f6023-c327-4ffe-8c54-606dc64ea620 |
| api | /api/me содержит правильный orgId | ✓ 5546bb9a-eb4c-4fea-b54b-059a3e65c249 |
## ✓ Step step04_create_storekeeper_and_cashier: Admin создаёт Storekeeper и Cashier через /settings/employees
Длительность: 1517мс
Длительность: 1320мс
| Тип | Проверка | Результат |
|---|---|---|
@ -51,7 +51,7 @@
## ✓ Step step05_login_as_cashier: Логин под Cashier — role-guard проверяется (sidebar/role guard)
Длительность: 61с
Длительность: 450мс
| Тип | Проверка | Результат |
|---|---|---|
@ -61,7 +61,7 @@
## ✓ Step step06_create_counterparty: Admin создаёт «ТОО Тест Поставщик» (БИН + телефон)
Длительность: 179мс
Длительность: 386мс
| Тип | Проверка | Результат |
|---|---|---|
@ -69,7 +69,7 @@
## ✓ Step step07_ensure_main_store: Проверить что есть main store (из bootstrap), иначе создать
Длительность: 62мс
Длительность: 51мс
| Тип | Проверка | Результат |
|---|---|---|
@ -78,11 +78,11 @@
## ✓ Step step08_create_supply: Admin создаёт Supply Draft (3-5 товаров) и проводит (Posted)
Длительность: 2159мс
Длительность: 282с
| Тип | Проверка | Результат |
|---|---|---|
| api | Создано 3 product (валидный barcode + price + group) | ✓ e2e Product 1 1779776876113, e2e Product 2 1779776876113, e2e Product 3 1779776876113 |
| api | Создано 3 product (валидный barcode + price + group) | ✓ e2e Product 1 1779778910662, e2e Product 2 1779778910662, e2e Product 3 1779778910662 |
| api | Supply без supplierId → 400/409 | ✓ 400 {"error":"Поле SupplierId обязательно.","field":"SupplierId"} |
| api | Supply с пустым lines[] → 400 | ✓ 400 |
| api | POST /api/purchases/supplies (Draft) | ✓ 201 |
@ -92,21 +92,21 @@
## ✓ Step step09_check_stock_after_supply: GET /api/inventory/stock — quantity увеличился на supplied amount
Длительность: 771мс
Длительность: 889мс
| Тип | Проверка | Результат |
|---|---|---|
| api | stock(e2e Product 1 1779776876113) +10 (было 0, стало 10) | ✓ delta=10, expected=10 |
| api | stock(e2e Product 2 1779776876113) +15 (было 0, стало 15) | ✓ delta=15, expected=15 |
| api | stock(e2e Product 3 1779776876113) +20 (было 0, стало 20) | ✓ delta=20, expected=20 |
| api | GET /api/inventory/stock без storeId возвращает строки на каждый склад | ✓ rows=1, stores=883b070c |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 1 1779776876113… | ✓ sum_movements=10 stocks.Quantity=10 |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 2 1779776876113… | ✓ sum_movements=15 stocks.Quantity=15 |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 3 1779776876113… | ✓ sum_movements=20 stocks.Quantity=20 |
| api | stock(e2e Product 1 1779778910662) +10 (было 0, стало 10) | ✓ delta=10, expected=10 |
| api | stock(e2e Product 2 1779778910662) +15 (было 0, стало 15) | ✓ delta=15, expected=15 |
| api | stock(e2e Product 3 1779778910662) +20 (было 0, стало 20) | ✓ delta=20, expected=20 |
| api | GET /api/inventory/stock без storeId возвращает строки на каждый склад | ✓ rows=1, stores=5e62ff98 |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 1 1779778910662… | ✓ sum_movements=10 stocks.Quantity=10 |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 2 1779778910662… | ✓ sum_movements=15 stocks.Quantity=15 |
| db | stocks.Quantity == SUM(stock_movements.Quantity) для e2e Product 3 1779778910662… | ✓ sum_movements=20 stocks.Quantity=20 |
## ✓ Step step10_ensure_retail_point: Проверить или создать розничную точку (кассу)
Длительность: 126мс
Длительность: 13с
| Тип | Проверка | Результат |
|---|---|---|
@ -115,7 +115,7 @@
## ✓ Step step11_create_retail_sale: Admin создаёт RetailSale, 2 позиции из приёмки, cash, Post
Длительность: 800мс
Длительность: 1000мс
| Тип | Проверка | Результат |
|---|---|---|
@ -128,14 +128,14 @@
## ✓ Step step12_check_stock_after_sale: GET /api/inventory/stock — quantity уменьшился на sold amount
Длительность: 694мс
Длительность: 762мс
| Тип | Проверка | Результат |
|---|---|---|
| api | stock product=5da542bb2 (было 10, стало 8) | ✓ delta=2, expected=2 |
| api | stock product=671f6e0e… 2 (было 15, стало 13) | ✓ delta=2, expected=2 |
| db | stock_movements запись на sale-line 5da542bb… | ✓ count=1, sum=-2 (expected sum=-2) |
| db | stock_movements запись на sale-line 671f6e0e… | ✓ count=1, sum=-2 (expected sum=-2) |
| api | stock product=a9e517d5… 2 (было 10, стало 8) | ✓ delta=2, expected=2 |
| api | stock product=0f3d33e82 (было 15, стало 13) | ✓ delta=2, expected=2 |
| db | stock_movements запись на sale-line a9e517d5… | ✓ count=1, sum=-2 (expected sum=-2) |
| db | stock_movements запись на sale-line 0f3d33e8… | ✓ count=1, sum=-2 (expected sum=-2) |
| db | stock_movements.Type = RetailSale (2) для sale документа | ✓ types=2 |
## Summary

View file

@ -1,24 +1,24 @@
# E2E report: moysklad-import
Запущен: 2026-05-26T06:29:04.213Z
Длительность: 10.6с
Запущен: 2026-05-26T07:03:03.819Z
Длительность: 11.3с
**Итог:** 7 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 7)
## ✓ Step step01_bootstrap_and_connect: Орг + mock MoySklad + сохранение токена (маскирование) + test-connection 200
Длительность: 130с
Длительность: 142с
| Тип | Проверка | Результат |
|---|---|---|
| api | Mock MoySklad поднят | ✓ http://127.0.0.1:5099/api/remap/1.2/ |
| api | PUT settings → hasToken + маска | ✓ status=200 masked=mock••••••••abcd |
| api | GET settings не отдаёт сырой токен | ✓ masked=mock••••••••abcd |
| api | POST test → 200, имя орги из mock | ✓ status=200 org={"organization":"Mock Org 1779776944213","inn":"600700800900"} |
| api | POST test → 200, имя орги из mock | ✓ status=200 org={"organization":"Mock Org 1779778983819","inn":"600700800900"} |
## ✓ Step step02_import_counterparties_create: Импорт контрагентов: job Succeeded, Created=2, маппинг полей верен
Длительность: 3930мс
Длительность: 4159мс
| Тип | Проверка | Результат |
|---|---|---|
@ -27,12 +27,12 @@
| api | Created=2, Skipped=0 | ✓ created=2 updated=0 skipped=0 |
| db | В БД 2 контрагента | ✓ count=2 |
| db | Ромашка: Type=1(LegalEntity), Bin=inn, TaxNumber=kpp | ✓ Type=1 Bin=123456789012 Tax=KPP001 |
| db | Ромашка: LegalName=legalTitle, Address=actualAddress, Notes=description | ✓ Legal=Товарищество Ромашка 1779776944213 Addr=Алматы Абая 1 |
| db | Ромашка: LegalName=legalTitle, Address=actualAddress, Notes=description | ✓ Legal=Товарищество Ромашка 1779778983819 Addr=Алматы Абая 1 |
| db | Иванов: Type=2(Individual), Address=legalAddress (fallback) | ✓ Type=2 Addr=Астана Победы 5 |
## ✓ Step step03_import_counterparties_idempotent: Повторный импорт (overwrite=false): Skipped=2, дублей нет
Длительность: 907мс
Длительность: 890мс
| Тип | Проверка | Результат |
|---|---|---|
@ -42,7 +42,7 @@
## ✓ Step step04_import_counterparties_overwrite: Импорт overwrite=true с изменёнными данными: Updated=2, поля обновлены
Длительность: 1191мс
Длительность: 1156мс
| Тип | Проверка | Результат |
|---|---|---|
@ -53,24 +53,24 @@
## ✓ Step step05_import_products_create: Импорт товаров: Created=1, группа создана, маппинг (артикул/НДС/цена/штрихкод/группа/страна)
Длительность: 2178мс
Длительность: 2446мс
| Тип | Проверка | Результат |
|---|---|---|
| api | import-products → jobId | ✓ status=200 |
| api | job Succeeded | ✓ status=Succeeded msg=Готово: 1 записей (создано/обновлено), 0 пропущено, 1 групп. errors=[] |
| api | Created=1, GroupsCreated>=1 | ✓ created=1 groups=1 |
| db | Товар создан с артикулом из MoySklad | ✓ id=81fa3d5b-8d2a-492f-9b72-ee467af12b4f |
| db | Товар создан с артикулом из MoySklad | ✓ id=bfef9503-64fc-4f71-b91f-5c2d9b956313 |
| db | Vat=12, Packaging=1(Piece, weighed=false), IsMarked=false | ✓ vat=12.00 pack=1 marked=f |
| db | ReferencePrice = buyPrice/100 = 250 | ✓ ref=250.0000 |
| db | Группа = productFolder «Бакалея» | ✓ group=Бакалея 1779776944213 |
| db | Группа = productFolder «Бакалея» | ✓ group=Бакалея 1779778983819 |
| db | CountryOfOrigin сопоставлена по имени | ✓ country=2b209c11-c7ed-415d-9947-c8b6604f712e expected=2b209c11-c7ed-415d-9947-c8b6604f712e |
| db | Розничная цена = 320 (salePrice «Розничная»/100) | ✓ amount=320.0000 |
| db | Штрихкод ean13 импортирован | ✓ bc=2069470270014 expected=2069470270014 |
| db | Штрихкод ean13 импортирован | ✓ bc=2089863540012 expected=2089863540012 |
## ✓ Step step06_import_products_idempotent: Повторный импорт товаров (overwrite=false): Skipped=1, дублей нет
Длительность: 1134мс
Длительность: 1182мс
| Тип | Проверка | Результат |
|---|---|---|

View file

@ -1,43 +1,43 @@
# E2E report: multi-tenant-isolation
Запущен: 2026-05-26T06:28:08.807Z
Длительность: 3.6с
Запущен: 2026-05-26T07:02:06.316Z
Длительность: 3.5с
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
## ✓ Step step01_create_two_orgs: SuperAdmin создаёт две независимые орги Alpha и Beta (каждая со своим админом)
Длительность: 1218мс
Длительность: 1184мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Создана Alpha | ✓ f37d8c51-9934-4d18-a285-7ba25dfc60be |
| api | Создана Beta | ✓ c372ef64-2328-4a19-af3c-90a058f44012 |
| api | Создана Alpha | ✓ 8cc09bb5-c09d-4e94-8e6c-9b5256c97381 |
| api | Создана Beta | ✓ 39c3e0ce-5b42-444e-80ff-9566868ee155 |
| api | orgId Alpha ≠ orgId Beta | ✓ |
## ✓ Step step02_login_both_admins: Логин под admin Alpha и admin Beta — получаем два разных org_id в JWT
Длительность: 792мс
Длительность: 744мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Login Alpha admin → 200 | ✓ |
| api | Login Beta admin → 200 | ✓ |
| api | Alpha orgId == ctx.alpha.orgId | ✓ claim=f37d8c51-9934-4d18-a285-7ba25dfc60be |
| api | Beta orgId == ctx.beta.orgId | ✓ claim=c372ef64-2328-4a19-af3c-90a058f44012 |
| api | Alpha orgId == ctx.alpha.orgId | ✓ claim=8cc09bb5-c09d-4e94-8e6c-9b5256c97381 |
| api | Beta orgId == ctx.beta.orgId | ✓ claim=39c3e0ce-5b42-444e-80ff-9566868ee155 |
## ✓ Step step03_seed_data_in_alpha: Admin Alpha создаёт counterparty + product → запоминаем их ID
Длительность: 160мс
Длительность: 135мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Alpha создаёт counterparty | ✓ 8103d4ec-e35c-4fd4-8214-931921a86783 |
| api | Alpha создаёт product | ✓ 647f7992-3df1-460c-845c-6ed68bc1903b |
| api | Alpha создаёт counterparty | ✓ 8501aa94-8f43-4046-830b-f36ce5797b8e |
| api | Alpha создаёт product | ✓ fa1da1ad-69ec-40e8-b1f4-90bf04555cd9 |
## ✓ Step step04_beta_cannot_read_alpha: Admin Beta GET /api/catalog/counterparties/{alphaId} и /products/{alphaId} → 404
Длительность: 159мс
Длительность: 126мс
| Тип | Проверка | Результат |
|---|---|---|
@ -46,7 +46,7 @@
## ✓ Step step05_beta_cannot_list_alpha_data: Admin Beta GET /api/catalog/counterparties|/products → пустые списки (нет данных Alpha)
Длительность: 207мс
Длительность: 223мс
| Тип | Проверка | Результат |
|---|---|---|
@ -55,7 +55,7 @@
## ✓ Step step06_beta_cannot_modify_alpha: Admin Beta PUT/DELETE /api/catalog/products/{alphaId} → 404 (не 200, не 403)
Длительность: 172мс
Длительность: 198мс
| Тип | Проверка | Результат |
|---|---|---|
@ -64,7 +64,7 @@
## ✓ Step step07_beta_cannot_link_to_alpha: Admin Beta POST product с DefaultSupplierId=alphaCounterpartyId → 400 (FK через query filter)
Длительность: 38мс
Длительность: 32мс
| Тип | Проверка | Результат |
|---|---|---|
@ -72,7 +72,7 @@
## ✓ Step step08_beta_cannot_forge_org_override: Admin Beta с заголовком X-Org-Override:{alphaId} → запрос всё равно идёт от Beta (не SuperAdmin)
Длительность: 2с
Длительность: 16мс
| Тип | Проверка | Результат |
|---|---|---|
@ -80,7 +80,7 @@
## ✓ Step step09_superadmin_sees_both: SuperAdmin без override GET /api/super-admin/organizations → видит и Alpha и Beta
Длительность: 18мс
Длительность: 16мс
| Тип | Проверка | Результат |
|---|---|---|
@ -89,7 +89,7 @@
## ✓ Step step10_superadmin_readonly_override: SuperAdmin с X-Org-Override:{alphaId} → GET товаров Alpha (200), PUT/POST без reason → 403
Длительность: 30мс
Длительность: 29мс
| Тип | Проверка | Результат |
|---|---|---|
@ -98,21 +98,21 @@
## ✓ Step step11_superadmin_edit_override_with_reason: SuperAdmin с X-Org-Override + X-Org-Override-Reason → PUT 200 + запись в audit_log
Длительность: 504мс
Длительность: 506мс
| Тип | Проверка | Результат |
|---|---|---|
| api | SuperAdmin+override+reason PUT counterparty → 200/204 | ✓ actual=204 |
| db | Counterparty.Name изменено в БД | ✓ name=edited-by-superadmin-1779776888807 |
| db | Counterparty.Name изменено в БД | ✓ name=edited-by-superadmin-1779778926316 |
| db | super_admin_audit_log выросло | ✓ before=2 after=3 |
## ✓ Step step12_stock_isolation: Остатки Alpha и Beta не смешиваются — Supply в Alpha не появляется в /inventory/stock у Beta
Длительность: 267мс
Длительность: 298мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Alpha создаёт supply | ✓ actual=201 {"id":"0aacfaea-1785-4761-8524-bc5bde177f0b","number":"П-2026-000001","date":"2026-05-26T06:28:13.685Z","status":0,"supp |
| api | Alpha создаёт supply | ✓ actual=201 {"id":"28b0326f-a65e-476f-bb69-ef3a61bba0cb","number":"П-2026-000001","date":"2026-05-26T07:02:10.853Z","status":0,"supp |
| api | Alpha проводит supply | ✓ actual=204 |
| api | Beta /inventory/stock не содержит Alpha product | ✓ total=0, утечка=нет |
| api | Beta /inventory/movements не содержит Alpha movement | ✓ total=0, утечка=нет |

View file

@ -0,0 +1,71 @@
# E2E report: platform-smtp
Запущен: 2026-05-26T07:03:40.841Z
Длительность: 0.7с
**Итог:** 6 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 6)
## ✓ Step step01_clean_state: Сброс SMTP в чистое состояние (hasSmtpPassword=false, host пуст)
Длительность: 391мс
| Тип | Проверка | Результат |
|---|---|---|
| api | PUT очистки → 204 | ✓ status=204 |
| api | hasSmtpPassword=false после очистки | ✓ has=false |
## ✓ Step step02_reason_required: PUT без причины / причина <10 400
Длительность: 11мс
| Тип | Проверка | Результат |
|---|---|---|
| api | PUT без причины → 400 | ✓ status=400 |
| api | PUT причина <10 400 | status=400 |
## ✓ Step step03_test_send_not_configured: test-send при ненастроенном SMTP → 400
Длительность: 15мс
| Тип | Проверка | Результат |
|---|---|---|
| api | test-send без настроенного SMTP → 400 | ✓ status=400 |
## ✓ Step step04_save_smtp: Сохранение SMTP с паролем → 204, GET отдаёт поля кроме пароля
Длительность: 30мс
| Тип | Проверка | Результат |
|---|---|---|
| api | PUT валидной конфигурации → 204 | ✓ status=204 |
| api | hasSmtpPassword=true | ✓ has=true |
| api | smtpHost/username сохранены и возвращаются | ✓ host=smtp.example.com |
## ✓ Step step05_password_encrypted: Пароль в БД зашифрован (не плейнтекст) и не возвращается клиенту
Длительность: 212мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Ответ GET не содержит пароль в открытом виде | ✓ ok |
| db | В БД пароль не плейнтекст и не пуст | ✓ len=155 |
## ✓ Step step06_clear_password: PUT newSmtpPassword=__clear__ → hasSmtpPassword=false
Длительность: 29мс
| Тип | Проверка | Результат |
|---|---|---|
| api | PUT __clear__ → 204 | ✓ status=204 |
| api | hasSmtpPassword=false | ✓ has=false |
## Summary
- Passed: 6
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.

View file

@ -1,21 +1,21 @@
# E2E report: reports-stats
Запущен: 2026-05-26T06:28:57.128Z
Длительность: 2.9с
Запущен: 2026-05-26T07:02:56.545Z
Длительность: 3.0с
**Итог:** 5 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 5)
## ✓ Step step01_bootstrap: Орг A + товар + приёмка (остаток под продажи)
Длительность: 1462мс
Длительность: 1544мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг A с товаром и остатком готова | ✓ 86c52764-ba04-43c5-b069-52ff3197e2b6 |
| api | Орг A с товаром и остатком готова | ✓ 168ac2c6-f257-4b71-b5e6-997ce6777abc |
## ✓ Step step02_stats_reflect_posted_sales: stats: RevenueToday/Transactions/AvgTicket = сумме проведённых чеков, серия непрерывна
Длительность: 333мс
Длительность: 405мс
| Тип | Проверка | Результат |
|---|---|---|
@ -30,7 +30,7 @@
## ✓ Step step03_draft_sale_excluded: Черновик чека (не проведён) не попадает в stats
Длительность: 53мс
Длительность: 49мс
| Тип | Проверка | Результат |
|---|---|---|
@ -40,7 +40,7 @@
## ✓ Step step04_stats_tenant_isolated: stats орг A не видит продажи орг B и наоборот
Длительность: 1027мс
Длительность: 988мс
| Тип | Проверка | Результат |
|---|---|---|
@ -51,7 +51,7 @@
## ✓ Step step05_days_param_and_gaps: Параметр days меняет длину серии; профит/ABC-отчёты отсутствуют (gap)
Длительность: 25мс
Длительность: 24мс
| Тип | Проверка | Результат |
|---|---|---|

View file

@ -0,0 +1,91 @@
# E2E report: roles
Запущен: 2026-05-26T07:03:26.970Z
Длительность: 1.6с
**Итог:** 8 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 8)
## ✓ Step step01_bootstrap: Орг A + логин админа
Длительность: 1201мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг A + админ готовы | ✓ |
## ✓ Step step02_system_roles_exist: Системные роли (IsSystem=true) созданы — минимум 4
Длительность: 15мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Системные роли (ядро Администратор/Кладовщик/Кассир) присутствуют, не удаляемы | ✓ system=[Администратор, Кладовщик, Кассир] |
## ✓ Step step03_create_custom_role: Создание кастомной роли с правами → права сохранены
Длительность: 94мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Создание кастомной роли → 200/201 | ✓ status=201 |
| api | Права сохранены (productsView=true, productsEdit=false) | ✓ {"productsView":true,"productsEdit":false,"productsDelete":false,"productGroupsManage":false,"priceTypesManage":false,"unitsManage":false,"suppliesView":true,"suppliesEdit":false,"suppliesPost":false, |
## ✓ Step step04_update_permissions: Изменение прав роли применяется (PUT)
Длительность: 143мс
| Тип | Проверка | Результат |
|---|---|---|
| api | PUT прав роли → 200/204 | ✓ status=204 |
| api | Новое право productsEdit=true применилось | ✓ {"productsView":true,"productsEdit":true,"productsDelete":false,"productGroupsManage":false,"priceTypesManage":false,"unitsManage":false,"suppliesView":true,"suppliesEdit":false,"suppliesPost":false," |
## ✓ Step step05_delete_system_role_409: Удаление системной роли → 409
Длительность: 41мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Системная роль найдена | ✓ Администратор |
| api | DELETE системной роли → 409 | ✓ status=409 |
## ✓ Step step06_delete_role_in_use_409: Удаление роли, занятой сотрудником → 409
Длительность: 51мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Сотрудник с кастомной ролью создан | ✓ status=200 |
| api | DELETE занятой роли → 409 | ✓ status=409 |
## ✓ Step step07_delete_unused_role_ok: Удаление неиспользуемой роли → 204/200
Длительность: 42мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Неиспользуемая роль создана | ✓ |
| api | DELETE неиспользуемой роли → 200/204 | ✓ status=204 |
## ✓ Step step08_permission_authz_gap: Permission-based authz не enforced на API — gap
Длительность: 1мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Permission-based authz — задокументированный gap (см. Logic gaps) | ✓ |
## Summary
- Passed: 8
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.
## Logic gaps
- ТЗ 2.7.2 ожидает 4-6 системных ролей, реально 3 (Phase4b_RolesSimplify): Администратор, Кладовщик, Кассир. Это намеренное упрощение, не багТЗ устарело.
- ТЗ 2.7.2: permission-based авторизация не enforced — эндпоинты используют только [Authorize(Roles=...)], флаги RolePermissions носят справочный характер для UI. Кастомная роль с ограниченными правами НЕ даёт 403 на запрещённых операциях (помечено «после P0-5»).

View file

@ -0,0 +1,67 @@
# E2E report: security-edge
Запущен: 2026-05-26T07:03:45.792Z
Длительность: 2.7с
**Итог:** 6 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 6)
## ✓ Step step01_protected_require_auth: Защищённые эндпоинты без токена → 401
Длительность: 46мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Все защищённые GET без токена → 401 | ✓ проверено 8 |
## ✓ Step step02_anonymous_open: Анонимные эндпоинты (/health) доступны без токена
Длительность: 36мс
| Тип | Проверка | Результат |
|---|---|---|
| api | /health без токена → 200 | ✓ status=200 |
| api | /connect/token анонимен (400, не 401) | ✓ status=400 |
## ✓ Step step03_path_traversal_uploads: Path-traversal /uploads/..%2f..%2fetc/passwd → не 200 (404)
Длительность: 18мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Path-traversal не отдаёт системные файлы | ✓ все отбиты (404) |
## ✓ Step step04_sql_injection_safe: SQL-инъекция в quick-search безопасна, таблица цела
Длительность: 1846мс
| Тип | Проверка | Результат |
|---|---|---|
| api | quick-search с инъекцией не падает (нет 5xx) | ✓ |
| db | Таблица products цела (count не изменился) | ✓ before=1 after=1 |
## ✓ Step step05_tenant_404_not_403: GET товара чужого тенанта → 404 (не 403, не 200)
Длительность: 781мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Товар чужого тенанта → 404 (не 200/403) | ✓ status=404 |
## ✓ Step step06_cors_evil_origin: CORS не отражает Origin http://evil.com
Длительность: 10мс
| Тип | Проверка | Результат |
|---|---|---|
| api | ACAO не равен http://evil.com | ✓ ACAO=(нет) |
## Summary
- Passed: 6
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.

View file

@ -1,24 +1,24 @@
# E2E report: stock-concurrency
Запущен: 2026-05-26T06:28:48.064Z
Запущен: 2026-05-26T07:02:47.787Z
Длительность: 4.9с
**Итог:** 4 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 4)
## ✓ Step step01_bootstrap: Орг + товар + стартовая приёмка qty=5 @100 (Stock=5, Cost=100)
Длительность: 2198мс
Длительность: 2212мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Bootstrap product создан | ✓ 1903eb84-9c8c-4335-9439-6d8fa6bd2102 |
| api | Bootstrap product создан | ✓ df50a87d-0f25-4fb0-9e35-d5e1077e5fb9 |
| db | Stock.Quantity == Σ StockMovement (invariant) | ✓ stock=5 sum=5 |
| db | Стартовый Stock == 5 | ✓ stock=5 |
| db | Стартовый Cost == 100 | ✓ cost=100 |
## ✓ Step step02_concurrent_distinct_supplies: Две разные приёмки (10@100 и 10@120) одновременно → Stock=25, инвариант, Cost=108
Длительность: 90с
Длительность: 87с
| Тип | Проверка | Результат |
|---|---|---|
@ -29,7 +29,7 @@
## ✓ Step step03_double_post_same_supply: Двойное проведение ОДНОЙ приёмки (7@100) одновременно → применяется один раз
Длительность: 1378мс
Длительность: 1373мс
| Тип | Проверка | Результат |
|---|---|---|
@ -41,7 +41,7 @@
## ✓ Step step04_final_invariant: Финальный инвариант Stock == Σ StockMovement
Длительность: 44с
Длительность: 415мс
| Тип | Проверка | Результат |
|---|---|---|

View file

@ -1,23 +1,23 @@
# E2E report: stock-invariant-deep
Запущен: 2026-05-26T06:28:37.859Z
Длительность: 5.9с
Запущен: 2026-05-26T07:02:38.469Z
Длительность: 5.7с
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
## ✓ Step step01_bootstrap: Орг + admin + product (стартовый остаток 0)
Длительность: 1776мс
Длительность: 1829мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Bootstrap product создан | ✓ d8c6c47a-dd41-48de-b0a0-9cc35769a8cb |
| api | Bootstrap product создан | ✓ e873c3b8-a3dd-4b32-a17a-91a09364f54d |
| db | Stock.Quantity == 0 | ✓ actual=0 |
| db | Stock.Quantity == Σ StockMovement (invariant) | ✓ stock=0 sum=0 |
## ✓ Step step02_supply_a_qty_20: Supply A qty=20 → invariant stock=20, Σ movement=20
Длительность: 448мс
Длительность: 463мс
| Тип | Проверка | Результат |
|---|---|---|
@ -27,7 +27,7 @@
## ✓ Step step03_sale_a_qty_5: RetailSale A qty=5 → invariant stock=15, Σ movement=15
Длительность: 456мс
Длительность: 442мс
| Тип | Проверка | Результат |
|---|---|---|
@ -37,7 +37,7 @@
## ✓ Step step04_supply_b_qty_10: Supply B qty=10 → invariant stock=25, Σ movement=25
Длительность: 510мс
Длительность: 452мс
| Тип | Проверка | Результат |
|---|---|---|
@ -47,7 +47,7 @@
## ✓ Step step05_sale_b_qty_8: RetailSale B qty=8 → invariant stock=17, Σ movement=17
Длительность: 467мс
Длительность: 455мс
| Тип | Проверка | Результат |
|---|---|---|
@ -57,7 +57,7 @@
## ✓ Step step06_unpost_sale_a: Unpost RetailSale A → invariant stock=22, Σ movement=22
Длительность: 475мс
Длительность: 44с
| Тип | Проверка | Результат |
|---|---|---|
@ -67,7 +67,7 @@
## ✓ Step step07_repost_sale_a: Re-post RetailSale A → invariant stock=17, Σ movement=17
Длительность: 485мс
Длительность: 447мс
| Тип | Проверка | Результат |
|---|---|---|
@ -77,7 +77,7 @@
## ✓ Step step08_movement_count_correct: Всего StockMovement по продукту = 6 строк (2 supply + 2 sale + reverse sale + repost sale)
Длительность: 217мс
Длительность: 202мс
| Тип | Проверка | Результат |
|---|---|---|
@ -86,7 +86,7 @@
## ✓ Step step09_concurrent_sales_serialized: Два POST /post одновременно на один остаток — один 200, второй 409
Длительность: 627мс
Длительность: 584мс
| Тип | Проверка | Результат |
|---|---|---|
@ -97,7 +97,7 @@
## ✓ Step step10_final_invariant: Финальный invariant после всех операций сохраняется
Длительность: 437мс
Длительность: 396мс
| Тип | Проверка | Результат |
|---|---|---|

View file

@ -0,0 +1,85 @@
# E2E report: superadmin-console
Запущен: 2026-05-26T07:03:32.749Z
Длительность: 4.0с
**Итог:** 6 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 6)
## ✓ Step step01_create_org_audited: Создание орг1 → 200 + запись CreateOrg в audit-log
Длительность: 1064мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Создание орг1 → 200 | ✓ status=200 |
| api | audit-log содержит CreateOrg | ✓ actions=CreateOrg |
## ✓ Step step02_archive: Архив: неверное имя → 400, верное → 204, IsArchived=true + ArchiveOrg
Длительность: 268мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Архив с неверным именем → 400 | ✓ status=400 |
| api | Архив с верным именем → 204 | ✓ status=204 |
| db | IsArchived=true, ArchivedAt задан | ✓ IsArchived=t |
| api | audit-log содержит ArchiveOrg | ✓ |
## ✓ Step step03_restore: Восстановление → 204, IsArchived=false + RestoreOrg
Длительность: 260мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Восстановление → 204 | ✓ status=204 |
| db | IsArchived=false | ✓ IsArchived=f |
| api | audit-log содержит RestoreOrg | ✓ |
## ✓ Step step04_change_owner: Смена владельца: без reason → 400, reason<10 400, валидно 204 + ChangeOwner
Длительность: 1064мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Второй пользователь орг1 создан (кандидат во владельцы) | ✓ |
| api | change-owner без reason → 400 | ✓ status=400 |
| api | change-owner reason<10 400 | status=400 |
| api | change-owner валидный reason → 204 | ✓ status=204 |
| db | AccountOwnerUserId обновлён на нового владельца | ✓ owner=72dc5c01-7e75-4fac-bb50-ae39bd958a6a |
| api | audit-log содержит ChangeOwner | ✓ actions=ChangeOwner,RestoreOrg,ArchiveOrg,CreateOrg |
## ✓ Step step05_hard_delete: Hard-delete: не-архив → 409, до retention → 409, retention=0 + верное имя → 204, юзеры деактивированы
Длительность: 1269мс
| Тип | Проверка | Результат |
|---|---|---|
| api | Орг2 создана | ✓ |
| api | Удаление неархивной орг → 409 | ✓ status=409 |
| api | Удаление до retention → 409 | ✓ status=409 {"error":"Доступно через 30 дней архива."} |
| api | Удаление с неверным именем → 400 | ✓ status=400 |
| api | Удаление архивной (retention=0, верное имя) → 204 | ✓ status=204 |
| db | Организация физически удалена | ✓ rows=0 |
| db | Юзеры отвязаны/деактивированы (нет привязки к удалённой орг) | ✓ before=1 stillLinked=0 |
## ✓ Step step06_audit_log_filters: audit-log: фильтр по org и actionType, ChangeOwner хранит reason
Длительность: 55мс
| Тип | Проверка | Результат |
|---|---|---|
| api | audit орг1 содержит CreateOrg/ArchiveOrg/RestoreOrg/ChangeOwner | ✓ actions=ChangeOwner,RestoreOrg,ArchiveOrg,CreateOrg |
| api | Фильтр actionType=DeleteOrg для орг2 возвращает запись | ✓ count=1 |
| api | Фильтр actionType отсекает прочее (все строки DeleteOrg) | ✓ types=DeleteOrg |
| api | ChangeOwner в журнале хранит reason | ✓ reason=Передача владения по тикету поддержки #4242 |
## Summary
- Passed: 6
- Failed: 0
- Warnings: 0
- Skipped: 0
## Critical bugs
Нет.

View file

@ -1,64 +1,78 @@
# Системное тестирование Food Market — 2026-05-26
> Инициировано Opus 4.7 по плану из `docs/TZ-тестирование.md` (продолжение сессии 2026-05-23, см. `systemic-2026-05-23.md`).
> Среда: docker `food-market-postgres` (postgres:16-alpine, 127.0.0.1:5434) + dotnet 8 API локально на :5081 + E2E через axios/psql.
> Инициировано Opus 4.7 по плану `docs/TZ-тестирование.md` (продолжение `systemic-2026-05-23.md`).
> Среда: docker `food-market-postgres` (postgres:16-alpine, 127.0.0.1:5434) + dotnet 8 API локально на :5081 + E2E через axios/psql + mock MoySklad.
> Запуск: `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 ✓** |
| Сценарий | Шаги | Результат |
|---|---|---|
| **full-cycle** | 12 | ✓ |
| **multi-tenant-isolation** | 12 | ✓ |
| **documents-edge** | 10 | ✓ |
| **auth-edge** | 10 | ✓ |
| **auth-password** | 6 | ✓ |
| **catalog-edge** | 12 | ✓ |
| **stock-invariant-deep** | 10 | ✓ |
| **stock-concurrency** | 4 | ✓ |
| **reports-stats** | 5 | ✓ |
| **moysklad-import** | 7 | ✓ |
| **employees** | 10 | ✓ |
| **roles** | 8 | ✓ |
| **superadmin-console** | 6 | ✓ |
| **platform-smtp** | 6 | ✓ |
| **security-edge** | 6 | ✓ |
**Итого 9 сценариев, 82 шага — все зелёные. Багов нет.**
**Итого 15 сценариев, 124 шага — все зелёные. Багов нет.**
**Найдено и исправлено в этой сессии: 3 бага** (1 critical, 1 high, + связка из 2 правок по безопасности refresh-токенов).
**Исправлено за день: 4 бага** (2 P0/critical, 1 high, 1 medium) + доработка тестируемости MoySklad.
## 1. Найденные баги и исправления
### BUG #1 — Старый refresh-token остаётся валидным после ротации (commit 32729e7)
### BUG #1 (P0) — Уволенный сотрудник продолжает логиниться (commit 5091d43)
`auth-edge` step03. Две причины, обе закрыты:
1. `AuthorizationController.Exchange` (refresh-ветка) строил новый principal с нуля и прокидывал только `AuthorizationId`, но не `TokenId`. Handler OpenIddict `RedeemTokenEntry` читает `TokenId` из подписываемого principal — без него старый refresh не помечался `Redeemed`.
2. Даже после починки редемпшна OpenIddict по умолчанию даёт 30-секундный **reuse-leeway** — погашенный refresh ещё принимается в этом окне. Для розничной админки это дыра: утёкший refresh живёт 30с после ротации.
`employees` step07. `EmployeesController.Delete` (увольнение и soft-delete) и `Update` (деактивация) меняли только `Employee.IsActive`, но не трогали связанный `AppUser`. Логин и refresh гейтятся на `User.IsActive` → уволенный сохранял полный доступ и обновлял токены до 30 дней (ТЗ 0.4).
**Fix:** `SetLinkedUserActiveAsync` — при деактивации сотрудника гасит `User.IsActive` и отзывает его valid OpenIddict-токены; при реактивации возвращает доступ.
**Severity:** high (одна утечка refresh → продлеваемый доступ).
**Fix:** прокидываем `TokenId` старого refresh в новый principal + `SetRefreshTokenReuseLeeway(TimeSpan.Zero)` в `Program.cs`. Проверено в БД: старый токен переходит в `redeemed` и немедленно отвергается (4xx).
### BUG #2 (critical) — Конкурентное проведение приёмки ломает инвариант остатков (commit 15f27fd)
### BUG #2 — Конкурентное проведение приёмки ломает инвариант остатков (commit 15f27fd)
`stock-concurrency` step03. `Supply.Post` шёл на Read Committed, `ApplyMovementAsync` делает read-modify-write по `Stock.Quantity` без RowVersion. Двойное проведение одной приёмки применяло остаток дважды (`Stock=32`, `Σ Movement=39`).
**Fix:** `IsolationLevel.Serializable` + перехват конфликта сериализации (40001/40P01) → 409.
`stock-concurrency` step03. `Supply.Post` шёл на дефолтной изоляции (Read Committed), а `StockService.ApplyMovementAsync` делает read-modify-write по `Stock.Quantity` без RowVersion. Под гонкой:
- двойное проведение ОДНОЙ приёмки (оба запроса читают `Status=Draft` до коммита соседа) применяло остаток дважды — 2 `StockMovement`, но `Stock` рос на одну партию → `Stock=32`, `Σ Movement=39`;
- две разные приёмки одного товара могли потерять обновление остатка и посчитать скользящее среднее `Cost` от устаревшего `currentQty`.
### BUG #3 (high) — Refresh-token остаётся валидным после ротации (commit 32729e7)
**Severity:** critical (нарушение главного учётного инварианта `Stock == Σ StockMovement`).
**Fix:** проведение переведено на `IsolationLevel.Serializable` (как `RetailSale.Post`), конфликт сериализации (SQLSTATE 40001/40P01) перехватывается → 409 (клиент повторяет, а не получает 500). После фикса: `Stock=32`, `Σ=32`, statuses 204+409.
`auth-edge` step03. Новый principal не получал `TokenId` старого refresh → `RedeemTokenEntry` не гасил его; плюс 30-секундный reuse-leeway OpenIddict.
**Fix:** проброс `TokenId` + `SetRefreshTokenReuseLeeway(TimeSpan.Zero)`.
### Доработка для тестируемости — базовый URL MoySklad из конфига (commit e78e921)
### BUG #4 (medium) — change-owner принимал слишком короткий reason (commit 01568ba)
`MoySkladClient.BaseUrl` был константой `api.moysklad.ru`, импорт нельзя было прогнать без боевого токена. Вынесли `BaseAddress` в `MoySklad:BaseUrl` (дефолт — прежний боевой URL); e2e наводит клиент на mock-сервер `lib/moysklad-mock.ts`. Прод-поведение не меняется.
`superadmin-console` step04. Смена владельца писала reason в аудит, но проверяла лишь непустоту. PlatformSettings уже требует ≥10 — привели change-owner к той же планке (ТЗ 2.8).
## 2. Что покрыто впервые в этой сессии
### Доработка — базовый URL MoySklad из конфигурации (commit e78e921)
- **Конкурентность приёмок** (`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.
`MoySklad:BaseUrl` (дефолт — боевой) позволяет наводить клиент на mock-сервер в e2e, не трогая прод.
## 3. Logic gaps (не баги — нереализованный функционал по ТЗ 2.12)
## 2. Покрытие по разделам ТЗ
- Отчёт **«прибыль»** (выручка себестоимость) не реализован: `RetailSaleLine` не хранит снимок себестоимости, `/stats` отдаёт только валовую выручку.
- **ABC-анализ**, **«остатки на дату»** (`SUM(Movement) до даты`), **экспорт CSV/XLSX** — отдельного `ReportsController` нет.
- `Supply.Unpost` использует те же read-modify-write по `Stock` без транзакции — под одновременным unpost теоретически уязвим к lost update (вне фокуса этой сессии; проведение `Post` закрыто).
P0/P1 функциональные области, реализованные в коде, — покрыты и зелёные:
Auth (login/refresh/signup/forgot-reset), Multi-tenancy, Catalog, Supplies, RetailSales, Stock (+ конкурентность), Employees, Roles, SuperAdmin Console, SMTP, MoySklad import, дашбордная выручка, безопасность (auth-гейт, traversal, SQLi, CORS, межтенантная 404).
## 4. Замечание по окружению
## 3. Logic gaps — нереализованный по ТЗ функционал (НЕ баги)
- На 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` некогерентны).
- **Отчёты (2.12):** профит по себестоимости, ABC-анализ, «остатки на дату», экспорт CSV/XLSX — нет `ReportsController`, `RetailSaleLine` без Cost-снимка. Есть только `/stats` (валовая выручка).
- **Складские документы (2.11):** Оприходование/Списание/Перемещение/Инвентаризация — не реализованы (только read-only `StockController`).
- **POS Sync API (2.13)** — не реализован.
- **Permission-based авторизация (2.7.2, «после P0-5»)** — эндпоинты только role-based `[Authorize(Roles=...)]`; флаги `RolePermissions` справочные.
- **Рейт-лимит логина (2.1.2, «после P0-3»)** — на `/connect/token` нет (у forgot-password — есть).
- **Системных ролей 3, а не 4-6** — намеренное упрощение (миграция `Phase4b_RolesSimplify`): Администратор/Кладовщик/Кассир. ТЗ устарело.
- `Supply.Unpost` использует тот же read-modify-write без транзакции — теоретически уязвим к гонке (вне фокуса; `Post` закрыт).
## 4. Вне зоны API-e2e (по ТЗ — отдельные инструменты)
UI/UX (2.14, Playwright), публичный сайт (2.15, Astro/Lighthouse), инфраструктура/DevOps (2.16), нагрузочное (2.18) — требуют браузера/нагрузочных стендов, в данном прогоне не покрывались.
## 5. Окружение
- SDK на dev-vm только **8.0.126**; `global.json` репозитория остаётся `8.0.417` (локальный даунгрейд в коммиты не включён).
- `admin.food-market.kz` — отдельный деплой с другой БД; e2e гоняются против локального API, подключённого к контейнеру `food-market-postgres`.