diff --git a/tests/e2e/reports/auth-edge-2026-05-26T06-01-45-719Z.md b/tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md similarity index 85% rename from tests/e2e/reports/auth-edge-2026-05-26T06-01-45-719Z.md rename to tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md index f05b404..8762e1c 100644 --- a/tests/e2e/reports/auth-edge-2026-05-26T06-01-45-719Z.md +++ b/tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md @@ -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 diff --git a/tests/e2e/reports/catalog-edge-2026-05-26T06-02-05-506Z.md b/tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md similarity index 91% rename from tests/e2e/reports/catalog-edge-2026-05-26T06-02-05-506Z.md rename to tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md index 6cd15be..d04586f 100644 --- a/tests/e2e/reports/catalog-edge-2026-05-26T06-02-05-506Z.md +++ b/tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md @@ -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мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md b/tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md new file mode 100644 index 0000000..c2ec60b --- /dev/null +++ b/tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md @@ -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остатка → /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 + +Нет. diff --git a/tests/e2e/reports/moysklad-import-2026-05-26T06-29-16-433Z.md b/tests/e2e/reports/moysklad-import-2026-05-26T06-29-16-433Z.md new file mode 100644 index 0000000..435ff53 --- /dev/null +++ b/tests/e2e/reports/moysklad-import-2026-05-26T06-29-16-433Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/multi-tenant-isolation-2026-05-26T06-28-13-936Z.md b/tests/e2e/reports/multi-tenant-isolation-2026-05-26T06-28-13-936Z.md new file mode 100644 index 0000000..c18204b --- /dev/null +++ b/tests/e2e/reports/multi-tenant-isolation-2026-05-26T06-28-13-936Z.md @@ -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-консоль невозможен. diff --git a/tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md b/tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md new file mode 100644 index 0000000..7d604f1 --- /dev/null +++ b/tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md @@ -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 нет. diff --git a/tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md b/tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md new file mode 100644 index 0000000..1317fae --- /dev/null +++ b/tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-02-16-375Z.md b/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md similarity index 88% rename from tests/e2e/reports/stock-invariant-deep-2026-05-26T06-02-16-375Z.md rename to tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md index 47e972a..cfee88d 100644 --- a/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-02-16-375Z.md +++ b/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md @@ -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мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/systemic-2026-05-26.md b/tests/e2e/reports/systemic-2026-05-26.md new file mode 100644 index 0000000..6946960 --- /dev/null +++ b/tests/e2e/reports/systemic-2026-05-26.md @@ -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 --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` некогерентны).