chore: remove tests from .gitignore and add presigned URLs for course thumbnails in admin approval service
This commit is contained in:
parent
4e0191ed1f
commit
50ff78c594
4 changed files with 194 additions and 16 deletions
2
Backend/.gitignore
vendored
2
Backend/.gitignore
vendored
|
|
@ -34,5 +34,3 @@ src/routes/routes.ts
|
||||||
# Uploads (if storing locally)
|
# Uploads (if storing locally)
|
||||||
uploads/
|
uploads/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
tests
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { config } from '../config';
|
||||||
import { logger } from '../config/logger';
|
import { logger } from '../config/logger';
|
||||||
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { getPresignedUrl } from '../config/minio';
|
||||||
import {
|
import {
|
||||||
ListPendingCoursesResponse,
|
ListPendingCoursesResponse,
|
||||||
GetCourseDetailForAdminResponse,
|
GetCourseDetailForAdminResponse,
|
||||||
|
|
@ -51,12 +52,21 @@ export class AdminCourseApprovalService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = courses.map(course => ({
|
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,
|
id: course.id,
|
||||||
title: course.title as { th: string; en: string },
|
title: course.title as { th: string; en: string },
|
||||||
slug: course.slug,
|
slug: course.slug,
|
||||||
description: course.description as { th: string; en: string },
|
description: course.description as { th: string; en: string },
|
||||||
thumbnail_url: course.thumbnail_url,
|
thumbnail_url: thumbnail_presigned_url,
|
||||||
status: course.status,
|
status: course.status,
|
||||||
created_at: course.created_at,
|
created_at: course.created_at,
|
||||||
updated_at: course.updated_at,
|
updated_at: course.updated_at,
|
||||||
|
|
@ -75,6 +85,7 @@ export class AdminCourseApprovalService {
|
||||||
created_at: course.courseApprovals[0].created_at,
|
created_at: course.courseApprovals[0].created_at,
|
||||||
submitter: course.courseApprovals[0].submitter
|
submitter: course.courseApprovals[0].submitter
|
||||||
} : null
|
} : null
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -143,6 +154,16 @@ export class AdminCourseApprovalService {
|
||||||
throw new NotFoundError('Course not found');
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Course details retrieved successfully',
|
message: 'Course details retrieved successfully',
|
||||||
|
|
@ -151,7 +172,7 @@ export class AdminCourseApprovalService {
|
||||||
title: course.title as { th: string; en: string },
|
title: course.title as { th: string; en: string },
|
||||||
slug: course.slug,
|
slug: course.slug,
|
||||||
description: course.description as { th: string; en: string },
|
description: course.description as { th: string; en: string },
|
||||||
thumbnail_url: course.thumbnail_url,
|
thumbnail_url: thumbnail_presigned_url,
|
||||||
price: Number(course.price),
|
price: Number(course.price),
|
||||||
is_free: course.is_free,
|
is_free: course.is_free,
|
||||||
have_certificate: course.have_certificate,
|
have_certificate: course.have_certificate,
|
||||||
|
|
|
||||||
105
Backend/tests/k6/register-students.js
Normal file
105
Backend/tests/k6/register-students.js
Normal file
|
|
@ -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
|
||||||
|
=====================================
|
||||||
|
`;
|
||||||
|
}
|
||||||
54
Backend/tests/k6/test-credentials.json
Normal file
54
Backend/tests/k6/test-credentials.json
Normal file
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue