Merge pull request #20 from internship-humic/feat/certificate-api

fix: assign member hanya intern yg gk ada active project
This commit is contained in:
Rafi Athallah
2026-06-27 18:17:54 +07:00
committed by GitHub
5 changed files with 201 additions and 85 deletions
@@ -131,27 +131,6 @@ class CertificateController {
} }
} }
async verifyCertificate(req, res) {
try {
const certificate_no = req.query.certificate_no || req.params.certificate_no;
const result = await certificateService.verifyCertificate(certificate_no);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Certificate validated successfully', SUCCESS.OK);
} catch (err) {
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Unexpected error occurred',
ERROR.INTERNAL_ERROR
);
}
}
async verifyCertificateByUuid(req, res) { async verifyCertificateByUuid(req, res) {
try { try {
const { uuid } = req.params; const { uuid } = req.params;
@@ -160,23 +160,6 @@ class CertificateService {
} }
} }
async verifyCertificate(certificate_no) {
try {
if (!certificate_no) {
return error(new BadRequestError('Certificate number is required'));
}
const certificate = await certificateRepository.findByCertificateNo(certificate_no);
if (!certificate) {
return error(new NotFoundError('Certificate not found or invalid'));
}
return data(this.mapCertificate(certificate));
} catch (err) {
return error(err);
}
}
async verifyCertificateByUuid(uuid) { async verifyCertificateByUuid(uuid) {
try { try {
if (!uuid) { if (!uuid) {
@@ -547,13 +547,30 @@ class ProjectService {
return error(new NotFoundError("User not found or inactive")); return error(new NotFoundError("User not found or inactive"));
} }
if (user.role !== "intern") {
return error(
new ForbiddenError("Access denied: only interns can be assigned to projects")
);
}
const activeMemberships = await projectRepository.findActiveMembershipsByUserIds([id_user]);
if (activeMemberships.length > 0) {
const activeProj = activeMemberships[0].project;
if (activeMemberships[0].id_project === parseInt(id_project)) {
return error(new ConflictError("User is already an active member of this project"));
} else {
return error(
new ConflictError(
`User is already an active member of another project: ${activeProj?.project_name || "Unknown Project"}`
)
);
}
}
const existingMembership = await projectRepository.findMembership(id_project, id_user); const existingMembership = await projectRepository.findMembership(id_project, id_user);
let membership; let membership;
if (existingMembership) { if (existingMembership) {
if (existingMembership.status === 'active') {
return error(new ConflictError("User is already an active member of this project"));
}
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active'); membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
} else { } else {
membership = await projectRepository.assignMember(id_project, id_user); membership = await projectRepository.assignMember(id_project, id_user);
-44
View File
@@ -267,50 +267,6 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail)
* description: Internal server error * description: Internal server error
*/ */
router.get('/project/:id_project', verifyJWT, isAdmin, certificateController.getProjectCertificates); router.get('/project/:id_project', verifyJWT, isAdmin, certificateController.getProjectCertificates);
/**
* @swagger
* /certificate-api/verify:
* get:
* summary: Verify a certificate (Public)
* description: Verify the validity of a certificate number publicly without authentication.
* tags: [Certificate]
* parameters:
* - in: query
* name: certificate_no
* schema:
* type: string
* required: true
* description: The certificate number to verify
* example: "CERT/20260609/PRJ2/USR5"
* responses:
* 200:
* description: Certificate is valid
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/Certificate'
* message:
* type: string
* example: "Certificate validated successfully"
* code:
* type: integer
* example: 200
* 400:
* description: Bad request (missing certificate number)
* 404:
* description: Certificate not found or invalid
* 500:
* description: Internal server error
*/
router.get('/verify', certificateController.verifyCertificate);
/** /**
* @swagger * @swagger
* /certificate-api/verify-uuid/{uuid}: * /certificate-api/verify-uuid/{uuid}:
+181
View File
@@ -278,6 +278,187 @@ async function runTests() {
} }
console.log(' PASSED: Intern successfully retrieved project detail containing certificate template URL.\n'); console.log(' PASSED: Intern successfully retrieved project detail containing certificate template URL.\n');
// 10. Claim Certificate Tests
console.log('10. Claim Certificate Tests...');
// Get Intern User ID
const internUser = await prisma.user.findUnique({
where: { email: internEmail }
});
const internUserId = internUser.id;
console.log(` Intern User ID: ${internUserId}`);
// A. Claim before tasks are created (Should fail - 400)
console.log(' A. Trying to claim certificate before any tasks are created...');
try {
await axios.post(
`${BASE_URL}/certificate-api/claim`,
{ id_project: project1Id },
getHeader(internToken)
);
throw new Error('TEST FAILED: Intern claimed certificate with no tasks!');
} catch (err) {
if (err.response && err.response.status === 400) {
console.log(' PASSED: Claim failed with 400 Bad Request as expected.');
console.log(' Error Message:', err.response.data.message);
} else {
throw err;
}
}
// B. Create a task for Project 1
console.log(' B. Creating a task for Project 1...');
const task = await prisma.task.create({
data: {
id_project: project1Id,
slug: `final-assignment-${Date.now()}`,
title: 'Final Assignment',
description: 'Submit your final report.',
deadline_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
submission_type: 'url_link'
}
});
console.log(` Task created successfully! ID: ${task.id}`);
// C. Claim before submitting tasks (Should fail - 400)
console.log(' C. Trying to claim certificate before task submission...');
try {
await axios.post(
`${BASE_URL}/certificate-api/claim`,
{ id_project: project1Id },
getHeader(internToken)
);
throw new Error('TEST FAILED: Intern claimed certificate without submitting tasks!');
} catch (err) {
if (err.response && err.response.status === 400) {
console.log(' PASSED: Claim failed with 400 Bad Request as expected.');
console.log(' Error Message:', err.response.data.message);
} else {
throw err;
}
}
// D. Submit the task
console.log(' D. Creating submission for the task...');
const submission = await prisma.taskSubmission.create({
data: {
id_task: task.id,
id_user: internUserId,
url_link: 'https://github.com/internify/report'
}
});
console.log(` Submission created successfully! ID: ${submission.id}`);
// E. Claim certificate (Should succeed - 201)
console.log(' E. Claiming certificate after submitting tasks...');
const claimRes = await axios.post(
`${BASE_URL}/certificate-api/claim`,
{ id_project: project1Id },
getHeader(internToken)
);
console.log(' Response status:', claimRes.status);
console.log(' Claimed Certificate No:', claimRes.data.data.certificate_no);
if (claimRes.status !== 201) {
throw new Error(`TEST FAILED: Expected status 201, got ${claimRes.status}`);
}
console.log(' PASSED: Certificate claimed successfully!\n');
// F. Claim certificate again (Should fail - 409)
console.log(' F. Trying to claim certificate again...');
try {
await axios.post(
`${BASE_URL}/certificate-api/claim`,
{ id_project: project1Id },
getHeader(internToken)
);
throw new Error('TEST FAILED: Intern claimed the same certificate twice!');
} catch (err) {
if (err.response && err.response.status === 409) {
console.log(' PASSED: Claim failed with 409 Conflict as expected.');
console.log(' Error Message:', err.response.data.message);
} else {
throw err;
}
}
// G. Retrieve my certificates (Should include the claimed certificate)
console.log(' G. Verifying certificate is in my certificates list...');
const myCertsRes = await axios.get(
`${BASE_URL}/certificate-api/my-certificates`,
getHeader(internToken)
);
console.log(' Certificates list length:', myCertsRes.data.data.length);
const foundCert = myCertsRes.data.data.find(c => c.id_project === project1Id);
if (!foundCert) {
throw new Error('TEST FAILED: Claimed certificate not found in my certificates list!');
}
console.log(' PASSED: Claimed certificate verified in list!\n');
// 11. Assign Member Restriction Tests
console.log('11. Assign Member Restriction Tests...');
// A. Assign Intern (who is active in Project 1) to Project 2 (Should fail - 409)
console.log(' A. Trying to assign intern (already active in Project 1) to Project 2...');
try {
await axios.post(
`${BASE_URL}/project-api/assign-member`,
{ id_project: project2Id, id_user: internUserId },
getHeader(mentor2Token)
);
throw new Error('TEST FAILED: Intern was assigned to two active projects!');
} catch (err) {
if (err.response && err.response.status === 409) {
console.log(' PASSED: Assign failed with 409 Conflict as expected.');
console.log(' Error Message:', err.response.data.message);
} else {
throw err;
}
}
// B. Remove intern from Project 1
console.log(' B. Removing intern from Project 1...');
const removeRes = await axios.post(
`${BASE_URL}/project-api/remove-member`,
{ id_project: project1Id, id_user: internUserId },
getHeader(mentor1Token)
);
console.log(' Response status:', removeRes.status);
if (removeRes.status !== 200) {
throw new Error(`TEST FAILED: Failed to remove member, got status ${removeRes.status}`);
}
console.log(' PASSED: Intern successfully removed from Project 1.');
// C. Assign intern to Project 2 (Should succeed now)
console.log(' C. Assigning intern to Project 2...');
const assignRes = await axios.post(
`${BASE_URL}/project-api/assign-member`,
{ id_project: project2Id, id_user: internUserId },
getHeader(mentor2Token)
);
console.log(' Response status:', assignRes.status);
if (assignRes.status !== 200) {
throw new Error(`TEST FAILED: Failed to assign member, got status ${assignRes.status}`);
}
console.log(' PASSED: Intern successfully assigned to Project 2.');
// D. Assign intern to Project 2 again (Should fail - 409)
console.log(' D. Trying to assign intern to Project 2 again...');
try {
await axios.post(
`${BASE_URL}/project-api/assign-member`,
{ id_project: project2Id, id_user: internUserId },
getHeader(mentor2Token)
);
throw new Error('TEST FAILED: Intern assigned to Project 2 twice!');
} catch (err) {
if (err.response && err.response.status === 409) {
console.log(' PASSED: Assign failed with 409 Conflict as expected.');
console.log(' Error Message:', err.response.data.message);
} else {
throw err;
}
}
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ==='); console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
await prisma.$disconnect(); await prisma.$disconnect();
process.exit(0); process.exit(0);