xpeditis2.0/apps/backend/load-tests/rate-search.test.js
2025-11-04 07:30:15 +01:00

153 lines
4.2 KiB
JavaScript

/**
* K6 Load Test - Rate Search Endpoint
*
* Target: 100 requests/second
* Duration: 5 minutes
*
* Run: k6 run rate-search.test.js
*/
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const searchDuration = new Trend('search_duration');
// Test configuration
export const options = {
stages: [
{ duration: '1m', target: 20 }, // Ramp up to 20 users
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '1m', target: 100 }, // Ramp up to 100 users
{ duration: '3m', target: 100 }, // Stay at 100 users
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95% of requests must complete below 2s
http_req_failed: ['rate<0.01'], // Error rate must be less than 1%
errors: ['rate<0.05'], // Business error rate must be less than 5%
},
};
// Base URL
const BASE_URL = __ENV.API_URL || 'http://localhost:4000/api/v1';
// Auth token (should be set via environment variable)
const AUTH_TOKEN = __ENV.AUTH_TOKEN || '';
// Test data - common trade lanes
const tradeLanes = [
{
origin: 'NLRTM', // Rotterdam
destination: 'CNSHA', // Shanghai
containerType: '40HC',
},
{
origin: 'USNYC', // New York
destination: 'GBLON', // London
containerType: '20ST',
},
{
origin: 'SGSIN', // Singapore
destination: 'USOAK', // Oakland
containerType: '40ST',
},
{
origin: 'DEHAM', // Hamburg
destination: 'BRRIO', // Rio de Janeiro
containerType: '40HC',
},
{
origin: 'AEDXB', // Dubai
destination: 'INMUN', // Mumbai
containerType: '20ST',
},
];
export default function () {
// Select random trade lane
const tradeLane = tradeLanes[Math.floor(Math.random() * tradeLanes.length)];
// Prepare request payload
const payload = JSON.stringify({
origin: tradeLane.origin,
destination: tradeLane.destination,
departureDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 2 weeks from now
containers: [
{
type: tradeLane.containerType,
quantity: 1,
},
],
});
const params = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${AUTH_TOKEN}`,
},
tags: { name: 'RateSearch' },
};
// Make request
const startTime = Date.now();
const response = http.post(`${BASE_URL}/rates/search`, payload, params);
const duration = Date.now() - startTime;
// Record metrics
searchDuration.add(duration);
// Check response
const success = check(response, {
'status is 200': r => r.status === 200,
'response has quotes': r => {
try {
const body = JSON.parse(r.body);
return body.quotes && body.quotes.length > 0;
} catch (e) {
return false;
}
},
'response time < 2s': r => duration < 2000,
});
errorRate.add(!success);
// Small delay between requests
sleep(1);
}
export function handleSummary(data) {
return {
stdout: textSummary(data, { indent: ' ', enableColors: true }),
'load-test-results/rate-search-summary.json': JSON.stringify(data),
};
}
function textSummary(data, options) {
const indent = options.indent || '';
const enableColors = options.enableColors || false;
return `
${indent}Test Summary - Rate Search Load Test
${indent}=====================================
${indent}
${indent}Total Requests: ${data.metrics.http_reqs.values.count}
${indent}Failed Requests: ${data.metrics.http_req_failed.values.rate * 100}%
${indent}
${indent}Response Times:
${indent} Average: ${data.metrics.http_req_duration.values.avg.toFixed(2)}ms
${indent} Median: ${data.metrics.http_req_duration.values.med.toFixed(2)}ms
${indent} 95th: ${data.metrics.http_req_duration.values['p(95)'].toFixed(2)}ms
${indent} 99th: ${data.metrics.http_req_duration.values['p(99)'].toFixed(2)}ms
${indent}
${indent}Requests/sec: ${data.metrics.http_reqs.values.rate.toFixed(2)}
${indent}
${indent}Business Metrics:
${indent} Error Rate: ${(data.metrics.errors.values.rate * 100).toFixed(2)}%
${indent} Avg Search Duration: ${data.metrics.search_duration.values.avg.toFixed(2)}ms
`;
}