From 3fa11474c157b1094efa6cd167097b3a0f2cee02 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:45:50 +0100 Subject: [PATCH] test(auth): add unit tests for AuthGuard and AuthController with mocked dependencies --- backend/src/auth/auth.controller.spec.ts | 184 +++++++++++++++++++++ backend/src/auth/guards/auth.guard.spec.ts | 89 ++++++++++ 2 files changed, 273 insertions(+) create mode 100644 backend/src/auth/auth.controller.spec.ts create mode 100644 backend/src/auth/guards/auth.guard.spec.ts diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..f8e68a4 --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,184 @@ +jest.mock("uuid", () => ({ + v4: jest.fn(() => "mocked-uuid"), +})); + +jest.mock("@noble/post-quantum/ml-kem.js", () => ({ + ml_kem768: { + keygen: jest.fn(), + encapsulate: jest.fn(), + decapsulate: jest.fn(), + }, +})); + +jest.mock("jose", () => ({ + SignJWT: jest.fn().mockReturnValue({ + setProtectedHeader: jest.fn().mockReturnThis(), + setIssuedAt: jest.fn().mockReturnThis(), + setExpirationTime: jest.fn().mockReturnThis(), + sign: jest.fn().mockResolvedValue("mocked-jwt"), + }), + jwtVerify: jest.fn(), +})); + +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthController } from "./auth.controller"; +import { AuthService } from "./auth.service"; + +jest.mock("iron-session", () => ({ + getIronSession: jest.fn().mockResolvedValue({ + save: jest.fn(), + destroy: jest.fn(), + }), +})); + +describe("AuthController", () => { + let controller: AuthController; + let authService: AuthService; + let _configService: ConfigService; + + const mockAuthService = { + register: jest.fn(), + login: jest.fn(), + verifyTwoFactorLogin: jest.fn(), + refresh: jest.fn(), + }; + + const mockConfigService = { + get: jest + .fn() + .mockReturnValue("complex_password_at_least_32_characters_long"), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { provide: AuthService, useValue: mockAuthService }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }).compile(); + + controller = module.get(AuthController); + authService = module.get(AuthService); + _configService = module.get(ConfigService); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); + + describe("register", () => { + it("should call authService.register", async () => { + const dto = { + email: "test@example.com", + password: "password", + username: "test", + }; + await controller.register(dto as any); + expect(authService.register).toHaveBeenCalledWith(dto); + }); + }); + + describe("login", () => { + it("should call authService.login and setup session if success", async () => { + const dto = { email: "test@example.com", password: "password" }; + const req = { ip: "127.0.0.1" } as any; + const res = { json: jest.fn() } as any; + const loginResult = { + access_token: "at", + refresh_token: "rt", + userId: "1", + message: "ok", + }; + mockAuthService.login.mockResolvedValue(loginResult); + + await controller.login(dto as any, "ua", req, res); + + expect(authService.login).toHaveBeenCalledWith(dto, "ua", "127.0.0.1"); + expect(res.json).toHaveBeenCalledWith({ message: "ok", userId: "1" }); + }); + + it("should return result if no access_token", async () => { + const dto = { email: "test@example.com", password: "password" }; + const req = { ip: "127.0.0.1" } as any; + const res = { json: jest.fn() } as any; + const loginResult = { message: "2fa_required", userId: "1" }; + mockAuthService.login.mockResolvedValue(loginResult); + + await controller.login(dto as any, "ua", req, res); + + expect(res.json).toHaveBeenCalledWith(loginResult); + }); + }); + + describe("verifyTwoFactor", () => { + it("should call authService.verifyTwoFactorLogin and setup session", async () => { + const dto = { userId: "1", token: "123456" }; + const req = { ip: "127.0.0.1" } as any; + const res = { json: jest.fn() } as any; + const verifyResult = { + access_token: "at", + refresh_token: "rt", + message: "ok", + }; + mockAuthService.verifyTwoFactorLogin.mockResolvedValue(verifyResult); + + await controller.verifyTwoFactor(dto, "ua", req, res); + + expect(authService.verifyTwoFactorLogin).toHaveBeenCalledWith( + "1", + "123456", + "ua", + "127.0.0.1", + ); + expect(res.json).toHaveBeenCalledWith({ message: "ok" }); + }); + }); + + describe("refresh", () => { + it("should refresh token if session has refresh token", async () => { + const { getIronSession } = require("iron-session"); + const session = { refreshToken: "rt", save: jest.fn() }; + getIronSession.mockResolvedValue(session); + const req = {} as any; + const res = { json: jest.fn() } as any; + mockAuthService.refresh.mockResolvedValue({ + access_token: "at2", + refresh_token: "rt2", + }); + + await controller.refresh(req, res); + + expect(authService.refresh).toHaveBeenCalledWith("rt"); + expect(res.json).toHaveBeenCalledWith({ message: "Token refreshed" }); + }); + + it("should return 401 if no refresh token", async () => { + const { getIronSession } = require("iron-session"); + const session = { save: jest.fn() }; + getIronSession.mockResolvedValue(session); + const req = {} as any; + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } as any; + + await controller.refresh(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + }); + + describe("logout", () => { + it("should destroy session", async () => { + const { getIronSession } = require("iron-session"); + const session = { destroy: jest.fn() }; + getIronSession.mockResolvedValue(session); + const req = {} as any; + const res = { json: jest.fn() } as any; + + await controller.logout(req, res); + + expect(session.destroy).toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ message: "User logged out" }); + }); + }); +}); diff --git a/backend/src/auth/guards/auth.guard.spec.ts b/backend/src/auth/guards/auth.guard.spec.ts new file mode 100644 index 0000000..db5645e --- /dev/null +++ b/backend/src/auth/guards/auth.guard.spec.ts @@ -0,0 +1,89 @@ +import { ExecutionContext, UnauthorizedException } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { getIronSession } from "iron-session"; +import { JwtService } from "../../crypto/services/jwt.service"; +import { AuthGuard } from "./auth.guard"; + +jest.mock("jose", () => ({})); +jest.mock("iron-session", () => ({ + getIronSession: jest.fn(), +})); + +describe("AuthGuard", () => { + let guard: AuthGuard; + let _jwtService: JwtService; + let _configService: ConfigService; + + const mockJwtService = { + verifyJwt: jest.fn(), + }; + + const mockConfigService = { + get: jest.fn().mockReturnValue("session-password"), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthGuard, + { provide: JwtService, useValue: mockJwtService }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }).compile(); + + guard = module.get(AuthGuard); + _jwtService = module.get(JwtService); + _configService = module.get(ConfigService); + }); + + it("should return true for valid token", async () => { + const request = { user: null }; + const context = { + switchToHttp: () => ({ + getRequest: () => request, + getResponse: () => ({}), + }), + } as unknown as ExecutionContext; + + (getIronSession as jest.Mock).mockResolvedValue({ + accessToken: "valid-token", + }); + mockJwtService.verifyJwt.mockResolvedValue({ sub: "user1" }); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(request.user).toEqual({ sub: "user1" }); + }); + + it("should throw UnauthorizedException if no token", async () => { + const context = { + switchToHttp: () => ({ + getRequest: () => ({}), + getResponse: () => ({}), + }), + } as ExecutionContext; + + (getIronSession as jest.Mock).mockResolvedValue({}); + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it("should throw UnauthorizedException if token invalid", async () => { + const context = { + switchToHttp: () => ({ + getRequest: () => ({}), + getResponse: () => ({}), + }), + } as ExecutionContext; + + (getIronSession as jest.Mock).mockResolvedValue({ accessToken: "invalid" }); + mockJwtService.verifyJwt.mockRejectedValue(new Error("invalid")); + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException, + ); + }); +});