diff --git a/backend/package-lock.json b/backend/package-lock.json index e435f21c..6b07f232 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2753,67 +2753,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/mongoose/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/mongoose/node_modules/mongodb": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", diff --git a/backend/tests/achievementController.unit.test.js b/backend/tests/achievementController.unit.test.js index e69de29b..c831a906 100644 --- a/backend/tests/achievementController.unit.test.js +++ b/backend/tests/achievementController.unit.test.js @@ -0,0 +1,165 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const User = require('../models/User'); +const { getAchievements, saveAchievements } = require('../controllers/achievementController'); + +const VALID_USER_ID = '507f1f77bcf86cd799439011'; + +beforeEach(() => { + vi.restoreAllMocks(); + User.findById = vi.fn(); + User.findByIdAndUpdate = vi.fn(); +}); + +function makeReq(body = {}) { + return { + user: { _id: VALID_USER_ID }, + body, + }; +} + +function makeRes() { + const res = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +} + +describe('achievementController', () => { + describe('getAchievements', () => { + it('returns 404 if user is not found', async () => { + User.findById.mockReturnValue({ + select: vi.fn().mockResolvedValue(null), + }); + + const req = makeReq(); + const res = makeRes(); + + await getAchievements(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ success: false, error: 'User not found' }); + }); + + it('returns user achievements and resets streak if diffDays > 1', async () => { + const mockUser = { + _id: VALID_USER_ID, + unlockedAchievements: ['First Interview'], + lastPracticeDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3 days ago + currentStreak: 5, + save: vi.fn().mockResolvedValue(true), + }; + + User.findById.mockReturnValue({ + select: vi.fn().mockResolvedValue(mockUser), + }); + + const req = makeReq(); + const res = makeRes(); + + await getAchievements(req, res); + + expect(mockUser.currentStreak).toBe(0); + expect(mockUser.save).toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ + success: true, + unlockedAchievements: ['First Interview'], + }); + }); + + it('does not reset streak if last practice was within 1 day', async () => { + const mockUser = { + _id: VALID_USER_ID, + unlockedAchievements: ['Interview Pro'], + lastPracticeDate: new Date(), // today + currentStreak: 5, + save: vi.fn().mockResolvedValue(true), + }; + + User.findById.mockReturnValue({ + select: vi.fn().mockResolvedValue(mockUser), + }); + + const req = makeReq(); + const res = makeRes(); + + await getAchievements(req, res); + + expect(mockUser.currentStreak).toBe(5); + expect(mockUser.save).not.toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ + success: true, + unlockedAchievements: ['Interview Pro'], + }); + }); + + it('returns 500 when database error occurs', async () => { + User.findById.mockReturnValue({ + select: vi.fn().mockRejectedValue(new Error('DB Error')), + }); + + const req = makeReq(); + const res = makeRes(); + + await getAchievements(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ success: false, error: 'A server error occurred' }); + }); + }); + + describe('saveAchievements', () => { + it('returns 400 if unlockedAchievements is missing or not an array', async () => { + const req = makeReq({ unlockedAchievements: 'First Interview' }); + const res = makeRes(); + + await saveAchievements(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + success: false, + error: 'unlockedAchievements must be a valid array', + }); + }); + + it('returns 400 if unlockedAchievements contains invalid IDs', async () => { + const req = makeReq({ unlockedAchievements: ['Invalid Achievement'] }); + const res = makeRes(); + + await saveAchievements(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + success: false, + error: 'Unknown achievement ID(s): Invalid Achievement', + }); + }); + + it('successfully saves achievements for a valid request', async () => { + User.findByIdAndUpdate.mockResolvedValue({}); + + const req = makeReq({ unlockedAchievements: ['First Interview', 'Interview Pro'] }); + const res = makeRes(); + + await saveAchievements(req, res); + + expect(User.findByIdAndUpdate).toHaveBeenCalledWith( + VALID_USER_ID, + { $addToSet: { unlockedAchievements: { $each: ['First Interview', 'Interview Pro'] } } } + ); + expect(res.json).toHaveBeenCalledWith({ success: true }); + }); + + it('returns 500 when database error occurs during save', async () => { + User.findByIdAndUpdate.mockRejectedValue(new Error('DB Error')); + + const req = makeReq({ unlockedAchievements: ['First Interview'] }); + const res = makeRes(); + + await saveAchievements(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ success: false, error: 'A server error occurred' }); + }); + }); +});