food-market/docs/TZ-тестирование.md
nns 35d70c5d80 docs: ТЗ на доработку и тестирование (полный аудит 2026-05-22)
Два документа после полного обхода кодовой базы:
- docs/TZ-доработка.md — что нужно сделать, P0/P1/P2 приоритеты,
  дорожная карта по спринтам, технический долг
- docs/TZ-тестирование.md — сценарии тестирования по модулям,
  multi-tenant изоляция, регрессионный чек-лист, стратегия
  покрытия unit/integration/E2E

Сводка готовности: ядро (auth/catalog/Supply/RetailSale/multi-tenancy)
85-95%, но критичные пробелы: ОФД, складские документы Enter/Loss/
Transfer/Inventory, Demand, отчёты, POS-приложение, observability.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:30:04 +05:00

46 KiB
Raw Blame History

ТЗ на тестирование Food Market

Дата составления: 2026-05-22 Автор: Claude Opus 4.7 Документ парный к TZ-доработка.md. Описывает что и как проверять до и после релизов.


0. Принципы тестирования

0.1. Пирамида тестов

        ▲   E2E (Playwright, full-cycle)             ~ 5%
       ▲▲▲  API integration (axios + Testcontainers) ~ 25%
      ▲▲▲▲▲ Unit-тесты бизнес-логики (xUnit)         ~ 70%

Сейчас реальное соотношение 5% / 0% / 0% — это нужно перевернуть.

0.2. Уровни тестирования

Уровень Когда запускается Что проверяет
Smoke Каждый push, ≤2 мин Сервис стартует, /health отвечает 200, миграции применены, главные страницы открываются.
Регрессия Каждый PR, ≤10 мин Основные сценарии не сломаны: signup→login, создание продукта/приёмки/продажи, multi-tenant isolation.
E2E full-cycle Каждый merge в main, ≤30 мин Полный путь от создания организации до отчёта о продажах.
Нагрузочные Перед мажорными релизами 1000 одновременных пользователей, 10000 товаров, 50000 движений.
Безопасность Перед релизом + регулярно OWASP Top-10, рейт-лимит, multi-tenant утечки.

0.3. Когда считать «работает корректно»

  • Бэкенд: код возвращает ожидаемый HTTP-код, тело ответа валидно по схеме, побочные эффекты в БД соответствуют ожиданиям (StockMovement, Stock.Quantity).
  • Фронтенд: интерфейс отображает корректные данные, формы валидируются (HTML5 + onBlur + submit), ошибки сервера показываются человекочитаемо.
  • Безопасность: запрещённые операции возвращают 401/403/404 без утечки информации (не «такого пользователя нет», а «неверный логин/пароль»).
  • Мультитенант: пользователь org A никогда не видит и не изменяет данные org B (ни через UI, ни через прямой API).

0.4. Что считать багом

  • Любой 500 без log-entry с причиной.
  • Отрицательный остаток после провёденной продажи (overselling без контроля).
  • Несоответствие Stock.Quantity сумме StockMovement.Quantity по тому же (Product, Store).
  • Возможность увидеть/изменить данные другой организации.
  • Возможность залогиниться как заархивированный/удалённый сотрудник.
  • Email-flow signup/reset не работает (письма не доходят).

1. Приоритезация по критичности модулей

Приоритет Модули Что попадает
P0 (smoke + регрессия в каждом PR) Auth, Supply, RetailSale, Stock, Multi-tenancy Без этого не работает ничего.
P1 (регрессия перед релизом) Catalog (Products, Groups, Counterparties), SuperAdmin Console, MoySklad import, Permissions Без этого магазин не функционален.
P2 (один раз перед мажорным релизом + после изменений) Reports, Email, Health checks, UI-валидация, локализация чисел/дат Не блокирует, но влияет на UX.
P3 (точечно по требованию) Public site (Astro), CI/CD-сценарии Меняется реже.

2. Сценарии тестирования по модулям

2.1. Аутентификация (P0)

2.1.1. Регистрация новой организации (POST /api/auth/signup)

Сценарий Шаги Ожидание
Happy: новая орга POST с уникальным email, паролем 8+, org name, валидным KZ-телефоном (+7 7XX...). 201, в БД: новый Organization, User с ролью Admin, Employee, bootstrap-данные (Stores=1, Roles=4, Units, PriceTypes).
Email уже занят Повторный POST с тем же email. 400 «email уже занят» (без раскрытия деталей о существующей орге).
Слабый пароль (<8 симв.) POST с password="abc". 400, поле password в ошибках.
Невалидный телефон POST с phone="+79161234567" (РФ). 400 «введите корректный номер Казахстана».
Без согласия с офертой UI: не отметить чекбокс. Submit заблокирован, поле agree красное.
Email с лишними пробелами " user@example.kz ". Нормализация: trim, lowercase. 201.
Bootstrap полнота После регистрации: GET /api/catalog/stores → 1 основной склад; GET /api/organization/employee-roles → 4 системных роли (Admin/Manager/Storekeeper/Cashier). Соответствие.

2.1.2. Логин (POST /connect/token)

Сценарий Шаги Ожидание
Happy: правильный пароль grant_type=password, valid creds. 200, access_token (jwt), refresh_token, claims.org_id, claims.role.
Неверный пароль Wrong password. 400 OAuth error="invalid_grant", без раскрытия «такой email есть» vs «нет».
Заблокированный пользователь User.IsActive=false. 400, токен не выдан.
Архивированная организация Organization.IsArchived=true. 400, токен не выдан, claim org_id отсутствует или вход запрещён.
SuperAdmin без org User с ролью SuperAdmin, OrganizationId=null. 200, токен выдан, claim role="SuperAdmin".
Refresh token flow grant_type=refresh_token с действующим refresh. 200, новый access + refresh (sliding).
Истёкший access токен Запрос с истёкшим токеном. 401, фронт делает refresh автоматически.
Истёкший refresh Подождать >30 дней. 400 на refresh, форс-логаут на фронте.
Rate limit (после P0-3) 6+ login-попыток за минуту с одного IP. 429 Too Many Requests.

2.1.3. Forgot/Reset password

Сценарий Шаги Ожидание
Happy: запрос восстановления POST /api/auth/forgot-password с email существующего. 200 (всегда), на email приходит ссылка с токеном (1 час).
Несуществующий email POST с unknown@example. 200 (anti-enumeration), без email.
Сброс по токену POST /api/auth/reset-password с email+token+newPassword. 200, все refresh_token'ы revoke'нуты.
Просроченный токен (>1 ч) POST с истёкшим токеном. 400 «ссылка устарела».
Rate limit forgot 4+ запроса за час с одного IP. 429 «слишком много попыток».

2.2. Multi-tenancy и изоляция данных (P0, КРИТИЧНО)

2.2.1. Изоляция через UI

Сценарий Шаги Ожидание
Видимость списков Залогиниться в org A, создать товар «Хлеб». Залогиниться в org B. /catalog/products org B не содержит «Хлеб».
Переключение организаций Один email в двух организациях (если возможно — сейчас нет). (Не поддерживается — каждый User принадлежит одной org).

2.2.2. Изоляция через прямой API

Сценарий Шаги Ожидание
GET по GUID чужой орги Залогиниться в org A, узнать productId org B (например, через БД). GET /api/catalog/products/{productIdOrgB}. 404 (не 200). Query filter скрывает.
PUT по GUID чужой орги PUT /api/catalog/products/{productIdOrgB} с теми же данными. 404 (не 200, не 200 с записью в чужую).
DELETE по GUID чужой орги DELETE того же. 404.
POST с FK на чужую сущность POST Supply в org A с supplierId, принадлежащим org B. 400 «contact not found» (FK проверка через query filter).
Подделка org_id в JWT Сгенерировать токен с org_id чужой орги (без подписи). 401 (подпись не валидна).

2.2.3. SuperAdmin override

Сценарий Шаги Ожидание
SuperAdmin без override GET /api/super-admin/organizations. Видит все организации.
SuperAdmin с override (read) GET /api/catalog/products с заголовком X-Org-Override: <orgId>. Видит товары org X.
SuperAdmin с override (write) PUT /api/catalog/products/{id} с X-Org-Override: <orgId> без X-Org-Override-Reason. 403 «Read-only mode...».
SuperAdmin с override + reason PUT с обоими заголовками (X-Org-Override-Reason: "Customer support ticket #123, исправляем дубль штрихкода"). 200, запись в SuperAdminAuditLog.
Reason слишком короткий reason="ok". 403.
Обычный Admin с X-Org-Override Admin org A пытается подделать заголовок и выйти в org B. 403 «only SuperAdmin can override».

2.2.4. Глобальные справочники

Сценарий Шаги Ожидание
Country / Currency видны всем Login org A: GET /api/catalog/countries → видит. Login org B: GET → тот же список. Идентичные списки.
System ProductGroup видна всем SuperAdmin создаёт ProductGroup OrganizationId=null. Login любая org: видит. Видна, но не редактируется обычным Admin.
System UnitOfMeasure (ОКЕИ) SuperAdmin: GET /api/super-admin/units-of-measure → видит все global. Admin org A: GET /api/catalog/units-of-measure → видит только enabled через junction. Корректная фильтрация.

2.3. Каталог (P1)

2.3.1. Товары (Products)

Сценарий Шаги Ожидание
Создание минимального POST /api/catalog/products с name + unit. 201, автогенерация артикула (числовой), штрихкода нет.
Создание с штрихкодом EAN13 POST с barcode value="4607034630092", type=Ean13. 201, валидация контрольной цифры.
EAN13 с невалидной контрольной цифрой barcode="4607034630099". 400 «невалидный EAN13».
Дубль штрихкода в одной орге POST два товара с одинаковым штрихкодом. 409 на втором.
Дубль штрихкода в разных оргах Org A: barcode X. Org B: тот же barcode. 201 для обеих (per-tenant unique).
Цена по типам POST с prices: розничная=1000 KZT, оптовая=800 KZT. Записи в ProductPrice.
Обязательная розничная цена POST без розничной. 400 «требуется розничная цена» (если PriceType.IsRequired=true).
Артикул вручную POST с article="ABC-001". 201, без автогенерации.
Дубль артикула Два товара с article="ABC-001". 409.
Поиск по barcode GET /api/catalog/products/by-barcode/4607034630092. Возвращает товар.
Quick search GET /api/catalog/products/quick-search?q=хле. Ранжирование: exact barcode → article → name prefix → name contains.
Фильтр по группе GET ?groupId=X. Только товары группы X.
Фильтр по упаковке ?packaging=Weight. Только весовые.
Удаление товара с приёмками DELETE товара, на который есть SupplyLine. 409 «нельзя удалить, есть документы».
Удаление без документов DELETE свежесозданного. 204.
Изображения товара POST /api/catalog/products/{id}/images с jpg <10MB. 201, файл в /uploads, ImageUrl обновлён.
Загрузка > 10MB POST с 15MB файлом. 400.
Установка main image POST /images/{imageId}/main. Product.ImageUrl ← путь, IsMain переброшен.

2.3.2. Группы товаров (ProductGroups)

Сценарий Шаги Ожидание
Создание корневой POST с name="Хлебобулочные", parentId=null. 201, Path="Хлебобулочные".
Создание дочерней POST с parentId="", name="Хлеб". 201, Path="Хлебобулочные/Хлеб".
Циклическая ссылка PUT группы с parentId=своим id или потомка. 400 «цикл».
Удаление с подгруппами DELETE родителя у которого есть дети. 409.
Удаление с товарами DELETE группы с привязанными товарами. 409.
Системная группа SuperAdmin SuperAdmin POST с OrganizationId=null. Login org → видна, но Edit/Delete недоступны. Корректное поведение.
Markup percent Группа с MarkupPercent=20%. Cost=1000 → RecalcRetail = 1200. Round up до целых (по AllowFractionalPrices).

2.3.3. PriceTypes

Сценарий Шаги Ожидание
Только один IsRetail Создать два PriceType с IsRetail=true. 409 на втором.
IsSystem нельзя удалить DELETE PriceType с IsSystem=true. 409.
Переименование системного PUT name. 200 (можно).
Toggle IsRequired системного PUT IsRequired=false для системного. 409 (зафиксирован).

2.3.4. UnitsOfMeasure

Сценарий Шаги Ожидание
Enable global unit POST /api/catalog/units-of-measure/{id}/enable. 204, junction record создан.
Disable enabled unit с ссылками DELETE enable у unit'а, который используется товаром. 409 «нельзя — товары используют».
SuperAdmin: создать global unit POST /api/super-admin/units-of-measure с code="МЛ". 201.
SuperAdmin: удалить global unit с ссылками DELETE того же если ссылается товар. 409 со списком орг.

2.3.5. Counterparties

Сценарий Шаги Ожидание
Юрлицо с БИН POST с type=LegalEntity, bin="123456789012". 201.
БИН не 12 цифр bin="1234". 400.
Физлицо с ИИН type=Individual, iin="850101300123". 201.
Невалидный KZ телефон phone="+79161234567". 400.

2.4. Документы: Приёмки (P0)

Сценарий Шаги Ожидание
Создание Draft POST Supply с supplier, store, currency, 2 lines. 201, status=Draft, Total=sum(qty*price), Stock не изменился.
Posting POST /posts/{id}/post. Stock.Quantity += sum(qty). StockMovement +N записей с type=Supply. Cost пересчитан (weighted avg).
Cost weighted average До: Stock.Cost=100, qty=10. Приёмка: qty=10, price=120. После: Cost = (10100+10120)/20 = 110. Соответствие.
ReferencePrice при первой приёмке Product без приёмок. После Supply.Post: Product.ReferencePrice = UnitPrice. Соответствие.
Автонаценка розничной ProductGroup.MarkupPercent=30, товар без override розницы, Cost=100. После Post: ProductPrice (IsRetail) = 130. Соответствие.
Unpost POST /{id}/unpost на провёденной. StockMovement удалены/инвертированы, Stock.Quantity вернулся, status=Draft.
Edit проведённой PUT провёденной (status=Posted). 409 «нельзя редактировать проведённую».
Delete Draft DELETE Draft без посту. 204.
Delete проведённой DELETE Posted. 409.
Posting → отрицательный остаток после unpost Post Supply (+10), затем Sale (-15), затем Unpost Supply. 409 «нельзя расковать, остаток уйдёт в минус» (по дизайну).
FK на удалённого поставщика Supplier удалён (если бы это было возможно), Supply ссылается. Корректная обработка (запрет удаления поставщика или nullable).
Пустые lines POST с lines=[]. 400.
Quantity ≤ 0 line.qty=0 или -5. 400.
Параллельный Post одного Supply Два запроса /post одновременно. Один 200, второй 409 (concurrency-конфликт после P1 RowVersion).

2.5. Документы: Розничные продажи (P0)

Сценарий Шаги Ожидание
Создание Draft POST RetailSale с retailPoint, lines, payments. 201, status=Draft.
Posting с достаточным остатком Stock=10. POST с qty=5, /post. Stock=5, StockMovement type=RetailSale qty=-5.
Posting с overselling Stock=3. POST с qty=5, /post. 409 «недостаточно товара» (по фиксу из git history).
PaidCash + PaidCard ≠ Total Total=1000, paidCash=300, paidCard=500. 400 «суммы не сходятся».
Расчёт суммы со скидкой Line qty=2, price=500, discount=100. Subtotal=1000, DiscountTotal=100, Total=900. Соответствие.
VAT snapshot в строке На момент продажи Product.VatPercent=12, после Sale.Post страну поменяли → VAT=0. Старая RetailSaleLine.VatPercent = 12. Снимок цены/НДС сохраняется.
Cashier из другого RetailPoint Cashier привязан к RP1. POST RetailSale с retailPointId=RP2. 403 (после реализации Permission).
Unpost продажи POST /unpost. Stock возвращается, StockMovement инвертирован.
Stats эндпоинт GET /stats?days=30. Daily series 30 дней, revenueToday, transactionsToday, avgTicketThisMonth корректны.
Возврат по чеку (после P1-6) POST CustomerReturn ссылается на RetailSale. Stock возвращается, новый StockMovement type=CustomerReturn.

2.6. Остатки и движения (P0)

Сценарий Шаги Ожидание
Целостность Stock vs Movement После N операций: для любых (ProductId, StoreId) Stock.Quantity == SUM(StockMovement.Quantity). Соответствие (инвариант).
Reserved (Если реализуется в P1) Сейчас Reserved=0 везде. После резервирования через Demand: Reserved += qty.
Доступно (Available) Available = Quantity - Reserved. Корректно.
Фильтр includeZero=false Stock.Quantity=0. GET /stock?includeZero=false. Не включается.
Movements: пагинация 1000 движений. GET ?page=1&pageSize=50. total=1000, items=50.
Сортировка по occurredAt desc GET ?sort=-occurredAt. Свежие сверху.
Фильтр по storeId Два склада, по 10 movements в каждом. GET ?storeId=X. Только 10 из X.

2.7. Сотрудники и роли (P1)

2.7.1. CRUD сотрудников

Сценарий Шаги Ожидание
Создание без учётки POST с createAccount=false. 201, Employee.UserId=null.
Создание с учёткой POST с createAccount=true, email. 201, User создан с temp password, password возвращён в response (один раз).
Email обязателен при createAccount=true POST с email="" и createAccount=true. 400.
Дубль email Два сотрудника, одинаковый email при createAccount. 409.
Soft-delete (увольнение) DELETE employeeId. IsActive=false, FiredAt=now, IsDeleted=false. User блокируется.
Полное удаление (после увольнения) DELETE дважды (после Fired). IsDeleted=true, DeletedAt=now.
Архивный сотрудник в документах После soft-delete: старые Supply показывают «Иванов И. (удалён)». Подпись сохраняется.
Защита OwnerUser DELETE главного администратора (Organization.AccountOwnerUserId == Employee.UserId). 409 «только SuperAdmin».
Защита самого себя Залогинен Иванов, пытается DELETE сам себя. 409 «нельзя удалить себя».

2.7.2. Роли и Permission-based authz (после P0-5)

Сценарий Шаги Ожидание
Системные роли созданы После signup: 4-6 системных ролей (Admin, Manager, Storekeeper, Cashier, ...). IsSystem=true, не удаляются.
Кастомная роль POST роль "Менеджер по продажам" с permissions {productsView:true, suppliesView:true, all_else:false}. 201.
Permission DENY Employee с ролью "Менеджер по продажам" → POST Product. 403 «нет права ProductsEdit».
Permission ALLOW Тот же → GET /api/catalog/products. 200.
Изменение прав роли PUT permissions роли. Применяется ко всем сотрудникам этой роли (без revoke токенов).
Удаление роли с сотрудниками DELETE используемой роли. 409.
Cashier → RetailPoint Cashier с EmployeeRetailPointAssignment (RP1). POST RetailSale (RP1). 200. С RP2: 403.

2.8. SuperAdmin Console (P1)

Сценарий Шаги Ожидание
Создание организации POST /api/super-admin/organizations с org+admin данными. 201, temp password возвращён, organization в БД.
Аудит создания SELECT FROM super_admin_audit_log WHERE action_type='OrganizationCreated'. Запись с reason, before/after JSON.
Архивирование POST /{id}/archive с confirmationName=правильное имя org. IsArchived=true, ArchivedAt=now. Логины этой org перестают работать.
Архивирование с неверным confirmationName confirmationName="wrong". 400.
Восстановление POST /{id}/restore. IsArchived=false.
Hard delete после retention period Через SystemSettings.ArchiveRetentionDays=0, DELETE архивной. 204, организация физически удалена (CASCADE).
Hard delete до retention DELETE архивной до истечения. 409 «до удаления ещё N дней».
Смена владельца POST /change-owner с newOwnerUserId, reason. Organization.AccountOwnerUserId обновлён, запись в audit log.
Reason требуется POST /change-owner без reason. 400.
Reason < 10 символов POST с reason="ok". 400.
Audit log фильтр GET /audit-log?orgId=X&actionType=Y. Корректная фильтрация.
Audit log CSV экспорт Через UI кнопка «Экспорт». CSV-файл скачивается.

2.9. SuperAdmin Platform Settings — SMTP (P1)

Сценарий Шаги Ожидание
Сохранение SMTP PUT с host, port, ssl, username, newSmtpPassword="pass123". 200, hasSmtpPassword=true. Пароль в БД зашифрован.
Получение настроек GET. Все поля кроме password. password не возвращается.
Очистка пароля PUT с newSmtpPassword="clear". hasSmtpPassword=false.
Test send POST /test-send. Письмо приходит на адрес супер-админа.
Test send без настроек POST /test-send когда SMTP не настроен. 400 «SMTP не настроен».
Forgot password после настройки Юзер делает forgot-password → письмо со ссылкой приходит. Соответствие.

2.10. MoySklad интеграция (P1)

Сценарий Шаги Ожидание
Сохранение токена PUT /api/admin/moysklad/settings с token. Token сохранён в Organization, masked при GET.
Test connection POST /test с валидным токеном. 200, возвращает {organization, inn}.
Test с невалидным токеном POST /test с "abc". 400 или 401 с понятным сообщением.
Import counterparties POST /import/counterparties. 202, jobId возвращён.
Job progress polling GET /api/admin/jobs/{id} раз в 1.5 сек. Status: InProgress → Succeeded. Stage обновляется.
Импортированные контрагенты GET /api/catalog/counterparties после import. N новых контрагентов с правильными BIN, телефонами.
OverwriteExisting=true Повторный import с overwrite. Обновляются по name (case-insensitive), не дубли.
Импорт товаров POST /import/products. Товары + группы + штрихкоды + остатки импортированы.
Архивные товары МойСклад имеет archived товары. Импортируются в Product.IsArchived=true.
Прерывание job (после P1) Рестарт API во время импорта. Job маркируется как Failed, можно перезапустить.

2.11. Складские документы (P1, после реализации)

2.11.1. Оприходование (Enter)

Сценарий Шаги Ожидание
Создание/Post POST Enter с lines, /post. Stock += qty, StockMovement type=Enter.
Без поставщика POST без supplierId. 201 (Enter не требует supplier).

2.11.2. Списание (Loss)

Сценарий Шаги Ожидание
Списание брака POST Loss с reason="брак", lines с qty. Stock -= qty, StockMovement type=WriteOff.
Списание сверх остатка Stock=3, Loss qty=5. 409 «недостаточно».

2.11.3. Перемещение (Transfer)

Сценарий Шаги Ожидание
Между складами POST Transfer (FromStore=A, ToStore=B), lines. /post. Stock[A] -= qty, Stock[B] += qty. Два StockMovement (TransferOut + TransferIn).
Атомарность Если ToStore Stock записать не удалось (например, БД упала между). Транзакция откатывается, Stock[A] не изменён.

2.11.4. Инвентаризация (Inventory)

Сценарий Шаги Ожидание
Создание с импортом текущих POST Inventory storeId=X. InventoryLine на каждый товар склада X с bookQty=Stock.Quantity, actualQty=null.
Заполнение фактических PUT с actualQty по каждой строке. Diff = actual - book.
Post POST /post. StockMovement type=InventoryAdjustment с qty=diff. Stock актуализирован.
CSV-импорт фактических UI: загрузка CSV (sku, qty). Заполнение строк.

2.12. Отчёты (P1, после реализации)

Сценарий Шаги Ожидание
Отчёт по продажам GET /api/reports/sales?from=...&to=...&groupBy=day. Series выручки по дням.
Отчёт по продажам с фильтром по кассиру ?cashierId=X. Только продажи этого кассира.
Отчёт «остатки на дату» GET /stock?date=2026-04-01. Stock восстановлен через SUM(Movement WHERE occurredAt <= date).
Отчёт «прибыль» GET /profit?from=...&to=... Выручка - себестоимость (использует Cost snapshot из RetailSaleLine).
ABC-анализ GET /abc?metric=revenue&period=last_quarter. Топ-20% товаров (группа A), следующие 30% (B), остаток (C).
Экспорт CSV/XLSX UI: кнопка «Экспорт». Скачивается файл с теми же данными.
Большой объём 100k продаж в периоде. <5 секунд ответ (с агрегацией на стороне БД).

2.13. POS Sync API (P1, после реализации)

Сценарий Шаги Ожидание
Initial sync POS первый раз: GET /api/pos/sync?since=0. Все товары, цены, остатки, контрагенты.
Incremental sync GET /api/pos/sync?since=. Только изменения после timestamp.
Batch upload sales POST /api/pos/sales [{idempotencyKey, ...}, ...]. Все продажи провёдены, idempotency повторных вызовов.
Idempotency POST с тем же idempotencyKey дважды. Второй вызов возвращает оригинальный результат, без дубля.
Offline → online POS работает offline 1 час, накопил 20 продаж. После online: upload. Все 20 синхронизированы корректно.
Conflict: товар удалён POS отправил продажу товара, который SuperAdmin удалил. 409, POS откатывает локально или маркирует как ошибку.

2.14. Web-админка UI/UX (P2)

Сценарий Что проверить
Темная тема Все 35 страниц — переключение dark mode через кнопку. Без артефактов, контраст AAA.
Адаптив Каждая страница на мобильном (375px), планшете (768px), десктопе (1280px).
onBlur валидация форм После только что внедрённого ФЛК (см. git ff44afc): SignupForm, LoginPage, ResetPasswordPage, ForgotPasswordPage, EmployeesPage, CounterpartiesPage, StoresPage, PriceTypesPage, EmployeeRolesPage, RetailPointsPage, OrganizationSettingsPage, SuperAdminOrgCreatePage. Каждое поле показывает ошибку при потере фокуса.
onChange сбрасывает ошибку После показа ошибки, начать вводить — ошибка должна убираться.
Обработка 401 в axios Истёкший токен → автоматический refresh → повторный запрос.
Обработка 403 Понятная страница «нет прав».
Обработка 500 Не белый экран, а toast/alert.
Loading states Спиннеры/skeleton'ы на всех таблицах при загрузке.
Empty state Пустые списки показывают «Нет данных», а не пустую таблицу.
Pagination Корректные счётчики, переходы. На последней странице нет «next».
Sticky header Заголовок таблицы остаётся при скролле длинных списков.
Локализация Все строки на русском. Числа в toLocaleString('ru-KZ') (1 234,56). Даты в dd.MM.yyyy.

2.15. Public site (Astro) (P3)

Сценарий Что проверить
Маршруты /, /pricing, /features, /pos, /import, /integrations, /about, /contacts, /signup, /blog/[slug], /kb/[slug], /legal/[slug] — все открываются.
SignupForm на /signup Заполнить корректно → редирект на admin.food-market.kz с токенами.
Tariff builder на /pricing Изменение количества касс/складов → перерасчёт стоимости.
SEO Каждая страница имеет title, description, canonical, og-image.
sitemap.xml GET /sitemap.xml → валидный XML со всеми статичными страницами.
robots.txt GET /robots.txt → ожидаемое содержимое.
Lighthouse Performance 90+, Accessibility 95+, Best Practices 95+, SEO 100.

2.16. Infrastructure / DevOps (P2)

Сценарий Что проверить
/health 200 OK, JSON {status, time}.
/health/ready (после P0-4) 200 если БД и миграции OK. 503 если БД упала.
CI: build на push Зелёный pipeline на каждый push в main.
CI: deploy on push api После push в src/food-market.api/* → docker-api workflow → деплой на stage → smoke /health → 200.
Backup script Запуск deploy/backup.sh local. Файл *.sql.gz создан, размер > 0.
Backup restore Восстановить из бэкапа в чистую БД, поднять API, проверить логин.
Postgres healthcheck в compose docker-compose ps → postgres healthy.
Persistent volumes После docker compose down && up данные сохраняются.
Persistent OpenIddict ключ После рестарта API: refresh_token продолжает работать.
Логи Serilog tail -f /app/logs/food-market-*.log — структурированные строки.
Ротация логов После 14 дней старые логи удаляются.
Локальный docker registry curl 127.0.0.1:5001/v2/_catalog → список образов.

2.17. Безопасность (P0+P1)

Сценарий Что проверить
HTTPS-only (после P0-2) HTTP → 301 redirect на HTTPS. HSTS-header установлен.
JWT signature tampering Изменить body токена, не подписать.
JWT expired Использовать токен старше 1 часа.
SQL injection Поиск со значением '; DROP TABLE products;--.
XSS в формах Создать товар с name=<script>alert(1)</script>.
CSRF на /connect/token Cookie-based auth не используется, CSRF неактуален.
CORS Запрос из http://evil.com → блокирован.
Path traversal в /uploads GET /uploads/../../etc/passwd.
Unauthenticated endpoints Список AllowAnonymous: /health, /api/auth/signup, /api/auth/forgot-password, /connect/token, /uploads/*.
Rate limit login (после P0-3) 10 попыток за минуту → 429.
Перебор email на signup 100 signup-запросов с разными email — должен 429 после 20.
Утечка через 404 vs 403 GET /api/catalog/products/<существующий-чужого-tenant> возвращает 404, не 403 (чтобы не подтверждать существование).

2.18. Производительность (P2)

Сценарий Цель
GET /api/catalog/products при 10k товаров < 500 мс с пагинацией pageSize=50.
POST Supply.Post с 100 lines < 2 сек.
GET /api/inventory/movements при 100k записей < 1 сек с пагинацией.
Quick search в каталоге < 200 мс при 10k товаров.
Dashboard /stats < 500 мс при 100k продаж.
N+1 запросы EF Profiler / Serilog log: на GET /products нет N+1 для prices/barcodes.
Concurrent signup 50 параллельных signup → все 201, БД консистентна.

3. Регрессионный чек-лист (перед каждым релизом)

[ ] Все миграции применяются на чистую БД
[ ] Smoke: GET /health → 200
[ ] Smoke: signup → новая org → login → /api/me → roles[admin]
[ ] Регрессия: создать товар → создать приёмку → /post → Stock обновился
[ ] Регрессия: создать продажу → /post → Stock уменьшился, чек создан
[ ] Регрессия: запрос с другого org_id → 404
[ ] Регрессия: SuperAdmin без override видит все организации
[ ] Регрессия: SuperAdmin с override read-only — мутация 403
[ ] Регрессия: SuperAdmin с reason — мутация 200, audit log запись
[ ] MoySklad: test connection → 200
[ ] Email: forgot password → письмо приходит
[ ] UI: все 35 страниц открываются, onBlur валидация работает
[ ] CI: docker-api workflow зелёный, smoke на /health → 200
[ ] Backup за последние сутки существует, размер > предыдущего > 50%
[ ] Логи за последний час: нет 5xx без log-entry
[ ] Метрики (после P1-17): error_rate < 1%, p95 latency < 1s

4. Стратегия покрытия тестами

4.1. Unit-тесты (xUnit, цель 70% coverage критичной логики)

food-market.tests.unit/
├── Domain/
│   ├── ProductTests.cs               — валидация конструктора, computed properties
│   ├── StockMovementTests.cs         — invariants
│   └── RolePermissionsTests.cs       — расчёт прав
├── Application/                       — после миграции на MediatR
│   ├── SuppliesPostHandlerTests.cs   — Cost weighted average, авто-наценка
│   ├── RetailSalePostHandlerTests.cs — Stock update, overselling check
│   └── ImportJobsTests.cs            — состояния job'а
└── Infrastructure/
    ├── TenantFilterTests.cs           — query filter включается/исключается правильно
    ├── StockServiceTests.cs           — атомарность ApplyMovement
    └── MoySkladClientTests.cs        — pagination, error handling (с mock HttpMessageHandler)

4.2. Integration-тесты (Testcontainers.PostgreSql + WebApplicationFactory)

food-market.tests.integration/
├── Auth/
│   ├── SignupFlowTests.cs            — POST /signup → bootstrap data
│   └── LoginFlowTests.cs             — token + refresh + revoke
├── Catalog/
│   ├── ProductsCrudTests.cs
│   └── ProductGroupsHierarchyTests.cs
├── Documents/
│   ├── SupplyPostUnpostTests.cs      — Stock consistency
│   └── RetailSalePostTests.cs        — overselling 409
├── Tenancy/
│   ├── MultiTenantIsolationTests.cs  — org A не видит org B
│   └── SuperAdminOverrideTests.cs    — read-only / edit mode
└── Authorization/
    └── PermissionBasedAuthzTests.cs   — после P0-5

4.3. E2E (расширить существующий full-cycle)

tests/e2e/scenarios/
├── full-cycle.yml                    — текущий: signup → import → supply → sale → report
├── multi-tenant-isolation.yml         — два юзера, два org, попытки кросс-доступа
├── superadmin-flow.yml               — создание org, архив, реор, edit-mode
├── permission-checks.yml              — кастомные роли, разрешения/отказы
└── moysklad-sync.yml                  — конец-в-конец импорт + проверки в БД

5. Инструменты тестирования

Инструмент Назначение Готовность
xUnit + FluentAssertions Backend unit-тесты нет (нужно завести проект)
Testcontainers.PostgreSql Integration-тесты с реальной БД в Docker нет
WebApplicationFactory In-memory test server нет
Playwright E2E браузерные тесты есть (через tests/e2e/run.sh full-cycle)
axios + pg (TS) E2E API + DB-проверки есть (tests/e2e/lib/)
Bombardier / k6 Нагрузочное тестирование нет
OWASP ZAP Сканер уязвимостей нет (рекомендуется использовать перед prod)
Lighthouse CI Public site performance нет
Codecov / Coverlet Coverage report нет

6. Метрики качества тестирования

Цели после внедрения P1-20, P1-21:

  Unit-тесты бизнес-логики:    ≥ 70%  (сейчас 0%)
  Integration-тесты API:        ≥ 60%  (сейчас 0%)
  E2E сценариев:                ≥ 5    (сейчас 1)
  Время прогона полного CI:    ≤ 15 мин
  Время smoke в PR:             ≤ 2 мин

Регрессионный SLA после релиза:
  Severity 1 (блокирующие): обнаружение → фикс ≤ 2 часа
  Severity 2 (важные):       обнаружение → фикс ≤ 1 рабочий день
  Severity 3 (косметика):    в плановый спринт

7. Финальный чек-лист «готовности к продакшен»

Прежде чем сказать «можно запускать прод»:

БЕЗОПАСНОСТЬ
[ ] HTTPS forced, HSTS установлен
[ ] OpenIddict prod-сертификаты
[ ] Rate limiting на login/signup
[ ] Permission-based authorization работает
[ ] Multi-tenant изоляция проверена (см. п. 2.2)
[ ] OWASP Top-10 сканер пройден

ФУНКЦИОНАЛ
[ ] Складские документы (Enter/Loss/Transfer/Inventory) реализованы
[ ] Отчёты (Sales/Stock/Profit/ABC) реализованы
[ ] ОФД фискализация чеков работает
[ ] Email-нотификации настроены и тестированы

ИНФРАСТРУКТУРА
[ ] Backup автоматический (cron/timer)
[ ] Restore-сценарий отрепетирован
[ ] Health checks детальные (ready/live)
[ ] Метрики Prometheus + Grafana dashboard
[ ] Алерты на error_rate, latency p95

ПРОЦЕССЫ
[ ] CI зелёный на main
[ ] Coverage > 70% unit, > 60% integration
[ ] E2E full-cycle зелёный
[ ] Regression checklist пройден
[ ] Release notes написаны
[ ] Документация .env.example актуальна
[ ] Rollback plan описан