const { authenticateToken, optionalAuth, requireVerifiedEmail, requireAdmin } = require('../../../middleware/auth'); const jwt = require('jsonwebtoken'); jest.mock('jsonwebtoken'); jest.mock('../../../models', () => ({ User: { findByPk: jest.fn() } })); const { User } = require('../../../models'); describe('Auth Middleware', () => { let req, res, next; beforeEach(() => { req = { cookies: {} }; res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); process.env.JWT_ACCESS_SECRET = 'test-secret'; }); describe('Valid token', () => { it('should verify valid token from cookie and call next', async () => { const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 1 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); await authenticateToken(req, res, next); expect(jwt.verify).toHaveBeenCalledWith('validtoken', process.env.JWT_ACCESS_SECRET); expect(User.findByPk).toHaveBeenCalledWith(1); expect(req.user).toEqual(mockUser); expect(next).toHaveBeenCalled(); }); it('should handle token with valid user', async () => { const mockUser = { id: 2, email: 'user@test.com', firstName: 'Test', jwtVersion: 1 }; req.cookies.accessToken = 'validtoken2'; jwt.verify.mockReturnValue({ id: 2, jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); await authenticateToken(req, res, next); expect(jwt.verify).toHaveBeenCalledWith('validtoken2', process.env.JWT_ACCESS_SECRET); expect(User.findByPk).toHaveBeenCalledWith(2); expect(req.user).toEqual(mockUser); expect(next).toHaveBeenCalled(); }); }); describe('Invalid token', () => { it('should return 401 for missing token', async () => { req.cookies = {}; await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Access token required', code: 'NO_TOKEN' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 for invalid token', async () => { req.cookies.accessToken = 'invalidtoken'; jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); }); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token', code: 'INVALID_TOKEN' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 for expired token', async () => { req.cookies.accessToken = 'expiredtoken'; const error = new Error('jwt expired'); error.name = 'TokenExpiredError'; jwt.verify.mockImplementation(() => { throw error; }); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Token expired', code: 'TOKEN_EXPIRED' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 for invalid token format (missing user id)', async () => { req.cookies.accessToken = 'tokenwithnoid'; jwt.verify.mockReturnValue({ email: 'test@test.com' }); // Missing id await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token format', code: 'INVALID_TOKEN_FORMAT' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 when user not found', async () => { req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 999 }); User.findByPk.mockResolvedValue(null); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'User not found', code: 'USER_NOT_FOUND' }); expect(next).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle empty string token', async () => { req.cookies.accessToken = ''; await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Access token required', code: 'NO_TOKEN' }); }); it('should handle JWT malformed error', async () => { req.cookies.accessToken = 'malformed.token'; const error = new Error('jwt malformed'); error.name = 'JsonWebTokenError'; jwt.verify.mockImplementation(() => { throw error; }); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token', code: 'INVALID_TOKEN' }); }); it('should handle database error when finding user', async () => { req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1 }); User.findByPk.mockRejectedValue(new Error('Database error')); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token', code: 'INVALID_TOKEN' }); expect(next).not.toHaveBeenCalled(); }); it('should handle undefined cookies', async () => { req.cookies = undefined; await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Access token required', code: 'NO_TOKEN' }); }); }); }); describe('requireVerifiedEmail Middleware', () => { let req, res, next; beforeEach(() => { req = { user: null }; res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); }); describe('Verified users', () => { it('should call next for verified user', () => { req.user = { id: 1, email: 'verified@test.com', isVerified: true }; requireVerifiedEmail(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); expect(res.json).not.toHaveBeenCalled(); }); it('should call next for verified OAuth user', () => { req.user = { id: 2, email: 'google@test.com', authProvider: 'google', isVerified: true }; requireVerifiedEmail(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); }); describe('Unverified users', () => { it('should return 403 for unverified user', () => { req.user = { id: 1, email: 'unverified@test.com', isVerified: false }; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Email verification required. Please verify your email address to perform this action.', code: 'EMAIL_NOT_VERIFIED' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when isVerified is null', () => { req.user = { id: 1, email: 'test@test.com', isVerified: null }; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Email verification required. Please verify your email address to perform this action.', code: 'EMAIL_NOT_VERIFIED' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when isVerified is undefined', () => { req.user = { id: 1, email: 'test@test.com' // isVerified is undefined }; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Email verification required. Please verify your email address to perform this action.', code: 'EMAIL_NOT_VERIFIED' }); expect(next).not.toHaveBeenCalled(); }); }); describe('No user', () => { it('should return 401 when user is not set', () => { req.user = null; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required', code: 'NO_AUTH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 when user is undefined', () => { req.user = undefined; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required', code: 'NO_AUTH' }); expect(next).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle user object with extra fields', () => { req.user = { id: 1, email: 'test@test.com', isVerified: true, firstName: 'Test', lastName: 'User', phone: '1234567890' }; requireVerifiedEmail(req, res, next); expect(next).toHaveBeenCalled(); }); it('should prioritize missing user over unverified user', () => { // If called without authenticateToken first req.user = null; requireVerifiedEmail(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required', code: 'NO_AUTH' }); }); }); }); describe('optionalAuth Middleware', () => { let req, res, next; beforeEach(() => { req = { cookies: {} }; res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); process.env.JWT_ACCESS_SECRET = 'test-secret'; }); describe('No token present', () => { it('should set req.user to null when no token present', async () => { req.cookies = {}; await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should set req.user to null when cookies is undefined', async () => { req.cookies = undefined; await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); }); it('should set req.user to null for empty string token', async () => { req.cookies.accessToken = ''; await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); }); }); describe('Valid token present', () => { it('should set req.user when valid token present', async () => { const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 1 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); await optionalAuth(req, res, next); expect(jwt.verify).toHaveBeenCalledWith('validtoken', process.env.JWT_ACCESS_SECRET); expect(User.findByPk).toHaveBeenCalledWith(1); expect(req.user).toEqual(mockUser); expect(next).toHaveBeenCalled(); }); }); describe('Invalid token handling', () => { it('should set req.user to null for invalid token (no error returned)', async () => { req.cookies.accessToken = 'invalidtoken'; jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); }); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should set req.user to null for expired token (no error returned)', async () => { req.cookies.accessToken = 'expiredtoken'; const error = new Error('jwt expired'); error.name = 'TokenExpiredError'; jwt.verify.mockImplementation(() => { throw error; }); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should set req.user to null when token has no user id', async () => { req.cookies.accessToken = 'tokenwithnoid'; jwt.verify.mockReturnValue({ email: 'test@test.com' }); // Missing id await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); }); }); describe('User state handling', () => { it('should set req.user to null for banned user', async () => { const mockUser = { id: 1, email: 'banned@test.com', isBanned: true, jwtVersion: 1 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should set req.user to null for JWT version mismatch', async () => { const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 2 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); // Old version User.findByPk.mockResolvedValue(mockUser); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should set req.user to null when user not found', async () => { req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 999, jwtVersion: 1 }); User.findByPk.mockResolvedValue(null); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle database error gracefully', async () => { req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); User.findByPk.mockRejectedValue(new Error('Database error')); await optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); }); }); describe('requireAdmin Middleware', () => { let req, res, next; beforeEach(() => { req = { user: null }; res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); }); describe('Admin users', () => { it('should call next() for admin user', () => { req.user = { id: 1, email: 'admin@test.com', role: 'admin' }; requireAdmin(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); expect(res.json).not.toHaveBeenCalled(); }); it('should call next() for admin user with additional properties', () => { req.user = { id: 1, email: 'admin@test.com', role: 'admin', firstName: 'Admin', lastName: 'User', isVerified: true }; requireAdmin(req, res, next); expect(next).toHaveBeenCalled(); }); }); describe('Non-admin users', () => { it('should return 403 for non-admin user', () => { req.user = { id: 1, email: 'user@test.com', role: 'user' }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Admin access required', code: 'INSUFFICIENT_PERMISSIONS' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 for host role', () => { req.user = { id: 1, email: 'host@test.com', role: 'host' }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Admin access required', code: 'INSUFFICIENT_PERMISSIONS' }); }); it('should return 403 for user with no role property', () => { req.user = { id: 1, email: 'user@test.com' // role is missing }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Admin access required', code: 'INSUFFICIENT_PERMISSIONS' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 for user with empty string role', () => { req.user = { id: 1, email: 'user@test.com', role: '' }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); }); }); describe('No user', () => { it('should return 401 when user is null', () => { req.user = null; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required', code: 'NO_AUTH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 401 when user is undefined', () => { req.user = undefined; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required', code: 'NO_AUTH' }); expect(next).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle case-sensitive role comparison', () => { req.user = { id: 1, email: 'user@test.com', role: 'Admin' // Capital A }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(next).not.toHaveBeenCalled(); }); it('should handle role with whitespace', () => { req.user = { id: 1, email: 'user@test.com', role: ' admin ' // With spaces }; requireAdmin(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(next).not.toHaveBeenCalled(); }); }); }); describe('authenticateToken - Additional Tests', () => { let req, res, next; beforeEach(() => { req = { cookies: {}, id: 'request-123' }; res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; next = jest.fn(); jest.clearAllMocks(); process.env.JWT_ACCESS_SECRET = 'test-secret'; }); describe('Banned user', () => { it('should return 403 USER_BANNED for banned user', async () => { const mockUser = { id: 1, email: 'banned@test.com', isBanned: true, jwtVersion: 1 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); User.findByPk.mockResolvedValue(mockUser); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Your account has been suspended. Please contact support for more information.', code: 'USER_BANNED' }); expect(next).not.toHaveBeenCalled(); }); }); describe('JWT version mismatch', () => { it('should return 401 JWT_VERSION_MISMATCH for version mismatch', async () => { const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 2 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 }); // Old version in token User.findByPk.mockResolvedValue(mockUser); await authenticateToken(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Session expired due to password change. Please log in again.', code: 'JWT_VERSION_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should pass when JWT version matches', async () => { const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 5 }; req.cookies.accessToken = 'validtoken'; jwt.verify.mockReturnValue({ id: 1, jwtVersion: 5 }); User.findByPk.mockResolvedValue(mockUser); await authenticateToken(req, res, next); expect(next).toHaveBeenCalled(); expect(req.user).toEqual(mockUser); }); }); });