- 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.
443 lines
13 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|