diff --git a/Backend/.gitignore b/Backend/.gitignore index e28abe31..50470b53 100644 --- a/Backend/.gitignore +++ b/Backend/.gitignore @@ -33,6 +33,4 @@ src/routes/routes.ts # Uploads (if storing locally) uploads/ -temp/ - -tests \ No newline at end of file +temp/ \ No newline at end of file diff --git a/Backend/src/services/AdminCourseApproval.service.ts b/Backend/src/services/AdminCourseApproval.service.ts index 90f79793..4a83495b 100644 --- a/Backend/src/services/AdminCourseApproval.service.ts +++ b/Backend/src/services/AdminCourseApproval.service.ts @@ -3,6 +3,7 @@ import { config } from '../config'; import { logger } from '../config/logger'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; +import { getPresignedUrl } from '../config/minio'; import { ListPendingCoursesResponse, GetCourseDetailForAdminResponse, @@ -51,13 +52,22 @@ export class AdminCourseApprovalService { } }); - const data = courses.map(course => ({ - id: course.id, - title: course.title as { th: string; en: string }, - slug: course.slug, - description: course.description as { th: string; en: string }, - thumbnail_url: course.thumbnail_url, - status: course.status, + const data = await Promise.all(courses.map(async (course) => { + let thumbnail_presigned_url: string | null = null; + if (course.thumbnail_url) { + try { + thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600); + } catch (err) { + logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); + } + } + return { + id: course.id, + title: course.title as { th: string; en: string }, + slug: course.slug, + description: course.description as { th: string; en: string }, + thumbnail_url: thumbnail_presigned_url, + status: course.status, created_at: course.created_at, updated_at: course.updated_at, created_by: course.created_by, @@ -70,11 +80,12 @@ export class AdminCourseApprovalService { chapters_count: course.chapters.length, lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0), latest_submission: course.courseApprovals[0] ? { - id: course.courseApprovals[0].id, - submitted_by: course.courseApprovals[0].submitted_by, - created_at: course.courseApprovals[0].created_at, - submitter: course.courseApprovals[0].submitter - } : null + id: course.courseApprovals[0].id, + submitted_by: course.courseApprovals[0].submitted_by, + created_at: course.courseApprovals[0].created_at, + submitter: course.courseApprovals[0].submitter + } : null + }; })); return { @@ -143,6 +154,16 @@ export class AdminCourseApprovalService { throw new NotFoundError('Course not found'); } + // Generate presigned URL for thumbnail + let thumbnail_presigned_url: string | null = null; + if (course.thumbnail_url) { + try { + thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600); + } catch (err) { + logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); + } + } + return { code: 200, message: 'Course details retrieved successfully', @@ -151,7 +172,7 @@ export class AdminCourseApprovalService { title: course.title as { th: string; en: string }, slug: course.slug, description: course.description as { th: string; en: string }, - thumbnail_url: course.thumbnail_url, + thumbnail_url: thumbnail_presigned_url, price: Number(course.price), is_free: course.is_free, have_certificate: course.have_certificate, diff --git a/Backend/tests/k6/register-students.js b/Backend/tests/k6/register-students.js new file mode 100644 index 00000000..74f8952c --- /dev/null +++ b/Backend/tests/k6/register-students.js @@ -0,0 +1,105 @@ +// Backend/tests/k6/register-students.js +// สคริปสำหรับ register นักเรียน 50 คน +// email: studenttest01-50@example.com +// username: student01-50 +// password: admin123 + +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Rate, Counter } from 'k6/metrics'; + +// Custom metrics +const errorRate = new Rate('errors'); +const successCount = new Counter('successful_registrations'); +const failCount = new Counter('failed_registrations'); + +// Configuration: Run 50 iterations sequentially (1 VU to avoid duplicate) +export const options = { + iterations: 50, + vus: 1, // 1 VU to ensure sequential registration (no duplicates) + thresholds: { + errors: ['rate<0.1'], // Error rate < 10% + http_req_duration: ['p(95)<3000'], // 95% of requests < 3s + }, +}; + +const BASE_URL = __ENV.APP_URL || 'http://192.168.1.137:4000'; + +export default function () { + // Calculate student number (1-50) based on iteration + // __ITER is unique per iteration across all VUs + const studentNum = __ITER + 1; + const paddedNum = String(studentNum).padStart(2, '0'); + + const email = `studenttest${paddedNum}@example.com`; + const username = `student${paddedNum}`; + const password = 'admin123'; + + console.log(`Registering student: ${username} (${email})`); + + const payload = JSON.stringify({ + username: username, + email: email, + password: password, + first_name: `Student`, + last_name: `Test${paddedNum}`, + prefix: { + en: 'Mr.', + th: 'นาย' + }, + phone: `08${paddedNum}000000${paddedNum}`, + }); + + const headers = { + 'Content-Type': 'application/json', + }; + + const res = http.post(`${BASE_URL}/api/auth/register-learner`, payload, { headers }); + + const isSuccess = res.status === 200 || res.status === 201; + const isAlreadyExists = res.status === 400 && res.body && res.body.includes('already'); + + errorRate.add(!isSuccess && !isAlreadyExists); + + if (isSuccess) { + successCount.add(1); + console.log(`✓ Successfully registered: ${username}`); + } else if (isAlreadyExists) { + console.log(`⚠ Already exists: ${username}`); + } else { + failCount.add(1); + console.log(`✗ Failed to register: ${username} - Status: ${res.status} - ${res.body}`); + } + + check(res, { + 'registration successful or already exists': (r) => r.status === 200 || r.status === 201 || r.status === 400, + 'response time < 3s': (r) => r.timings.duration < 3000, + }); + + sleep(0.5); // Small delay between registrations +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + }; +} + +function textSummary(data, opts) { + const metrics = data.metrics; + const iterations = metrics.iterations ? metrics.iterations.values.count : 0; + const successRegs = metrics.successful_registrations ? metrics.successful_registrations.values.count : 0; + const failRegs = metrics.failed_registrations ? metrics.failed_registrations.values.count : 0; + + return ` +===================================== + Student Registration Summary +===================================== + Total Iterations: ${iterations} + Successful Registrations: ${successRegs} + Failed Registrations: ${failRegs} + Error Rate: ${(metrics.errors.values.rate * 100).toFixed(2)}% + Avg Response Time: ${metrics.http_req_duration.values.avg.toFixed(2)}ms +===================================== +`; +} diff --git a/Backend/tests/k6/test-credentials.json b/Backend/tests/k6/test-credentials.json new file mode 100644 index 00000000..11c02ec9 --- /dev/null +++ b/Backend/tests/k6/test-credentials.json @@ -0,0 +1,54 @@ +{ + "students": [ + { "email": "studenttest01@example.com", "username": "student01", "password": "admin123" }, + { "email": "studenttest02@example.com", "username": "student02", "password": "admin123" }, + { "email": "studenttest03@example.com", "username": "student03", "password": "admin123" }, + { "email": "studenttest04@example.com", "username": "student04", "password": "admin123" }, + { "email": "studenttest05@example.com", "username": "student05", "password": "admin123" }, + { "email": "studenttest06@example.com", "username": "student06", "password": "admin123" }, + { "email": "studenttest07@example.com", "username": "student07", "password": "admin123" }, + { "email": "studenttest08@example.com", "username": "student08", "password": "admin123" }, + { "email": "studenttest09@example.com", "username": "student09", "password": "admin123" }, + { "email": "studenttest10@example.com", "username": "student10", "password": "admin123" }, + { "email": "studenttest11@example.com", "username": "student11", "password": "admin123" }, + { "email": "studenttest12@example.com", "username": "student12", "password": "admin123" }, + { "email": "studenttest13@example.com", "username": "student13", "password": "admin123" }, + { "email": "studenttest14@example.com", "username": "student14", "password": "admin123" }, + { "email": "studenttest15@example.com", "username": "student15", "password": "admin123" }, + { "email": "studenttest16@example.com", "username": "student16", "password": "admin123" }, + { "email": "studenttest17@example.com", "username": "student17", "password": "admin123" }, + { "email": "studenttest18@example.com", "username": "student18", "password": "admin123" }, + { "email": "studenttest19@example.com", "username": "student19", "password": "admin123" }, + { "email": "studenttest20@example.com", "username": "student20", "password": "admin123" }, + { "email": "studenttest21@example.com", "username": "student21", "password": "admin123" }, + { "email": "studenttest22@example.com", "username": "student22", "password": "admin123" }, + { "email": "studenttest23@example.com", "username": "student23", "password": "admin123" }, + { "email": "studenttest24@example.com", "username": "student24", "password": "admin123" }, + { "email": "studenttest25@example.com", "username": "student25", "password": "admin123" }, + { "email": "studenttest26@example.com", "username": "student26", "password": "admin123" }, + { "email": "studenttest27@example.com", "username": "student27", "password": "admin123" }, + { "email": "studenttest28@example.com", "username": "student28", "password": "admin123" }, + { "email": "studenttest29@example.com", "username": "student29", "password": "admin123" }, + { "email": "studenttest30@example.com", "username": "student30", "password": "admin123" }, + { "email": "studenttest31@example.com", "username": "student31", "password": "admin123" }, + { "email": "studenttest32@example.com", "username": "student32", "password": "admin123" }, + { "email": "studenttest33@example.com", "username": "student33", "password": "admin123" }, + { "email": "studenttest34@example.com", "username": "student34", "password": "admin123" }, + { "email": "studenttest35@example.com", "username": "student35", "password": "admin123" }, + { "email": "studenttest36@example.com", "username": "student36", "password": "admin123" }, + { "email": "studenttest37@example.com", "username": "student37", "password": "admin123" }, + { "email": "studenttest38@example.com", "username": "student38", "password": "admin123" }, + { "email": "studenttest39@example.com", "username": "student39", "password": "admin123" }, + { "email": "studenttest40@example.com", "username": "student40", "password": "admin123" }, + { "email": "studenttest41@example.com", "username": "student41", "password": "admin123" }, + { "email": "studenttest42@example.com", "username": "student42", "password": "admin123" }, + { "email": "studenttest43@example.com", "username": "student43", "password": "admin123" }, + { "email": "studenttest44@example.com", "username": "student44", "password": "admin123" }, + { "email": "studenttest45@example.com", "username": "student45", "password": "admin123" }, + { "email": "studenttest46@example.com", "username": "student46", "password": "admin123" }, + { "email": "studenttest47@example.com", "username": "student47", "password": "admin123" }, + { "email": "studenttest48@example.com", "username": "student48", "password": "admin123" }, + { "email": "studenttest49@example.com", "username": "student49", "password": "admin123" }, + { "email": "studenttest50@example.com", "username": "student50", "password": "admin123" } + ] +}