/** * EN: k6 load test — Catalog listing endpoint (GET /api/v1/products). * VI: k6 load test — endpoint danh sách catalog (GET /api/v1/products). * * Usage: * k6 run tests/load/k6/catalog-listing.js * k6 run --env BASE_URL=http://api.staging.goodgo.vn tests/load/k6/catalog-listing.js * * Thresholds: * - 95th-percentile response time < 200ms (read-heavy endpoint must be fast) * - Error rate < 0.5% */ import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate, Trend, Counter } from 'k6/metrics'; // --------------------------------------------------------------------------- // Custom metrics // --------------------------------------------------------------------------- const catalogErrors = new Rate('catalog_errors'); const catalogListDuration = new Trend('catalog_list_duration', true); const catalogDetailDuration = new Trend('catalog_detail_duration', true); const catalogRequests = new Counter('catalog_total_requests'); // --------------------------------------------------------------------------- // Test options — higher concurrency for read endpoint // --------------------------------------------------------------------------- export const options = { stages: [ { duration: '30s', target: 50 }, { duration: '2m', target: 100 }, { duration: '30s', target: 200 }, // EN: Stress test / VI: Stress test { duration: '30s', target: 0 }, ], thresholds: { // EN: Listing must be fast — p95 < 200ms / VI: Listing phải nhanh — p95 < 200ms http_req_duration: ['p(95)<200', 'p(99)<500'], catalog_errors: ['rate<0.005'], catalog_list_duration: ['p(95)<200'], catalog_detail_duration: ['p(95)<150'], }, }; // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- const BASE_URL = __ENV.BASE_URL || 'http://localhost:5020'; const JWT_TOKEN = __ENV.JWT_TOKEN || 'test-bearer-token'; const SHOP_ID = __ENV.SHOP_ID || '00000000-0000-0000-0000-000000000001'; const SAMPLE_PRODUCT_ID = __ENV.PRODUCT_ID || '00000000-0000-0000-0000-000000000002'; const headers = { Authorization: `Bearer ${JWT_TOKEN}`, 'X-Shop-Id': SHOP_ID, }; // --------------------------------------------------------------------------- // Scenarios // --------------------------------------------------------------------------- export default function () { // EN: Scenario 1 — List all products with pagination / VI: Tình huống 1 — danh sách sản phẩm có phân trang group('list products', () => { const page = Math.floor(Math.random() * 5) + 1; const limit = [10, 20, 50][Math.floor(Math.random() * 3)]; const res = http.get( `${BASE_URL}/api/v1/products?page=${page}&limit=${limit}&shopId=${SHOP_ID}`, { headers } ); catalogListDuration.add(res.timings.duration); catalogRequests.add(1); const ok = check(res, { 'list status 200': (r) => r.status === 200, 'list body has items': (r) => { try { const body = JSON.parse(r.body); return body.success === true && Array.isArray(body.data?.items ?? body.data); } catch { return false; } }, 'list response < 200ms': (r) => r.timings.duration < 200, }); if (!ok) catalogErrors.add(1); else catalogErrors.add(0); }); // EN: Scenario 2 — Get product by ID / VI: Tình huống 2 — lấy sản phẩm theo ID group('get product detail', () => { const res = http.get( `${BASE_URL}/api/v1/products/${SAMPLE_PRODUCT_ID}`, { headers } ); catalogDetailDuration.add(res.timings.duration); catalogRequests.add(1); const ok = check(res, { 'detail status 200 or 404': (r) => r.status === 200 || r.status === 404, 'detail response < 150ms': (r) => r.timings.duration < 150, }); if (!ok) catalogErrors.add(1); else catalogErrors.add(0); }); // EN: Scenario 3 — Search products / VI: Tình huống 3 — tìm kiếm sản phẩm group('search products', () => { const terms = ['cafe', 'tra sua', 'banh', 'pho', 'com']; const keyword = terms[Math.floor(Math.random() * terms.length)]; const res = http.get( `${BASE_URL}/api/v1/products?q=${encodeURIComponent(keyword)}&shopId=${SHOP_ID}`, { headers } ); catalogRequests.add(1); const ok = check(res, { 'search status 200': (r) => r.status === 200, }); if (!ok) catalogErrors.add(1); else catalogErrors.add(0); }); sleep(0.2 + Math.random() * 0.3); }