import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { NotFoundException, ForbiddenException, UnauthorizedException, ConflictException, } from "@nestjs/common"; import { LocalAuthController } from "./local-auth.controller"; import { LocalAuthService } from "./local-auth.service"; import { LocalAuthEnabledGuard } from "./local-auth.guard"; describe("LocalAuthController", () => { let controller: LocalAuthController; let localAuthService: LocalAuthService; const mockLocalAuthService = { setup: vi.fn(), login: vi.fn(), }; const mockRequest = { headers: { "user-agent": "TestAgent/1.0" }, ip: "127.0.0.1", socket: { remoteAddress: "127.0.0.1" }, }; const originalEnv = { ENABLE_LOCAL_AUTH: process.env.ENABLE_LOCAL_AUTH, }; beforeEach(async () => { process.env.ENABLE_LOCAL_AUTH = "true"; const module: TestingModule = await Test.createTestingModule({ controllers: [LocalAuthController], providers: [ { provide: LocalAuthService, useValue: mockLocalAuthService, }, ], }) .overrideGuard(LocalAuthEnabledGuard) .useValue({ canActivate: () => true }) .compile(); controller = module.get(LocalAuthController); localAuthService = module.get(LocalAuthService); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); if (originalEnv.ENABLE_LOCAL_AUTH !== undefined) { process.env.ENABLE_LOCAL_AUTH = originalEnv.ENABLE_LOCAL_AUTH; } else { delete process.env.ENABLE_LOCAL_AUTH; } }); describe("setup", () => { const setupDto = { email: "admin@example.com", name: "Break Glass Admin", password: "securePassword123!", setupToken: "valid-token-123", }; const mockSetupResult = { user: { id: "user-uuid-123", email: "admin@example.com", name: "Break Glass Admin", isLocalAuth: true, createdAt: new Date("2026-02-28T00:00:00Z"), }, session: { token: "session-token-abc", expiresAt: new Date("2026-03-07T00:00:00Z"), }, }; it("should create a break-glass user and return user data with session", async () => { mockLocalAuthService.setup.mockResolvedValue(mockSetupResult); const result = await controller.setup(setupDto, mockRequest as never); expect(result).toEqual({ user: mockSetupResult.user, session: mockSetupResult.session, }); expect(mockLocalAuthService.setup).toHaveBeenCalledWith( "admin@example.com", "Break Glass Admin", "securePassword123!", "valid-token-123", "127.0.0.1", "TestAgent/1.0" ); }); it("should extract client IP from x-forwarded-for header", async () => { mockLocalAuthService.setup.mockResolvedValue(mockSetupResult); const reqWithProxy = { ...mockRequest, headers: { ...mockRequest.headers, "x-forwarded-for": "203.0.113.50, 70.41.3.18", }, }; await controller.setup(setupDto, reqWithProxy as never); expect(mockLocalAuthService.setup).toHaveBeenCalledWith( expect.any(String) as string, expect.any(String) as string, expect.any(String) as string, expect.any(String) as string, "203.0.113.50", "TestAgent/1.0" ); }); it("should propagate ForbiddenException from service", async () => { mockLocalAuthService.setup.mockRejectedValue(new ForbiddenException("Invalid setup token")); await expect(controller.setup(setupDto, mockRequest as never)).rejects.toThrow( ForbiddenException ); }); it("should propagate ConflictException from service", async () => { mockLocalAuthService.setup.mockRejectedValue( new ConflictException("A user with this email already exists") ); await expect(controller.setup(setupDto, mockRequest as never)).rejects.toThrow( ConflictException ); }); }); describe("login", () => { const loginDto = { email: "admin@example.com", password: "securePassword123!", }; const mockLoginResult = { user: { id: "user-uuid-123", email: "admin@example.com", name: "Break Glass Admin", }, session: { token: "session-token-abc", expiresAt: new Date("2026-03-07T00:00:00Z"), }, }; it("should authenticate and return user data with session", async () => { mockLocalAuthService.login.mockResolvedValue(mockLoginResult); const result = await controller.login(loginDto, mockRequest as never); expect(result).toEqual({ user: mockLoginResult.user, session: mockLoginResult.session, }); expect(mockLocalAuthService.login).toHaveBeenCalledWith( "admin@example.com", "securePassword123!", "127.0.0.1", "TestAgent/1.0" ); }); it("should propagate UnauthorizedException from service", async () => { mockLocalAuthService.login.mockRejectedValue( new UnauthorizedException("Invalid email or password") ); await expect(controller.login(loginDto, mockRequest as never)).rejects.toThrow( UnauthorizedException ); }); }); }); describe("LocalAuthEnabledGuard", () => { let guard: LocalAuthEnabledGuard; const originalEnv = process.env.ENABLE_LOCAL_AUTH; beforeEach(() => { guard = new LocalAuthEnabledGuard(); }); afterEach(() => { if (originalEnv !== undefined) { process.env.ENABLE_LOCAL_AUTH = originalEnv; } else { delete process.env.ENABLE_LOCAL_AUTH; } }); it("should allow access when ENABLE_LOCAL_AUTH is true", () => { process.env.ENABLE_LOCAL_AUTH = "true"; expect(guard.canActivate()).toBe(true); }); it("should throw NotFoundException when ENABLE_LOCAL_AUTH is not set", () => { delete process.env.ENABLE_LOCAL_AUTH; expect(() => guard.canActivate()).toThrow(NotFoundException); }); it("should throw NotFoundException when ENABLE_LOCAL_AUTH is false", () => { process.env.ENABLE_LOCAL_AUTH = "false"; expect(() => guard.canActivate()).toThrow(NotFoundException); }); it("should throw NotFoundException when ENABLE_LOCAL_AUTH is empty", () => { process.env.ENABLE_LOCAL_AUTH = ""; expect(() => guard.canActivate()).toThrow(NotFoundException); }); });