160 lines
7.5 KiB
JavaScript
160 lines
7.5 KiB
JavaScript
// Backend/tests/k6/enroll-load-test.js
|
|
//
|
|
// จำลองนักเรียนหลายคน login แล้ว enroll คอร์สพร้อมกัน
|
|
//
|
|
// Flow:
|
|
// 1. Login
|
|
// 2. Enroll คอร์ส
|
|
// 3. ตรวจสอบ enrolled courses
|
|
//
|
|
// Usage:
|
|
// k6 run -e APP_URL=http://192.168.1.137:4000 -e COURSE_ID=1 tests/k6/enroll-load-test.js
|
|
|
|
import http from 'k6/http';
|
|
import { check, sleep, group } from 'k6';
|
|
import { Rate, Trend, Counter } from 'k6/metrics';
|
|
import { SharedArray } from 'k6/data';
|
|
|
|
// ─── Custom Metrics ───────────────────────────────────────────────────────────
|
|
const errorRate = new Rate('errors');
|
|
const loginTime = new Trend('login_duration', true);
|
|
const enrollTime = new Trend('enroll_duration', true);
|
|
const enrolledCount = new Counter('successful_enrollments');
|
|
|
|
// ─── Load student credentials ─────────────────────────────────────────────────
|
|
const students = new SharedArray('students', function () {
|
|
return JSON.parse(open('./test-credentials.json')).students;
|
|
});
|
|
|
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
const BASE_URL = __ENV.APP_URL || 'http://192.168.1.137:4000';
|
|
const COURSE_ID = __ENV.COURSE_ID || '1';
|
|
|
|
// ─── Test Options ─────────────────────────────────────────────────────────────
|
|
export const options = {
|
|
stages: [
|
|
{ duration: '20s', target: 10 }, // Ramp up
|
|
{ duration: '1m', target: 30 }, // Increase
|
|
{ duration: '30s', target: 50 }, // Peak: 50 คน enroll พร้อมกัน
|
|
{ duration: '30s', target: 0 }, // Ramp down
|
|
],
|
|
thresholds: {
|
|
'login_duration': ['p(95)<2000'], // Login < 2s
|
|
'enroll_duration': ['p(95)<1000'], // Enroll < 1s
|
|
'errors': ['rate<0.05'],
|
|
'http_req_failed': ['rate<0.05'],
|
|
},
|
|
};
|
|
|
|
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
function jsonHeaders(token) {
|
|
const h = { 'Content-Type': 'application/json' };
|
|
if (token) h['Authorization'] = `Bearer ${token}`;
|
|
return h;
|
|
}
|
|
|
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
export default function () {
|
|
const student = students[__VU % students.length];
|
|
let token = null;
|
|
|
|
// ── Step 1: Login ──────────────────────────────────────────────────────────
|
|
group('1. Login', () => {
|
|
const res = http.post(
|
|
`${BASE_URL}/api/auth/login`,
|
|
JSON.stringify({ email: student.email, password: student.password }),
|
|
{ headers: jsonHeaders(null) }
|
|
);
|
|
|
|
loginTime.add(res.timings.duration);
|
|
errorRate.add(res.status !== 200);
|
|
|
|
check(res, {
|
|
'login: status 200': (r) => r.status === 200,
|
|
'login: has token': (r) => { try { return !!r.json('data.token'); } catch { return false; } },
|
|
});
|
|
|
|
if (res.status === 200) {
|
|
try { token = res.json('data.token'); } catch {}
|
|
}
|
|
});
|
|
|
|
if (!token) {
|
|
console.warn(`[VU ${__VU}] Login failed for ${student.email} — skipping`);
|
|
sleep(1);
|
|
return;
|
|
}
|
|
|
|
sleep(0.5);
|
|
|
|
// ── Step 2: Enroll ─────────────────────────────────────────────────────────
|
|
group('2. Enroll Course', () => {
|
|
const res = http.post(
|
|
`${BASE_URL}/api/students/courses/${COURSE_ID}/enroll`,
|
|
null,
|
|
{ headers: jsonHeaders(token) }
|
|
);
|
|
|
|
enrollTime.add(res.timings.duration);
|
|
|
|
// 200 = enrolled, 409 = already enrolled (ถือว่าโอเค)
|
|
const ok = res.status === 200 || res.status === 409;
|
|
errorRate.add(!ok);
|
|
|
|
if (res.status === 200) enrolledCount.add(1);
|
|
|
|
check(res, {
|
|
'enroll: 200 or 409': (r) => r.status === 200 || r.status === 409,
|
|
'enroll: fast response': (r) => r.timings.duration < 1000,
|
|
});
|
|
});
|
|
|
|
sleep(0.5);
|
|
|
|
// ── Step 3: Verify — ดึงรายการคอร์สที่ลงทะเบียน ─────────────────────────
|
|
group('3. Get Enrolled Courses', () => {
|
|
const res = http.get(
|
|
`${BASE_URL}/api/students/courses`,
|
|
{ headers: jsonHeaders(token) }
|
|
);
|
|
|
|
errorRate.add(res.status !== 200);
|
|
|
|
check(res, {
|
|
'enrolled courses: status 200': (r) => r.status === 200,
|
|
});
|
|
});
|
|
|
|
sleep(1);
|
|
}
|
|
|
|
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
export function handleSummary(data) {
|
|
const m = data.metrics;
|
|
const avg = (k) => m[k]?.values?.avg?.toFixed(0) ?? 'N/A';
|
|
const p95 = (k) => m[k]?.values?.['p(95)']?.toFixed(0) ?? 'N/A';
|
|
const rate = (k) => ((m[k]?.values?.rate ?? 0) * 100).toFixed(2);
|
|
const cnt = (k) => m[k]?.values?.count ?? 0;
|
|
|
|
return {
|
|
stdout: `
|
|
╔══════════════════════════════════════════════════════════╗
|
|
║ Course Enroll — Load Test ║
|
|
╠══════════════════════════════════════════════════════════╣
|
|
║ Course ID : ${String(COURSE_ID).padEnd(43)}║
|
|
╠══════════════════════════════════════════════════════════╣
|
|
║ RESPONSE TIMES (avg / p95) ║
|
|
║ Login : ${avg('login_duration')}ms / ${p95('login_duration')}ms
|
|
║ Enroll : ${avg('enroll_duration')}ms / ${p95('enroll_duration')}ms
|
|
╠══════════════════════════════════════════════════════════╣
|
|
║ COUNTS ║
|
|
║ Total Requests : ${String(cnt('http_reqs')).padEnd(33)}║
|
|
║ New Enrollments : ${String(cnt('successful_enrollments')).padEnd(33)}║
|
|
╠══════════════════════════════════════════════════════════╣
|
|
║ ERROR RATES ║
|
|
║ HTTP Failed : ${(rate('http_req_failed') + '%').padEnd(39)}║
|
|
║ Custom Errors : ${(rate('errors') + '%').padEnd(39)}║
|
|
╚══════════════════════════════════════════════════════════╝
|
|
`,
|
|
};
|
|
}
|