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:
@@ -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);
|
||||||
|
|||||||
@@ -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}:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user