From ccf192a14d2d61e015ae8cd9325d0f992c3a32ea Mon Sep 17 00:00:00 2001 From: Rafi Athallah <65345768+rafiathallah3@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:12:42 +0700 Subject: [PATCH] fix: assign member hanya intern yg gk ada active project --- .../controllers/certificate.controller.js | 21 -- .../services/certificate.service.js | 17 -- .../project/services/project.service.js | 23 ++- src/routes/certificate.routes.js | 44 ----- test/test_certificate_template.js | 181 ++++++++++++++++++ 5 files changed, 201 insertions(+), 85 deletions(-) diff --git a/src/modules/certificate/controllers/certificate.controller.js b/src/modules/certificate/controllers/certificate.controller.js index deffe36..a758c2d 100644 --- a/src/modules/certificate/controllers/certificate.controller.js +++ b/src/modules/certificate/controllers/certificate.controller.js @@ -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) { try { const { uuid } = req.params; diff --git a/src/modules/certificate/services/certificate.service.js b/src/modules/certificate/services/certificate.service.js index 4b61a99..cb46a06 100644 --- a/src/modules/certificate/services/certificate.service.js +++ b/src/modules/certificate/services/certificate.service.js @@ -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) { try { if (!uuid) { diff --git a/src/modules/project/services/project.service.js b/src/modules/project/services/project.service.js index 39f3aea..f49bed2 100644 --- a/src/modules/project/services/project.service.js +++ b/src/modules/project/services/project.service.js @@ -547,13 +547,30 @@ class ProjectService { 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); let membership; 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'); } else { membership = await projectRepository.assignMember(id_project, id_user); diff --git a/src/routes/certificate.routes.js b/src/routes/certificate.routes.js index e58cf1c..e86787e 100644 --- a/src/routes/certificate.routes.js +++ b/src/routes/certificate.routes.js @@ -267,50 +267,6 @@ router.get('/detail/:id', verifyJWT, certificateController.getCertificateDetail) * description: Internal server error */ 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 * /certificate-api/verify-uuid/{uuid}: diff --git a/test/test_certificate_template.js b/test/test_certificate_template.js index ffd06f6..c187f22 100644 --- a/test/test_certificate_template.js +++ b/test/test_certificate_template.js @@ -278,6 +278,187 @@ async function runTests() { } 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! ==='); await prisma.$disconnect(); process.exit(0);