import { Request, Response, NextFunction } from 'express'; import * as jwt from 'jsonwebtoken'; import { authenticate, authorize } from '../auth.middleware'; import { JWT_SECRET } from '../auth.constants'; import { AuthError } from '../auth.error'; // Mock dependencies jest.mock('jsonwebtoken'); jest.mock('../auth.constants', () => ({ JWT_SECRET: 'test-secret-key', })); jest.mock('../auth.error'); const mockJwt = jwt as jest.Mocked; const mockAuthError = AuthError as jest.MockedClass; const mockHandleAuthError = require('../auth.error') .handleAuthError as jest.Mock; describe('Auth Middleware', () => { let mockRequest: Partial; let mockResponse: Partial; let mockNext: NextFunction; beforeEach(() => { jest.clearAllMocks(); mockRequest = { headers: {}, user: undefined, }; mockResponse = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; mockNext = jest.fn(); // Reset mocks mockJwt.verify.mockReset(); mockAuthError.mockReset(); mockHandleAuthError.mockReset(); }); describe('authenticate middleware', () => { test('should authenticate valid Bearer token', () => { const mockDecoded = { userId: '123', email: 'test@example.com' }; mockRequest.headers = { authorization: 'Bearer valid-token', }; mockJwt.verify.mockReturnValue(mockDecoded); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockJwt.verify).toHaveBeenCalledWith('valid-token', JWT_SECRET); expect(mockRequest.user).toBe(mockDecoded); expect(mockNext).toHaveBeenCalledWith(); expect(mockHandleAuthError).not.toHaveBeenCalled(); }); test('should reject request without authorization header', () => { mockRequest.headers = {}; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); expect(mockNext).not.toHaveBeenCalled(); }); test('should reject request with empty authorization header', () => { mockRequest.headers = { authorization: '', }; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should reject request without Bearer prefix', () => { mockRequest.headers = { authorization: 'Basic token123', }; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should reject request with malformed Bearer token', () => { mockRequest.headers = { authorization: 'Bearer', }; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should handle JWT verification errors', () => { const jwtError = new Error('Invalid token'); mockRequest.headers = { authorization: 'Bearer invalid-token', }; mockJwt.verify.mockImplementation(() => { throw jwtError; }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockJwt.verify).toHaveBeenCalledWith('invalid-token', JWT_SECRET); expect(mockHandleAuthError).toHaveBeenCalledWith( jwtError, mockRequest, mockResponse, mockNext ); expect(mockNext).not.toHaveBeenCalled(); }); test('should handle expired JWT tokens', () => { const expiredError = new jwt.TokenExpiredError( 'Token expired', new Date() ); mockRequest.headers = { authorization: 'Bearer expired-token', }; mockJwt.verify.mockImplementation(() => { throw expiredError; }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockHandleAuthError).toHaveBeenCalledWith( expiredError, mockRequest, mockResponse, mockNext ); }); test('should handle malformed JWT tokens', () => { const malformedError = new jwt.JsonWebTokenError('Malformed token'); mockRequest.headers = { authorization: 'Bearer malformed-token', }; mockJwt.verify.mockImplementation(() => { throw malformedError; }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockHandleAuthError).toHaveBeenCalledWith( malformedError, mockRequest, mockResponse, mockNext ); }); test('should extract token correctly from Bearer header with extra spaces', () => { const mockDecoded = { userId: '123' }; mockRequest.headers = { authorization: ' Bearer token-with-spaces ', }; mockJwt.verify.mockReturnValue(mockDecoded); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should handle case-sensitive Bearer prefix', () => { mockRequest.headers = { authorization: 'bearer lowercase-token', }; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'No authentication token provided', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should work with different JWT payloads', () => { const complexPayload = { userId: '123', email: 'test@example.com', roles: ['user', 'admin'], permissions: ['read', 'write'], iat: 1234567890, exp: 1234567900, }; mockRequest.headers = { authorization: 'Bearer complex-token', }; mockJwt.verify.mockReturnValue(complexPayload); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockRequest.user).toBe(complexPayload); expect(mockNext).toHaveBeenCalled(); }); }); describe('authorize middleware', () => { test('should authorize request with authenticated user', () => { mockRequest.user = { userId: '123', email: 'test@example.com' }; const middleware = authorize('admin', 'user'); middleware(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(); expect(mockHandleAuthError).not.toHaveBeenCalled(); }); test('should reject request without authenticated user', () => { mockRequest.user = undefined; const middleware = authorize('admin'); middleware(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'Authentication required', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); expect(mockNext).not.toHaveBeenCalled(); }); test('should handle null user', () => { mockRequest.user = null; const middleware = authorize('user'); middleware(mockRequest as Request, mockResponse as Response, mockNext); expect(mockAuthError).toHaveBeenCalledWith( 'Authentication required', 401 ); expect(mockHandleAuthError).toHaveBeenCalled(); }); test('should work with multiple allowed roles', () => { mockRequest.user = { userId: '123', role: 'admin' }; const middleware = authorize('admin', 'moderator', 'user'); middleware(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(); }); test('should work with no roles specified', () => { mockRequest.user = { userId: '123' }; const middleware = authorize(); middleware(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(); }); test('should handle errors thrown during authorization', () => { mockRequest.user = { userId: '123' }; // Mock next to throw an error const errorMiddleware = authorize('admin'); const throwingNext = jest.fn(() => { throw new Error('Unexpected error'); }); // This should be caught by the try-catch in authorize errorMiddleware( mockRequest as Request, mockResponse as Response, throwingNext ); expect(throwingNext).toHaveBeenCalled(); }); test('should return a function when called', () => { const middleware = authorize('admin'); expect(typeof middleware).toBe('function'); expect(middleware.length).toBe(3); // Should accept req, res, next }); test('should create different middleware instances for different role sets', () => { const adminMiddleware = authorize('admin'); const userMiddleware = authorize('user'); expect(adminMiddleware).not.toBe(userMiddleware); }); }); describe('middleware integration', () => { test('should work together in a middleware chain', () => { // First authenticate const mockDecoded = { userId: '123', role: 'admin' }; mockRequest.headers = { authorization: 'Bearer valid-token', }; mockJwt.verify.mockReturnValue(mockDecoded); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockRequest.user).toBe(mockDecoded); expect(mockNext).toHaveBeenCalledWith(); // Reset next mock mockNext.mockClear(); // Then authorize const authorizationMiddleware = authorize('admin'); authorizationMiddleware( mockRequest as Request, mockResponse as Response, mockNext ); expect(mockNext).toHaveBeenCalledWith(); }); test('should fail authorization if authentication failed', () => { // Authentication fails mockRequest.headers = {}; authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockRequest.user).toBeUndefined(); expect(mockHandleAuthError).toHaveBeenCalled(); // Authorization should fail const authorizationMiddleware = authorize('admin'); authorizationMiddleware( mockRequest as Request, mockResponse as Response, mockNext ); expect(mockAuthError).toHaveBeenCalledWith( 'Authentication required', 401 ); }); }); describe('error handling', () => { test('should handle unexpected errors in authenticate', () => { const unexpectedError = new Error('Unexpected error'); mockRequest.headers = { authorization: 'Bearer token', }; // Mock jwt.verify to throw unexpected error mockJwt.verify.mockImplementation(() => { throw unexpectedError; }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockHandleAuthError).toHaveBeenCalledWith( unexpectedError, mockRequest, mockResponse, mockNext ); }); test('should handle unexpected errors in authorize', () => { // Setup a scenario where an error might occur mockRequest.user = { userId: '123' }; // Mock the authorize middleware to have an internal error const middleware = authorize('admin'); // This test ensures the try-catch block in authorize works expect(() => { middleware(mockRequest as Request, mockResponse as Response, mockNext); }).not.toThrow(); }); }); describe('security considerations', () => { test('should not expose sensitive information in errors', () => { const jwtError = new Error('Invalid token'); mockRequest.headers = { authorization: 'Bearer sensitive-token-data', }; mockJwt.verify.mockImplementation(() => { throw jwtError; }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockJwt.verify).toHaveBeenCalledWith( 'sensitive-token-data', JWT_SECRET ); expect(mockHandleAuthError).toHaveBeenCalledWith( jwtError, mockRequest, mockResponse, mockNext ); // The error message should not contain the actual token }); test('should use constant-time comparison for token validation', () => { // This is more of a documentation test since jwt.verify handles this mockRequest.headers = { authorization: 'Bearer token', }; mockJwt.verify.mockReturnValue({ userId: '123' }); authenticate(mockRequest as Request, mockResponse as Response, mockNext); expect(mockJwt.verify).toHaveBeenCalledWith('token', JWT_SECRET); }); }); });