mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:52:08 +01:00
- 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
257 lines
7.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|