feat: Nambahin template certificate endpoint
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `project` ADD COLUMN `certificate_template` TEXT NULL;
|
||||||
@@ -212,6 +212,7 @@ model Project {
|
|||||||
start_date DateTime @db.Date
|
start_date DateTime @db.Date
|
||||||
end_date DateTime @db.Date
|
end_date DateTime @db.Date
|
||||||
status ProjectStatus @default(active)
|
status ProjectStatus @default(active)
|
||||||
|
certificate_template String? @db.Text
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const certificateService = require('../services/certificate.service');
|
const certificateService = require('../services/certificate.service');
|
||||||
const { response, error } = require('../../../helpers/utils/wrapper');
|
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 } = 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 } = require('../models/certificate.model');
|
||||||
|
|
||||||
@@ -172,6 +172,40 @@ class CertificateController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadCertificateTemplate(req, res) {
|
||||||
|
try {
|
||||||
|
const { id_project } = req.params;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new BadRequestError('Certificate template file is required')),
|
||||||
|
'Certificate template file is required',
|
||||||
|
ERROR.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await certificateService.uploadCertificateTemplate(id_project, file, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Certificate template uploaded successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new CertificateController();
|
module.exports = new CertificateController();
|
||||||
|
|||||||
@@ -95,6 +95,19 @@ class CertificateRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findProjectById(id_project) {
|
||||||
|
return prisma.project.findUnique({
|
||||||
|
where: { id: parseInt(id_project) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectTemplate(id_project, templatePath) {
|
||||||
|
return prisma.project.update({
|
||||||
|
where: { id: parseInt(id_project) },
|
||||||
|
data: { certificate_template: templatePath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
certificateDetailInclude() {
|
certificateDetailInclude() {
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
@@ -109,6 +122,7 @@ class CertificateRepository {
|
|||||||
id: true,
|
id: true,
|
||||||
project_name: true,
|
project_name: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
certificate_template: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -199,6 +199,47 @@ class CertificateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadCertificateTemplate(id_project, file, actor) {
|
||||||
|
try {
|
||||||
|
if (!id_project) {
|
||||||
|
return error(new BadRequestError('Project ID is required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return error(new BadRequestError('No file uploaded'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actor?.id || !actor?.role) {
|
||||||
|
return error(new ForbiddenError('Access denied: authentication data is missing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await certificateRepository.findProjectById(id_project);
|
||||||
|
if (!project) {
|
||||||
|
return error(new NotFoundError('Project not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization checks:
|
||||||
|
if (actor.role === 'mentor') {
|
||||||
|
if (project.id_admin !== parseInt(actor.id)) {
|
||||||
|
return error(new ForbiddenError('Access denied: you are not the mentor of this project'));
|
||||||
|
}
|
||||||
|
} else if (actor.role !== 'admin') {
|
||||||
|
return error(new ForbiddenError('Access denied: only admins and mentors can upload certificate templates'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePath = `/uploads/${file.filename}`;
|
||||||
|
|
||||||
|
await certificateRepository.updateProjectTemplate(id_project, templatePath);
|
||||||
|
|
||||||
|
return data({
|
||||||
|
id_project: parseInt(id_project),
|
||||||
|
certificate_template: templatePath,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mapCertificate(cert) {
|
mapCertificate(cert) {
|
||||||
if (!cert) return null;
|
if (!cert) return null;
|
||||||
return {
|
return {
|
||||||
@@ -217,6 +258,7 @@ class CertificateService {
|
|||||||
id: cert.project.id,
|
id: cert.project.id,
|
||||||
project_name: cert.project.project_name,
|
project_name: cert.project.project_name,
|
||||||
description: cert.project.description,
|
description: cert.project.description,
|
||||||
|
certificate_template: cert.project.certificate_template,
|
||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ class ProjectService {
|
|||||||
project_icon: project.project_icon,
|
project_icon: project.project_icon,
|
||||||
project_name: project.project_name,
|
project_name: project.project_name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
|
certificate_template: project.certificate_template,
|
||||||
start_date: project.start_date,
|
start_date: project.start_date,
|
||||||
end_date: project.end_date,
|
end_date: project.end_date,
|
||||||
status: project.status,
|
status: project.status,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const express = require('express');
|
|||||||
const { certificateController } = require('../modules/certificate');
|
const { certificateController } = require('../modules/certificate');
|
||||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||||
const isAdmin = require('../middleware/isAdmin');
|
const isAdmin = require('../middleware/isAdmin');
|
||||||
|
const isMentorOrAdmin = require('../middleware/isMentorOrAdmin');
|
||||||
|
const multer = require('../middleware/multer');
|
||||||
|
const { multerErrorHandler, checkFileSizes } = require('../middleware/multer');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -366,4 +369,83 @@ router.get('/verify', certificateController.verifyCertificate);
|
|||||||
*/
|
*/
|
||||||
router.get('/verify-uuid/:uuid', certificateController.verifyCertificateByUuid);
|
router.get('/verify-uuid/:uuid', certificateController.verifyCertificateByUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /certificate-api/projects/{id_project}/template:
|
||||||
|
* post:
|
||||||
|
* summary: Upload certificate template for a project
|
||||||
|
* description: Admin or Mentor can upload a certificate template file (image/pdf) for a specific project. Mentors can only upload to projects they own.
|
||||||
|
* tags: [Certificate]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id_project
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: The ID of the project to associate the template with
|
||||||
|
* example: 1
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* multipart/form-data:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - template
|
||||||
|
* properties:
|
||||||
|
* template:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* description: The certificate template file (JPEG, JPG, PNG, or PDF). Max size 5MB.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Certificate template uploaded successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id_project:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* certificate_template:
|
||||||
|
* type: string
|
||||||
|
* example: "/uploads/1718000000000-987654321.png"
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "Certificate template uploaded successfully"
|
||||||
|
* code:
|
||||||
|
* type: integer
|
||||||
|
* example: 200
|
||||||
|
* 400:
|
||||||
|
* description: Bad request (missing file or invalid input)
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden (Mentor does not own the project, or unauthorized role)
|
||||||
|
* 404:
|
||||||
|
* description: Project not found
|
||||||
|
* 413:
|
||||||
|
* description: File size too large (max 5MB)
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/projects/:id_project/template',
|
||||||
|
verifyJWT,
|
||||||
|
isMentorOrAdmin,
|
||||||
|
multer.single('template'),
|
||||||
|
checkFileSizes,
|
||||||
|
multerErrorHandler,
|
||||||
|
certificateController.uploadCertificateTemplate
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
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 runTests() {
|
||||||
|
console.log('=== STARTING INTEGRATION TESTS FOR CERTIFICATE TEMPLATE UPLOAD ===\n');
|
||||||
|
|
||||||
|
let adminToken = '';
|
||||||
|
let mentor1Token = '';
|
||||||
|
let mentor2Token = '';
|
||||||
|
let internToken = '';
|
||||||
|
let project1Id = null;
|
||||||
|
let project2Id = null;
|
||||||
|
|
||||||
|
const password = 'password123';
|
||||||
|
const internEmail = `new_intern_cert_${Date.now()}@internify.com`;
|
||||||
|
|
||||||
|
const getHeader = (token) => ({
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMultipartHeader = (token, boundary) => ({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': `multipart/form-data; boundary=${boundary}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Login as 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;
|
||||||
|
console.log(' Admin logged in successfully!\n');
|
||||||
|
|
||||||
|
// 2. Logging in as Mentors
|
||||||
|
console.log('2. Logging in as Mentors...');
|
||||||
|
const mentor1LoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'mentor1@internify.com',
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
mentor1Token = mentor1LoginRes.data.data.token;
|
||||||
|
|
||||||
|
const mentor2LoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'mentor2@internify.com',
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
mentor2Token = mentor2LoginRes.data.data.token;
|
||||||
|
console.log(' Mentors logged in successfully!\n');
|
||||||
|
|
||||||
|
// 3. Registering a new Intern to assign to Mentor 1 Project
|
||||||
|
console.log(`3. Registering a new Intern (${internEmail})...`);
|
||||||
|
|
||||||
|
// Create a new vacancy with an image upload to get a valid UUID id_lowongan_magang
|
||||||
|
const lwnBoundary = '----WebKitFormBoundaryLowonganUpload';
|
||||||
|
const lwnFields = {
|
||||||
|
posisi: 'Web Developer Test',
|
||||||
|
kelompok_peminatan: 'Software Engineering',
|
||||||
|
jobdesk: 'Testing task and certificate templates.',
|
||||||
|
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);
|
||||||
|
|
||||||
|
console.log(' Creating lowongan magang (vacancy) dynamically...');
|
||||||
|
const createLwnRes = await axios.post(
|
||||||
|
`${BASE_URL}/lowongan-magang-api/add`,
|
||||||
|
lwnPayload,
|
||||||
|
getMultipartHeader(adminToken, lwnBoundary)
|
||||||
|
);
|
||||||
|
const lowonganId = createLwnRes.data.data.id;
|
||||||
|
console.log(' Vacancy created successfully! ID:', lowonganId);
|
||||||
|
|
||||||
|
// Submit application (lamaran) using the add-mobile endpoint which also registers the student
|
||||||
|
console.log(' Submitting internship application (which registers the student profile)...');
|
||||||
|
const appBoundary = '----WebKitFormBoundaryApplicationSubmission';
|
||||||
|
const appFields = {
|
||||||
|
nama_depan: 'Intern',
|
||||||
|
nama_belakang: 'Cert',
|
||||||
|
email: internEmail,
|
||||||
|
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;
|
||||||
|
console.log(' Application submitted successfully! Student ID:', idMahasiswa);
|
||||||
|
|
||||||
|
// Fetch all lamaran to get the lamaran ID
|
||||||
|
console.log(' Retrieving lamaran list to find the newly created lamaran ID...');
|
||||||
|
const listLamaranRes = await axios.get(`${BASE_URL}/lamaran-magang-api/get?limit=100`, getHeader(adminToken));
|
||||||
|
const lamaranList = listLamaranRes.data.data;
|
||||||
|
const lamaranItem = lamaranList.find(l => l.id_mahasiswa === idMahasiswa);
|
||||||
|
if (!lamaranItem) {
|
||||||
|
throw new Error('TEST FAILED: Created lamaran not found in lamaran list!');
|
||||||
|
}
|
||||||
|
const lamaranId = lamaranItem.id;
|
||||||
|
console.log(' Found Lamaran ID:', lamaranId);
|
||||||
|
|
||||||
|
// Accept application to create user account
|
||||||
|
console.log(' Accepting application to generate Intern user account...');
|
||||||
|
await axios.patch(`${BASE_URL}/lamaran-magang-api/update/${lamaranId}`, {
|
||||||
|
status: 'diterima'
|
||||||
|
}, getHeader(adminToken));
|
||||||
|
|
||||||
|
// Wait a brief moment for the user insertion to complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Force override user's password in the database directly
|
||||||
|
console.log(' Overriding intern user password in DB directly to password123...');
|
||||||
|
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { email: internEmail },
|
||||||
|
data: { password: hashedPassword }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login as new Intern
|
||||||
|
const internLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: internEmail,
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
internToken = internLoginRes.data.data.token;
|
||||||
|
console.log(' Intern logged in successfully!\n');
|
||||||
|
|
||||||
|
// 4. Mentors create projects dynamically to establish ownership
|
||||||
|
console.log('4. Creating projects dynamically for Mentors...');
|
||||||
|
|
||||||
|
// Mentor 1 creates Project 1 and assigns Intern (newly registered)
|
||||||
|
const project1Res = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'code',
|
||||||
|
project_name: `Mentor 1 Project - ${Date.now()}`,
|
||||||
|
description: 'Owned by Mentor 1.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: [internEmail]
|
||||||
|
}, getHeader(mentor1Token));
|
||||||
|
project1Id = project1Res.data.data.id;
|
||||||
|
|
||||||
|
// Mentor 2 creates Project 2
|
||||||
|
const project2Res = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'shield',
|
||||||
|
project_name: `Mentor 2 Project - ${Date.now()}`,
|
||||||
|
description: 'Owned by Mentor 2.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: []
|
||||||
|
}, getHeader(mentor2Token));
|
||||||
|
project2Id = project2Res.data.data.id;
|
||||||
|
|
||||||
|
console.log(` Projects created: Project 1 ID = ${project1Id}, Project 2 ID = ${project2Id}\n`);
|
||||||
|
|
||||||
|
// Prepare dummy file contents for upload
|
||||||
|
const uploadBoundary = '----WebKitFormBoundaryTemplateUpload';
|
||||||
|
const dummyTemplateContent = Buffer.from('fake-png-template-content');
|
||||||
|
const uploadPayload = createMultipartPayload(uploadBoundary, {}, {
|
||||||
|
template: { name: 'cert_template.png', type: 'image/png', content: dummyTemplateContent }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Mentor 1 uploads template to Project 1 (Should succeed)
|
||||||
|
console.log(`5. Mentor 1 uploading certificate template to Project 1...`);
|
||||||
|
const uploadRes = await axios.post(
|
||||||
|
`${BASE_URL}/certificate-api/projects/${project1Id}/template`,
|
||||||
|
uploadPayload,
|
||||||
|
getMultipartHeader(mentor1Token, uploadBoundary)
|
||||||
|
);
|
||||||
|
console.log(' Response status:', uploadRes.status);
|
||||||
|
console.log(' Uploaded template path:', uploadRes.data.data.certificate_template);
|
||||||
|
if (!uploadRes.data.data.certificate_template.startsWith('/uploads/')) {
|
||||||
|
throw new Error('TEST FAILED: Uploaded template path does not start with /uploads/');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Mentor 1 successfully uploaded template to their own project.\n');
|
||||||
|
|
||||||
|
// 6. Mentor 2 tries to upload to Project 1 (Should fail - 403)
|
||||||
|
console.log(`6. Testing isolation: Mentor 2 trying to upload template to Project 1 (owned by Mentor 1)...`);
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${BASE_URL}/certificate-api/projects/${project1Id}/template`,
|
||||||
|
uploadPayload,
|
||||||
|
getMultipartHeader(mentor2Token, uploadBoundary)
|
||||||
|
);
|
||||||
|
throw new Error('TEST FAILED: Mentor 2 was able to upload template to Project 1!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 403) {
|
||||||
|
console.log(' PASSED: Access denied with 403 Forbidden as expected.\n');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Admin uploads template to Project 1 (Should succeed)
|
||||||
|
console.log(`7. Admin uploading certificate template to Project 1...`);
|
||||||
|
const adminUploadRes = await axios.post(
|
||||||
|
`${BASE_URL}/certificate-api/projects/${project1Id}/template`,
|
||||||
|
uploadPayload,
|
||||||
|
getMultipartHeader(adminToken, uploadBoundary)
|
||||||
|
);
|
||||||
|
console.log(' Response status:', adminUploadRes.status);
|
||||||
|
console.log(' PASSED: Admin successfully uploaded template to project.\n');
|
||||||
|
|
||||||
|
// 8. Intern tries to upload template to Project 1 (Should fail - 401/403)
|
||||||
|
console.log(`8. Testing role restriction: Intern trying to upload template to Project 1...`);
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${BASE_URL}/certificate-api/projects/${project1Id}/template`,
|
||||||
|
uploadPayload,
|
||||||
|
getMultipartHeader(internToken, uploadBoundary)
|
||||||
|
);
|
||||||
|
throw new Error('TEST FAILED: Intern was able to upload template to Project 1!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && (err.response.status === 403 || err.response.status === 401)) {
|
||||||
|
console.log(` PASSED: Access denied with ${err.response.status} as expected.\n`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Intern retrieves project details and verifies certificate template path is returned
|
||||||
|
console.log(`9. Intern retrieving Project 1 details to verify certificate template exposure...`);
|
||||||
|
const projectDetailRes = await axios.get(`${BASE_URL}/project-api/get/${project1Id}`, getHeader(internToken));
|
||||||
|
console.log(' Project detail certificate_template:', projectDetailRes.data.data.certificate_template);
|
||||||
|
if (!projectDetailRes.data.data.certificate_template) {
|
||||||
|
throw new Error('TEST FAILED: Project detail response does not include certificate_template!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Intern successfully retrieved project detail containing certificate template URL.\n');
|
||||||
|
|
||||||
|
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('=== 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