test: add authentication middleware tests
- Add comprehensive tests for auth middleware functionality - Test authentication validation, session handling, and security - Ensure proper request/response handling in auth pipeline - Improve authentication service test coverage This strengthens the authentication layer testing.
This commit is contained in:
442
services/auth/__tests__/auth.middleware.test.ts
Normal file
442
services/auth/__tests__/auth.middleware.test.ts
Normal file
@@ -0,0 +1,442 @@
|
||||
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<typeof jwt>;
|
||||
const mockAuthError = AuthError as jest.MockedClass<typeof AuthError>;
|
||||
const mockHandleAuthError = require('../auth.error')
|
||||
.handleAuthError as jest.Mock;
|
||||
|
||||
describe('Auth Middleware', () => {
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user