feat(load-tests): add K6 load testing suite for critical API paths
K6 scripts for 4 critical paths: - Auth (100 VU): login, register, profile - Listings (500 VU): search with filters, detail view - Search (200 VU): full-text + geo search - Payments (50 VU): create payment, list transactions SLA thresholds: p50<200ms, p95<500ms, p99<1s, error<1%. CI: manual workflow_dispatch with suite selector. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
121
load-tests/scripts/auth.js
Normal file
121
load-tests/scripts/auth.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { BASE_URL, SLA_THRESHOLDS, uniquePhone } from '../lib/config.js';
|
||||
|
||||
const loginDuration = new Trend('login_duration', true);
|
||||
const registerDuration = new Trend('register_duration', true);
|
||||
const loginFailRate = new Rate('login_failures');
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
auth_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '30s', target: 50 }, // ramp up
|
||||
{ duration: '1m', target: 100 }, // sustain 100 concurrent
|
||||
{ duration: '30s', target: 0 }, // ramp down
|
||||
],
|
||||
gracefulRampDown: '10s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
...SLA_THRESHOLDS,
|
||||
login_duration: ['p(95)<500'],
|
||||
register_duration: ['p(95)<800'],
|
||||
login_failures: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export function setup() {
|
||||
// Pre-register a pool of users for login tests
|
||||
const users = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const phone = `0910${String(i).padStart(6, '0')}`;
|
||||
const payload = JSON.stringify({
|
||||
phone,
|
||||
password: 'LoadTest@1234!',
|
||||
fullName: `K6 Auth User ${i}`,
|
||||
email: `k6-auth-${i}@goodgo.test`,
|
||||
});
|
||||
|
||||
const res = http.post(`${BASE_URL}/auth/register`, payload, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: 'setup_register' },
|
||||
});
|
||||
|
||||
if (res.status === 200 || res.status === 201 || res.status === 409) {
|
||||
users.push({ phone, password: 'LoadTest@1234!' });
|
||||
}
|
||||
}
|
||||
return { users };
|
||||
}
|
||||
|
||||
export default function (data) {
|
||||
const vu = __VU;
|
||||
const iter = __ITER;
|
||||
|
||||
// Alternate between register and login
|
||||
if (iter % 3 === 0) {
|
||||
// Register new user
|
||||
const phone = uniquePhone();
|
||||
const payload = JSON.stringify({
|
||||
phone,
|
||||
password: 'LoadTest@1234!',
|
||||
fullName: `K6 New User ${phone}`,
|
||||
email: `k6-new-${phone}@goodgo.test`,
|
||||
});
|
||||
|
||||
const res = http.post(`${BASE_URL}/auth/register`, payload, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: 'POST /auth/register' },
|
||||
});
|
||||
|
||||
registerDuration.add(res.timings.duration);
|
||||
check(res, {
|
||||
'register: status 200|201': (r) => r.status === 200 || r.status === 201,
|
||||
'register: has accessToken': (r) => {
|
||||
try { return JSON.parse(r.body).accessToken !== undefined; } catch { return false; }
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Login existing user
|
||||
const user = data.users[vu % data.users.length];
|
||||
if (!user) return;
|
||||
|
||||
const res = http.post(
|
||||
`${BASE_URL}/auth/login`,
|
||||
JSON.stringify({ phone: user.phone, password: user.password }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: 'POST /auth/login' },
|
||||
},
|
||||
);
|
||||
|
||||
loginDuration.add(res.timings.duration);
|
||||
const ok = check(res, {
|
||||
'login: status 200|201': (r) => r.status === 200 || r.status === 201,
|
||||
'login: has accessToken': (r) => {
|
||||
try { return JSON.parse(r.body).accessToken !== undefined; } catch { return false; }
|
||||
},
|
||||
});
|
||||
loginFailRate.add(!ok);
|
||||
|
||||
// Optionally hit profile with the token
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
try {
|
||||
const tokens = JSON.parse(res.body);
|
||||
const profileRes = http.get(`${BASE_URL}/auth/profile`, {
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
tags: { name: 'GET /auth/profile' },
|
||||
});
|
||||
check(profileRes, {
|
||||
'profile: status 200': (r) => r.status === 200,
|
||||
});
|
||||
} catch (_) { /* ignore parse errors */ }
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Math.random() * 2 + 0.5);
|
||||
}
|
||||
Reference in New Issue
Block a user