2026-02-06 15:26:04 +07:00
|
|
|
// Backend/tests/k6/login-load-test.js
|
|
|
|
|
// Load test สำหรับ Login API โดยใช้ test credentials ที่สร้างไว้
|
|
|
|
|
import http from 'k6/http';
|
|
|
|
|
import { check, sleep } from 'k6';
|
|
|
|
|
import { Rate, Trend, Counter } from 'k6/metrics';
|
|
|
|
|
import { SharedArray } from 'k6/data';
|
|
|
|
|
|
|
|
|
|
// Load test credentials
|
|
|
|
|
const students = new SharedArray('students', function () {
|
|
|
|
|
return JSON.parse(open('./test-credentials.json')).students;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Custom metrics
|
|
|
|
|
const errorRate = new Rate('errors');
|
|
|
|
|
const loginDuration = new Trend('login_duration');
|
|
|
|
|
const successfulLogins = new Counter('successful_logins');
|
|
|
|
|
const failedLogins = new Counter('failed_logins');
|
|
|
|
|
|
|
|
|
|
// Test configuration
|
|
|
|
|
export const options = {
|
|
|
|
|
// Ramp-up pattern
|
|
|
|
|
stages: [
|
|
|
|
|
{ duration: '10s', target: 10 }, // Ramp up to 10 users
|
|
|
|
|
{ duration: '30s', target: 10 }, // Stay at 10 users
|
|
|
|
|
{ duration: '10s', target: 25 }, // Ramp up to 25 users
|
|
|
|
|
{ duration: '30s', target: 25 }, // Stay at 25 users
|
|
|
|
|
{ duration: '10s', target: 50 }, // Ramp up to 50 users
|
|
|
|
|
{ duration: '30s', target: 50 }, // Stay at 50 users
|
|
|
|
|
{ duration: '10s', target: 0 }, // Ramp down
|
|
|
|
|
],
|
|
|
|
|
thresholds: {
|
|
|
|
|
http_req_duration: ['p(95)<2000'], // 95% of requests < 2s
|
|
|
|
|
errors: ['rate<0.1'], // Error rate < 10%
|
2026-02-23 13:18:38 +07:00
|
|
|
login_duration: ['p(95)<2000'], // 95% pof logins < 2s
|
2026-02-06 15:26:04 +07:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const BASE_URL = __ENV.BASE_URL || 'http://192.168.1.137:4000';
|
|
|
|
|
|
|
|
|
|
export default function () {
|
|
|
|
|
// เลือก student แบบ random จาก credentials
|
|
|
|
|
const student = students[Math.floor(Math.random() * students.length)];
|
|
|
|
|
|
|
|
|
|
const payload = JSON.stringify({
|
|
|
|
|
email: student.email,
|
|
|
|
|
password: student.password,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const params = {
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Test Login
|
|
|
|
|
const startTime = new Date();
|
|
|
|
|
const res = http.post(`${BASE_URL}/api/auth/login`, payload, params);
|
|
|
|
|
const duration = new Date() - startTime;
|
|
|
|
|
|
|
|
|
|
// Record metrics
|
|
|
|
|
loginDuration.add(duration);
|
|
|
|
|
|
|
|
|
|
const isSuccess = res.status === 200;
|
|
|
|
|
errorRate.add(!isSuccess);
|
|
|
|
|
|
|
|
|
|
if (isSuccess) {
|
|
|
|
|
successfulLogins.add(1);
|
|
|
|
|
} else {
|
|
|
|
|
failedLogins.add(1);
|
|
|
|
|
console.log(`Login failed for ${student.email}: ${res.status} - ${res.body}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assertions
|
|
|
|
|
check(res, {
|
|
|
|
|
'status is 200': (r) => r.status === 200,
|
|
|
|
|
'has access token': (r) => {
|
|
|
|
|
try {
|
|
|
|
|
const body = JSON.parse(r.body);
|
|
|
|
|
return body.data && body.data.accessToken;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
'has refresh token': (r) => {
|
|
|
|
|
try {
|
|
|
|
|
const body = JSON.parse(r.body);
|
|
|
|
|
return body.data && body.data.refreshToken;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
'response time < 2s': (r) => r.timings.duration < 2000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Think time between requests (simulate real user behavior)
|
|
|
|
|
sleep(Math.random() * 2 + 1); // 1-3 seconds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Summary handler
|
|
|
|
|
export function handleSummary(data) {
|
|
|
|
|
return {
|
|
|
|
|
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
|
|
|
|
|
'login-load-test-summary.json': JSON.stringify(data, null, 2),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function textSummary(data, options) {
|
|
|
|
|
const metrics = data.metrics;
|
|
|
|
|
|
|
|
|
|
let summary = '\n========== LOGIN LOAD TEST SUMMARY ==========\n\n';
|
|
|
|
|
|
|
|
|
|
// Request stats
|
|
|
|
|
if (metrics.http_reqs) {
|
|
|
|
|
summary += `Total Requests: ${metrics.http_reqs.values.count}\n`;
|
|
|
|
|
}
|
|
|
|
|
if (metrics.successful_logins) {
|
|
|
|
|
summary += `Successful Logins: ${metrics.successful_logins.values.count}\n`;
|
|
|
|
|
}
|
|
|
|
|
if (metrics.failed_logins) {
|
|
|
|
|
summary += `Failed Logins: ${metrics.failed_logins.values.count}\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Duration stats
|
|
|
|
|
if (metrics.http_req_duration) {
|
|
|
|
|
summary += `\nResponse Time:\n`;
|
|
|
|
|
summary += ` - Avg: ${metrics.http_req_duration.values.avg.toFixed(2)}ms\n`;
|
|
|
|
|
summary += ` - Min: ${metrics.http_req_duration.values.min.toFixed(2)}ms\n`;
|
|
|
|
|
summary += ` - Max: ${metrics.http_req_duration.values.max.toFixed(2)}ms\n`;
|
|
|
|
|
summary += ` - p(95): ${metrics.http_req_duration.values['p(95)'].toFixed(2)}ms\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Error rate
|
|
|
|
|
if (metrics.errors) {
|
|
|
|
|
summary += `\nError Rate: ${(metrics.errors.values.rate * 100).toFixed(2)}%\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
summary += '\n==============================================\n';
|
|
|
|
|
|
|
|
|
|
return summary;
|
|
|
|
|
}
|