// Set CSRF_SECRET before requiring the middleware process.env.CSRF_SECRET = 'test-csrf-secret-that-is-at-least-32-chars-long'; const mockTokensInstance = { secretSync: jest.fn().mockReturnValue(process.env.CSRF_SECRET), create: jest.fn().mockReturnValue('mock-token-123'), verify: jest.fn().mockReturnValue(true) }; jest.mock('csrf', () => { return jest.fn().mockImplementation(() => mockTokensInstance); }); jest.mock('cookie-parser', () => { return jest.fn().mockReturnValue((req, res, next) => next()); }); jest.mock('../../../utils/logger', () => ({ error: jest.fn(), info: jest.fn(), warn: jest.fn(), withRequestId: jest.fn(() => ({ error: jest.fn(), info: jest.fn(), warn: jest.fn(), })), })); const { csrfProtection, generateCSRFToken, getCSRFToken } = require('../../../middleware/csrf'); describe('CSRF Middleware', () => { let req, res, next; beforeEach(() => { req = { method: 'POST', headers: {}, body: {}, query: {}, cookies: {} }; res = { status: jest.fn().mockReturnThis(), json: jest.fn(), send: jest.fn(), cookie: jest.fn(), set: jest.fn(), locals: {} }; next = jest.fn(); jest.clearAllMocks(); }); describe('csrfProtection', () => { describe('Safe methods', () => { it('should skip CSRF protection for GET requests', () => { req.method = 'GET'; csrfProtection(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should skip CSRF protection for HEAD requests', () => { req.method = 'HEAD'; csrfProtection(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should skip CSRF protection for OPTIONS requests', () => { req.method = 'OPTIONS'; csrfProtection(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); }); describe('Token validation', () => { beforeEach(() => { req.cookies = { 'csrf-token': 'mock-token-123' }; }); it('should validate token from x-csrf-token header', () => { req.headers['x-csrf-token'] = 'mock-token-123'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should validate token from request body', () => { req.body.csrfToken = 'mock-token-123'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should validate token from query parameters', () => { req.query.csrfToken = 'mock-token-123'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should prefer header token over body token', () => { req.headers['x-csrf-token'] = 'mock-token-123'; req.body.csrfToken = 'different-token'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); it('should prefer header token over query token', () => { req.headers['x-csrf-token'] = 'mock-token-123'; req.query.csrfToken = 'different-token'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); it('should prefer body token over query token', () => { req.body.csrfToken = 'mock-token-123'; req.query.csrfToken = 'different-token'; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); }); describe('Missing tokens', () => { it('should return 403 when no token provided', () => { req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when no cookie token provided', () => { req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = {}; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when cookies object is missing', () => { req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = undefined; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when both tokens are missing', () => { req.cookies = {}; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); }); describe('Token mismatch', () => { it('should return 403 when tokens do not match', () => { req.headers['x-csrf-token'] = 'token-from-header'; req.cookies = { 'csrf-token': 'token-from-cookie' }; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when header token is empty but cookie exists', () => { req.headers['x-csrf-token'] = ''; req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); it('should return 403 when cookie token is empty but header exists', () => { req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': '' }; csrfProtection(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_MISMATCH' }); expect(next).not.toHaveBeenCalled(); }); }); describe('Token verification', () => { beforeEach(() => { req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': 'mock-token-123' }; }); it('should return 403 when token verification fails', () => { mockTokensInstance.verify.mockReturnValue(false); csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', code: 'CSRF_TOKEN_INVALID' }); expect(next).not.toHaveBeenCalled(); }); it('should call next when token verification succeeds', () => { mockTokensInstance.verify.mockReturnValue(true); csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { it('should handle case-insensitive HTTP methods', () => { req.method = 'post'; req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); it('should handle PUT requests', () => { req.method = 'PUT'; req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); it('should handle DELETE requests', () => { req.method = 'DELETE'; req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); it('should handle PATCH requests', () => { req.method = 'PATCH'; req.headers['x-csrf-token'] = 'mock-token-123'; req.cookies = { 'csrf-token': 'mock-token-123' }; csrfProtection(req, res, next); expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); }); }); describe('generateCSRFToken', () => { it('should generate token and set cookie with proper options', () => { process.env.NODE_ENV = 'production'; generateCSRFToken(req, res, next); expect(mockTokensInstance.create).toHaveBeenCalledWith(process.env.CSRF_SECRET); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); expect(next).toHaveBeenCalled(); }); it('should set secure flag to false in dev environment', () => { process.env.NODE_ENV = 'dev'; generateCSRFToken(req, res, next); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: false, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should set secure flag to true in non-dev environment', () => { process.env.NODE_ENV = 'production'; generateCSRFToken(req, res, next); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should set token in response header', () => { generateCSRFToken(req, res, next); expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'mock-token-123'); }); it('should make token available in res.locals', () => { generateCSRFToken(req, res, next); expect(res.locals.csrfToken).toBe('mock-token-123'); }); it('should call next after setting up token', () => { generateCSRFToken(req, res, next); expect(next).toHaveBeenCalled(); }); it('should handle test environment', () => { process.env.NODE_ENV = 'test'; generateCSRFToken(req, res, next); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should handle undefined NODE_ENV', () => { delete process.env.NODE_ENV; generateCSRFToken(req, res, next); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); }); describe('getCSRFToken', () => { it('should generate token and return it in response', () => { process.env.NODE_ENV = 'production'; getCSRFToken(req, res); expect(mockTokensInstance.create).toHaveBeenCalledWith(process.env.CSRF_SECRET); expect(res.status).toHaveBeenCalledWith(204); expect(res.send).toHaveBeenCalled(); }); it('should set token in cookie with proper options', () => { process.env.NODE_ENV = 'production'; getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should set secure flag to false in dev environment', () => { process.env.NODE_ENV = 'dev'; getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: false, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should set secure flag to true in production environment', () => { process.env.NODE_ENV = 'production'; getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should handle test environment', () => { process.env.NODE_ENV = 'test'; getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 1000 }); }); it('should generate new token each time', () => { mockTokensInstance.create .mockReturnValueOnce('token-1') .mockReturnValueOnce('token-2'); getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'token-1', expect.any(Object)); expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'token-1'); jest.clearAllMocks(); getCSRFToken(req, res); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'token-2', expect.any(Object)); expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'token-2'); }); }); describe('Integration scenarios', () => { it('should handle complete CSRF flow', () => { // First, generate a token generateCSRFToken(req, res, next); const generatedToken = res.locals.csrfToken; // Reset mocks jest.clearAllMocks(); // Now test protection with the generated token req.method = 'POST'; req.headers['x-csrf-token'] = generatedToken; req.cookies = { 'csrf-token': generatedToken }; csrfProtection(req, res, next); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); it('should handle token generation endpoint flow', () => { getCSRFToken(req, res); const cookieCall = res.cookie.mock.calls[0]; const headerCall = res.set.mock.calls[0]; expect(cookieCall[0]).toBe('csrf-token'); expect(cookieCall[1]).toBe('mock-token-123'); expect(headerCall[0]).toBe('X-CSRF-Token'); expect(headerCall[1]).toBe('mock-token-123'); expect(res.status).toHaveBeenCalledWith(204); expect(res.send).toHaveBeenCalled(); }); }); });