Files
2026-01-21 19:00:55 -05:00

542 lines
15 KiB
JavaScript

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