docs(e2e): финальный системный отчёт 2026-05-26 — все 9 сценариев зелёные
Сводный отчёт 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>
This commit is contained in:
parent
c7ecc39590
commit
f2f64646b1
|
|
@ -1,13 +1,13 @@
|
|||
# E2E report: auth-edge
|
||||
|
||||
Запущен: 2026-05-26T06:01:37.536Z
|
||||
Длительность: 5.3с
|
||||
Запущен: 2026-05-26T06:28:24.035Z
|
||||
Длительность: 3.9с
|
||||
|
||||
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
|
||||
|
||||
## ✓ Step step01_bootstrap_admin: SuperAdmin создаёт орг + админа, получаем access+refresh
|
||||
|
||||
Длительность: 2224мс
|
||||
Длительность: 1083мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
## ✓ Step step02_refresh_token_works: Refresh: старый access обменивается на новые access+refresh
|
||||
|
||||
Длительность: 394мс
|
||||
Длительность: 348мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
## ✓ Step step03_refresh_token_rotates: После refresh — старый refresh-token больше не работает (rotation)
|
||||
|
||||
Длительность: 145мс
|
||||
Длительность: 124мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
## ✓ Step step04_invalid_refresh_rejected: Невалидный refresh-token возвращает 400 invalid_grant
|
||||
|
||||
Длительность: 88мс
|
||||
Длительность: 81мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
## ✓ Step step05_tampered_jwt_rejected: JWT с подделанным org_id (изменён без переподписи) отшивается 401
|
||||
|
||||
Длительность: 41мс
|
||||
Длительность: 19мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
## ✓ Step step06_random_jwt_rejected: Случайный JWT-подобный токен из другого ключа отшивается 401
|
||||
|
||||
Длительность: 17мс
|
||||
Длительность: 12мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
|
||||
## ✓ Step step07_deactivated_user_blocked: Деактивация User.IsActive=false: повторный login и refresh возвращают 400
|
||||
|
||||
Длительность: 503мс
|
||||
Длительность: 520мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
## ✓ Step step08_archived_org_blocks_login: Архивная организация: login существующего админа возвращает 400 invalid_grant
|
||||
|
||||
Длительность: 489мс
|
||||
Длительность: 391мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
## ✓ Step step09_duplicate_signup_blocked: Повторный signup с тем же email живой орги отвергается 400
|
||||
|
||||
Длительность: 64мс
|
||||
Длительность: 49мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -85,12 +85,12 @@
|
|||
|
||||
## ✓ Step step10_orphan_signup_reactivates: Signup с email orphan-юзера (его org удалена) — реактивирует с новой org
|
||||
|
||||
Длительность: 1313мс
|
||||
Длительность: 1273мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | First signup → 200/201 | ✓ status=200 {"organizationId":"c629b9ea-ffe2-44ae-b091-f9fb61c27956","email":"orphan-1779775297536@example.kz"} |
|
||||
| api | Re-signup orphan email → 200/201 (реактивация) | ✓ status=200 {"organizationId":"c536d5d9-f5fd-4d80-8e3e-91c2345ef2aa","email":"orphan-1779775297536@example.kz"} |
|
||||
| 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 | Login после реактивации → 200 | ✓ status=200 |
|
||||
|
||||
## Summary
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# E2E report: catalog-edge
|
||||
|
||||
Запущен: 2026-05-26T06:02:00.235Z
|
||||
Длительность: 3.7с
|
||||
Запущен: 2026-05-26T06:28:32.030Z
|
||||
Длительность: 1.6с
|
||||
|
||||
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
|
||||
|
||||
## ✓ Step step01_bootstrap: Орг + admin + lookups
|
||||
|
||||
Длительность: 1393мс
|
||||
Длительность: 1128мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
## ✓ Step step02_empty_product_name_rejected: POST product с пустым name → 400
|
||||
|
||||
Длительность: 120мс
|
||||
Длительность: 10мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
## ✓ Step step03_negative_price_rejected: POST product с отрицательной ценой amount=-100 → 400
|
||||
|
||||
Длительность: 11мс
|
||||
Длительность: 8мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
## ✓ Step step04_oversized_name_truncated_or_rejected: POST product с name > 500 символов → 400 (превышение maxLength)
|
||||
|
||||
Длительность: 8мс
|
||||
Длительность: 10мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
## ✓ Step step05_duplicate_product_article: POST второго product с тем же article → 4xx (если уникальный) или OK + проверка БД
|
||||
|
||||
Длительность: 1027мс
|
||||
Длительность: 77мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
## ✓ Step step06_self_parent_group_rejected: POST product-group с parentId=собственный id (цикл) → 400
|
||||
|
||||
Длительность: 54мс
|
||||
Длительность: 48мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
## ✓ Step step07_delete_group_with_children: DELETE group у которой есть подгруппы → 409
|
||||
|
||||
Длительность: 80мс
|
||||
Длительность: 73мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
|
||||
## ✓ Step step08_delete_group_with_products: DELETE group в которой есть продукты → 409
|
||||
|
||||
Длительность: 11мс
|
||||
Длительность: 10мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
## ✓ Step step09_delete_unit_with_products: DELETE enable у unit, на которую ссылаются продукты → 409
|
||||
|
||||
Длительность: 34мс
|
||||
Длительность: 29мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
|
||||
## ✓ Step step10_delete_system_price_type: DELETE PriceType.IsSystem=true → 409
|
||||
|
||||
Длительность: 51мс
|
||||
Длительность: 37мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
|
||||
## ✓ Step step11_second_retail_price_type: POST PriceType с IsRetail=true когда уже есть Retail → 409
|
||||
|
||||
Длительность: 64мс
|
||||
Длительность: 52мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
## ✓ Step step12_delete_counterparty_with_supply: DELETE counterparty который использован в Supply → 409
|
||||
|
||||
Длительность: 800мс
|
||||
Длительность: 109мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
105
tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md
Normal file
105
tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# E2E report: documents-edge
|
||||
|
||||
Запущен: 2026-05-26T06:28:16.561Z
|
||||
Длительность: 2.9с
|
||||
|
||||
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
|
||||
|
||||
## ✓ Step step01_bootstrap: SuperAdmin создаёт орг Test + admin, делаем product + supply (10 шт по 100 KZT)
|
||||
|
||||
Длительность: 1265мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Орг + админ созданы | ✓ org=cdbad68a-b0fd-47ea-85dc-32272d39c2d8 |
|
||||
| api | Counterparty создан | ✓ |
|
||||
| api | Product создан | ✓ 08c989ff-3ad7-4e14-beef-e6e744959e07 |
|
||||
| api | Supply Draft создана | ✓ 0c635259-abec-40fa-8087-a73054d8009a |
|
||||
|
||||
## ✓ Step step02_post_supply_stock_10: Supply провести: stock=10, ReferencePrice=100, Cost=100
|
||||
|
||||
Длительность: 88мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Supply.Post → 200/204 | ✓ actual=204 |
|
||||
| db | Stock.Quantity == 10 | ✓ qty=10 |
|
||||
|
||||
## ✓ Step step03_oversell_blocked: RetailSale qty=15 (больше остатка 10), POST /post возвращает 409
|
||||
|
||||
Длительность: 122мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
||||
## ✓ Step step04_oversell_stock_unchanged: После заблокированного post stock остался 10, StockMovement не добавлен
|
||||
|
||||
Длительность: 260мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| db | Stock остался 10 после заблокированного post | ✓ qty=10 |
|
||||
| db | Stock == Σ StockMovement (invariant) | ✓ sum=10 qty=10 |
|
||||
|
||||
## ✓ Step step05_payment_mismatch_blocked: RetailSale с PaidCash+PaidCard не равной Total отвергается на post
|
||||
|
||||
Длительность: 40мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Платёж ≠ Total → 4xx на post | ✓ actual=400 {"error":"Сумма оплаты 300.00 меньше итога 400.00. Доплатите или измените позиции чека.","field":"PaidCash"} |
|
||||
|
||||
## ✓ Step step06_edit_posted_supply_blocked: PUT проведённой Supply (Posted) возвращает 409
|
||||
|
||||
Длительность: 49мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | PUT проведённой Supply → 409 | ✓ actual=409 {"error":"Только черновик может быть изменён. Сначала отмени проведение."} |
|
||||
|
||||
## ✓ Step step07_delete_posted_supply_blocked: DELETE проведённой Supply возвращает 409
|
||||
|
||||
Длительность: 26мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | DELETE проведённой Supply → 409 | ✓ actual=409 {"error":"Нельзя удалить проведённый документ. Сначала отмени проведение."} |
|
||||
|
||||
## ✓ Step step08_unpost_negative_blocked: После Sale qty=5 unpost Supply qty=10 возвращает 409 (stock минус)
|
||||
|
||||
Длительность: 98мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Sale qty=5 проведён | ✓ actual=204 |
|
||||
| api | Unpost Supply при stock<unpost-qty → 409 | ✓ actual=409 {"error":"Нельзя отменить проведение: остаток уйдёт в минус (часть товара уже расходована).","lines":[{"productId":"08c9 |
|
||||
|
||||
## ✓ Step step09_barcode_unique_within_org: Дубль штрихкода в одной орге, POST второго product отвергается
|
||||
|
||||
Длительность: 16мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | POST product с тем же barcode → 4xx | ✓ actual=400 {"error":"Штрихкод 206899370undefined3 уже используется товаром «Edge Product 1779776896561»."} |
|
||||
|
||||
## ✓ Step step10_barcode_per_tenant: Тот же штрихкод в другой орге допустим (per-tenant unique)
|
||||
|
||||
Длительность: 972мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | POST product с тем же barcode в другой орге → 201 | ✓ actual=201 {"id":"4fe7e2aa-cef3-458b-ba34-984ce7af8f2e","name":"Tenant-2 product (same barcode)","article":"1","description":null," |
|
||||
| db | В product_barcodes 2 записи с этим Code (одна на орг) | ✓ count=2 |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 10
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
150
tests/e2e/reports/full-cycle-2026-05-26T06-28-06-463Z.md
Normal file
150
tests/e2e/reports/full-cycle-2026-05-26T06-28-06-463Z.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# E2E report: full-cycle
|
||||
|
||||
Запущен: 2026-05-26T06:27:56.114Z
|
||||
Длительность: 8.6с
|
||||
|
||||
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
|
||||
|
||||
## ✓ Step step01_create_organization: SuperAdmin создаёт «Test Shop {timestamp}» (KZ, KZT, ФЛК телефона)
|
||||
|
||||
Длительность: 847мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | POST /api/super-admin/organizations → 200 | ✓ org=Test Shop 1779776876113 |
|
||||
| api | GET /api/super-admin/organizations включает созданную org | ✓ |
|
||||
| api | Невалидный phone отвергается | ✓ 400 |
|
||||
|
||||
## ✓ Step step02_create_first_admin: SuperAdmin создаёт первого Admin сотрудника организации (Employee + AppUser)
|
||||
|
||||
Длительность: 603мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Temp password возвращён CreateOrgResult | ✓ len=12 |
|
||||
| db | employees содержит ровно 1 запись для новой org | ✓ count=1 |
|
||||
| db | AspNetUserRoles содержит role=Admin для нового user | ✓ Admin |
|
||||
|
||||
## ✓ Step step03_login_as_admin: Логин под admin (не SuperAdmin override) — JWT с org_id и role=Admin
|
||||
|
||||
Длительность: 235мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | /connect/token password-grant выдал токен | ✓ |
|
||||
| api | /api/me содержит role=Admin | ✓ Admin |
|
||||
| api | /api/me содержит правильный orgId | ✓ b43f6023-c327-4ffe-8c54-606dc64ea620 |
|
||||
|
||||
## ✓ Step step04_create_storekeeper_and_cashier: Admin создаёт Storekeeper и Cashier через /settings/employees
|
||||
|
||||
Длительность: 1517мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | employee-roles list | ✓ 200, total=3 |
|
||||
| api | Системная роль «Кладовщик» существует | ✓ |
|
||||
| api | Системная роль «Кассир» существует | ✓ |
|
||||
| api | POST /api/organization/employees (Кладовщик) | ✓ 200 |
|
||||
| api | POST /api/organization/employees (Кассир) | ✓ 200 |
|
||||
| db | employees total = 3 (admin + keeper + cashier) | ✓ count=3 |
|
||||
| api | Невалидный email отвергается при createAccount | ✓ 400 |
|
||||
|
||||
## ✓ Step step05_login_as_cashier: Логин под Cashier — role-guard проверяется (sidebar/role guard)
|
||||
|
||||
Длительность: 614мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | /api/me содержит роль соответствующую системной Cashier | ✓ Cashier |
|
||||
| api | Cashier → GET /api/organization/employees → 403 | ✓ 403 |
|
||||
| api | Cashier → GET /api/sales/retail — доступен | ✓ 200 |
|
||||
|
||||
## ✓ Step step06_create_counterparty: Admin создаёт «ТОО Тест Поставщик» (БИН + телефон)
|
||||
|
||||
Длительность: 179мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | POST /api/catalog/counterparties | ✓ 201 |
|
||||
|
||||
## ✓ Step step07_ensure_main_store: Проверить что есть main store (из bootstrap), иначе создать
|
||||
|
||||
Длительность: 62мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | GET /api/catalog/stores | ✓ 200 |
|
||||
| db | Main store существует (от bootstrap) | ✓ Основной склад |
|
||||
|
||||
## ✓ Step step08_create_supply: Admin создаёт Supply Draft (3-5 товаров) и проводит (Posted)
|
||||
|
||||
Длительность: 2159мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Создано 3 product (валидный barcode + price + group) | ✓ e2e Product 1 1779776876113, e2e Product 2 1779776876113, e2e Product 3 1779776876113 |
|
||||
| api | Supply без supplierId → 400/409 | ✓ 400 {"error":"Поле SupplierId обязательно.","field":"SupplierId"} |
|
||||
| api | Supply с пустым lines[] → 400 | ✓ 400 |
|
||||
| api | POST /api/purchases/supplies (Draft) | ✓ 201 |
|
||||
| api | POST /api/purchases/supplies/{id}/post (Draft → Posted) | ✓ 204 |
|
||||
| api | Повторный post Supply → 409 (idempotency) | ✓ 409 {"error":"Документ уже проведён."} |
|
||||
| db | stock_movements содержат запись на каждую строку Supply | ✓ count=3, expected=3 |
|
||||
|
||||
## ✓ Step step09_check_stock_after_supply: GET /api/inventory/stock — quantity увеличился на supplied amount
|
||||
|
||||
Длительность: 771мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
||||
## ✓ Step step10_ensure_retail_point: Проверить или создать розничную точку (кассу)
|
||||
|
||||
Длительность: 126мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | RetailPoint существует | ✓ Касса 1 |
|
||||
| api | RetailPoint с несуществующим storeId → 400/404 | ✓ 400 |
|
||||
|
||||
## ✓ Step step11_create_retail_sale: Admin создаёт RetailSale, 2 позиции из приёмки, cash, Post
|
||||
|
||||
Длительность: 800мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Продажа qty>остатка → /post должен 4xx | ✓ 400 |
|
||||
| api | Продажа с отрицательным qty/price → 400 | ✓ 400 |
|
||||
| api | discount=10 на line(price=100,qty=1) → lineTotal=90 | ✓ lineTotal=90 |
|
||||
| api | POST /api/sales/retail (Draft) | ✓ 201 |
|
||||
| api | POST /retail/{id}/post | ✓ 204 |
|
||||
| api | Повторный post RetailSale → 409 | ✓ 409 |
|
||||
|
||||
## ✓ Step step12_check_stock_after_sale: GET /api/inventory/stock — quantity уменьшился на sold amount
|
||||
|
||||
Длительность: 694мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | stock product=5da542bb… −2 (было 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) |
|
||||
| db | stock_movements.Type = RetailSale (2) для sale документа | ✓ types=2 |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 12
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# E2E report: moysklad-import
|
||||
|
||||
Запущен: 2026-05-26T06:29:04.213Z
|
||||
Длительность: 10.6с
|
||||
|
||||
**Итог:** 7 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 7)
|
||||
|
||||
## ✓ Step step01_bootstrap_and_connect: Орг + mock MoySklad + сохранение токена (маскирование) + test-connection 200
|
||||
|
||||
Длительность: 1305мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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"} |
|
||||
|
||||
## ✓ Step step02_import_counterparties_create: Импорт контрагентов: job Succeeded, Created=2, маппинг полей верен
|
||||
|
||||
Длительность: 3930мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | import-counterparties → jobId | ✓ status=200 |
|
||||
| api | job Succeeded | ✓ status=Succeeded msg=Готово: 2 записей, 0 пропущено. |
|
||||
| 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 | Иванов: Type=2(Individual), Address=legalAddress (fallback) | ✓ Type=2 Addr=Астана Победы 5 |
|
||||
|
||||
## ✓ Step step03_import_counterparties_idempotent: Повторный импорт (overwrite=false): Skipped=2, дублей нет
|
||||
|
||||
Длительность: 907мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | job Succeeded | ✓ status=Succeeded |
|
||||
| api | Skipped=2, Created=0 (идемпотентность по имени) | ✓ created=0 skipped=2 |
|
||||
| db | Дублей нет — в БД по-прежнему 2 | ✓ count=2 |
|
||||
|
||||
## ✓ Step step04_import_counterparties_overwrite: Импорт overwrite=true с изменёнными данными: Updated=2, поля обновлены
|
||||
|
||||
Длительность: 1191мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | job Succeeded | ✓ status=Succeeded |
|
||||
| api | Updated=2, Created=0 | ✓ created=0 updated=2 skipped=0 |
|
||||
| db | Телефон Ромашки обновлён на новый | ✓ phone=+77011112299 |
|
||||
| db | Кол-во не выросло (обновление, не вставка) | ✓ count=2 |
|
||||
|
||||
## ✓ Step step05_import_products_create: Импорт товаров: Created=1, группа создана, маппинг (артикул/НДС/цена/штрихкод/группа/страна)
|
||||
|
||||
Длительность: 2178мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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 | 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 | 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 |
|
||||
|
||||
## ✓ Step step06_import_products_idempotent: Повторный импорт товаров (overwrite=false): Skipped=1, дублей нет
|
||||
|
||||
Длительность: 1134мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | job Succeeded | ✓ status=Succeeded |
|
||||
| api | Skipped=1, Created=0 (идемпотентность по артикулу) | ✓ created=0 skipped=1 |
|
||||
| db | Кол-во товаров не изменилось (дублей нет) | ✓ before=1 after=1 |
|
||||
|
||||
## ✓ Step step07_cleanup: Остановка mock-сервера
|
||||
|
||||
Длительность: 2мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Mock MoySklad остановлен | ✓ |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 7
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# E2E report: multi-tenant-isolation
|
||||
|
||||
Запущен: 2026-05-26T06:28:08.807Z
|
||||
Длительность: 3.6с
|
||||
|
||||
**Итог:** 12 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 12)
|
||||
|
||||
## ✓ Step step01_create_two_orgs: SuperAdmin создаёт две независимые орги Alpha и Beta (каждая со своим админом)
|
||||
|
||||
Длительность: 1218мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Создана Alpha | ✓ f37d8c51-9934-4d18-a285-7ba25dfc60be |
|
||||
| api | Создана Beta | ✓ c372ef64-2328-4a19-af3c-90a058f44012 |
|
||||
| api | orgId Alpha ≠ orgId Beta | ✓ |
|
||||
|
||||
## ✓ Step step02_login_both_admins: Логин под admin Alpha и admin Beta — получаем два разных org_id в JWT
|
||||
|
||||
Длительность: 792мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
||||
## ✓ Step step03_seed_data_in_alpha: Admin Alpha создаёт counterparty + product → запоминаем их ID
|
||||
|
||||
Длительность: 160мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Alpha создаёт counterparty | ✓ 8103d4ec-e35c-4fd4-8214-931921a86783 |
|
||||
| api | Alpha создаёт product | ✓ 647f7992-3df1-460c-845c-6ed68bc1903b |
|
||||
|
||||
## ✓ Step step04_beta_cannot_read_alpha: Admin Beta GET /api/catalog/counterparties/{alphaId} и /products/{alphaId} → 404
|
||||
|
||||
Длительность: 159мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Beta GET counterparties/{alphaId} → 404 | ✓ actual=404 |
|
||||
| api | Beta GET products/{alphaId} → 404 | ✓ actual=404 |
|
||||
|
||||
## ✓ Step step05_beta_cannot_list_alpha_data: Admin Beta GET /api/catalog/counterparties|/products → пустые списки (нет данных Alpha)
|
||||
|
||||
Длительность: 207мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Beta GET counterparties не содержит Alpha counterparty | ✓ всего=0, утечек=0 |
|
||||
| api | Beta GET products не содержит Alpha product | ✓ всего=0, утечек=0 |
|
||||
|
||||
## ✓ Step step06_beta_cannot_modify_alpha: Admin Beta PUT/DELETE /api/catalog/products/{alphaId} → 404 (не 200, не 403)
|
||||
|
||||
Длительность: 172мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Beta PUT products/{alphaId} с валидным телом → 404/403 | ✓ actual=404 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"trace |
|
||||
| api | Beta DELETE products/{alphaId} → 404/403 | ✓ actual=404 |
|
||||
|
||||
## ✓ Step step07_beta_cannot_link_to_alpha: Admin Beta POST product с DefaultSupplierId=alphaCounterpartyId → 400 (FK через query filter)
|
||||
|
||||
Длительность: 38мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Beta POST product с supplierId Alpha → 4xx | ✓ actual=400 |
|
||||
|
||||
## ✓ Step step08_beta_cannot_forge_org_override: Admin Beta с заголовком X-Org-Override:{alphaId} → запрос всё равно идёт от Beta (не SuperAdmin)
|
||||
|
||||
Длительность: 21мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Beta admin + X-Org-Override → 404/403 | ✓ actual=404 |
|
||||
|
||||
## ✓ Step step09_superadmin_sees_both: SuperAdmin без override GET /api/super-admin/organizations → видит и Alpha и Beta
|
||||
|
||||
Длительность: 18мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | SuperAdmin видит Alpha в списке орг | ✓ total=2 |
|
||||
| api | SuperAdmin видит Beta в списке орг | ✓ |
|
||||
|
||||
## ✓ Step step10_superadmin_readonly_override: SuperAdmin с X-Org-Override:{alphaId} → GET товаров Alpha (200), PUT/POST без reason → 403
|
||||
|
||||
Длительность: 30мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | SuperAdmin+override GET → 200 | ✓ actual=200 |
|
||||
| api | SuperAdmin+override PUT без reason → 403 | ✓ actual=403 |
|
||||
|
||||
## ✓ Step step11_superadmin_edit_override_with_reason: SuperAdmin с X-Org-Override + X-Org-Override-Reason → PUT 200 + запись в audit_log
|
||||
|
||||
Длительность: 504мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | SuperAdmin+override+reason PUT counterparty → 200/204 | ✓ actual=204 |
|
||||
| db | Counterparty.Name изменено в БД | ✓ name=edited-by-superadmin-1779776888807 |
|
||||
| db | super_admin_audit_log выросло | ✓ before=2 after=3 |
|
||||
|
||||
## ✓ Step step12_stock_isolation: Остатки Alpha и Beta не смешиваются — Supply в Alpha не появляется в /inventory/stock у Beta
|
||||
|
||||
Длительность: 267мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| 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=204 |
|
||||
| api | Beta /inventory/stock не содержит Alpha product | ✓ total=0, утечка=нет |
|
||||
| api | Beta /inventory/movements не содержит Alpha movement | ✓ total=0, утечка=нет |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 12
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
|
||||
## Logic gaps
|
||||
|
||||
- ProductsController.Put в режиме X-Org-Override роняет DbUpdateConcurrencyException при пересылке prices/barcodes — merge-логика не учитывает override-режим. Ремонт PUT product через override-консоль невозможен.
|
||||
75
tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md
Normal file
75
tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# E2E report: reports-stats
|
||||
|
||||
Запущен: 2026-05-26T06:28:57.128Z
|
||||
Длительность: 2.9с
|
||||
|
||||
**Итог:** 5 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 5)
|
||||
|
||||
## ✓ Step step01_bootstrap: Орг A + товар + приёмка (остаток под продажи)
|
||||
|
||||
Длительность: 1462мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Орг A с товаром и остатком готова | ✓ 86c52764-ba04-43c5-b069-52ff3197e2b6 |
|
||||
|
||||
## ✓ Step step02_stats_reflect_posted_sales: stats: RevenueToday/Transactions/AvgTicket = сумме проведённых чеков, серия непрерывна
|
||||
|
||||
Длительность: 333мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Проведено 3 чека | ✓ totals=600,500,400 |
|
||||
| api | GET stats → 200 | ✓ status=200 |
|
||||
| api | RevenueToday == Σ проведённых чеков | ✓ today=1500 expected=1500 |
|
||||
| api | TransactionsToday == 3 | ✓ tx=3 |
|
||||
| api | RevenueThisMonth == RevenueToday (все чеки сегодня) | ✓ month=1500 |
|
||||
| api | AvgTicketThisMonth == Revenue/Transactions | ✓ avg=500 expected=500.00 |
|
||||
| api | Серия длиной 30 дней (default) | ✓ len=30 |
|
||||
| api | Последний бакет серии (сегодня) == RevenueToday | ✓ last={rev:1500,tx:3} |
|
||||
|
||||
## ✓ Step step03_draft_sale_excluded: Черновик чека (не проведён) не попадает в stats
|
||||
|
||||
Длительность: 53мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Черновик чека создан (Status=Draft) | ✓ status=201 |
|
||||
| api | RevenueToday не изменился (черновик исключён) | ✓ today=1500 expected=1500 |
|
||||
| api | TransactionsToday остался 3 | ✓ tx=3 |
|
||||
|
||||
## ✓ Step step04_stats_tenant_isolated: stats орг A не видит продажи орг B и наоборот
|
||||
|
||||
Длительность: 1027мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Орг B готова | ✓ |
|
||||
| api | В орг B проведён 1 чек | ✓ total=600 |
|
||||
| api | stats орг A не включает продажу орг B | ✓ A.today=1500 A.tx=3 |
|
||||
| api | stats орг B == только её 1 чек | ✓ B.today=600 B.tx=1 |
|
||||
|
||||
## ✓ Step step05_days_param_and_gaps: Параметр days меняет длину серии; профит/ABC-отчёты отсутствуют (gap)
|
||||
|
||||
Длительность: 25мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | days=7 → серия длиной 7 | ✓ len=7 |
|
||||
| api | Отчётов /api/reports/* нет (ожидаемо 404) | ✓ profit=404 sales=404 |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 5
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
|
||||
## Logic gaps
|
||||
|
||||
- ТЗ 2.12: отчёт «прибыль» (выручка − себестоимость по Cost-снимку RetailSaleLine) не реализован — RetailSaleLine не хранит снимок себестоимости, /stats отдаёт только валовую выручку.
|
||||
- ТЗ 2.12: ABC-анализ, «остатки на дату» (SUM Movement до даты) и экспорт CSV/XLSX отсутствуют — отдельного ReportsController нет.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# E2E report: stock-concurrency
|
||||
|
||||
Запущен: 2026-05-26T06:28:48.064Z
|
||||
Длительность: 4.9с
|
||||
|
||||
**Итог:** 4 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 4)
|
||||
|
||||
## ✓ Step step01_bootstrap: Орг + товар + стартовая приёмка qty=5 @100 (Stock=5, Cost=100)
|
||||
|
||||
Длительность: 2198мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Bootstrap product создан | ✓ 1903eb84-9c8c-4335-9439-6d8fa6bd2102 |
|
||||
| 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
|
||||
|
||||
Длительность: 903мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Две приёмки-черновика созданы | ✓ |
|
||||
| db | Stock.Quantity == Σ StockMovement (invariant) | ✓ stock=25 sum=25 |
|
||||
| db | Stock == 25 (5 + 10 + 10, без потери приёмки) | ✓ stock=25 sum=25 statuses=204,409 |
|
||||
| db | Cost == 108 (взвешенное среднее по всем трём приёмкам) | ✓ cost=108 |
|
||||
|
||||
## ✓ Step step03_double_post_same_supply: Двойное проведение ОДНОЙ приёмки (7@100) одновременно → применяется один раз
|
||||
|
||||
Длительность: 1378мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Приёмка-черновик 7@100 создана | ✓ |
|
||||
| api | Не более одного успешного проведения | ✓ statuses=204,409 |
|
||||
| db | Stock вырос ровно на 7 (приёмка применена один раз) | ✓ before=25 after=32 |
|
||||
| db | Добавлено ровно одно StockMovement | ✓ +1 movements |
|
||||
| db | Stock.Quantity == Σ StockMovement (invariant) | ✓ stock=32 sum=32 |
|
||||
|
||||
## ✓ Step step04_final_invariant: Финальный инвариант Stock == Σ StockMovement
|
||||
|
||||
Длительность: 441мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| db | Финальный invariant Stock == Σ StockMovement | ✓ stock=32 sum=32 |
|
||||
|
||||
## Summary
|
||||
|
||||
- Passed: 4
|
||||
- Failed: 0
|
||||
- Warnings: 0
|
||||
- Skipped: 0
|
||||
|
||||
## Critical bugs
|
||||
|
||||
Нет.
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
# E2E report: stock-invariant-deep
|
||||
|
||||
Запущен: 2026-05-26T06:02:08.247Z
|
||||
Длительность: 6.5с
|
||||
Запущен: 2026-05-26T06:28:37.859Z
|
||||
Длительность: 5.9с
|
||||
|
||||
**Итог:** 10 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 10)
|
||||
|
||||
## ✓ Step step01_bootstrap: Орг + admin + product (стартовый остаток 0)
|
||||
|
||||
Длительность: 1555мс
|
||||
Длительность: 1776мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
| api | Bootstrap product создан | ✓ 54af3580-d626-42e0-92b1-cc70bb3fb90d |
|
||||
| api | Bootstrap product создан | ✓ d8c6c47a-dd41-48de-b0a0-9cc35769a8cb |
|
||||
| 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
|
||||
|
||||
Длительность: 969мс
|
||||
Длительность: 448мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
## ✓ Step step03_sale_a_qty_5: RetailSale A qty=5 → invariant stock=15, Σ movement=15
|
||||
|
||||
Длительность: 951мс
|
||||
Длительность: 456мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
## ✓ Step step04_supply_b_qty_10: Supply B qty=10 → invariant stock=25, Σ movement=25
|
||||
|
||||
Длительность: 514мс
|
||||
Длительность: 510мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
## ✓ Step step05_sale_b_qty_8: RetailSale B qty=8 → invariant stock=17, Σ movement=17
|
||||
|
||||
Длительность: 527мс
|
||||
Длительность: 467мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
|
||||
## ✓ Step step06_unpost_sale_a: Unpost RetailSale A → invariant stock=22, Σ movement=22
|
||||
|
||||
Длительность: 433мс
|
||||
Длительность: 475мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
## ✓ Step step07_repost_sale_a: Re-post RetailSale A → invariant stock=17, Σ movement=17
|
||||
|
||||
Длительность: 424мс
|
||||
Длительность: 485мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
## ✓ Step step08_movement_count_correct: Всего StockMovement по продукту = 6 строк (2 supply + 2 sale + reverse sale + repost sale)
|
||||
|
||||
Длительность: 182мс
|
||||
Длительность: 217мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
|
||||
## ✓ Step step09_concurrent_sales_serialized: Два POST /post одновременно на один остаток — один 200, второй 409
|
||||
|
||||
Длительность: 600мс
|
||||
Длительность: 627мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
## ✓ Step step10_final_invariant: Финальный invariant после всех операций сохраняется
|
||||
|
||||
Длительность: 369мс
|
||||
Длительность: 437мс
|
||||
|
||||
| Тип | Проверка | Результат |
|
||||
|---|---|---|
|
||||
64
tests/e2e/reports/systemic-2026-05-26.md
Normal file
64
tests/e2e/reports/systemic-2026-05-26.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Системное тестирование 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.
|
||||
> Запуск: `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. Две причины, обе закрыты:
|
||||
1. `AuthorizationController.Exchange` (refresh-ветка) строил новый principal с нуля и прокидывал только `AuthorizationId`, но не `TokenId`. Handler OpenIddict `RedeemTokenEntry` читает `TokenId` из подписываемого principal — без него старый refresh не помечался `Redeemed`.
|
||||
2. Даже после починки редемпшна 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` до коммита соседа) применяло остаток дважды — 2 `StockMovement`, но `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` некогерентны).
|
||||
Loading…
Reference in a new issue