Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
233 lines
6.4 KiB
TypeScript
233 lines
6.4 KiB
TypeScript
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>(LocalAuthController);
|
|
localAuthService = module.get<LocalAuthService>(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);
|
|
});
|
|
});
|