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>
122 lines
3.6 KiB
JavaScript
122 lines
3.6 KiB
JavaScript
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);
|
|
}
|