food-market/tests/e2e/lib/db.ts
nns 7bb941259a
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 1m20s
CI / Web (React + Vite) (push) Successful in 42s
feat(e2e): infrastructure + first full-cycle scenario + baseline report
Декларативные end-to-end сценарии в tests/e2e/. YAML описывает шаги,
TypeScript-handler — конкретные API/UI/DB-проверки. Отчёт в Markdown.

Структура:
- runner.ts        : entry, парсит YAML, прогоняет steps, пишет report
- run.sh           : pnpm install + tsx
- lib/api.ts       : axios + login() (через /connect/token + /api/me)
- lib/db.ts        : docker exec psql, resetTenantData(), countRows()
- lib/report.ts    : Markdown-аккумулятор (steps + bugs + ux + gap + perf)
- scenarios/full-cycle.yml       : 12 шагов
- scenarios/full-cycle.steps.ts  : handlers (один на шаг)
- README.md        : как добавить новый сценарий

reset_db в preconditions:
- TRUNCATE tenant-таблиц CASCADE
- AspNet*/users — оставляем только admin@food-market.local
- OpenIddict tokens — все valid → revoked
- Реестр products + системные справочники + миграции + platform_settings — НЕ трогаем

Запуск: tests/e2e/run.sh full-cycle [--api-only]

Первый прогон (--api-only, baseline в reports/full-cycle-2026-05-07-baseline.md):
- 8 ✓ / 1 ✗ / 3 ◯ из 12.
- Critical bug: Cashier видит /api/organization/employees через API
  (нет [Authorize(Roles="Admin")] на List endpoint).
- High: при CreateOrg через SuperAdmin не сидируются tenant-units —
  пустой каталог измерений у новой org (DevDataSeeder.SeedTenantReferencesAsync
  должен вызываться, но не вызывается).
- Logic gaps: реестр products tenant-scoped и новая org стартует с
  пустым каталогом; SuperAdmin /organizations не валидирует ФЛК
  телефона; Cashier не получает Identity-роль "Cashier" при создании
  через /employees.

UI-шаги (Playwright) в этом коммите не покрыты — runner работает в
--api-only режиме. UI-extension добавим следующим коммитом, не блокирует
получение полезного отчёта.
2026-05-08 00:05:52 +05:00

103 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Прямой доступ к Postgres для preconditions (TRUNCATE) и data-checks
* (SELECT). Работает через docker exec на контейнер food-market-postgres,
* так что не зависит от того, проброшен ли порт наружу.
*/
import { execFileSync } from 'node:child_process'
export function psql(sql: string): string {
// Использую pgcli/psql внутри контейнера. -tA = bare output без заголовков.
return execFileSync(
'docker',
['exec', '-i', 'food-market-postgres', 'psql', '-U', 'food_market', '-d', 'food_market', '-tAv', 'ON_ERROR_STOP=1', '-c', sql],
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 },
)
}
export function psqlRows(sql: string): string[][] {
const out = psql(sql).trim()
if (!out) return []
return out.split('\n').map((line) => line.split('|'))
}
/**
* TRUNCATE для preconditions full-cycle сценария.
*
* Реестр товаров и системные справочники — не трогаем. Платформенные настройки,
* миграции и OpenIddict — не трогаем. Identity SuperAdmin (admin@food-market.local)
* сохраняем; всех остальных AspNetUsers/UserRoles/Employees/Organizations
* чистим.
*/
export function resetTenantData(): void {
// Список таблиц получаем из информационной схемы — устойчиво к новым доменным
// сущностям. Исключаем «keep-list» по подстроке (lowercase). Ниже — порядок
// вырезания: сначала зависимые таблицы, потом ссылочные.
const KEEP = new Set([
// Реестр товаров
'products', 'product_groups', 'units_of_measure', 'product_packagings',
'product_barcodes', 'product_prices', 'product_images',
// Справочники
'countries', 'currencies', 'price_types', 'employee_roles',
// Инфраструктура
'__EFMigrationsHistory', 'platform_settings', 'system_settings',
])
const KEEP_PREFIXES = ['OpenIddict']
const all = psqlRows(`SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename`)
.map((r) => r[0])
.filter(Boolean)
// Identity-табицы (AspNet*, users, roles) обрабатываем отдельно с фильтром
// SuperAdmin — нельзя просто TRUNCATE.
const IDENTITY = ['AspNetUserClaims', 'AspNetUserLogins', 'AspNetUserTokens', 'AspNetUserRoles', 'AspNetRoleClaims', 'users', 'roles']
const isIdentity = (t: string) => IDENTITY.includes(t)
const tenantTables = all
.filter((t) => !KEEP.has(t))
.filter((t) => !KEEP_PREFIXES.some((p) => t.startsWith(p)))
.filter((t) => !isIdentity(t))
if (tenantTables.length > 0) {
// Bulk TRUNCATE с CASCADE — порядок не важен.
const list = tenantTables.map((t) => `"${t}"`).join(', ')
psql(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`)
}
// Identity-зачистка: оставляем только SuperAdmin'а.
psql(`
DELETE FROM "AspNetUserRoles" ur
USING users u
WHERE ur."UserId" = u."Id"
AND lower(u."Email") <> 'admin@food-market.local';
DELETE FROM "AspNetUserClaims" uc
USING users u
WHERE uc."UserId" = u."Id"
AND lower(u."Email") <> 'admin@food-market.local';
DELETE FROM "AspNetUserLogins" ul
USING users u
WHERE ul."UserId" = u."Id"
AND lower(u."Email") <> 'admin@food-market.local';
DELETE FROM "AspNetUserTokens" ut
USING users u
WHERE ut."UserId" = u."Id"
AND lower(u."Email") <> 'admin@food-market.local';
DELETE FROM users WHERE lower("Email") <> 'admin@food-market.local';
-- SuperAdmin отвязываем от любой удалённой org.
UPDATE users SET "OrganizationId" = NULL, "IsActive" = true
WHERE lower("Email") = 'admin@food-market.local';
-- OpenIddict tokens деактивируем (refresh-tokens чужих юзеров).
UPDATE "OpenIddictTokens" SET "Status" = 'revoked' WHERE "Status" = 'valid';
`)
}
export function countRows(table: string, where = ''): number {
const sql = `SELECT count(*) FROM "${table}" ${where ? 'WHERE ' + where : ''};`
const out = psql(sql).trim()
return Number(out) || 0
}