Merge pull request #26 from internship-humic/feat/certificate-api
feat: nambahin API route untuk generate certificate bagi Admin/Mentor
This commit is contained in:
@@ -3,7 +3,7 @@ const { response, error } = require('../../../helpers/utils/wrapper');
|
|||||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||||
const { InternalServerError, BadRequestError } = require('../../../helpers/error');
|
const { InternalServerError, BadRequestError } = require('../../../helpers/error');
|
||||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||||
const { claimCertificateModel } = require('../models/certificate.model');
|
const { claimCertificateModel, generateCertificatesModel } = require('../models/certificate.model');
|
||||||
|
|
||||||
const getActor = (req) => ({
|
const getActor = (req) => ({
|
||||||
id: req.id,
|
id: req.id,
|
||||||
@@ -45,6 +45,39 @@ class CertificateController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateCertificates(req, res) {
|
||||||
|
try {
|
||||||
|
const validatePayload = isValidPayload(req.body, generateCertificatesModel);
|
||||||
|
|
||||||
|
if (validatePayload.err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
{ err: validatePayload.err, data: null },
|
||||||
|
'Data generate certificate tidak valid',
|
||||||
|
ERROR.EXPECTATION_FAILED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await certificateService.generateCertificates(validatePayload.data, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Certificates generated successfully', SUCCESS.CREATED);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getMyCertificates(req, res) {
|
async getMyCertificates(req, res) {
|
||||||
try {
|
try {
|
||||||
const actor = getActor(req);
|
const actor = getActor(req);
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ const claimCertificateModel = joi.object().keys({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const generateCertificatesModel = joi.object().keys({
|
||||||
|
id_project: joi.number().integer().required().messages({
|
||||||
|
'number.base': 'Project ID harus berupa angka.',
|
||||||
|
'number.integer': 'Project ID harus berupa bilangan bulat.',
|
||||||
|
'any.required': 'Project ID wajib diisi.',
|
||||||
|
}),
|
||||||
|
id_users: joi.array().items(joi.number().integer()).min(1).required().messages({
|
||||||
|
'array.base': 'Intern IDs harus berupa array.',
|
||||||
|
'array.min': 'Pilih minimal satu intern.',
|
||||||
|
'any.required': 'Intern IDs wajib diisi.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
claimCertificateModel,
|
claimCertificateModel,
|
||||||
|
generateCertificatesModel,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,6 +80,102 @@ class CertificateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateCertificates(payload, actor) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id) {
|
||||||
|
return error(new BadRequestError('User ID is required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role !== 'admin' && actor.role !== 'mentor') {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError('Access denied: only admins and mentors can generate certificates'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id_project, id_users } = payload;
|
||||||
|
|
||||||
|
const project = await certificateRepository.findProjectById(id_project);
|
||||||
|
if (!project) {
|
||||||
|
return error(new NotFoundError('Project not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization check:
|
||||||
|
if (actor.role === 'mentor' && project.id_admin !== parseInt(actor.id)) {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError('Access denied: you are not the mentor of this project'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatedUsers = [];
|
||||||
|
for (const userId of id_users) {
|
||||||
|
const existingCert = await certificateRepository.findByUserAndProject(userId, id_project);
|
||||||
|
if (existingCert) {
|
||||||
|
return error(
|
||||||
|
new ConflictError(`Certificate already generated/claimed for user ID ${userId}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectInfo = await certificateRepository.getProjectTasksAndSubmissions(id_project, userId);
|
||||||
|
if (!projectInfo) {
|
||||||
|
return error(new NotFoundError('Project not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectInfo.members || projectInfo.members.length === 0) {
|
||||||
|
return error(
|
||||||
|
new ForbiddenError(`User ID ${userId} is not an active member of this project`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectInfo.tasks || projectInfo.tasks.length === 0) {
|
||||||
|
return error(
|
||||||
|
new BadRequestError(`Project does not have any tasks assigned yet`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedTasks = projectInfo.tasks.filter(
|
||||||
|
(task) => task.submissions && task.submissions.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (completedTasks.length < projectInfo.tasks.length) {
|
||||||
|
return error(
|
||||||
|
new BadRequestError(
|
||||||
|
`User ID ${userId} is not eligible: completed ${completedTasks.length} out of ${projectInfo.tasks.length} tasks`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedUsers.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedCertificates = [];
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const date = String(now.getDate()).padStart(2, '0');
|
||||||
|
const dateString = `${year}${month}${date}`;
|
||||||
|
|
||||||
|
for (const userId of validatedUsers) {
|
||||||
|
const certificateNo = `CERT/${dateString}/PRJ${id_project}/USR${userId}`;
|
||||||
|
const certificateData = {
|
||||||
|
id_project: parseInt(id_project),
|
||||||
|
id_user: parseInt(userId),
|
||||||
|
certificate_no: certificateNo,
|
||||||
|
issued_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const certificate = await certificateRepository.create(certificateData);
|
||||||
|
generatedCertificates.push(this.mapCertificate(certificate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data({
|
||||||
|
message: `${generatedCertificates.length} certificates generated successfully.`,
|
||||||
|
certificates: generatedCertificates,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getMyCertificates(actor) {
|
async getMyCertificates(actor) {
|
||||||
try {
|
try {
|
||||||
if (!actor?.id) {
|
if (!actor?.id) {
|
||||||
|
|||||||
@@ -210,11 +210,25 @@ class ProjectRepository {
|
|||||||
submission_type: true,
|
submission_type: true,
|
||||||
created_at: true,
|
created_at: true,
|
||||||
updated_at: true,
|
updated_at: true,
|
||||||
|
submissions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
id_user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
deadline_at: "asc",
|
deadline_at: "asc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
certificates: {
|
||||||
|
select: {
|
||||||
|
id_user: true,
|
||||||
|
certificate_no: true,
|
||||||
|
uuid: true,
|
||||||
|
issued_at: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ class ProjectService {
|
|||||||
members:
|
members:
|
||||||
project.members?.map((member) => this.mapProjectMember(member)) || [],
|
project.members?.map((member) => this.mapProjectMember(member)) || [],
|
||||||
tasks: project.tasks || [],
|
tasks: project.tasks || [],
|
||||||
|
certificates: project.certificates || [],
|
||||||
total_members: project.members?.length || 0,
|
total_members: project.members?.length || 0,
|
||||||
total_tasks: project.tasks?.length || 0,
|
total_tasks: project.tasks?.length || 0,
|
||||||
created_at: project.created_at,
|
created_at: project.created_at,
|
||||||
|
|||||||
@@ -114,6 +114,50 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
router.post('/claim', verifyJWT, certificateController.claimCertificate);
|
router.post('/claim', verifyJWT, certificateController.claimCertificate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /certificate-api/generate:
|
||||||
|
* post:
|
||||||
|
* summary: Batch generate certificates (Admin/Mentor only)
|
||||||
|
* description: Mentors or Admins can batch-generate certificates for selected eligible interns of a project.
|
||||||
|
* tags: [Certificate]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id_project
|
||||||
|
* - id_users
|
||||||
|
* properties:
|
||||||
|
* id_project:
|
||||||
|
* type: integer
|
||||||
|
* example: 2
|
||||||
|
* id_users:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: integer
|
||||||
|
* example: [5, 6]
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Certificates generated successfully
|
||||||
|
* 400:
|
||||||
|
* description: Bad request (not eligible or missing parameters)
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden (only mentors/admins of the project can generate)
|
||||||
|
* 409:
|
||||||
|
* description: Conflict (certificate already generated for one or more interns)
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.post('/generate', verifyJWT, isMentorOrAdmin, certificateController.generateCertificates);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /certificate-api/my-certificates:
|
* /certificate-api/my-certificates:
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const { PrismaClient } = require('../src/generated/prisma');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:9000';
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
function createMultipartPayload(boundary, fields, files) {
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(fields)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
chunks.push(Buffer.from(`--${boundary}\r\n` +
|
||||||
|
`Content-Disposition: form-data; name="${key}"\r\n\r\n` +
|
||||||
|
`${value}\r\n`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, file] of Object.entries(files)) {
|
||||||
|
if (file) {
|
||||||
|
chunks.push(Buffer.from(`--${boundary}\r\n` +
|
||||||
|
`Content-Disposition: form-data; name="${key}"; filename="${file.name}"\r\n` +
|
||||||
|
`Content-Type: ${file.type}\r\n\r\n`));
|
||||||
|
chunks.push(file.content);
|
||||||
|
chunks.push(Buffer.from('\r\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerIntern(adminToken, email, firstName, lastName) {
|
||||||
|
// Create lowongan magang
|
||||||
|
const lwnBoundary = '----WebKitFormBoundaryLowonganUpload';
|
||||||
|
const lwnFields = {
|
||||||
|
posisi: `Web Developer Test - ${Date.now()}`,
|
||||||
|
kelompok_peminatan: 'Software Engineering',
|
||||||
|
jobdesk: 'Testing batch certificate generation.',
|
||||||
|
lokasi: 'Remote',
|
||||||
|
kualifikasi: 'NodeJS',
|
||||||
|
benefit: 'Certificate',
|
||||||
|
durasi_awal: '2026-07-01',
|
||||||
|
durasi_akhir: '2026-10-01',
|
||||||
|
paid: 'unpaid'
|
||||||
|
};
|
||||||
|
const lwnFiles = {
|
||||||
|
image: { name: 'poster.png', type: 'image/png', content: Buffer.from('fake-image-data') }
|
||||||
|
};
|
||||||
|
const lwnPayload = createMultipartPayload(lwnBoundary, lwnFields, lwnFiles);
|
||||||
|
|
||||||
|
const createLwnRes = await axios.post(
|
||||||
|
`${BASE_URL}/lowongan-magang-api/add`,
|
||||||
|
lwnPayload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${adminToken}`,
|
||||||
|
'Content-Type': `multipart/form-data; boundary=${lwnBoundary}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const lowonganId = createLwnRes.data.data.id;
|
||||||
|
|
||||||
|
// Submit application
|
||||||
|
const appBoundary = '----WebKitFormBoundaryApplicationSubmission';
|
||||||
|
const appFields = {
|
||||||
|
nama_depan: firstName,
|
||||||
|
nama_belakang: lastName,
|
||||||
|
email: email,
|
||||||
|
kontak: '08123456789',
|
||||||
|
jurusan: 'Computer Science',
|
||||||
|
universitas: 'Intern University',
|
||||||
|
negara: 'Indonesia',
|
||||||
|
motivasi: 'I want to learn.',
|
||||||
|
relevant_skills: 'NodeJS, React'
|
||||||
|
};
|
||||||
|
const appFiles = {
|
||||||
|
cv: { name: 'cv.pdf', type: 'application/pdf', content: Buffer.from('fake-pdf-cv') },
|
||||||
|
portofolio: { name: 'portfolio.pdf', type: 'application/pdf', content: Buffer.from('fake-pdf-portfolio') }
|
||||||
|
};
|
||||||
|
const appPayload = createMultipartPayload(appBoundary, appFields, appFiles);
|
||||||
|
|
||||||
|
const applyRes = await axios.post(
|
||||||
|
`${BASE_URL}/lamaran-magang-api/add-mobile/${lowonganId}`,
|
||||||
|
appPayload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `multipart/form-data; boundary=${appBoundary}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const idMahasiswa = applyRes.data.data.id_mahasiswa;
|
||||||
|
|
||||||
|
// Wait a brief moment
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Find lamaran ID
|
||||||
|
const listLamaranRes = await axios.get(`${BASE_URL}/lamaran-magang-api/get?limit=100`, {
|
||||||
|
headers: { Authorization: `Bearer ${adminToken}` }
|
||||||
|
});
|
||||||
|
const lamaranItem = listLamaranRes.data.data.find(l => l.id_mahasiswa === idMahasiswa);
|
||||||
|
if (!lamaranItem) {
|
||||||
|
throw new Error(`Failed to find lamaran for student ID: ${idMahasiswa}`);
|
||||||
|
}
|
||||||
|
const lamaranId = lamaranItem.id;
|
||||||
|
|
||||||
|
// Accept application
|
||||||
|
await axios.patch(`${BASE_URL}/lamaran-magang-api/update/${lamaranId}`, {
|
||||||
|
status: 'diterima'
|
||||||
|
}, {
|
||||||
|
headers: { Authorization: `Bearer ${adminToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Force override password
|
||||||
|
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { email },
|
||||||
|
data: { password: hashedPassword }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
const loginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email,
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
token: loginRes.data.data.token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('=== STARTING BATCH GENERATE CERTIFICATE TESTS ===\n');
|
||||||
|
|
||||||
|
let adminToken = '';
|
||||||
|
let mentor1Token = '';
|
||||||
|
let mentor2Token = '';
|
||||||
|
let project1Id = null;
|
||||||
|
|
||||||
|
const password = 'password123';
|
||||||
|
const intern1Email = `intern1_batch_${Date.now()}@internify.com`;
|
||||||
|
const intern2Email = `intern2_batch_${Date.now()}@internify.com`;
|
||||||
|
|
||||||
|
const getHeader = (token) => ({
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Login Admin
|
||||||
|
console.log('1. Logging in as Admin (admin1@internify.com)...');
|
||||||
|
const adminLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'admin1@internify.com',
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
adminToken = adminLoginRes.data.data.token;
|
||||||
|
|
||||||
|
// 2. Login Mentors
|
||||||
|
console.log('2. Logging in as Mentors...');
|
||||||
|
const mentor1Login = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'mentor1@internify.com',
|
||||||
|
password
|
||||||
|
});
|
||||||
|
mentor1Token = mentor1Login.data.data.token;
|
||||||
|
|
||||||
|
const mentor2Login = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'mentor2@internify.com',
|
||||||
|
password
|
||||||
|
});
|
||||||
|
mentor2Token = mentor2Login.data.data.token;
|
||||||
|
|
||||||
|
// 3. Register Intern 1 and Intern 2
|
||||||
|
console.log('3. Registering Intern 1 & Intern 2...');
|
||||||
|
const intern1 = await registerIntern(adminToken, intern1Email, 'InternOne', 'Batch');
|
||||||
|
console.log(` Registered Intern 1: User ID ${intern1.userId}`);
|
||||||
|
|
||||||
|
const intern2 = await registerIntern(adminToken, intern2Email, 'InternTwo', 'Batch');
|
||||||
|
console.log(` Registered Intern 2: User ID ${intern2.userId}`);
|
||||||
|
|
||||||
|
// 4. Create Project owned by Mentor 1 and assign both interns
|
||||||
|
console.log('4. Creating Project under Mentor 1 with both interns...');
|
||||||
|
const createProjectRes = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'code',
|
||||||
|
project_name: `Batch Certificate Project - ${Date.now()}`,
|
||||||
|
description: 'Project to test batch generation.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: [intern1Email, intern2Email]
|
||||||
|
}, getHeader(mentor1Token));
|
||||||
|
project1Id = createProjectRes.data.data.id;
|
||||||
|
console.log(` Project created successfully! ID: ${project1Id}`);
|
||||||
|
|
||||||
|
// 5. Create a task in the project
|
||||||
|
console.log('5. Creating a required project task...');
|
||||||
|
const task = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
id_project: project1Id,
|
||||||
|
slug: `required-task-${Date.now()}`,
|
||||||
|
title: 'Project Submission',
|
||||||
|
description: 'Complete project tasks.',
|
||||||
|
deadline_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
|
||||||
|
submission_type: 'url_link'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(` Task created! ID: ${task.id}`);
|
||||||
|
|
||||||
|
// 6. Submit task for Intern 1 only
|
||||||
|
console.log('6. Submitting task for Intern 1 only...');
|
||||||
|
await prisma.taskSubmission.create({
|
||||||
|
data: {
|
||||||
|
id_task: task.id,
|
||||||
|
id_user: intern1.userId,
|
||||||
|
url_link: 'https://github.com/intern1/solution'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(' Intern 1 task submitted successfully. Intern 2 has NO submissions.');
|
||||||
|
|
||||||
|
// 7. Test Authorization: Call generate endpoint using Intern 1 token (Should fail - 401)
|
||||||
|
console.log('\n--- API TEST 1: Intern calls generate endpoint (Should fail - 401) ---');
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/certificate-api/generate`, {
|
||||||
|
id_project: project1Id,
|
||||||
|
id_users: [intern1.userId]
|
||||||
|
}, getHeader(intern1.token));
|
||||||
|
throw new Error('TEST FAILED: Intern generated certificate!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 401) {
|
||||||
|
console.log(' PASSED: Denied access to intern with 401 Unauthorized.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Test Authorization: Call generate endpoint using Mentor 2 token (Should fail - 403)
|
||||||
|
console.log('\n--- API TEST 2: Mentor 2 (non-owner) calls generate endpoint (Should fail - 403) ---');
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/certificate-api/generate`, {
|
||||||
|
id_project: project1Id,
|
||||||
|
id_users: [intern1.userId]
|
||||||
|
}, getHeader(mentor2Token));
|
||||||
|
throw new Error('TEST FAILED: Unauthorized mentor generated certificate!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 403) {
|
||||||
|
console.log(' PASSED: Denied access to unauthorized mentor with 403 Forbidden.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Test Eligibility: Request generation for both Intern 1 (completed) and Intern 2 (incomplete) (Should fail - 400)
|
||||||
|
console.log('\n--- API TEST 3: Generate batch containing ineligible intern (Should fail - 400) ---');
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/certificate-api/generate`, {
|
||||||
|
id_project: project1Id,
|
||||||
|
id_users: [intern1.userId, intern2.userId]
|
||||||
|
}, getHeader(mentor1Token));
|
||||||
|
throw new Error('TEST FAILED: Generated certificate for batch containing ineligible intern!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 400) {
|
||||||
|
console.log(' PASSED: Rejected batch due to ineligible intern with 400 Bad Request.');
|
||||||
|
console.log(' Error Message:', err.response.data.message);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Success case: Generate certificate for Intern 1 only (Should succeed - 201)
|
||||||
|
console.log('\n--- API TEST 4: Generate certificate for eligible Intern 1 (Should succeed - 201) ---');
|
||||||
|
const genRes = await axios.post(`${BASE_URL}/certificate-api/generate`, {
|
||||||
|
id_project: project1Id,
|
||||||
|
id_users: [intern1.userId]
|
||||||
|
}, getHeader(mentor1Token));
|
||||||
|
console.log(' Response status:', genRes.status);
|
||||||
|
if (genRes.status !== 201) {
|
||||||
|
throw new Error(`TEST FAILED: Expected 201 Created, got ${genRes.status}`);
|
||||||
|
}
|
||||||
|
console.log(' Generated Certificate No:', genRes.data.data.certificates[0].certificate_no);
|
||||||
|
console.log(' PASSED: Certificate generated successfully.');
|
||||||
|
|
||||||
|
// 11. Conflict check: Try to generate for Intern 1 again (Should fail - 409)
|
||||||
|
console.log('\n--- API TEST 5: Re-generate certificate for Intern 1 (Should fail - 409) ---');
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/certificate-api/generate`, {
|
||||||
|
id_project: project1Id,
|
||||||
|
id_users: [intern1.userId]
|
||||||
|
}, getHeader(mentor1Token));
|
||||||
|
throw new Error('TEST FAILED: Re-generated certificate for Intern 1!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 409) {
|
||||||
|
console.log(' PASSED: Blocked duplicate generation with 409 Conflict.');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Verify Project Details mapping: Check task submissions and certificates in details response
|
||||||
|
console.log('\n--- API TEST 6: Fetch project details and verify database mapping ---');
|
||||||
|
const projectDetailsRes = await axios.get(`${BASE_URL}/project-api/get/${project1Id}`, getHeader(mentor1Token));
|
||||||
|
const projectData = projectDetailsRes.data.data;
|
||||||
|
|
||||||
|
// Verify certificates list exists
|
||||||
|
if (!projectData.certificates || projectData.certificates.length !== 1) {
|
||||||
|
throw new Error('TEST FAILED: Certificates list in project details is missing or incorrect!');
|
||||||
|
}
|
||||||
|
console.log(' Found Certificates in Project details:', projectData.certificates);
|
||||||
|
|
||||||
|
// Verify task submissions exist
|
||||||
|
const projectTask = projectData.tasks.find(t => t.id === task.id);
|
||||||
|
if (!projectTask || !projectTask.submissions || projectTask.submissions.length !== 1) {
|
||||||
|
throw new Error('TEST FAILED: Task submissions list in project details is missing or incorrect!');
|
||||||
|
}
|
||||||
|
console.log(' Found Submissions in Project Task:', projectTask.submissions);
|
||||||
|
console.log(' PASSED: Database mapping checked successfully.');
|
||||||
|
|
||||||
|
console.log('\n=== ALL BATCH GENERATE CERTIFICATE TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
|
||||||
|
if (err.response) {
|
||||||
|
console.error(`Status: ${err.response.status}`);
|
||||||
|
console.error('Response Data:', JSON.stringify(err.response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
Reference in New Issue
Block a user