import { describe, it, expect, beforeEach, vi } from "vitest"; import { ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { ApiKeyGuard } from "./api-key.guard"; describe("ApiKeyGuard", () => { let guard: ApiKeyGuard; let mockConfigService: ConfigService; beforeEach(() => { mockConfigService = { get: vi.fn(), } as unknown as ConfigService; guard = new ApiKeyGuard(mockConfigService); }); const createMockExecutionContext = (headers: Record): ExecutionContext => { return { switchToHttp: () => ({ getRequest: () => ({ headers, }), }), } as ExecutionContext; }; describe("canActivate", () => { it("should return true when valid API key is provided", () => { const validApiKey = "test-api-key-12345"; vi.mocked(mockConfigService.get).mockReturnValue(validApiKey); const context = createMockExecutionContext({ "x-api-key": validApiKey, }); const result = guard.canActivate(context); expect(result).toBe(true); expect(mockConfigService.get).toHaveBeenCalledWith("COORDINATOR_API_KEY"); }); it("should throw UnauthorizedException when no API key is provided", () => { const context = createMockExecutionContext({}); expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); expect(() => guard.canActivate(context)).toThrow("No API key provided"); }); it("should throw UnauthorizedException when API key is invalid", () => { const validApiKey = "correct-api-key"; const invalidApiKey = "wrong-api-key"; vi.mocked(mockConfigService.get).mockReturnValue(validApiKey); const context = createMockExecutionContext({ "x-api-key": invalidApiKey, }); expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); expect(() => guard.canActivate(context)).toThrow("Invalid API key"); }); it("should throw UnauthorizedException when COORDINATOR_API_KEY is not configured", () => { vi.mocked(mockConfigService.get).mockReturnValue(undefined); const context = createMockExecutionContext({ "x-api-key": "some-key", }); expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); expect(() => guard.canActivate(context)).toThrow("API key authentication not configured"); }); it("should handle uppercase header name (X-API-Key)", () => { const validApiKey = "test-api-key-12345"; vi.mocked(mockConfigService.get).mockReturnValue(validApiKey); const context = createMockExecutionContext({ "X-API-Key": validApiKey, }); const result = guard.canActivate(context); expect(result).toBe(true); }); it("should handle mixed case header name (X-Api-Key)", () => { const validApiKey = "test-api-key-12345"; vi.mocked(mockConfigService.get).mockReturnValue(validApiKey); const context = createMockExecutionContext({ "X-Api-Key": validApiKey, }); const result = guard.canActivate(context); expect(result).toBe(true); }); it("should reject empty string API key", () => { vi.mocked(mockConfigService.get).mockReturnValue("valid-key"); const context = createMockExecutionContext({ "x-api-key": "", }); expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); expect(() => guard.canActivate(context)).toThrow("No API key provided"); }); it("should use constant-time comparison to prevent timing attacks", () => { const validApiKey = "test-api-key-12345"; vi.mocked(mockConfigService.get).mockReturnValue(validApiKey); const startTime = Date.now(); const context1 = createMockExecutionContext({ "x-api-key": "wrong-key-short", }); try { guard.canActivate(context1); } catch { // Expected to fail } const shortKeyTime = Date.now() - startTime; const startTime2 = Date.now(); const context2 = createMockExecutionContext({ "x-api-key": "test-api-key-12344", // Very close to correct key }); try { guard.canActivate(context2); } catch { // Expected to fail } const longKeyTime = Date.now() - startTime2; // Times should be similar (within 10ms) to prevent timing attacks // Note: This is a simplified test; real timing attack prevention // is handled by crypto.timingSafeEqual expect(Math.abs(shortKeyTime - longKeyTime)).toBeLessThan(10); }); }); });