From 6098c03e1aa2913044d5310ddeab98b527dadf7a Mon Sep 17 00:00:00 2001 From: nns Date: Tue, 26 May 2026 12:05:23 +0500 Subject: [PATCH] =?UTF-8?q?docs(e2e):=20=D0=B8=D1=82=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D0=BE=D1=82=D1=87=D1=91=D1=82=202026-05-2?= =?UTF-8?q?6=20=E2=80=94=2015=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B5=D0=B2=20=D0=B7=D0=B5=D0=BB=D1=91=D0=BD=D1=8B=D0=B5=20(12?= =?UTF-8?q?4=20=D1=88=D0=B0=D0=B3=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Полная регрессия всех сценариев + 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 --- ... => auth-edge-2026-05-26T07-02-25-461Z.md} | 24 ++-- .../auth-password-2026-05-26T07-02-30-329Z.md | 65 +++++++++++ ... catalog-edge-2026-05-26T07-02-35-811Z.md} | 28 ++--- ...ocuments-edge-2026-05-26T07-02-17-822Z.md} | 38 +++--- .../employees-2026-05-26T07-03-24-714Z.md | 110 ++++++++++++++++++ ...=> full-cycle-2026-05-26T07-02-03-862Z.md} | 56 ++++----- ...ysklad-import-2026-05-26T07-03-16-516Z.md} | 26 ++--- ...ant-isolation-2026-05-26T07-02-11-129Z.md} | 44 +++---- .../platform-smtp-2026-05-26T07-03-43-053Z.md | 71 +++++++++++ ...reports-stats-2026-05-26T07-03-01-171Z.md} | 16 +-- .../reports/roles-2026-05-26T07-03-30-039Z.md | 91 +++++++++++++++ .../security-edge-2026-05-26T07-03-49-977Z.md | 67 +++++++++++ ...k-concurrency-2026-05-26T07-02-53-977Z.md} | 12 +- ...nvariant-deep-2026-05-26T07-02-45-558Z.md} | 26 ++--- ...radmin-console-2026-05-26T07-03-38-197Z.md | 85 ++++++++++++++ tests/e2e/reports/systemic-2026-05-26.md | 94 ++++++++------- 16 files changed, 678 insertions(+), 175 deletions(-) rename tests/e2e/reports/{auth-edge-2026-05-26T06-28-29-427Z.md => auth-edge-2026-05-26T07-02-25-461Z.md} (87%) create mode 100644 tests/e2e/reports/auth-password-2026-05-26T07-02-30-329Z.md rename tests/e2e/reports/{catalog-edge-2026-05-26T06-28-35-003Z.md => catalog-edge-2026-05-26T07-02-35-811Z.md} (90%) rename tests/e2e/reports/{documents-edge-2026-05-26T06-28-21-130Z.md => documents-edge-2026-05-26T07-02-17-822Z.md} (75%) create mode 100644 tests/e2e/reports/employees-2026-05-26T07-03-24-714Z.md rename tests/e2e/reports/{full-cycle-2026-05-26T06-28-06-463Z.md => full-cycle-2026-05-26T07-02-03-862Z.md} (82%) rename tests/e2e/reports/{moysklad-import-2026-05-26T06-29-16-433Z.md => moysklad-import-2026-05-26T07-03-16-516Z.md} (89%) rename tests/e2e/reports/{multi-tenant-isolation-2026-05-26T06-28-13-936Z.md => multi-tenant-isolation-2026-05-26T07-02-11-129Z.md} (79%) create mode 100644 tests/e2e/reports/platform-smtp-2026-05-26T07-03-43-053Z.md rename tests/e2e/reports/{reports-stats-2026-05-26T06-29-01-563Z.md => reports-stats-2026-05-26T07-03-01-171Z.md} (91%) create mode 100644 tests/e2e/reports/roles-2026-05-26T07-03-30-039Z.md create mode 100644 tests/e2e/reports/security-edge-2026-05-26T07-03-49-977Z.md rename tests/e2e/reports/{stock-concurrency-2026-05-26T06-28-54-456Z.md => stock-concurrency-2026-05-26T07-02-53-977Z.md} (89%) rename tests/e2e/reports/{stock-invariant-deep-2026-05-26T06-28-45-322Z.md => stock-invariant-deep-2026-05-26T07-02-45-558Z.md} (88%) create mode 100644 tests/e2e/reports/superadmin-console-2026-05-26T07-03-38-197Z.md diff --git a/tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md b/tests/e2e/reports/auth-edge-2026-05-26T07-02-25-461Z.md similarity index 87% rename from tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md rename to tests/e2e/reports/auth-edge-2026-05-26T07-02-25-461Z.md index 8762e1c..a65ccbd 100644 --- a/tests/e2e/reports/auth-edge-2026-05-26T06-28-29-427Z.md +++ b/tests/e2e/reports/auth-edge-2026-05-26T07-02-25-461Z.md @@ -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 -Длительность: 49мс +Длительность: 39мс | Тип | Проверка | Результат | |---|---|---| @@ -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 diff --git a/tests/e2e/reports/auth-password-2026-05-26T07-02-30-329Z.md b/tests/e2e/reports/auth-password-2026-05-26T07-02-30-329Z.md new file mode 100644 index 0000000..3f9a08d --- /dev/null +++ b/tests/e2e/reports/auth-password-2026-05-26T07-02-30-329Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md b/tests/e2e/reports/catalog-edge-2026-05-26T07-02-35-811Z.md similarity index 90% rename from tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md rename to tests/e2e/reports/catalog-edge-2026-05-26T07-02-35-811Z.md index d04586f..e504d8e 100644 --- a/tests/e2e/reports/catalog-edge-2026-05-26T06-28-35-003Z.md +++ b/tests/e2e/reports/catalog-edge-2026-05-26T07-02-35-811Z.md @@ -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 -Длительность: 37мс +Длительность: 47мс | Тип | Проверка | Результат | |---|---|---| @@ -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мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md b/tests/e2e/reports/documents-edge-2026-05-26T07-02-17-822Z.md similarity index 75% rename from tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md rename to tests/e2e/reports/documents-edge-2026-05-26T07-02-17-822Z.md index c2ec60b..5801448 100644 --- a/tests/e2e/reports/documents-edge-2026-05-26T06-28-21-130Z.md +++ b/tests/e2e/reports/documents-edge-2026-05-26T07-02-17-822Z.md @@ -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 -Длительность: 40мс +Длительность: 50мс | Тип | Проверка | Результат | |---|---|---| @@ -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=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мс | Тип | Проверка | Результат | |---|---|---| 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-26T07-02-11-129Z.md similarity index 79% rename from tests/e2e/reports/multi-tenant-isolation-2026-05-26T06-28-13-936Z.md rename to tests/e2e/reports/multi-tenant-isolation-2026-05-26T07-02-11-129Z.md index c18204b..200fc52 100644 --- a/tests/e2e/reports/multi-tenant-isolation-2026-05-26T06-28-13-936Z.md +++ b/tests/e2e/reports/multi-tenant-isolation-2026-05-26T07-02-11-129Z.md @@ -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) -Длительность: 21мс +Длительность: 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, утечка=нет | diff --git a/tests/e2e/reports/platform-smtp-2026-05-26T07-03-43-053Z.md b/tests/e2e/reports/platform-smtp-2026-05-26T07-03-43-053Z.md new file mode 100644 index 0000000..e056108 --- /dev/null +++ b/tests/e2e/reports/platform-smtp-2026-05-26T07-03-43-053Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md b/tests/e2e/reports/reports-stats-2026-05-26T07-03-01-171Z.md similarity index 91% rename from tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md rename to tests/e2e/reports/reports-stats-2026-05-26T07-03-01-171Z.md index 7d604f1..e2e87bd 100644 --- a/tests/e2e/reports/reports-stats-2026-05-26T06-29-01-563Z.md +++ b/tests/e2e/reports/reports-stats-2026-05-26T07-03-01-171Z.md @@ -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мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/roles-2026-05-26T07-03-30-039Z.md b/tests/e2e/reports/roles-2026-05-26T07-03-30-039Z.md new file mode 100644 index 0000000..045172f --- /dev/null +++ b/tests/e2e/reports/roles-2026-05-26T07-03-30-039Z.md @@ -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»). diff --git a/tests/e2e/reports/security-edge-2026-05-26T07-03-49-977Z.md b/tests/e2e/reports/security-edge-2026-05-26T07-03-49-977Z.md new file mode 100644 index 0000000..4a3836e --- /dev/null +++ b/tests/e2e/reports/security-edge-2026-05-26T07-03-49-977Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md b/tests/e2e/reports/stock-concurrency-2026-05-26T07-02-53-977Z.md similarity index 89% rename from tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md rename to tests/e2e/reports/stock-concurrency-2026-05-26T07-02-53-977Z.md index 1317fae..09d35ce 100644 --- a/tests/e2e/reports/stock-concurrency-2026-05-26T06-28-54-456Z.md +++ b/tests/e2e/reports/stock-concurrency-2026-05-26T07-02-53-977Z.md @@ -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 -Длительность: 903мс +Длительность: 873мс | Тип | Проверка | Результат | |---|---|---| @@ -29,7 +29,7 @@ ## ✓ Step step03_double_post_same_supply: Двойное проведение ОДНОЙ приёмки (7@100) одновременно → применяется один раз -Длительность: 1378мс +Длительность: 1373мс | Тип | Проверка | Результат | |---|---|---| @@ -41,7 +41,7 @@ ## ✓ Step step04_final_invariant: Финальный инвариант Stock == Σ StockMovement -Длительность: 441мс +Длительность: 415мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md b/tests/e2e/reports/stock-invariant-deep-2026-05-26T07-02-45-558Z.md similarity index 88% rename from tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md rename to tests/e2e/reports/stock-invariant-deep-2026-05-26T07-02-45-558Z.md index cfee88d..77f478a 100644 --- a/tests/e2e/reports/stock-invariant-deep-2026-05-26T06-28-45-322Z.md +++ b/tests/e2e/reports/stock-invariant-deep-2026-05-26T07-02-45-558Z.md @@ -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мс +Длительность: 447мс | Тип | Проверка | Результат | |---|---|---| @@ -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мс | Тип | Проверка | Результат | |---|---|---| diff --git a/tests/e2e/reports/superadmin-console-2026-05-26T07-03-38-197Z.md b/tests/e2e/reports/superadmin-console-2026-05-26T07-03-38-197Z.md new file mode 100644 index 0000000..f6fa0ef --- /dev/null +++ b/tests/e2e/reports/superadmin-console-2026-05-26T07-03-38-197Z.md @@ -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 + +Нет. diff --git a/tests/e2e/reports/systemic-2026-05-26.md b/tests/e2e/reports/systemic-2026-05-26.md index 6946960..3e2e335 100644 --- a/tests/e2e/reports/systemic-2026-05-26.md +++ b/tests/e2e/reports/systemic-2026-05-26.md @@ -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 --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`.