Files
livedash-node/tests/integration/csrf-protection.test.ts
Kaj Kowalski 1eea2cc3e4 refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191
- Replace explicit 'any' types with proper TypeScript interfaces
- Fix React hooks dependencies and useCallback patterns
- Resolve unused variables and parameter assignment issues
- Improve accessibility with proper label associations
- Add comprehensive API documentation for admin and security features
- Update README.md with accurate PostgreSQL setup and current tech stack
- Create complete documentation for audit logging, CSP monitoring, and batch processing
- Fix outdated project information and missing developer workflows
2025-07-12 00:28:09 +02:00

257 lines
7.2 KiB
TypeScript

/**
* CSRF Protection Integration Tests
*
* End-to-end tests for CSRF protection in API endpoints and middleware.
*/
import { describe, it, expect, beforeEach } from "vitest";
import { createMocks } from "node-mocks-http";
import type { NextRequest } from "next/server";
import {
csrfProtectionMiddleware,
csrfTokenMiddleware,
} from "../../middleware/csrfProtection";
import { generateCSRFToken } from "../../lib/csrf";
describe("CSRF Protection Integration", () => {
describe("CSRF Token Middleware", () => {
it("should serve CSRF token on GET /api/csrf-token", async () => {
const { req } = createMocks({
method: "GET",
url: "/api/csrf-token",
});
const request = {
method: "GET",
nextUrl: { pathname: "/api/csrf-token" },
} as NextRequest;
const response = csrfTokenMiddleware(request);
expect(response).not.toBeNull();
if (response) {
const body = await response.json();
expect(body.success).toBe(true);
expect(body.token).toBeDefined();
expect(typeof body.token).toBe("string");
}
});
it("should return null for non-csrf-token paths", async () => {
const request = {
method: "GET",
nextUrl: { pathname: "/api/other" },
} as NextRequest;
const response = csrfTokenMiddleware(request);
expect(response).toBeNull();
});
});
describe("CSRF Protection Middleware", () => {
it("should allow GET requests without CSRF token", async () => {
const request = {
method: "GET",
nextUrl: { pathname: "/api/dashboard" },
} as NextRequest;
const response = await csrfProtectionMiddleware(request);
expect(response.status).not.toBe(403);
});
it("should allow HEAD requests without CSRF token", async () => {
const request = {
method: "HEAD",
nextUrl: { pathname: "/api/dashboard" },
} as NextRequest;
const response = await csrfProtectionMiddleware(request);
expect(response.status).not.toBe(403);
});
it("should allow OPTIONS requests without CSRF token", async () => {
const request = {
method: "OPTIONS",
nextUrl: { pathname: "/api/dashboard" },
} as NextRequest;
const response = await csrfProtectionMiddleware(request);
expect(response.status).not.toBe(403);
});
it("should block POST request to protected endpoint without CSRF token", async () => {
const request = {
method: "POST",
nextUrl: { pathname: "/api/dashboard/sessions" },
headers: new Headers({
"Content-Type": "application/json",
}),
cookies: {
get: () => undefined,
},
clone: () => ({
json: async () => ({}),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
const body = await response.json();
expect(body.success).toBe(false);
expect(body.error).toContain("CSRF token");
});
it("should allow POST request to unprotected endpoint without CSRF token", async () => {
const request = {
method: "POST",
nextUrl: { pathname: "/api/unprotected" },
} as NextRequest;
const response = await csrfProtectionMiddleware(request);
expect(response.status).not.toBe(403);
});
it("should allow POST request with valid CSRF token", async () => {
const token = generateCSRFToken();
const request = {
method: "POST",
nextUrl: { pathname: "/api/dashboard/sessions" },
headers: new Headers({
"Content-Type": "application/json",
"x-csrf-token": token,
}),
cookies: {
get: () => ({ value: token }),
},
clone: () => ({
json: async () => ({ csrfToken: token }),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).not.toBe(403);
});
it("should block POST request with mismatched CSRF tokens", async () => {
const headerToken = generateCSRFToken();
const cookieToken = generateCSRFToken();
const request = {
method: "POST",
nextUrl: { pathname: "/api/dashboard/sessions" },
headers: new Headers({
"Content-Type": "application/json",
"x-csrf-token": headerToken,
}),
cookies: {
get: () => ({ value: cookieToken }),
},
clone: () => ({
json: async () => ({ csrfToken: headerToken }),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
});
it("should protect all state-changing methods", async () => {
const methods = ["POST", "PUT", "DELETE", "PATCH"];
for (const method of methods) {
const request = {
method,
nextUrl: { pathname: "/api/trpc/test" },
headers: new Headers({
"Content-Type": "application/json",
}),
cookies: {
get: () => undefined,
},
clone: () => ({
json: async () => ({}),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
}
});
});
describe("Protected Endpoints", () => {
const protectedPaths = [
"/api/auth/signin",
"/api/register",
"/api/forgot-password",
"/api/reset-password",
"/api/dashboard/sessions",
"/api/platform/companies",
"/api/trpc/test",
];
protectedPaths.forEach((path) => {
it(`should protect ${path} endpoint`, async () => {
const request = {
method: "POST",
nextUrl: { pathname: path },
headers: new Headers({
"Content-Type": "application/json",
}),
cookies: {
get: () => undefined,
},
clone: () => ({
json: async () => ({}),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
});
});
});
describe("Error Handling", () => {
it("should handle malformed requests gracefully", async () => {
const request = {
method: "POST",
nextUrl: { pathname: "/api/dashboard/sessions" },
headers: new Headers({
"Content-Type": "application/json",
}),
cookies: {
get: () => undefined,
},
clone: () => ({
json: async () => {
throw new Error("Malformed JSON");
},
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
});
it("should handle missing headers gracefully", async () => {
const request = {
method: "POST",
nextUrl: { pathname: "/api/dashboard/sessions" },
headers: new Headers(),
cookies: {
get: () => undefined,
},
clone: () => ({
json: async () => ({}),
}),
} as any;
const response = await csrfProtectionMiddleware(request);
expect(response.status).toBe(403);
});
});
});