- Updated username validation to allow only lowercase letters, numbers, and underscores. - Strengthened password requirements to include at least 8 characters, one uppercase letter, one lowercase letter, one number, and one special character. - Adjusted frontend forms and backend DTOs to reflect new validation rules.
262 lines
7.7 KiB
TypeScript
262 lines
7.7 KiB
TypeScript
jest.mock("uuid", () => ({
|
|
v4: jest.fn(() => "mocked-uuid"),
|
|
}));
|
|
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
|
|
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(),
|
|
jwtVerify: jest.fn(),
|
|
}));
|
|
|
|
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { authenticator } from "otplib";
|
|
import * as qrcode from "qrcode";
|
|
import { HashingService } from "../crypto/services/hashing.service";
|
|
import { JwtService } from "../crypto/services/jwt.service";
|
|
import { SessionsService } from "../sessions/sessions.service";
|
|
import { UsersService } from "../users/users.service";
|
|
import { AuthService } from "./auth.service";
|
|
|
|
jest.mock("otplib");
|
|
jest.mock("qrcode");
|
|
jest.mock("../users/users.service");
|
|
jest.mock("../sessions/sessions.service");
|
|
|
|
describe("AuthService", () => {
|
|
let service: AuthService;
|
|
|
|
const mockUsersService = {
|
|
findOne: jest.fn(),
|
|
setTwoFactorSecret: jest.fn(),
|
|
getTwoFactorSecret: jest.fn(),
|
|
toggleTwoFactor: jest.fn(),
|
|
create: jest.fn(),
|
|
findByEmailHash: jest.fn(),
|
|
findOneWithPrivateData: jest.fn(),
|
|
};
|
|
|
|
const mockHashingService = {
|
|
hashPassword: jest.fn(),
|
|
hashEmail: jest.fn(),
|
|
verifyPassword: jest.fn(),
|
|
};
|
|
|
|
const mockJwtService = {
|
|
generateJwt: jest.fn(),
|
|
};
|
|
|
|
const mockSessionsService = {
|
|
createSession: jest.fn(),
|
|
refreshSession: jest.fn(),
|
|
};
|
|
|
|
const mockConfigService = {
|
|
get: jest.fn(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
AuthService,
|
|
{ provide: UsersService, useValue: mockUsersService },
|
|
{ provide: HashingService, useValue: mockHashingService },
|
|
{ provide: JwtService, useValue: mockJwtService },
|
|
{ provide: SessionsService, useValue: mockSessionsService },
|
|
{ provide: ConfigService, useValue: mockConfigService },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<AuthService>(AuthService);
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
describe("generateTwoFactorSecret", () => {
|
|
it("should generate a 2FA secret", async () => {
|
|
const userId = "user-id";
|
|
const user = { username: "testuser" };
|
|
mockUsersService.findOne.mockResolvedValue(user);
|
|
(authenticator.generateSecret as jest.Mock).mockReturnValue("secret");
|
|
(authenticator.keyuri as jest.Mock).mockReturnValue("otpauth://...");
|
|
(qrcode.toDataURL as jest.Mock).mockResolvedValue(
|
|
"data:image/png;base64,...",
|
|
);
|
|
|
|
const result = await service.generateTwoFactorSecret(userId);
|
|
|
|
expect(result).toEqual({
|
|
secret: "secret",
|
|
qrCodeDataUrl: "data:image/png;base64,...",
|
|
});
|
|
expect(mockUsersService.setTwoFactorSecret).toHaveBeenCalledWith(
|
|
userId,
|
|
"secret",
|
|
);
|
|
});
|
|
|
|
it("should throw UnauthorizedException if user not found", async () => {
|
|
mockUsersService.findOne.mockResolvedValue(null);
|
|
await expect(service.generateTwoFactorSecret("invalid")).rejects.toThrow(
|
|
UnauthorizedException,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("enableTwoFactor", () => {
|
|
it("should enable 2FA", async () => {
|
|
const userId = "user-id";
|
|
const token = "123456";
|
|
mockUsersService.getTwoFactorSecret.mockResolvedValue("secret");
|
|
(authenticator.verify as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await service.enableTwoFactor(userId, token);
|
|
|
|
expect(result).toEqual({ message: "2FA enabled successfully" });
|
|
expect(mockUsersService.toggleTwoFactor).toHaveBeenCalledWith(userId, true);
|
|
});
|
|
|
|
it("should throw BadRequestException if 2FA not initiated", async () => {
|
|
mockUsersService.getTwoFactorSecret.mockResolvedValue(null);
|
|
await expect(service.enableTwoFactor("user-id", "token")).rejects.toThrow(
|
|
BadRequestException,
|
|
);
|
|
});
|
|
|
|
it("should throw BadRequestException if token is invalid", async () => {
|
|
mockUsersService.getTwoFactorSecret.mockResolvedValue("secret");
|
|
(authenticator.verify as jest.Mock).mockReturnValue(false);
|
|
await expect(service.enableTwoFactor("user-id", "invalid")).rejects.toThrow(
|
|
BadRequestException,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("register", () => {
|
|
it("should register a user", async () => {
|
|
const dto = {
|
|
username: "test",
|
|
email: "test@example.com",
|
|
password: "Password1!",
|
|
};
|
|
mockHashingService.hashPassword.mockResolvedValue("hashed-password");
|
|
mockHashingService.hashEmail.mockResolvedValue("hashed-email");
|
|
mockUsersService.create.mockResolvedValue({ uuid: "new-user-id" });
|
|
|
|
const result = await service.register(dto);
|
|
|
|
expect(result).toEqual({
|
|
message: "User registered successfully",
|
|
userId: "new-user-id",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("login", () => {
|
|
it("should login a user", async () => {
|
|
const dto = { email: "test@example.com", password: "Password1!" };
|
|
const user = {
|
|
uuid: "user-id",
|
|
username: "test",
|
|
passwordHash: "hash",
|
|
isTwoFactorEnabled: false,
|
|
};
|
|
mockHashingService.hashEmail.mockResolvedValue("hashed-email");
|
|
mockUsersService.findByEmailHash.mockResolvedValue(user);
|
|
mockHashingService.verifyPassword.mockResolvedValue(true);
|
|
mockJwtService.generateJwt.mockResolvedValue("access-token");
|
|
mockSessionsService.createSession.mockResolvedValue({
|
|
refreshToken: "refresh-token",
|
|
});
|
|
|
|
const result = await service.login(dto);
|
|
|
|
expect(result).toEqual({
|
|
message: "User logged in successfully",
|
|
access_token: "access-token",
|
|
refresh_token: "refresh-token",
|
|
});
|
|
});
|
|
|
|
it("should return requires2FA if 2FA is enabled", async () => {
|
|
const dto = { email: "test@example.com", password: "password" };
|
|
const user = {
|
|
uuid: "user-id",
|
|
username: "test",
|
|
passwordHash: "hash",
|
|
isTwoFactorEnabled: true,
|
|
};
|
|
mockHashingService.hashEmail.mockResolvedValue("hashed-email");
|
|
mockUsersService.findByEmailHash.mockResolvedValue(user);
|
|
mockHashingService.verifyPassword.mockResolvedValue(true);
|
|
|
|
const result = await service.login(dto);
|
|
|
|
expect(result).toEqual({
|
|
message: "2FA required",
|
|
requires2FA: true,
|
|
userId: "user-id",
|
|
});
|
|
});
|
|
|
|
it("should throw UnauthorizedException for invalid credentials", async () => {
|
|
mockUsersService.findByEmailHash.mockResolvedValue(null);
|
|
await expect(service.login({ email: "x", password: "y" })).rejects.toThrow(
|
|
UnauthorizedException,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("verifyTwoFactorLogin", () => {
|
|
it("should verify 2FA login", async () => {
|
|
const userId = "user-id";
|
|
const token = "123456";
|
|
const user = { uuid: userId, username: "test", isTwoFactorEnabled: true };
|
|
mockUsersService.findOneWithPrivateData.mockResolvedValue(user);
|
|
mockUsersService.getTwoFactorSecret.mockResolvedValue("secret");
|
|
(authenticator.verify as jest.Mock).mockReturnValue(true);
|
|
mockJwtService.generateJwt.mockResolvedValue("access-token");
|
|
mockSessionsService.createSession.mockResolvedValue({
|
|
refreshToken: "refresh-token",
|
|
});
|
|
|
|
const result = await service.verifyTwoFactorLogin(userId, token);
|
|
|
|
expect(result).toEqual({
|
|
message: "User logged in successfully (2FA)",
|
|
access_token: "access-token",
|
|
refresh_token: "refresh-token",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("refresh", () => {
|
|
it("should refresh tokens", async () => {
|
|
const refreshToken = "old-refresh";
|
|
const session = { userId: "user-id", refreshToken: "new-refresh" };
|
|
const user = { uuid: "user-id", username: "test" };
|
|
mockSessionsService.refreshSession.mockResolvedValue(session);
|
|
mockUsersService.findOne.mockResolvedValue(user);
|
|
mockJwtService.generateJwt.mockResolvedValue("new-access");
|
|
|
|
const result = await service.refresh(refreshToken);
|
|
|
|
expect(result).toEqual({
|
|
access_token: "new-access",
|
|
refresh_token: "new-refresh",
|
|
});
|
|
});
|
|
});
|
|
});
|