feat: Add user role retrieval, enhance recommended course filtering and detail, and introduce new k6 load tests.
This commit is contained in:
parent
c118e5c3dc
commit
ef70d2db3f
11 changed files with 515 additions and 139 deletions
|
|
@ -29,6 +29,7 @@ const progressSaveTime = new Trend('progress_save_duration', true);
|
|||
const completeLessonTime = new Trend('complete_lesson_duration', true);
|
||||
const completedCount = new Counter('completed_lessons');
|
||||
const progressSaveCount = new Counter('progress_saves');
|
||||
const videoLoadTime = new Trend('video_load_duration', true);
|
||||
|
||||
// ─── Load student credentials ────────────────────────────────────────────────
|
||||
// อ่านจาก test-credentials.json (50 accounts)
|
||||
|
|
@ -42,7 +43,7 @@ const COURSE_ID = __ENV.COURSE_ID || '1';
|
|||
const LESSON_ID = __ENV.LESSON_ID || '1';
|
||||
|
||||
// วีดีโอความยาว (วินาที) — ปรับตามจริง
|
||||
const VIDEO_DURATION_SECONDS = parseInt(__ENV.VIDEO_DURATION || '300'); // default 5 นาที
|
||||
const VIDEO_DURATION_SECONDS = parseInt(__ENV.VIDEO_DURATION || '98'); // default 5 นาที
|
||||
|
||||
// save progress interval: ทุก 5 วินาที (เหมือน client จริง)
|
||||
// แต่ในการ test เราจะ simulate เร็วขึ้นโดยใช้ sleep น้อยลง
|
||||
|
|
@ -63,6 +64,7 @@ export const options = {
|
|||
'login_duration': ['p(95)<2000'], // Login < 2s
|
||||
'course_learning_duration': ['p(95)<1000'], // Load course page < 1s
|
||||
'lesson_load_duration': ['p(95)<1000'], // Load lesson < 1s
|
||||
'video_load_duration': ['p(95)<3000'], // Fetch video from MinIO < 3s
|
||||
'progress_save_duration': ['p(95)<500'], // Save progress < 500ms (critical — บ่อย)
|
||||
'complete_lesson_duration': ['p(95)<1000'], // Complete lesson < 1s
|
||||
|
||||
|
|
@ -79,116 +81,114 @@ function jsonHeaders(token) {
|
|||
return h;
|
||||
}
|
||||
|
||||
// ─── Per-VU persistent state (จำข้ามรอบ iteration) ──────────────────────────
|
||||
// ตัวแปรนี้อยู่ระดับ module → k6 สร้างแยกต่างหากสำหรับแต่ละ VU
|
||||
// ค่าจะถูกจำไว้ตลอดอายุของ VU (ข้ามหลายรอบ iteration)
|
||||
let vuToken = null; // token ที่ login ไว้แล้ว
|
||||
let vuSetupDone = false; // เคย load course+lesson แล้วหรือยัง
|
||||
let vuProgress = 0; // ตำแหน่งวีดีโอปัจจุบัน (วินาที)
|
||||
let vuCompleted = false; // lesson complete แล้วหรือยัง
|
||||
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
export default function () {
|
||||
// แต่ละ VU ใช้ account คนละคน (round-robin ถ้า VU มากกว่า 50)
|
||||
const student = students[__VU % students.length];
|
||||
|
||||
let token = null;
|
||||
// ── Step 1: Login (ทำครั้งเดียวตอน VU เริ่มต้น หรือถ้า token หาย) ─────────
|
||||
if (!vuToken) {
|
||||
group('1. Login', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/auth/login`,
|
||||
JSON.stringify({ email: student.email, password: student.password }),
|
||||
{ headers: jsonHeaders(null) }
|
||||
);
|
||||
|
||||
// ── Step 1: Login ──────────────────────────────────────────────────────────
|
||||
group('1. Login', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/auth/login`,
|
||||
JSON.stringify({ identifier: student.email, password: student.password }),
|
||||
{ headers: jsonHeaders(null) }
|
||||
);
|
||||
loginTime.add(res.timings.duration);
|
||||
const ok = res.status === 200;
|
||||
errorRate.add(!ok);
|
||||
|
||||
loginTime.add(res.timings.duration);
|
||||
const ok = res.status === 200;
|
||||
errorRate.add(!ok);
|
||||
check(res, {
|
||||
'login: status 200': (r) => r.status === 200,
|
||||
'login: has token': (r) => { try { return !!r.json('data.token'); } catch { return false; } },
|
||||
});
|
||||
|
||||
check(res, {
|
||||
'login: status 200': (r) => r.status === 200,
|
||||
'login: has token': (r) => {
|
||||
try { return !!r.json('data.token'); } catch { return false; }
|
||||
},
|
||||
if (ok) {
|
||||
try { vuToken = res.json('data.token'); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
try { token = res.json('data.token'); } catch {}
|
||||
if (!vuToken) {
|
||||
console.warn(`[VU ${__VU}] Login failed for ${student.email} — skipping`);
|
||||
sleep(2);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
console.warn(`[VU ${__VU}] Login failed for ${student.email} — skipping`);
|
||||
sleep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
// ── Step 2 (removed): Enroll ทำผ่าน enroll-load-test.js แยกต่างหาก ─────────
|
||||
|
||||
// ── Step 2: Enroll (ถ้ายังไม่ได้ enroll) ──────────────────────────────────
|
||||
group('2. Enroll Course', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/enroll`,
|
||||
null,
|
||||
{ headers: jsonHeaders(token) }
|
||||
);
|
||||
|
||||
// 200 = enrolled OK, 409 = already enrolled (ทั้งคู่ถือว่าดี)
|
||||
const ok = res.status === 200 || res.status === 409;
|
||||
errorRate.add(!ok);
|
||||
check(res, {
|
||||
'enroll: enrolled or already enrolled': (r) => r.status === 200 || r.status === 409,
|
||||
// ── Step 3+4: Setup — Load course และ open lesson (ทำครั้งเดียวต่อ VU) ─────
|
||||
if (!vuSetupDone) {
|
||||
group('3. Load Course Learning Page', () => {
|
||||
const res = http.get(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/learn`,
|
||||
{ headers: jsonHeaders(vuToken) }
|
||||
);
|
||||
courseLearningTime.add(res.timings.duration);
|
||||
errorRate.add(res.status !== 200);
|
||||
check(res, { 'course learn: status 200': (r) => r.status === 200 });
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
sleep(1);
|
||||
|
||||
// ── Step 3: Load course learning page ─────────────────────────────────────
|
||||
group('3. Load Course Learning Page', () => {
|
||||
const res = http.get(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/learn`,
|
||||
{ headers: jsonHeaders(token) }
|
||||
);
|
||||
|
||||
courseLearningTime.add(res.timings.duration);
|
||||
const ok = res.status === 200;
|
||||
errorRate.add(!ok);
|
||||
check(res, {
|
||||
'course learn: status 200': (r) => r.status === 200,
|
||||
let videoUrl = null;
|
||||
group('4. Open Lesson', () => {
|
||||
const res = http.get(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}`,
|
||||
{ headers: jsonHeaders(vuToken) }
|
||||
);
|
||||
lessonLoadTime.add(res.timings.duration);
|
||||
errorRate.add(res.status !== 200);
|
||||
check(res, { 'lesson: status 200': (r) => r.status === 200 });
|
||||
if (res.status === 200) {
|
||||
try { videoUrl = res.json('data.video_url'); } catch {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
// ── Step 4.5: Fetch video จาก MinIO ──────────────────────────────────────
|
||||
if (videoUrl) {
|
||||
group('4.5 Fetch Video from MinIO', () => {
|
||||
const res = http.get(videoUrl, {
|
||||
headers: { 'Range': 'bytes=0-1048575' }, // ขอแค่ 1MB แรก
|
||||
timeout: '10s',
|
||||
});
|
||||
videoLoadTime.add(res.timings.duration);
|
||||
const ok = res.status === 200 || res.status === 206;
|
||||
errorRate.add(!ok);
|
||||
check(res, {
|
||||
'minio video: 200 or 206': (r) => r.status === 200 || r.status === 206,
|
||||
'minio video: fast': (r) => r.timings.duration < 3000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log(`[VU ${__VU}] No video_url returned — skipping MinIO fetch`);
|
||||
}
|
||||
|
||||
// ── Step 4: Open lesson (load video content) ───────────────────────────────
|
||||
group('4. Open Lesson', () => {
|
||||
const res = http.get(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}`,
|
||||
{ headers: jsonHeaders(token) }
|
||||
);
|
||||
sleep(2); // รอ buffer เริ่มต้น
|
||||
vuSetupDone = true;
|
||||
}
|
||||
|
||||
lessonLoadTime.add(res.timings.duration);
|
||||
const ok = res.status === 200;
|
||||
errorRate.add(!ok);
|
||||
check(res, {
|
||||
'lesson: status 200': (r) => r.status === 200,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(2); // รอ video buffer เล็กน้อย
|
||||
|
||||
// ── Step 5: Simulate watching — save progress ทุก PROGRESS_INTERVAL วิ ─────
|
||||
// จำลองการดูวีดีโอ: progress เพิ่มขึ้นเรื่อยๆ จนถึง 95% (auto-complete threshold)
|
||||
group('5. Watch Video (Save Progress)', () => {
|
||||
const targetSeconds = Math.floor(VIDEO_DURATION_SECONDS * 0.95); // ดูถึง 95%
|
||||
const steps = Math.ceil(targetSeconds / PROGRESS_INTERVAL_SECONDS); // จำนวนครั้งที่ save
|
||||
|
||||
// เพื่อไม่ให้ test นาน เกินไป จำกัดไว้ที่ 20 saves per VU iteration
|
||||
const maxSaves = Math.min(steps, 20);
|
||||
|
||||
for (let i = 1; i <= maxSaves; i++) {
|
||||
const currentSeconds = i * PROGRESS_INTERVAL_SECONDS;
|
||||
// ── Step 5: Save Progress ทีละ tick (ต่อจากตำแหน่งเดิม) ────────────────────
|
||||
// แต่ละ iteration ของ VU = ส่ง progress 1 ครั้ง แล้ว sleep ตาม interval จริง
|
||||
if (!vuCompleted) {
|
||||
vuProgress += PROGRESS_INTERVAL_SECONDS;
|
||||
|
||||
group('5. Watch Video (Save Progress)', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/students/lessons/${LESSON_ID}/progress`,
|
||||
JSON.stringify({
|
||||
video_progress_seconds: currentSeconds,
|
||||
video_progress_seconds: vuProgress,
|
||||
video_duration_seconds: VIDEO_DURATION_SECONDS,
|
||||
}),
|
||||
{ headers: jsonHeaders(token) }
|
||||
{ headers: jsonHeaders(vuToken) }
|
||||
);
|
||||
|
||||
progressSaveTime.add(res.timings.duration);
|
||||
|
|
@ -201,32 +201,32 @@ export default function () {
|
|||
'progress save: fast': (r) => r.timings.duration < 500,
|
||||
});
|
||||
|
||||
// รอ interval จริง (ย่อจาก 5s จริง เหลือ 1s เพื่อให้ test ไม่นานเกิน)
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Step 6: Mark lesson complete ───────────────────────────────────────────
|
||||
group('6. Complete Lesson', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}/complete`,
|
||||
null,
|
||||
{ headers: jsonHeaders(token) }
|
||||
);
|
||||
|
||||
completeLessonTime.add(res.timings.duration);
|
||||
const ok = res.status === 200;
|
||||
|
||||
// 200 = completed now, อาจ 409 ถ้า already completed (ถือว่าโอเค)
|
||||
errorRate.add(res.status !== 200 && res.status !== 409);
|
||||
if (ok) completedCount.add(1);
|
||||
|
||||
check(res, {
|
||||
'complete: status 200 or 409': (r) => r.status === 200 || r.status === 409,
|
||||
console.log(`[VU ${__VU}] progress: ${vuProgress}s / ${VIDEO_DURATION_SECONDS}s (${Math.round(vuProgress / VIDEO_DURATION_SECONDS * 100)}%)`);
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
// ── Step 6: Mark complete เมื่อดูครบ ≥95% ──────────────────────────────
|
||||
if (vuProgress >= VIDEO_DURATION_SECONDS * 0.95) {
|
||||
group('6. Complete Lesson', () => {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}/complete`,
|
||||
null,
|
||||
{ headers: jsonHeaders(vuToken) }
|
||||
);
|
||||
completeLessonTime.add(res.timings.duration);
|
||||
errorRate.add(res.status !== 200 && res.status !== 409);
|
||||
if (res.status === 200) completedCount.add(1);
|
||||
check(res, {
|
||||
'complete: status 200 or 409': (r) => r.status === 200 || r.status === 409,
|
||||
});
|
||||
});
|
||||
|
||||
vuCompleted = true;
|
||||
console.log(`[VU ${__VU}] ✓ Lesson completed`);
|
||||
}
|
||||
}
|
||||
|
||||
// sleep ตาม interval จริง — ทุก VU ส่ง progress ทุก PROGRESS_INTERVAL_SECONDS วินาที
|
||||
sleep(PROGRESS_INTERVAL_SECONDS);
|
||||
}
|
||||
|
||||
// ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -251,6 +251,7 @@ export function handleSummary(data) {
|
|||
║ Login : ${avg('login_duration')}ms / ${p95('login_duration')}ms${' '.repeat(Math.max(0, 20 - avg('login_duration').length - p95('login_duration').length))}║
|
||||
║ Course Learning Page: ${avg('course_learning_duration')}ms / ${p95('course_learning_duration')}ms${' '.repeat(Math.max(0, 20 - avg('course_learning_duration').length - p95('course_learning_duration').length))}║
|
||||
║ Lesson Load : ${avg('lesson_load_duration')}ms / ${p95('lesson_load_duration')}ms${' '.repeat(Math.max(0, 20 - avg('lesson_load_duration').length - p95('lesson_load_duration').length))}║
|
||||
║ MinIO Video Fetch : ${avg('video_load_duration')}ms / ${p95('video_load_duration')}ms${' '.repeat(Math.max(0, 20 - avg('video_load_duration').length - p95('video_load_duration').length))}║
|
||||
║ Save Progress : ${avg('progress_save_duration')}ms / ${p95('progress_save_duration')}ms${' '.repeat(Math.max(0, 20 - avg('progress_save_duration').length - p95('progress_save_duration').length))}║
|
||||
║ Complete Lesson : ${avg('complete_lesson_duration')}ms / ${p95('complete_lesson_duration')}ms${' '.repeat(Math.max(0, 20 - avg('complete_lesson_duration').length - p95('complete_lesson_duration').length))}║
|
||||
╠══════════════════════════════════════════════════════════╣
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue