const request = require('supertest'); const express = require('express'); const cookieParser = require('cookie-parser'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { OAuth2Client } = require('google-auth-library'); // Mock dependencies jest.mock('jsonwebtoken'); jest.mock('google-auth-library'); jest.mock('sequelize', () => ({ Op: { or: 'or' } })); jest.mock('../../../models', () => ({ User: { findOne: jest.fn(), create: jest.fn(), findByPk: jest.fn() }, AlphaInvitation: { findOne: jest.fn() } })); // Mock middleware jest.mock('../../../middleware/validation', () => ({ sanitizeInput: (req, res, next) => next(), validateRegistration: (req, res, next) => next(), validateLogin: (req, res, next) => next(), validateGoogleAuth: (req, res, next) => next(), validateForgotPassword: (req, res, next) => next(), validateResetPassword: (req, res, next) => next(), validateVerifyResetToken: (req, res, next) => next(), })); jest.mock('../../../middleware/csrf', () => ({ csrfProtection: (req, res, next) => next(), getCSRFToken: (req, res) => res.json({ csrfToken: 'test-csrf-token' }) })); jest.mock('../../../middleware/rateLimiter', () => ({ loginLimiter: (req, res, next) => next(), registerLimiter: (req, res, next) => next(), passwordResetLimiter: (req, res, next) => next(), emailVerificationLimiter: (req, res, next) => next(), })); jest.mock('../../../middleware/auth', () => ({ optionalAuth: (req, res, next) => next(), authenticateToken: (req, res, next) => { req.user = { id: 'user-123' }; next(); }, })); jest.mock('../../../services/email', () => ({ auth: { sendVerificationEmail: jest.fn().mockResolvedValue(), sendPasswordResetEmail: jest.fn().mockResolvedValue(), sendPasswordChangedEmail: jest.fn().mockResolvedValue(), } })); jest.mock('../../../utils/logger', () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), withRequestId: jest.fn(() => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), })), })); const { User, AlphaInvitation } = require('../../../models'); const emailService = require('../../../services/email'); // Set up OAuth2Client mock before requiring authRoutes const mockGoogleClient = { verifyIdToken: jest.fn(), getToken: jest.fn() }; OAuth2Client.mockImplementation(() => mockGoogleClient); const authRoutes = require('../../../routes/auth'); const app = express(); app.use(express.json()); app.use(cookieParser()); app.use('/auth', authRoutes); // Add error handler app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); describe('Auth Routes', () => { beforeEach(() => { jest.clearAllMocks(); // Reset environment process.env.JWT_ACCESS_SECRET = 'test-access-secret'; process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; process.env.GOOGLE_CLIENT_ID = 'test-google-client-id'; process.env.NODE_ENV = 'test'; delete process.env.ALPHA_TESTING_ENABLED; // Reset JWT mock to return different tokens for each call let tokenCallCount = 0; jwt.sign.mockImplementation(() => { tokenCallCount++; return tokenCallCount % 2 === 1 ? 'access-token' : 'refresh-token'; }); }); describe('GET /auth/csrf-token', () => { it('should return CSRF token', async () => { const response = await request(app) .get('/auth/csrf-token'); expect(response.status).toBe(200); expect(response.body).toHaveProperty('csrfToken'); expect(response.body.csrfToken).toBe('test-csrf-token'); }); }); describe('POST /auth/register', () => { it('should register a new user successfully', async () => { User.findOne.mockResolvedValue(null); const newUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, jwtVersion: 1, role: 'user', verificationToken: 'test-verification-token', generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.auth.sendVerificationEmail.mockResolvedValue(); const response = await request(app) .post('/auth/register') .send({ email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User', phone: '1234567890' }); expect(response.status).toBe(201); expect(response.body.user).toMatchObject({ id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false }); expect(response.body.verificationEmailSent).toBe(true); expect(newUser.generateVerificationToken).toHaveBeenCalled(); expect(emailService.auth.sendVerificationEmail).toHaveBeenCalledWith(newUser, newUser.verificationToken); // Check that cookies are set expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken'), expect.stringContaining('refreshToken') ]) ); }); it('should reject registration with existing email', async () => { User.findOne.mockResolvedValue({ id: 1, email: 'test@example.com' }); const response = await request(app) .post('/auth/register') .send({ email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Registration failed'); expect(response.body.details[0].message).toBe('An account with this email already exists'); }); it('should handle registration errors', async () => { User.findOne.mockResolvedValue(null); User.create.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/register') .send({ email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User' }); expect(response.status).toBe(500); expect(response.body.error).toBe('Registration failed. Please try again.'); }); it('should continue registration even if verification email fails', async () => { User.findOne.mockResolvedValue(null); const newUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, jwtVersion: 1, role: 'user', verificationToken: 'test-verification-token', generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.auth.sendVerificationEmail.mockRejectedValue(new Error('Email service down')); const response = await request(app) .post('/auth/register') .send({ email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User' }); expect(response.status).toBe(201); expect(response.body.user.id).toBe(1); expect(response.body.verificationEmailSent).toBe(false); }); }); describe('POST /auth/login', () => { it('should login user with valid credentials', async () => { const mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: true, jwtVersion: 1, role: 'user', isLocked: jest.fn().mockReturnValue(false), comparePassword: jest.fn().mockResolvedValue(true), resetLoginAttempts: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(200); expect(response.body.user).toMatchObject({ id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User' }); expect(mockUser.resetLoginAttempts).toHaveBeenCalled(); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken'), expect.stringContaining('refreshToken') ]) ); }); it('should reject login with invalid email', async () => { User.findOne.mockResolvedValue(null); const response = await request(app) .post('/auth/login') .send({ email: 'nonexistent@example.com', password: 'password123' }); expect(response.status).toBe(401); expect(response.body.error).toBe('Unable to log in. Please check your email and password, or create an account.'); }); it('should reject login with invalid password', async () => { const mockUser = { id: 1, isLocked: jest.fn().mockReturnValue(false), comparePassword: jest.fn().mockResolvedValue(false), incLoginAttempts: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'wrongpassword' }); expect(response.status).toBe(401); expect(response.body.error).toBe('Unable to log in. Please check your email and password, or create an account.'); expect(mockUser.incLoginAttempts).toHaveBeenCalled(); }); it('should reject login for locked account', async () => { const mockUser = { id: 1, isLocked: jest.fn().mockReturnValue(true) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(423); expect(response.body.error).toContain('Account is temporarily locked'); }); it('should handle login errors', async () => { User.findOne.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(500); expect(response.body.error).toBe('Login failed. Please try again.'); }); }); describe('POST /auth/google', () => { const mockGooglePayload = { sub: 'google123', email: 'test@gmail.com', given_name: 'Test', family_name: 'User', picture: 'https://example.com/profile.jpg' }; beforeEach(() => { mockGoogleClient.getToken.mockResolvedValue({ tokens: { id_token: 'mock-id-token' } }); mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockGooglePayload }); }); it('should handle Google OAuth login for new user', async () => { User.findOne .mockResolvedValueOnce(null) // No existing Google user .mockResolvedValueOnce(null); // No existing email user const newUser = { id: 1, email: 'test@gmail.com', firstName: 'Test', lastName: 'User', imageFilename: 'https://example.com/profile.jpg', isVerified: true, jwtVersion: 1, role: 'user' }; User.create.mockResolvedValue(newUser); const response = await request(app) .post('/auth/google') .send({ code: 'valid-auth-code' }); expect(response.status).toBe(200); expect(response.body.user).toMatchObject({ id: 1, email: 'test@gmail.com', firstName: 'Test', lastName: 'User' }); expect(User.create).toHaveBeenCalledWith(expect.objectContaining({ email: 'test@gmail.com', firstName: 'Test', lastName: 'User', authProvider: 'google', providerId: 'google123', isVerified: true })); }); it('should handle Google OAuth login for existing user', async () => { const existingUser = { id: 1, email: 'test@gmail.com', firstName: 'Test', lastName: 'User', isVerified: true, jwtVersion: 1, role: 'user' }; User.findOne.mockResolvedValue(existingUser); const response = await request(app) .post('/auth/google') .send({ code: 'valid-auth-code' }); expect(response.status).toBe(200); expect(response.body.user).toMatchObject({ id: 1, email: 'test@gmail.com' }); }); it('should reject when email exists with different auth provider', async () => { User.findOne .mockResolvedValueOnce(null) // No Google user .mockResolvedValueOnce({ id: 1, email: 'test@gmail.com', authProvider: 'local' }); // Existing email user const response = await request(app) .post('/auth/google') .send({ code: 'valid-auth-code' }); expect(response.status).toBe(409); expect(response.body.error).toContain('An account with this email already exists'); }); it('should reject missing authorization code', async () => { const response = await request(app) .post('/auth/google') .send({}); expect(response.status).toBe(400); expect(response.body.error).toBe('Authorization code is required'); }); it('should handle invalid authorization code', async () => { mockGoogleClient.getToken.mockRejectedValue(new Error('invalid_grant')); const response = await request(app) .post('/auth/google') .send({ code: 'invalid-code' }); expect(response.status).toBe(401); expect(response.body.error).toContain('Invalid or expired authorization code'); }); it('should handle redirect URI mismatch', async () => { mockGoogleClient.getToken.mockRejectedValue(new Error('redirect_uri_mismatch')); const response = await request(app) .post('/auth/google') .send({ code: 'some-code' }); expect(response.status).toBe(400); expect(response.body.error).toContain('Redirect URI mismatch'); }); it('should handle missing email permission', async () => { mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => ({ sub: 'google123' }) // No email }); const response = await request(app) .post('/auth/google') .send({ code: 'valid-auth-code' }); expect(response.status).toBe(400); expect(response.body.error).toContain('Email permission is required'); }); it('should generate fallback name from email when not provided', async () => { mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => ({ sub: 'google123', email: 'john.doe@gmail.com' // No given_name or family_name }) }); User.findOne .mockResolvedValueOnce(null) .mockResolvedValueOnce(null); const newUser = { id: 1, email: 'john.doe@gmail.com', firstName: 'John', lastName: 'Doe', isVerified: true, jwtVersion: 1, role: 'user' }; User.create.mockResolvedValue(newUser); const response = await request(app) .post('/auth/google') .send({ code: 'valid-auth-code' }); expect(response.status).toBe(200); expect(User.create).toHaveBeenCalledWith(expect.objectContaining({ firstName: 'John', lastName: 'Doe' })); }); it('should handle Google auth errors gracefully', async () => { mockGoogleClient.getToken.mockRejectedValue(new Error('Unknown error')); const response = await request(app) .post('/auth/google') .send({ code: 'some-code' }); expect(response.status).toBe(500); expect(response.body.error).toBe('Google authentication failed. Please try again.'); }); }); describe('POST /auth/verify-email', () => { it('should verify email with valid 6-digit code', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', isVerified: false, verificationToken: '123456', verificationTokenExpiry: new Date(Date.now() + 3600000), // 1 hour from now verificationAttempts: 0, isVerificationLocked: jest.fn().mockReturnValue(false), isVerificationTokenValid: jest.fn().mockReturnValue(true), verifyEmail: jest.fn().mockResolvedValue() }; User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(200); expect(response.body.message).toBe('Email verified successfully'); expect(response.body.user).toMatchObject({ id: 'user-123', email: 'test@example.com', isVerified: true }); expect(mockUser.verifyEmail).toHaveBeenCalled(); }); it('should reject missing code', async () => { const response = await request(app) .post('/auth/verify-email') .send({}); expect(response.status).toBe(400); expect(response.body.error).toBe('Verification code required'); expect(response.body.code).toBe('CODE_REQUIRED'); }); it('should reject invalid code format (not 6 digits)', async () => { const response = await request(app) .post('/auth/verify-email') .send({ code: '12345' }); // Only 5 digits expect(response.status).toBe(400); expect(response.body.error).toBe('Verification code must be 6 digits'); expect(response.body.code).toBe('INVALID_CODE_FORMAT'); }); it('should reject when user not found', async () => { User.findByPk.mockResolvedValue(null); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(404); expect(response.body.error).toBe('User not found'); expect(response.body.code).toBe('USER_NOT_FOUND'); }); it('should reject already verified user', async () => { const mockUser = { id: 'user-123', isVerified: true }; User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Email already verified'); expect(response.body.code).toBe('ALREADY_VERIFIED'); }); it('should reject when too many verification attempts', async () => { const mockUser = { id: 'user-123', isVerified: false, isVerificationLocked: jest.fn().mockReturnValue(true) }; User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(429); expect(response.body.error).toContain('Too many verification attempts'); expect(response.body.code).toBe('TOO_MANY_ATTEMPTS'); }); it('should reject when no verification code exists', async () => { const mockUser = { id: 'user-123', isVerified: false, verificationToken: null, isVerificationLocked: jest.fn().mockReturnValue(false) }; User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(400); expect(response.body.error).toContain('No verification code found'); expect(response.body.code).toBe('NO_CODE'); }); it('should reject expired verification code', async () => { const mockUser = { id: 'user-123', isVerified: false, verificationToken: '123456', verificationTokenExpiry: new Date(Date.now() - 3600000), // 1 hour ago (expired) isVerificationLocked: jest.fn().mockReturnValue(false) }; User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(400); expect(response.body.error).toContain('expired'); expect(response.body.code).toBe('VERIFICATION_EXPIRED'); }); it('should handle verification errors', async () => { User.findByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/verify-email') .send({ code: '123456' }); expect(response.status).toBe(500); expect(response.body.error).toBe('Email verification failed. Please try again.'); }); }); describe('POST /auth/resend-verification', () => { it('should resend verification email for authenticated unverified user', async () => { const mockUser = { id: 1, email: 'test@example.com', isVerified: false, verificationToken: 'new-token', generateVerificationToken: jest.fn().mockResolvedValue() }; jwt.verify.mockReturnValue({ id: 1 }); User.findByPk.mockResolvedValue(mockUser); emailService.auth.sendVerificationEmail.mockResolvedValue(); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=valid-token']); expect(response.status).toBe(200); expect(response.body.message).toBe('Verification email sent successfully'); expect(mockUser.generateVerificationToken).toHaveBeenCalled(); expect(emailService.auth.sendVerificationEmail).toHaveBeenCalledWith(mockUser, mockUser.verificationToken); }); it('should reject when no access token provided', async () => { const response = await request(app) .post('/auth/resend-verification'); expect(response.status).toBe(401); expect(response.body.error).toBe('Authentication required'); expect(response.body.code).toBe('NO_TOKEN'); }); it('should reject expired access token', async () => { const error = new Error('jwt expired'); error.name = 'TokenExpiredError'; jwt.verify.mockImplementation(() => { throw error; }); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=expired-token']); expect(response.status).toBe(401); expect(response.body.error).toContain('Session expired'); expect(response.body.code).toBe('TOKEN_EXPIRED'); }); it('should reject when user not found', async () => { jwt.verify.mockReturnValue({ id: 999 }); User.findByPk.mockResolvedValue(null); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=valid-token']); expect(response.status).toBe(404); expect(response.body.error).toBe('User not found'); }); it('should reject when user already verified', async () => { const mockUser = { id: 1, isVerified: true }; jwt.verify.mockReturnValue({ id: 1 }); User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=valid-token']); expect(response.status).toBe(400); expect(response.body.error).toBe('Email already verified'); }); it('should handle email service failure', async () => { const mockUser = { id: 1, isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; jwt.verify.mockReturnValue({ id: 1 }); User.findByPk.mockResolvedValue(mockUser); emailService.auth.sendVerificationEmail.mockRejectedValue(new Error('Email failed')); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=valid-token']); expect(response.status).toBe(500); expect(response.body.error).toContain('Failed to send verification email'); }); }); describe('POST /auth/refresh', () => { it('should refresh access token with valid refresh token', async () => { const mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: true, jwtVersion: 1, role: 'user' }; jwt.verify.mockReturnValue({ id: 1, type: 'refresh', jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=valid-refresh-token']); expect(response.status).toBe(200); expect(response.body.user).toMatchObject({ id: 1, email: 'test@example.com' }); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken') ]) ); }); it('should reject missing refresh token', async () => { const response = await request(app) .post('/auth/refresh'); expect(response.status).toBe(401); expect(response.body.error).toBe('Refresh token required'); }); it('should reject invalid refresh token', async () => { jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); }); const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=invalid-token']); expect(response.status).toBe(401); expect(response.body.error).toBe('Invalid or expired refresh token'); }); it('should reject non-refresh token type', async () => { jwt.verify.mockReturnValue({ id: 1 }); // Missing type: 'refresh' const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=access-token']); expect(response.status).toBe(401); expect(response.body.error).toBe('Invalid refresh token'); }); it('should reject refresh token for non-existent user', async () => { jwt.verify.mockReturnValue({ id: 999, type: 'refresh' }); User.findByPk.mockResolvedValue(null); const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=valid-token']); expect(response.status).toBe(401); expect(response.body.error).toBe('User not found'); }); it('should reject token with mismatched jwtVersion', async () => { const mockUser = { id: 1, jwtVersion: 2 // Different from token's jwtVersion }; jwt.verify.mockReturnValue({ id: 1, type: 'refresh', jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=old-token']); expect(response.status).toBe(401); expect(response.body.error).toContain('password change'); expect(response.body.code).toBe('JWT_VERSION_MISMATCH'); }); }); describe('POST /auth/logout', () => { it('should logout user and clear cookies', async () => { const response = await request(app) .post('/auth/logout'); expect(response.status).toBe(200); expect(response.body.message).toBe('Logged out successfully'); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken=;'), expect.stringContaining('refreshToken=;') ]) ); }); }); describe('GET /auth/status', () => { it('should return authenticated true when user is logged in', async () => { // The optionalAuth middleware sets req.user if authenticated // We need to modify the mock for this specific test const mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: true }; // Create a custom app for this test with user set const statusApp = express(); statusApp.use(express.json()); statusApp.use((req, res, next) => { req.user = mockUser; next(); }); statusApp.use('/auth', authRoutes); const response = await request(statusApp) .get('/auth/status'); expect(response.status).toBe(200); expect(response.body.authenticated).toBe(true); expect(response.body.user).toMatchObject({ id: 1, email: 'test@example.com' }); }); it('should return authenticated false when user is not logged in', async () => { const response = await request(app) .get('/auth/status'); expect(response.status).toBe(200); expect(response.body.authenticated).toBe(false); expect(response.body.user).toBeUndefined(); }); }); describe('POST /auth/forgot-password', () => { it('should send password reset email for existing user', async () => { const mockUser = { id: 1, email: 'test@example.com', authProvider: 'local', generatePasswordResetToken: jest.fn().mockResolvedValue('reset-token') }; User.findOne.mockResolvedValue(mockUser); emailService.auth.sendPasswordResetEmail.mockResolvedValue(); const response = await request(app) .post('/auth/forgot-password') .send({ email: 'test@example.com' }); expect(response.status).toBe(200); expect(response.body.message).toContain('If an account exists'); expect(mockUser.generatePasswordResetToken).toHaveBeenCalled(); expect(emailService.auth.sendPasswordResetEmail).toHaveBeenCalledWith(mockUser, 'reset-token'); }); it('should return success even for non-existent email (security)', async () => { User.findOne.mockResolvedValue(null); const response = await request(app) .post('/auth/forgot-password') .send({ email: 'nonexistent@example.com' }); expect(response.status).toBe(200); expect(response.body.message).toContain('If an account exists'); }); it('should return success for OAuth user (security)', async () => { User.findOne.mockResolvedValue(null); // Query for local provider returns null const response = await request(app) .post('/auth/forgot-password') .send({ email: 'google@example.com' }); expect(response.status).toBe(200); expect(response.body.message).toContain('If an account exists'); }); it('should handle errors gracefully', async () => { User.findOne.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/forgot-password') .send({ email: 'test@example.com' }); expect(response.status).toBe(500); expect(response.body.error).toContain('Failed to process password reset'); }); }); describe('POST /auth/verify-reset-token', () => { it('should verify valid reset token', async () => { const mockUser = { id: 1, passwordResetToken: crypto.createHash('sha256').update('valid-token').digest('hex'), isPasswordResetTokenValid: jest.fn().mockReturnValue(true) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-reset-token') .send({ token: 'valid-token' }); expect(response.status).toBe(200); expect(response.body.valid).toBe(true); }); it('should reject invalid reset token', async () => { User.findOne.mockResolvedValue(null); const response = await request(app) .post('/auth/verify-reset-token') .send({ token: 'invalid-token' }); expect(response.status).toBe(400); expect(response.body.valid).toBe(false); expect(response.body.code).toBe('TOKEN_INVALID'); }); it('should reject expired reset token', async () => { const mockUser = { id: 1, isPasswordResetTokenValid: jest.fn().mockReturnValue(false) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-reset-token') .send({ token: 'expired-token' }); expect(response.status).toBe(400); expect(response.body.valid).toBe(false); expect(response.body.code).toBe('TOKEN_EXPIRED'); }); }); describe('POST /auth/reset-password', () => { it('should reset password with valid token', async () => { const mockUser = { id: 1, email: 'test@example.com', passwordResetToken: crypto.createHash('sha256').update('valid-token').digest('hex'), isPasswordResetTokenValid: jest.fn().mockReturnValue(true), resetPassword: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); emailService.auth.sendPasswordChangedEmail.mockResolvedValue(); const response = await request(app) .post('/auth/reset-password') .send({ token: 'valid-token', newPassword: 'NewStrongPass123!' }); expect(response.status).toBe(200); expect(response.body.message).toContain('Password has been reset successfully'); expect(mockUser.resetPassword).toHaveBeenCalledWith('NewStrongPass123!'); expect(emailService.auth.sendPasswordChangedEmail).toHaveBeenCalledWith(mockUser); }); it('should reject invalid reset token', async () => { User.findOne.mockResolvedValue(null); const response = await request(app) .post('/auth/reset-password') .send({ token: 'invalid-token', newPassword: 'NewStrongPass123!' }); expect(response.status).toBe(400); expect(response.body.error).toContain('Invalid or expired reset token'); expect(response.body.code).toBe('TOKEN_INVALID'); }); it('should reject expired reset token', async () => { const mockUser = { id: 1, isPasswordResetTokenValid: jest.fn().mockReturnValue(false) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/reset-password') .send({ token: 'expired-token', newPassword: 'NewStrongPass123!' }); expect(response.status).toBe(400); expect(response.body.error).toContain('expired'); expect(response.body.code).toBe('TOKEN_EXPIRED'); }); it('should handle reset password errors', async () => { User.findOne.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/reset-password') .send({ token: 'some-token', newPassword: 'NewStrongPass123!' }); expect(response.status).toBe(500); expect(response.body.error).toContain('Failed to reset password'); }); }); describe('Cookie settings', () => { it('should set secure cookies in production', async () => { process.env.NODE_ENV = 'prod'; const mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: true, jwtVersion: 1, role: 'user', isLocked: jest.fn().mockReturnValue(false), comparePassword: jest.fn().mockResolvedValue(true), resetLoginAttempts: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(200); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringMatching(/accessToken=.*Secure/i), expect.stringMatching(/refreshToken=.*Secure/i) ]) ); }); it('should set httpOnly cookies', async () => { const mockUser = { id: 1, email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: true, jwtVersion: 1, role: 'user', isLocked: jest.fn().mockReturnValue(false), comparePassword: jest.fn().mockResolvedValue(true), resetLoginAttempts: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(200); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('HttpOnly'), expect.stringContaining('HttpOnly') ]) ); }); }); });