Files
rxminder/services/auth/__tests__/auth.middleware.test.ts
William Valentin 7615600090 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.
2025-09-08 11:36:36 -07:00

443 lines
13 KiB
TypeScript

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);
});
});
});