// sales-report-heavy.js — нагрузка чтения отчётов. // // Сценарий: один tenant с уже-нагенерированными чеками (1500 через // YearDemoSeeder или 10000+ через несколько подряд запусков // retail-sales-parallel.js). 30 секунд VU дёргают // GET /api/reports/sales и GET /api/reports/abc, мерим p95/p99. // // Это «чтение тяжёлого агрегата», самый показательный bench для // PG-индексов и плана запроса. Если p95 > 2с при 1500 чеках — // нужен EXPLAIN. import http from 'k6/http'; import { check } from 'k6'; import { Trend, Rate } from 'k6/metrics'; const BASE_URL = __ENV.BASE_URL || 'http://localhost:5081'; const DURATION_S = Number(__ENV.DURATION_S || 30); const VUS = Number(__ENV.VUS || 10); // Email/пароль уже-существующего tenant'а — нужно подсунуть. По дефолту // — admin@food-market.local (SuperAdmin без своей орги, отчёты будут // пустыми, но мы мерим только время ответа эндпоинтов). const EMAIL = __ENV.EMAIL || 'admin@food-market.local'; const PASSWORD = __ENV.PASSWORD || 'Admin12345!'; const salesTrend = new Trend('reports_sales_ms', true); const abcTrend = new Trend('reports_abc_ms', true); const dashboardTrend = new Trend('dashboard_top_ms', true); const non2xx = new Rate('reports_non_2xx'); export const options = { scenarios: { reports: { executor: 'constant-vus', vus: VUS, duration: `${DURATION_S}s`, }, }, thresholds: { reports_sales_ms: ['p(95)<2500', 'p(99)<5000'], reports_abc_ms: ['p(95)<3000', 'p(99)<6000'], reports_non_2xx: ['rate<0.01'], }, }; export function setup() { const r = http.post(`${BASE_URL}/connect/token`, `grant_type=password&username=${encodeURIComponent(EMAIL)}&password=${encodeURIComponent(PASSWORD)}&client_id=food-market-web&scope=openid profile email roles api offline_access`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (r.status !== 200) throw new Error(`token failed for ${EMAIL}: ${r.status} ${r.body}`); return { token: r.json('access_token') }; } export default function (ctx) { const auth = { headers: { Authorization: `Bearer ${ctx.token}` }, tags: {} }; // Период «весь год» — стандартный запрос дашборда «за год». const dateFrom = '2026-01-01'; const dateTo = '2026-12-31'; let t0 = Date.now(); let r = http.get(`${BASE_URL}/api/reports/sales?dateFrom=${dateFrom}&dateTo=${dateTo}&groupBy=day`, auth); salesTrend.add(Date.now() - t0); non2xx.add(r.status >= 400); check(r, { 'sales 2xx': (x) => x.status >= 200 && x.status < 300 }); t0 = Date.now(); r = http.get(`${BASE_URL}/api/reports/abc?dateFrom=${dateFrom}&dateTo=${dateTo}`, auth); abcTrend.add(Date.now() - t0); non2xx.add(r.status >= 400); check(r, { 'abc 2xx': (x) => x.status >= 200 && x.status < 300 }); t0 = Date.now(); r = http.get(`${BASE_URL}/api/dashboard/top-products?limit=5`, auth); dashboardTrend.add(Date.now() - t0); non2xx.add(r.status >= 400); check(r, { 'dashboard 2xx': (x) => x.status >= 200 && x.status < 300 }); }