test: add comprehensive unit tests for core services
Added unit tests for the `api-keys`, `auth`, `categories`, `contents`, `favorites`, `media`, and `purge` services to improve test coverage and ensure core functionality integrity.
This commit is contained in:
253
backend/src/auth/auth.service.spec.ts
Normal file
253
backend/src/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
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 { CryptoService } from "../crypto/crypto.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("../crypto/crypto.service");
|
||||
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 mockCryptoService = {
|
||||
hashPassword: jest.fn(),
|
||||
hashEmail: jest.fn(),
|
||||
verifyPassword: jest.fn(),
|
||||
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: CryptoService, useValue: mockCryptoService },
|
||||
{ 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: "password",
|
||||
};
|
||||
mockCryptoService.hashPassword.mockResolvedValue("hashed-password");
|
||||
mockCryptoService.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: "password" };
|
||||
const user = {
|
||||
uuid: "user-id",
|
||||
username: "test",
|
||||
passwordHash: "hash",
|
||||
isTwoFactorEnabled: false,
|
||||
};
|
||||
mockCryptoService.hashEmail.mockResolvedValue("hashed-email");
|
||||
mockUsersService.findByEmailHash.mockResolvedValue(user);
|
||||
mockCryptoService.verifyPassword.mockResolvedValue(true);
|
||||
mockCryptoService.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,
|
||||
};
|
||||
mockCryptoService.hashEmail.mockResolvedValue("hashed-email");
|
||||
mockUsersService.findByEmailHash.mockResolvedValue(user);
|
||||
mockCryptoService.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);
|
||||
mockCryptoService.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);
|
||||
mockCryptoService.generateJwt.mockResolvedValue("new-access");
|
||||
|
||||
const result = await service.refresh(refreshToken);
|
||||
|
||||
expect(result).toEqual({
|
||||
access_token: "new-access",
|
||||
refresh_token: "new-refresh",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
69
backend/src/auth/rbac.service.spec.ts
Normal file
69
backend/src/auth/rbac.service.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { DatabaseService } from "../database/database.service";
|
||||
import { RbacService } from "./rbac.service";
|
||||
|
||||
describe("RbacService", () => {
|
||||
let service: RbacService;
|
||||
|
||||
const mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
innerJoin: jest.fn().mockReturnThis(),
|
||||
where: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RbacService,
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
db: mockDb,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<RbacService>(RbacService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("getUserRoles", () => {
|
||||
it("should return user roles", async () => {
|
||||
const userId = "user-id";
|
||||
const mockRoles = [{ slug: "admin" }, { slug: "user" }];
|
||||
mockDb.where.mockResolvedValue(mockRoles);
|
||||
|
||||
const result = await service.getUserRoles(userId);
|
||||
|
||||
expect(result).toEqual(["admin", "user"]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.innerJoin).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserPermissions", () => {
|
||||
it("should return unique user permissions", async () => {
|
||||
const userId = "user-id";
|
||||
const mockPermissions = [
|
||||
{ slug: "read" },
|
||||
{ slug: "write" },
|
||||
{ slug: "read" }, // Duplicate
|
||||
];
|
||||
mockDb.where.mockResolvedValue(mockPermissions);
|
||||
|
||||
const result = await service.getUserPermissions(userId);
|
||||
|
||||
expect(result).toEqual(["read", "write"]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.innerJoin).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user