/** * CSRF Controller Tests * * Tests CSRF token generation endpoint with session binding. */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Request, Response } from "express"; import { CsrfController } from "./csrf.controller"; import { CsrfService } from "../services/csrf.service"; import type { AuthenticatedUser } from "../types/user.types"; interface AuthenticatedRequest extends Request { user?: AuthenticatedUser; } describe("CsrfController", () => { let controller: CsrfController; let csrfService: CsrfService; const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; process.env.CSRF_SECRET = "test-secret-0123456789abcdef0123456789abcdef"; csrfService = new CsrfService(); csrfService.onModuleInit(); controller = new CsrfController(csrfService); }); afterEach(() => { process.env = originalEnv; }); const createMockRequest = (userId?: string): AuthenticatedRequest => { return { user: userId ? { id: userId, email: "test@example.com", name: "Test User" } : undefined, } as AuthenticatedRequest; }; const createMockResponse = (): Response => { return { cookie: vi.fn(), } as unknown as Response; }; describe("getCsrfToken", () => { it("should generate and return a CSRF token with session binding", () => { const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); const result = controller.getCsrfToken(mockRequest, mockResponse); expect(result).toHaveProperty("token"); expect(typeof result.token).toBe("string"); // Token format: random:hmac (64 hex chars : 64 hex chars) expect(result.token).toContain(":"); const parts = result.token.split(":"); expect(parts[0]).toHaveLength(64); expect(parts[1]).toHaveLength(64); }); it("should set CSRF token in httpOnly cookie", () => { const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); const result = controller.getCsrfToken(mockRequest, mockResponse); expect(mockResponse.cookie).toHaveBeenCalledWith( "csrf-token", result.token, expect.objectContaining({ httpOnly: true, sameSite: "strict", }) ); }); it("should set secure flag in production", () => { process.env.NODE_ENV = "production"; const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); controller.getCsrfToken(mockRequest, mockResponse); expect(mockResponse.cookie).toHaveBeenCalledWith( "csrf-token", expect.any(String), expect.objectContaining({ secure: true, }) ); }); it("should not set secure flag in development", () => { process.env.NODE_ENV = "development"; const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); controller.getCsrfToken(mockRequest, mockResponse); expect(mockResponse.cookie).toHaveBeenCalledWith( "csrf-token", expect.any(String), expect.objectContaining({ secure: false, }) ); }); it("should generate unique tokens on each call", () => { const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); const result1 = controller.getCsrfToken(mockRequest, mockResponse); const result2 = controller.getCsrfToken(mockRequest, mockResponse); expect(result1.token).not.toBe(result2.token); }); it("should set cookie with 24 hour expiry", () => { const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); controller.getCsrfToken(mockRequest, mockResponse); expect(mockResponse.cookie).toHaveBeenCalledWith( "csrf-token", expect.any(String), expect.objectContaining({ maxAge: 24 * 60 * 60 * 1000, // 24 hours }) ); }); it("should throw error when user is not authenticated", () => { const mockRequest = createMockRequest(); // No user ID const mockResponse = createMockResponse(); expect(() => controller.getCsrfToken(mockRequest, mockResponse)).toThrow( "User ID not available after authentication" ); }); it("should generate token bound to specific user session", () => { const mockRequest = createMockRequest("user-123"); const mockResponse = createMockResponse(); const result = controller.getCsrfToken(mockRequest, mockResponse); // Token should be valid for user-123 expect(csrfService.validateToken(result.token, "user-123")).toBe(true); // Token should be invalid for different user expect(csrfService.validateToken(result.token, "user-456")).toBe(false); }); it("should generate different tokens for different users", () => { const mockResponse = createMockResponse(); const request1 = createMockRequest("user-A"); const request2 = createMockRequest("user-B"); const result1 = controller.getCsrfToken(request1, mockResponse); const result2 = controller.getCsrfToken(request2, mockResponse); expect(result1.token).not.toBe(result2.token); // Each token only valid for its user expect(csrfService.validateToken(result1.token, "user-A")).toBe(true); expect(csrfService.validateToken(result1.token, "user-B")).toBe(false); expect(csrfService.validateToken(result2.token, "user-B")).toBe(true); expect(csrfService.validateToken(result2.token, "user-A")).toBe(false); }); }); });