// 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)}║ ╚══════════════════════════════════════════════════════════╝ `, }; }