// Set CSRF_SECRET before requiring the middleware process.env.CSRF_SECRET = "test-csrf-secret"; 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 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(); }); }); 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: false, 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: false, 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: false, 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(); }); }); });