diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 308662d..7f2e4f9 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -337,7 +337,7 @@ class AdminController { static async overrideSubmission(req, res, next) { try { const submissionId = req.params.id; - const { status, adminNotes, reason } = req.body; + const { status, credibility, adminNotes, reason } = req.body; const submission = await Submission.findById(submissionId); if (!submission) { @@ -350,6 +350,11 @@ class AdminController { // Update submission submission.status = status; + if (status === 'approved') { + submission.credibility = credibility; + } else { + submission.credibility = undefined; + } submission.verifier = req.user._id; submission.verifiedAt = new Date(); submission.verifierNotes = adminNotes; diff --git a/backend/tests/integration/admin.test.js b/backend/tests/integration/admin.test.js new file mode 100644 index 0000000..d4f0d2f --- /dev/null +++ b/backend/tests/integration/admin.test.js @@ -0,0 +1,237 @@ +import request from 'supertest'; +import { createTestApp } from '../utils/testApp.js'; +import { + createTestUser, + createTestAdmin, + createTestSubmission, + getAuthHeader, + seedTestData +} from '../utils/testHelpers.js'; +import User from '../../src/models/User.js'; +import Submission from '../../src/models/Submission.js'; +import CountryStats from '../../src/models/CountryStats.js'; + +describe('Admin API Integration Tests', () => { + let app; + + beforeAll(() => { + app = createTestApp(); + }); + + describe('Access Control', () => { + it('should return 403 when a non-admin verifier tries to access admin routes', async () => { + const verifier = await createTestUser({ role: 'verifier' }); + const authHeader = getAuthHeader(verifier); + + await request(app) + .get('/api/admin/dashboard') + .set('Authorization', authHeader) + .expect(403); + }); + + it('should return 403 when a contributor tries to access admin routes', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + + await request(app) + .get('/api/admin/dashboard') + .set('Authorization', authHeader) + .expect(403); + }); + + it('should return 401 when unauthenticated', async () => { + await request(app) + .get('/api/admin/dashboard') + .expect(401); + }); + }); + + describe('GET /api/admin/dashboard', () => { + it('should get admin dashboard statistics', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ submissions: 5 }); + + const response = await request(app) + .get('/api/admin/dashboard') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('globalStats'); + expect(response.body).toHaveProperty('charts'); + expect(response.body).toHaveProperty('recentActivity'); + }); + }); + + describe('GET /api/admin/analytics', () => { + it('should get analytics data', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ submissions: 2 }); + + const response = await request(app) + .get('/api/admin/analytics?period=7d') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('trends'); + expect(response.body).toHaveProperty('verificationSpeed'); + }); + }); + + describe('GET /api/admin/users', () => { + it('should list all users with pagination and stats', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ users: 5 }); + + const response = await request(app) + .get('/api/admin/users?page=1&limit=5') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.users.length).toBeGreaterThan(0); + expect(response.body).toHaveProperty('pagination'); + expect(response.body.users[0]).toHaveProperty('submissionStats'); + }); + + it('should filter users by role and search', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const testUser = await createTestUser({ username: 'specialadminuser', role: 'contributor' }); + + const response = await request(app) + .get('/api/admin/users?role=contributor&search=specialadminuser') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.users.some(u => u.username === 'specialadminuser')).toBe(true); + }); + }); + + describe('PUT /api/admin/users/:id', () => { + it('should update user role and details', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + // Use 2-letter country code so getOrCreate doesn't fail Mongoose validation limit of 2 characters + const user = await createTestUser({ role: 'contributor', country: 'GH' }); + + // Ensure country stats exists for the logic + const countryGH = new CountryStats({ countryCode: 'GH', countryName: 'Ghana' }); + await countryGH.save(); + + const response = await request(app) + .put(`/api/admin/users/${user._id}`) + .set('Authorization', authHeader) + .send({ + role: 'verifier', + points: 120 + }) + .expect(200); + + expect(response.body.user.role).toBe('verifier'); + expect(response.body.user.points).toBe(120); + + // Verify user is added to CountryStats verifiers + const updatedCountry = await CountryStats.findOne({ countryCode: 'GH' }); + expect(updatedCountry.verifiers.some(v => v.userId.toString() === user._id.toString())).toBe(true); + }); + + it('should return 400 when admin tries to update their own account', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + await request(app) + .put(`/api/admin/users/${admin._id}`) + .set('Authorization', authHeader) + .send({ + role: 'contributor' + }) + .expect(400); + }); + }); + + describe('DELETE /api/admin/users/:id', () => { + it('should soft delete user by setting isActive to false', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const user = await createTestUser(); + + const response = await request(app) + .delete(`/api/admin/users/${user._id}`) + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.user.isActive).toBe(false); + expect(response.body.user.email).toContain('deleted_'); + + const dbUser = await User.findById(user._id); + expect(dbUser.isActive).toBe(false); + }); + + it('should return 400 when admin tries to delete their own account', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + await request(app) + .delete(`/api/admin/users/${admin._id}`) + .set('Authorization', authHeader) + .expect(400); + }); + }); + + describe('GET /api/admin/submissions', () => { + it('should list submissions for admin', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ submissions: 3 }); + + const response = await request(app) + .get('/api/admin/submissions') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.submissions.length).toBeGreaterThanOrEqual(3); + }); + }); + + describe('PUT /api/admin/submissions/:id/override', () => { + it('should override a submission status and notes', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const submission = await createTestSubmission({ status: 'pending' }); + + const response = await request(app) + .put(`/api/admin/submissions/${submission._id}/override`) + .set('Authorization', authHeader) + .send({ + status: 'approved', + credibility: 'credible', // Required when status is 'approved' + adminNotes: 'Overridden by administrator', + reason: 'Valid source' + }) + .expect(200); + + expect(response.body.submission.status).toBe('approved'); + expect(response.body.submission.verifierNotes).toBe('Overridden by administrator'); + expect(response.body.submission.verifier.toString()).toBe(admin._id.toString()); + }); + }); + + describe('DELETE /api/admin/submissions/:id', () => { + it('should delete a submission', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const submission = await createTestSubmission(); + + await request(app) + .delete(`/api/admin/submissions/${submission._id}`) + .set('Authorization', authHeader) + .expect(200); + + const dbSubmission = await Submission.findById(submission._id); + expect(dbSubmission).toBeNull(); + }); + }); +}); diff --git a/backend/tests/integration/countries.test.js b/backend/tests/integration/countries.test.js new file mode 100644 index 0000000..738c6e3 --- /dev/null +++ b/backend/tests/integration/countries.test.js @@ -0,0 +1,293 @@ +import request from 'supertest'; +import { createTestApp } from '../utils/testApp.js'; +import { + createTestUser, + createTestAdmin, + createTestVerifier, + getAuthHeader, + seedTestData +} from '../utils/testHelpers.js'; +import CountryStats from '../../src/models/CountryStats.js'; +import User from '../../src/models/User.js'; + +describe('Country Management API Integration Tests', () => { + let app; + + beforeAll(() => { + app = createTestApp(); + }); + + // Seed test country stats for public endpoint testing + let countryGH; + let countryNG; + + beforeEach(async () => { + // Clear and seed test countries + await CountryStats.deleteMany({}); + + countryGH = new CountryStats({ + countryCode: 'GH', + countryName: 'Ghana', + statistics: { totalSubmissions: 5, verifiedSources: 2 } + }); + await countryGH.save(); + + countryNG = new CountryStats({ + countryCode: 'NG', + countryName: 'Nigeria', + statistics: { totalSubmissions: 3, verifiedSources: 1 } + }); + await countryNG.save(); + }); + + describe('GET /api/countries', () => { + it('should list countries (public)', async () => { + const response = await request(app) + .get('/api/countries') + .expect(200); + + expect(response.body.countries.length).toBe(2); + expect(response.body).toHaveProperty('pagination'); + }); + + it('should filter countries by search query', async () => { + const response = await request(app) + .get('/api/countries?search=Ghana') + .expect(200); + + expect(response.body.countries.length).toBe(1); + expect(response.body.countries[0].countryCode).toBe('GH'); + }); + + it('should sort countries by totalSubmissions descending', async () => { + const response = await request(app) + .get('/api/countries?sortBy=submissions') + .expect(200); + + expect(response.body.countries[0].countryCode).toBe('GH'); // 5 > 3 + }); + }); + + describe('GET /api/countries/:code/stats', () => { + it('should fetch statistics for a specific country (public)', async () => { + const response = await request(app) + .get('/api/countries/GH/stats') + .expect(200); + + expect(response.body.countryName).toBe('Ghana'); + expect(response.body.statistics.totalSubmissions).toBe(5); + }); + + it('should return 404 for a non-existent country code', async () => { + const response = await request(app) + .get('/api/countries/XX/stats') + .expect(404); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/countries/:code/submissions', () => { + it('should fetch submissions for a specific country (public)', async () => { + const response = await request(app) + .get('/api/countries/GH/submissions') + .expect(200); + + expect(response.body.success).toBe(true); + expect(Array.isArray(response.body.submissions)).toBe(true); + }); + }); + + describe('POST /api/countries (Admin only)', () => { + it('should allow admin to create a new country record', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + const response = await request(app) + .post('/api/countries') + .set('Authorization', authHeader) + .send({ + countryCode: 'KE', + countryName: 'Kenya' + }) + .expect(201); + + expect(response.body.country.countryCode).toBe('KE'); + expect(response.body.country.countryName).toBe('Kenya'); + + const exists = await CountryStats.findOne({ countryCode: 'KE' }); + expect(exists).not.toBeNull(); + }); + + it('should return 400 when country already exists', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + await request(app) + .post('/api/countries') + .set('Authorization', authHeader) + .send({ + countryCode: 'GH', + countryName: 'Ghana Duplicate' + }) + .expect(400); + }); + + it('should return 403 when accessed by non-admin verifier', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + + await request(app) + .post('/api/countries') + .set('Authorization', authHeader) + .send({ + countryCode: 'KE', + countryName: 'Kenya' + }) + .expect(403); + }); + }); + + describe('PUT /api/countries/:code (Admin only)', () => { + it('should allow admin to update country name', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + const response = await request(app) + .put('/api/countries/GH') + .set('Authorization', authHeader) + .send({ + countryName: 'Ghana Updated' + }) + .expect(200); + + expect(response.body.country.countryName).toBe('Ghana Updated'); + }); + + it('should return 403 for non-admin user', async () => { + const contributor = await createTestUser(); + const authHeader = getAuthHeader(contributor); + + await request(app) + .put('/api/countries/GH') + .set('Authorization', authHeader) + .send({ + countryName: 'Ghana Updated' + }) + .expect(403); + }); + }); + + describe('DELETE /api/countries/:code (Admin only)', () => { + it('should allow admin to delete a country', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + await request(app) + .delete('/api/countries/GH') + .set('Authorization', authHeader) + .expect(200); + + const exists = await CountryStats.findOne({ countryCode: 'GH' }); + expect(exists).toBeNull(); + }); + + it('should return 403 for non-admin user', async () => { + const contributor = await createTestUser(); + const authHeader = getAuthHeader(contributor); + + await request(app) + .delete('/api/countries/GH') + .set('Authorization', authHeader) + .expect(403); + }); + }); + + describe('POST /api/countries/:code/update-stats (Admin only)', () => { + it('should allow admin to trigger statistics recalculation', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + const response = await request(app) + .post('/api/countries/GH/update-stats') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.message).toBe('Country statistics updated successfully'); + }); + }); + + describe('POST /api/countries/:code/assign-verifier (Admin only)', () => { + it('should assign a verifier and update user role if needed', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const user = await createTestUser({ role: 'contributor' }); + + const response = await request(app) + .post('/api/countries/GH/assign-verifier') + .set('Authorization', authHeader) + .send({ + userId: user._id.toString(), + specializations: ['Academic journals'] + }) + .expect(200); + + expect(response.body.message).toBe('Verifier assigned successfully'); + + // Verify role is updated + const updatedUser = await User.findById(user._id); + expect(updatedUser.role).toBe('verifier'); + + // Verify verifier list in country stats + const updatedCountry = await CountryStats.findOne({ countryCode: 'GH' }); + expect(updatedCountry.verifiers.some(v => v.userId.toString() === user._id.toString())).toBe(true); + }); + + it('should return 400 when user is already a verifier for this country', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const verifier = await createTestVerifier(); + + // Assign once + await request(app) + .post('/api/countries/GH/assign-verifier') + .set('Authorization', authHeader) + .send({ userId: verifier._id.toString() }); + + // Assign twice + const response = await request(app) + .post('/api/countries/GH/assign-verifier') + .set('Authorization', authHeader) + .send({ userId: verifier._id.toString() }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); + + describe('POST /api/countries/:code/remove-verifier (Admin only)', () => { + it('should remove a verifier from a country', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const verifier = await createTestVerifier(); + + // Assign first + await request(app) + .post('/api/countries/GH/assign-verifier') + .set('Authorization', authHeader) + .send({ userId: verifier._id.toString() }); + + // Remove + const response = await request(app) + .post('/api/countries/GH/remove-verifier') + .set('Authorization', authHeader) + .send({ userId: verifier._id.toString() }) + .expect(200); + + expect(response.body.message).toBe('Verifier removed successfully'); + + const updatedCountry = await CountryStats.findOne({ countryCode: 'GH' }); + expect(updatedCountry.verifiers.some(v => v.userId.toString() === verifier._id.toString())).toBe(false); + }); + }); +}); diff --git a/backend/tests/integration/reports.test.js b/backend/tests/integration/reports.test.js new file mode 100644 index 0000000..a3aaa80 --- /dev/null +++ b/backend/tests/integration/reports.test.js @@ -0,0 +1,166 @@ +import request from 'supertest'; +import { createTestApp } from '../utils/testApp.js'; +import { + createTestUser, + createTestAdmin, + createTestVerifier, + getAuthHeader, + seedTestData +} from '../utils/testHelpers.js'; +import User from '../../src/models/User.js'; +import CountryStats from '../../src/models/CountryStats.js'; + +describe('Reports API Integration Tests', () => { + let app; + + beforeAll(() => { + app = createTestApp(); + }); + + describe('GET /api/reports/overview', () => { + it('should generate overview report when authenticated as admin', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ submissions: 5 }); + + const response = await request(app) + .get('/api/reports/overview') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('summary'); + expect(response.body.summary).toHaveProperty('totalSubmissions'); + expect(response.body.summary).toHaveProperty('approvedSubmissions'); + expect(response.body.summary).toHaveProperty('rejectedSubmissions'); + expect(response.body.summary).toHaveProperty('pendingSubmissions'); + expect(response.body.summary).toHaveProperty('newUsers'); + }); + + it('should generate overview report when authenticated as verifier', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + await seedTestData({ submissions: 3 }); + + const response = await request(app) + .get('/api/reports/overview') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('summary'); + expect(response.body.summary).toHaveProperty('totalSubmissions'); + }); + + it('should filter overview report by country', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + await seedTestData({ submissions: 4, countries: ['Ghana', 'Nigeria'] }); + + const response = await request(app) + .get('/api/reports/overview?country=Ghana') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('summary'); + expect(response.body.summary).toHaveProperty('totalSubmissions'); + }); + + it('should return 403 when accessed by contributor', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + + const response = await request(app) + .get('/api/reports/overview') + .set('Authorization', authHeader) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should return 401 when not authenticated', async () => { + const response = await request(app) + .get('/api/reports/overview') + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/reports/country/:country', () => { + it('should generate country report when authenticated as admin', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + + // Seed CountryStats since the controller searches for it and throws 404 if not found + const countryGH = new CountryStats({ + countryCode: 'GH', + countryName: 'Ghana' + }); + await countryGH.save(); + + await seedTestData({ submissions: 5 }); + + const response = await request(app) + .get('/api/reports/country/GH') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('summary'); + expect(response.body.summary).toHaveProperty('totalSubmissions'); + expect(response.body.summary).toHaveProperty('approvedSubmissions'); + expect(response.body.summary).toHaveProperty('contributors'); + expect(response.body.summary).toHaveProperty('verifiers'); + }); + + it('should return 403 when accessed by contributor', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + + await request(app) + .get('/api/reports/country/GH') + .set('Authorization', authHeader) + .expect(403); + }); + }); + + describe('GET /api/reports/user/:userId', () => { + it('should generate user report when authenticated as admin', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const user = await createTestUser(); + + const response = await request(app) + .get(`/api/reports/user/${user._id}`) + .set('Authorization', authHeader) + .expect(200); + + expect(response.body).toHaveProperty('summary'); + expect(response.body.summary).toHaveProperty('totalSubmissions'); + expect(response.body.summary).toHaveProperty('approvedSubmissions'); + expect(response.body).toHaveProperty('breakdown'); + expect(response.body.breakdown).toHaveProperty('byCategory'); + expect(response.body.breakdown).toHaveProperty('timeline'); + }); + + it('should return 404 for a non-existent user ID', async () => { + const admin = await createTestAdmin(); + const authHeader = getAuthHeader(admin); + const fakeId = '507f1f77bcf86cd799439011'; + + await request(app) + .get(`/api/reports/user/${fakeId}`) + .set('Authorization', authHeader) + .expect(404); + }); + + it('should return 403 when accessed by contributor', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + const user = await createTestUser(); + + await request(app) + .get(`/api/reports/user/${user._id}`) + .set('Authorization', authHeader) + .expect(403); + }); + }); +}); diff --git a/backend/tests/integration/verification.test.js b/backend/tests/integration/verification.test.js new file mode 100644 index 0000000..875a5ae --- /dev/null +++ b/backend/tests/integration/verification.test.js @@ -0,0 +1,201 @@ +import request from 'supertest'; +import { createTestApp } from '../utils/testApp.js'; +import { + createTestUser, + createTestSubmission, + createTestAdmin, + createTestVerifier, + getAuthHeader, + seedTestData +} from '../utils/testHelpers.js'; +import Submission from '../../src/models/Submission.js'; +import User from '../../src/models/User.js'; + +describe('Verification Workflow API Integration Tests', () => { + let app; + + beforeAll(() => { + app = createTestApp(); + }); + + describe('PUT /api/submissions/:id/verify', () => { + it('should approve a pending submission with credibility rating and award points', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + const submitter = await createTestUser({ points: 10 }); + const submission = await createTestSubmission({ status: 'pending', submitter: submitter._id }); + + const response = await request(app) + .put(`/api/submissions/${submission._id}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'approved', + credibility: 'credible', + verifierNotes: 'Verified to be credible.' + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.submission.status).toBe('approved'); + expect(response.body.submission.credibility).toBe('credible'); + expect(response.body.submission.verifierNotes).toBe('Verified to be credible.'); + expect(response.body.submission.verifier._id).toBe(verifier._id.toString()); + + // Verify submitter points: 10 + 25 = 35 + const updatedSubmitter = await User.findById(submitter._id); + expect(updatedSubmitter.points).toBe(35); + + // Verify verifier points: 0 + 5 = 5 + const updatedVerifier = await User.findById(verifier._id); + expect(updatedVerifier.points).toBe(5); + }); + + it('should reject a pending submission and award verifier points', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + const submitter = await createTestUser({ points: 10 }); + const submission = await createTestSubmission({ status: 'pending', submitter: submitter._id }); + + const response = await request(app) + .put(`/api/submissions/${submission._id}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'rejected', + verifierNotes: 'Rejected due to invalid links.' + }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.submission.status).toBe('rejected'); + expect(response.body.submission.verifierNotes).toBe('Rejected due to invalid links.'); + + // Submitter points should remain same + const updatedSubmitter = await User.findById(submitter._id); + expect(updatedSubmitter.points).toBe(10); + + // Verifier points: 0 + 5 = 5 + const updatedVerifier = await User.findById(verifier._id); + expect(updatedVerifier.points).toBe(5); + }); + + it('should return 400 when status is approved but credibility rating is missing', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + const submission = await createTestSubmission({ status: 'pending' }); + + const response = await request(app) + .put(`/api/submissions/${submission._id}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'approved', + verifierNotes: 'Approved.' + }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should return 400 when attempting to verify an already verified submission', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + const submission = await createTestSubmission({ status: 'approved', credibility: 'credible' }); + + const response = await request(app) + .put(`/api/submissions/${submission._id}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'rejected', + verifierNotes: 'Cannot reject approved.' + }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should return 403 when user does not have verifier or admin role', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + const submission = await createTestSubmission({ status: 'pending' }); + + const response = await request(app) + .put(`/api/submissions/${submission._id}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'approved', + credibility: 'credible' + }) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should return 404 when submission does not exist', async () => { + const verifier = await createTestVerifier(); + const authHeader = getAuthHeader(verifier); + const fakeId = '507f1f77bcf86cd799439011'; + + const response = await request(app) + .put(`/api/submissions/${fakeId}/verify`) + .set('Authorization', authHeader) + .send({ + status: 'approved', + credibility: 'credible' + }) + .expect(404); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/submissions/pending/country', () => { + it('should get pending submissions for verifier\'s country only', async () => { + const verifier = await createTestVerifier({ country: 'Ghana' }); + const authHeader = getAuthHeader(verifier); + + await createTestSubmission({ country: 'Ghana', status: 'pending' }); + await createTestSubmission({ country: 'Ghana', status: 'pending' }); + await createTestSubmission({ country: 'Nigeria', status: 'pending' }); + + const response = await request(app) + .get('/api/submissions/pending/country') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.submissions.length).toBe(2); + response.body.submissions.forEach(submission => { + expect(submission.country).toBe('Ghana'); + expect(submission.status).toBe('pending'); + }); + }); + + it('should get all pending submissions regardless of country for admin', async () => { + const admin = await createTestAdmin({ country: 'Ghana' }); + const authHeader = getAuthHeader(admin); + + await createTestSubmission({ country: 'Ghana', status: 'pending' }); + await createTestSubmission({ country: 'Nigeria', status: 'pending' }); + + const response = await request(app) + .get('/api/submissions/pending/country') + .set('Authorization', authHeader) + .expect(200); + + expect(response.body.success).toBe(true); + // Clean test database setup cleans collections after each test, so we should see exactly 2 pending submissions. + expect(response.body.submissions.length).toBe(2); + }); + + it('should return 403 when user is contributor', async () => { + const contributor = await createTestUser({ role: 'contributor' }); + const authHeader = getAuthHeader(contributor); + + const response = await request(app) + .get('/api/submissions/pending/country') + .set('Authorization', authHeader) + .expect(403); + + expect(response.body.success).toBe(false); + }); + }); +}); diff --git a/backend/tests/setup.js b/backend/tests/setup.js index 74eba5c..a586209 100644 --- a/backend/tests/setup.js +++ b/backend/tests/setup.js @@ -1,5 +1,11 @@ import mongoose from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: path.resolve(__dirname, '../.env') }); let mongoServer; let useMemoryServer = true; diff --git a/backend/tests/utils/testApp.js b/backend/tests/utils/testApp.js index edb5865..5065868 100644 --- a/backend/tests/utils/testApp.js +++ b/backend/tests/utils/testApp.js @@ -6,7 +6,7 @@ import errorHandler from '../../src/middleware/errorHandler.js'; import { optionalAuth } from '../../src/middleware/auth.js'; import { userRateLimiter } from '../../src/middleware/rateLimiter.js'; import authRoutes from '../../src/routes/authRoutes.js'; -import wikimediaOAuth1Routes from '../../src/routes/wikimediaOAuth1Routes.js'; +// import wikimediaOAuth1Routes from '../../src/routes/wikimediaOAuth1Routes.js'; import submissionRoutes from '../../src/routes/submissionRoutes.js'; import userRoutes from '../../src/routes/userRoutes.js'; import adminRoutes from '../../src/routes/adminRoutes.js'; @@ -60,7 +60,7 @@ export const createTestApp = () => { // API routes app.use('/api/auth', authRoutes); - app.use('/api/auth/wikimedia', wikimediaOAuth1Routes); + // app.use('/api/auth/wikimedia', wikimediaOAuth1Routes); app.use('/api/submissions', submissionRoutes); app.use('/api/users', userRoutes); app.use('/api/admin', adminRoutes);