Декларативные 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 добавим следующим коммитом, не блокирует получение полезного отчёта.
52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
import axios, { AxiosInstance } from 'axios'
|
|
import { Agent as HttpsAgent } from 'node:https'
|
|
|
|
export const ADMIN_BASE = process.env.E2E_ADMIN_URL ?? 'https://admin.food-market.kz'
|
|
|
|
const httpsAgent = new HttpsAgent({ rejectUnauthorized: false })
|
|
|
|
export interface AuthSession {
|
|
accessToken: string
|
|
refreshToken?: string
|
|
email: string
|
|
roles: string[]
|
|
orgId: string | null
|
|
}
|
|
|
|
export function makeClient(token?: string): AxiosInstance {
|
|
return axios.create({
|
|
baseURL: ADMIN_BASE,
|
|
httpsAgent,
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
validateStatus: () => true, // не кидать на 4xx — runner сам решает
|
|
})
|
|
}
|
|
|
|
export async function login(email: string, password: string): Promise<AuthSession> {
|
|
const body = new URLSearchParams({
|
|
grant_type: 'password',
|
|
username: email,
|
|
password,
|
|
client_id: 'food-market-web',
|
|
scope: 'openid profile email roles api offline_access',
|
|
})
|
|
const res = await makeClient().post('/connect/token', body, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
})
|
|
if (res.status !== 200) {
|
|
throw new Error(`login failed for ${email}: ${res.status} ${JSON.stringify(res.data)}`)
|
|
}
|
|
const tok = res.data
|
|
const me = await makeClient(tok.access_token).get('/api/me')
|
|
if (me.status !== 200) {
|
|
throw new Error(`/api/me after login failed: ${me.status} ${JSON.stringify(me.data)}`)
|
|
}
|
|
return {
|
|
accessToken: tok.access_token,
|
|
refreshToken: tok.refresh_token,
|
|
email: me.data.email,
|
|
roles: me.data.roles ?? [],
|
|
orgId: me.data.orgId ?? null,
|
|
}
|
|
}
|