129 lines
4.5 KiB
JavaScript
129 lines
4.5 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|