const request = require('supertest'); const express = require('express'); const cookieParser = require('cookie-parser'); const jwt = require('jsonwebtoken'); 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() } })); // 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(), })); 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(), })); jest.mock('../../../services/emailService', () => ({ sendVerificationEmail: jest.fn() })); const { User } = require('../../../models'); const emailService = require('../../../services/emailService'); // 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); describe('Auth Routes', () => { beforeEach(() => { jest.clearAllMocks(); // Reset environment process.env.JWT_SECRET = 'test-secret'; process.env.GOOGLE_CLIENT_ID = 'test-google-client-id'; process.env.NODE_ENV = 'test'; // Reset JWT mock to return different tokens for each call let tokenCallCount = 0; jwt.sign.mockImplementation(() => { tokenCallCount++; return tokenCallCount === 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); // No existing user const newUser = { id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.sendVerificationEmail.mockResolvedValue(); const response = await request(app) .post('/auth/register') .send({ username: 'testuser', email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User', phone: '1234567890' }); expect(response.status).toBe(201); expect(response.body.user).toEqual({ id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false }); expect(response.body.verificationEmailSent).toBe(true); expect(newUser.generateVerificationToken).toHaveBeenCalled(); expect(emailService.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({ username: 'testuser', 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 reject registration with existing username', async () => { User.findOne.mockResolvedValue({ id: 1, username: 'testuser' }); const response = await request(app) .post('/auth/register') .send({ username: 'testuser', email: 'test@example.com', password: 'StrongPass123!', firstName: 'Test', lastName: 'User' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Registration failed'); }); 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({ username: 'testuser', 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.'); }); }); describe('POST /auth/login', () => { it('should login user with valid credentials', async () => { const mockUser = { id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User', isLocked: jest.fn().mockReturnValue(false), comparePassword: jest.fn().mockResolvedValue(true), resetLoginAttempts: jest.fn().mockResolvedValue() }; User.findOne.mockResolvedValue(mockUser); jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); const response = await request(app) .post('/auth/login') .send({ email: 'test@example.com', password: 'password123' }); expect(response.status).toBe(200); expect(response.body.user).toEqual({ id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User' }); expect(mockUser.resetLoginAttempts).toHaveBeenCalled(); }); 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('Invalid credentials'); }); 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('Invalid credentials'); 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', () => { it('should handle Google OAuth login for new user', async () => { const mockPayload = { sub: 'google123', email: 'test@gmail.com', given_name: 'Test', family_name: 'User', picture: 'profile.jpg' }; mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); User.findOne .mockResolvedValueOnce(null) // No existing Google user .mockResolvedValueOnce(null); // No existing email user const newUser = { id: 1, username: 'test_gle123', email: 'test@gmail.com', firstName: 'Test', lastName: 'User', imageFilename: 'profile.jpg' }; User.create.mockResolvedValue(newUser); const response = await request(app) .post('/auth/google') .send({ idToken: 'valid-google-token' }); expect(response.status).toBe(200); expect(response.body.user).toEqual(newUser); expect(User.create).toHaveBeenCalledWith({ email: 'test@gmail.com', firstName: 'Test', lastName: 'User', authProvider: 'google', providerId: 'google123', imageFilename: 'profile.jpg', username: 'test_gle123' }); }); it('should handle Google OAuth login for existing user', async () => { const mockPayload = { sub: 'google123', email: 'test@gmail.com', given_name: 'Test', family_name: 'User' }; mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); const existingUser = { id: 1, username: 'testuser', email: 'test@gmail.com', firstName: 'Test', lastName: 'User' }; User.findOne.mockResolvedValue(existingUser); jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); const response = await request(app) .post('/auth/google') .send({ idToken: 'valid-google-token' }); expect(response.status).toBe(200); expect(response.body.user).toEqual(existingUser); }); it('should reject when email exists with different auth provider', async () => { const mockPayload = { sub: 'google123', email: 'test@example.com', given_name: 'Test', family_name: 'User' }; mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); User.findOne .mockResolvedValueOnce(null) // No Google user .mockResolvedValueOnce({ id: 1, email: 'test@example.com' }); // Existing email user const response = await request(app) .post('/auth/google') .send({ idToken: 'valid-google-token' }); expect(response.status).toBe(409); expect(response.body.error).toContain('An account with this email already exists'); }); it('should reject missing ID token', async () => { const response = await request(app) .post('/auth/google') .send({}); expect(response.status).toBe(400); expect(response.body.error).toBe('ID token is required'); }); it('should handle expired Google token', async () => { const error = new Error('Token used too late'); mockGoogleClient.verifyIdToken.mockRejectedValue(error); const response = await request(app) .post('/auth/google') .send({ idToken: 'expired-token' }); expect(response.status).toBe(401); expect(response.body.error).toBe('Google token has expired. Please try again.'); }); it('should handle invalid Google token', async () => { const error = new Error('Invalid token signature'); mockGoogleClient.verifyIdToken.mockRejectedValue(error); const response = await request(app) .post('/auth/google') .send({ idToken: 'invalid-token' }); expect(response.status).toBe(401); expect(response.body.error).toBe('Invalid Google token. Please try again.'); }); it('should handle malformed Google token', async () => { const error = new Error('Wrong number of segments in token'); mockGoogleClient.verifyIdToken.mockRejectedValue(error); const response = await request(app) .post('/auth/google') .send({ idToken: 'malformed.token' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Malformed Google token. Please try again.'); }); it('should handle missing required user information', async () => { const mockPayload = { sub: 'google123', email: 'test@gmail.com', // Missing given_name and family_name }; mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); const response = await request(app) .post('/auth/google') .send({ idToken: 'valid-token' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Required user information not provided by Google'); }); it('should handle unexpected Google auth errors', async () => { const unexpectedError = new Error('Unexpected Google error'); mockGoogleClient.verifyIdToken.mockRejectedValue(unexpectedError); const response = await request(app) .post('/auth/google') .send({ idToken: 'error-token' }); expect(response.status).toBe(500); expect(response.body.error).toBe('Google authentication failed. Please try again.'); }); }); describe('POST /auth/refresh', () => { it('should refresh access token with valid refresh token', async () => { const mockUser = { id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User' }; jwt.verify.mockReturnValue({ id: 1, type: 'refresh' }); User.findByPk.mockResolvedValue(mockUser); jwt.sign.mockReturnValue('new-access-token'); const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=valid-refresh-token']); expect(response.status).toBe(200); expect(response.body.user).toEqual(mockUser); expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken=new-access-token') ]) ); }); 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, type: 'access' }); 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'); }); }); 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'); // Check that cookies are cleared expect(response.headers['set-cookie']).toEqual( expect.arrayContaining([ expect.stringContaining('accessToken=;'), expect.stringContaining('refreshToken=;') ]) ); }); }); describe('Security features', () => { it('should set secure cookies in production', async () => { process.env.NODE_ENV = 'prod'; User.findOne.mockResolvedValue(null); const newUser = { id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.sendVerificationEmail.mockResolvedValue(); jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh'); const response = await request(app) .post('/auth/register') .send({ username: 'test', email: 'test@example.com', password: 'Password123!', firstName: 'Test', lastName: 'User' }); expect(response.status).toBe(201); // In production, cookies should have secure flag expect(response.headers['set-cookie'][0]).toContain('Secure'); }); it('should generate unique username for Google users', async () => { const mockPayload = { sub: 'google123456', email: 'test@gmail.com', given_name: 'Test', family_name: 'User' }; mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); User.findOne .mockResolvedValueOnce(null) .mockResolvedValueOnce(null); User.create.mockResolvedValue({ id: 1, username: 'test_123456', email: 'test@gmail.com' }); jwt.sign.mockReturnValueOnce('token').mockReturnValueOnce('refresh'); await request(app) .post('/auth/google') .send({ idToken: 'valid-token' }); expect(User.create).toHaveBeenCalledWith( expect.objectContaining({ username: 'test_123456' // email prefix + last 6 chars of Google ID }) ); }); }); describe('Token management', () => { it('should generate both access and refresh tokens on registration', async () => { User.findOne.mockResolvedValue(null); const newUser = { id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.sendVerificationEmail.mockResolvedValue(); jwt.sign .mockReturnValueOnce('access-token') .mockReturnValueOnce('refresh-token'); await request(app) .post('/auth/register') .send({ username: 'test', email: 'test@example.com', password: 'Password123!', firstName: 'Test', lastName: 'User' }); expect(jwt.sign).toHaveBeenCalledWith( { id: 1 }, 'test-secret', { expiresIn: '15m' } ); expect(jwt.sign).toHaveBeenCalledWith( { id: 1, type: 'refresh' }, 'test-secret', { expiresIn: '7d' } ); }); it('should set correct cookie options', async () => { User.findOne.mockResolvedValue(null); const newUser = { id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.sendVerificationEmail.mockResolvedValue(); jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh'); const response = await request(app) .post('/auth/register') .send({ username: 'test', email: 'test@example.com', password: 'Password123!', firstName: 'Test', lastName: 'User' }); const cookies = response.headers['set-cookie']; expect(cookies[0]).toContain('HttpOnly'); expect(cookies[0]).toContain('SameSite=Strict'); expect(cookies[1]).toContain('HttpOnly'); expect(cookies[1]).toContain('SameSite=Strict'); }); }); describe('Email Verification', () => { describe('Registration with verification', () => { it('should continue registration even if verification email fails', async () => { User.findOne.mockResolvedValue(null); const newUser = { id: 1, username: 'testuser', email: 'test@example.com', firstName: 'Test', lastName: 'User', isVerified: false, generateVerificationToken: jest.fn().mockResolvedValue() }; User.create.mockResolvedValue(newUser); emailService.sendVerificationEmail.mockRejectedValue(new Error('Email service down')); const response = await request(app) .post('/auth/register') .send({ username: 'testuser', 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); expect(newUser.generateVerificationToken).toHaveBeenCalled(); }); }); describe('Google OAuth auto-verification', () => { it('should auto-verify Google OAuth users', async () => { const mockPayload = { sub: 'google456', email: 'oauth@gmail.com', given_name: 'OAuth', family_name: 'User', picture: 'pic.jpg' }; mockGoogleClient.getToken.mockResolvedValue({ tokens: { id_token: 'google-id-token' } }); mockGoogleClient.verifyIdToken.mockResolvedValue({ getPayload: () => mockPayload }); User.findOne .mockResolvedValueOnce(null) // No Google user .mockResolvedValueOnce(null); // No email user const createdUser = { id: 1, username: 'oauth_gle456', email: 'oauth@gmail.com', firstName: 'OAuth', lastName: 'User', imageFilename: 'pic.jpg', isVerified: true }; User.create.mockResolvedValue(createdUser); jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token'); const response = await request(app) .post('/auth/google') .send({ code: 'google-auth-code' }); expect(response.status).toBe(200); expect(User.create).toHaveBeenCalledWith( expect.objectContaining({ isVerified: true, verifiedAt: expect.any(Date) }) ); }); }); describe('POST /auth/verify-email', () => { it('should verify email with valid token', async () => { const mockUser = { id: 1, email: 'test@example.com', verificationToken: 'valid-token-123', verificationTokenExpiry: new Date(Date.now() + 60 * 60 * 1000), isVerified: false, isVerificationTokenValid: function(token) { return this.verificationToken === token && new Date() < new Date(this.verificationTokenExpiry); }, verifyEmail: jest.fn().mockResolvedValue({ id: 1, email: 'test@example.com', isVerified: true }) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ token: 'valid-token-123' }); expect(response.status).toBe(200); expect(response.body.message).toBe('Email verified successfully'); expect(response.body.user).toEqual({ id: 1, email: 'test@example.com', isVerified: true }); expect(mockUser.verifyEmail).toHaveBeenCalled(); }); it('should reject missing token', async () => { const response = await request(app) .post('/auth/verify-email') .send({}); expect(response.status).toBe(400); expect(response.body.error).toBe('Verification token required'); expect(response.body.code).toBe('TOKEN_REQUIRED'); }); it('should reject invalid token', async () => { User.findOne.mockResolvedValue(null); const response = await request(app) .post('/auth/verify-email') .send({ token: 'invalid-token' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Invalid verification token'); expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID'); }); it('should reject already verified user', async () => { const mockUser = { id: 1, email: 'verified@example.com', isVerified: true }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ token: 'some-token' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Email already verified'); expect(response.body.code).toBe('ALREADY_VERIFIED'); }); it('should reject expired token', async () => { const mockUser = { id: 1, email: 'test@example.com', verificationToken: 'expired-token', verificationTokenExpiry: new Date(Date.now() - 60 * 60 * 1000), isVerified: false, isVerificationTokenValid: jest.fn().mockReturnValue(false) }; User.findOne.mockResolvedValue(mockUser); const response = await request(app) .post('/auth/verify-email') .send({ token: 'expired-token' }); expect(response.status).toBe(400); expect(response.body.error).toBe('Verification token has expired. Please request a new one.'); expect(response.body.code).toBe('VERIFICATION_TOKEN_EXPIRED'); }); it('should handle verification errors', async () => { User.findOne.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/auth/verify-email') .send({ token: 'valid-token' }); 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', firstName: 'Test', isVerified: false, verificationToken: 'new-token', generateVerificationToken: jest.fn().mockResolvedValue() }; jwt.verify.mockReturnValue({ id: 1 }); User.findByPk.mockResolvedValue(mockUser); emailService.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.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).toBe('Session expired. Please log in again.'); 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'); expect(response.body.code).toBe('USER_NOT_FOUND'); }); it('should reject when user already verified', async () => { const mockUser = { id: 1, email: 'verified@example.com', 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'); expect(response.body.code).toBe('ALREADY_VERIFIED'); }); it('should handle email service failure', 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.sendVerificationEmail.mockRejectedValue(new Error('Email service down')); const response = await request(app) .post('/auth/resend-verification') .set('Cookie', ['accessToken=valid-token']); expect(response.status).toBe(500); expect(response.body.error).toBe('Failed to send verification email. Please try again.'); expect(mockUser.generateVerificationToken).toHaveBeenCalled(); }); }); }); });