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:
Mathis HERRIOT
2026-01-08 16:22:23 +01:00
parent cc2823db7d
commit 399bdab86c
13 changed files with 1502 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
import { createHash } from "node:crypto";
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { apiKeys } from "../database/schemas";
import { ApiKeysService } from "./api-keys.service";
describe("ApiKeysService", () => {
let service: ApiKeysService;
const mockDb = {
insert: jest.fn(),
values: jest.fn(),
select: jest.fn(),
from: jest.fn(),
where: jest.fn(),
limit: jest.fn(),
update: jest.fn(),
set: jest.fn(),
returning: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
mockDb.insert.mockReturnThis();
mockDb.values.mockResolvedValue(undefined);
mockDb.select.mockReturnThis();
mockDb.from.mockReturnThis();
mockDb.where.mockReturnThis();
mockDb.limit.mockReturnThis();
mockDb.update.mockReturnThis();
mockDb.set.mockReturnThis();
mockDb.returning.mockResolvedValue([]);
// Default for findAll which is awaited on where()
mockDb.where.mockImplementation(() => {
const chain = {
returning: jest.fn().mockResolvedValue([]),
};
return Object.assign(Promise.resolve([]), chain);
});
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiKeysService,
{
provide: DatabaseService,
useValue: {
db: mockDb,
},
},
],
}).compile();
service = module.get<ApiKeysService>(ApiKeysService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create an API key", async () => {
const userId = "user-id";
const name = "Test Key";
const expiresAt = new Date();
const result = await service.create(userId, name, expiresAt);
expect(mockDb.insert).toHaveBeenCalledWith(apiKeys);
expect(mockDb.values).toHaveBeenCalledWith(
expect.objectContaining({
userId,
name,
prefix: "mg_live_",
expiresAt,
}),
);
expect(result).toHaveProperty("key");
expect(result.name).toBe(name);
expect(result.expiresAt).toBe(expiresAt);
expect(result.key).toMatch(/^mg_live_/);
});
});
describe("findAll", () => {
it("should find all API keys for a user", async () => {
const userId = "user-id";
const expectedKeys = [{ id: "1", name: "Key 1" }];
(mockDb.where as jest.Mock).mockResolvedValue(expectedKeys);
const result = await service.findAll(userId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalledWith(apiKeys);
expect(result).toEqual(expectedKeys);
});
});
describe("revoke", () => {
it("should revoke an API key", async () => {
const userId = "user-id";
const keyId = "key-id";
const expectedResult = [{ id: keyId, isActive: false }];
mockDb.where.mockReturnValue({
returning: jest.fn().mockResolvedValue(expectedResult),
});
const result = await service.revoke(userId, keyId);
expect(mockDb.update).toHaveBeenCalledWith(apiKeys);
expect(mockDb.set).toHaveBeenCalledWith(
expect.objectContaining({ isActive: false }),
);
expect(result).toEqual(expectedResult);
});
});
describe("validateKey", () => {
it("should validate a valid API key", async () => {
const key = "mg_live_testkey";
const keyHash = createHash("sha256").update(key).digest("hex");
const apiKey = { id: "1", keyHash, isActive: true, expiresAt: null };
(mockDb.limit as jest.Mock).mockResolvedValue([apiKey]);
(mockDb.where as jest.Mock).mockResolvedValue([apiKey]); // For the update later if needed, but here it's for select
// We need to be careful with chaining mockDb.where is used in both select and update
const mockSelect = {
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([apiKey]),
};
const mockUpdate = {
set: jest.fn().mockReturnThis(),
where: jest.fn().mockResolvedValue(undefined),
};
(mockDb.select as jest.Mock).mockReturnValue(mockSelect);
(mockDb.update as jest.Mock).mockReturnValue(mockUpdate);
const result = await service.validateKey(key);
expect(result).toEqual(apiKey);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.update).toHaveBeenCalledWith(apiKeys);
});
it("should return null for invalid API key", async () => {
(mockDb.select as jest.Mock).mockReturnValue({
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([]),
});
const result = await service.validateKey("invalid-key");
expect(result).toBeNull();
});
it("should return null for expired API key", async () => {
const key = "mg_live_testkey";
const expiredDate = new Date();
expiredDate.setFullYear(expiredDate.getFullYear() - 1);
const apiKey = { id: "1", isActive: true, expiresAt: expiredDate };
(mockDb.select as jest.Mock).mockReturnValue({
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([apiKey]),
});
const result = await service.validateKey(key);
expect(result).toBeNull();
});
});
});

View 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",
});
});
});
});

View 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);
});
});
});

View File

@@ -0,0 +1,122 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { categories } from "../database/schemas";
import { CategoriesService } from "./categories.service";
import { CreateCategoryDto } from "./dto/create-category.dto";
import { UpdateCategoryDto } from "./dto/update-category.dto";
describe("CategoriesService", () => {
let service: CategoriesService;
const mockDb = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([]),
orderBy: jest.fn().mockResolvedValue([]),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
returning: jest.fn().mockResolvedValue([]),
};
beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
CategoriesService,
{
provide: DatabaseService,
useValue: {
db: mockDb,
},
},
],
}).compile();
service = module.get<CategoriesService>(CategoriesService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("findAll", () => {
it("should return all categories ordered by name", async () => {
const mockCategories = [{ name: "A" }, { name: "B" }];
mockDb.orderBy.mockResolvedValue(mockCategories);
const result = await service.findAll();
expect(result).toEqual(mockCategories);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalledWith(categories);
});
});
describe("findOne", () => {
it("should return a category by id", async () => {
const mockCategory = { id: "1", name: "Cat" };
mockDb.limit.mockResolvedValue([mockCategory]);
const result = await service.findOne("1");
expect(result).toEqual(mockCategory);
});
it("should return null if category not found", async () => {
mockDb.limit.mockResolvedValue([]);
const result = await service.findOne("999");
expect(result).toBeNull();
});
});
describe("create", () => {
it("should create a category and generate slug", async () => {
const dto: CreateCategoryDto = { name: "Test Category" };
mockDb.returning.mockResolvedValue([{ ...dto, slug: "test-category" }]);
const result = await service.create(dto);
expect(mockDb.insert).toHaveBeenCalledWith(categories);
expect(mockDb.values).toHaveBeenCalledWith({
name: "Test Category",
slug: "test-category",
});
expect(result[0].slug).toBe("test-category");
});
});
describe("update", () => {
it("should update a category and regenerate slug", async () => {
const id = "1";
const dto: UpdateCategoryDto = { name: "New Name" };
mockDb.returning.mockResolvedValue([{ id, ...dto, slug: "new-name" }]);
const result = await service.update(id, dto);
expect(mockDb.update).toHaveBeenCalledWith(categories);
expect(mockDb.set).toHaveBeenCalledWith(
expect.objectContaining({
name: "New Name",
slug: "new-name",
}),
);
expect(result[0].slug).toBe("new-name");
});
});
describe("remove", () => {
it("should remove a category", async () => {
const id = "1";
mockDb.returning.mockResolvedValue([{ id }]);
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalledWith(categories);
expect(result).toEqual([{ id }]);
});
});
});

View File

@@ -0,0 +1,67 @@
import { Logger } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../../database/database.service";
import { PurgeService } from "./purge.service";
describe("PurgeService", () => {
let service: PurgeService;
const mockDb = {
delete: jest.fn(),
where: jest.fn(),
returning: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
jest.spyOn(Logger.prototype, "error").mockImplementation(() => {});
jest.spyOn(Logger.prototype, "log").mockImplementation(() => {});
const chain = {
delete: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
returning: jest.fn().mockResolvedValue([]),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
mock.mockImplementation(mockImplementation);
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
PurgeService,
{ provide: DatabaseService, useValue: { db: mockDb } },
],
}).compile();
service = module.get<PurgeService>(PurgeService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("purgeExpiredData", () => {
it("should purge data", async () => {
mockDb.returning
.mockResolvedValueOnce([{ id: "s1" }]) // sessions
.mockResolvedValueOnce([{ id: "r1" }]) // reports
.mockResolvedValueOnce([{ id: "u1" }]) // users
.mockResolvedValueOnce([{ id: "c1" }]); // contents
await service.purgeExpiredData();
expect(mockDb.delete).toHaveBeenCalledTimes(4);
expect(mockDb.returning).toHaveBeenCalledTimes(4);
});
it("should handle errors", async () => {
mockDb.delete.mockImplementation(() => {
throw new Error("Db error");
});
await expect(service.purgeExpiredData()).resolves.not.toThrow();
});
});
});

View File

@@ -0,0 +1,174 @@
jest.mock("uuid", () => ({
v4: jest.fn(() => "mocked-uuid"),
}));
import { BadRequestException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { MediaService } from "../media/media.service";
import { S3Service } from "../s3/s3.service";
import { ContentsService } from "./contents.service";
describe("ContentsService", () => {
let service: ContentsService;
let s3Service: S3Service;
let mediaService: MediaService;
const mockDb = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
returning: jest.fn().mockResolvedValue([]),
onConflictDoNothing: jest.fn().mockReturnThis(),
transaction: jest.fn().mockImplementation((cb) => cb(mockDb)),
execute: jest.fn().mockResolvedValue([]),
};
const mockS3Service = {
getUploadUrl: jest.fn(),
uploadFile: jest.fn(),
};
const mockMediaService = {
scanFile: jest.fn(),
processImage: jest.fn(),
processVideo: jest.fn(),
};
const mockConfigService = {
get: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
onConflictDoNothing: jest.fn().mockReturnThis(),
};
const mockImplementation = () => {
return Object.assign(Promise.resolve([]), chain);
};
for (const mock of Object.values(chain)) {
if (mock.mockReturnValue) {
mock.mockImplementation(mockImplementation);
}
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
ContentsService,
{ provide: DatabaseService, useValue: { db: mockDb } },
{ provide: S3Service, useValue: mockS3Service },
{ provide: MediaService, useValue: mockMediaService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();
service = module.get<ContentsService>(ContentsService);
s3Service = module.get<S3Service>(S3Service);
mediaService = module.get<MediaService>(MediaService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("getUploadUrl", () => {
it("should return an upload URL", async () => {
mockS3Service.getUploadUrl.mockResolvedValue("http://s3/url");
const result = await service.getUploadUrl("user1", "test.png");
expect(result).toHaveProperty("url", "http://s3/url");
expect(result).toHaveProperty("key");
expect(result.key).toContain("uploads/user1/");
});
});
describe("uploadAndProcess", () => {
const file = {
buffer: Buffer.from("test"),
originalname: "test.png",
mimetype: "image/png",
size: 1000,
} as Express.Multer.File;
it("should upload and process an image", async () => {
mockConfigService.get.mockReturnValue(1024); // max size
mockMediaService.scanFile.mockResolvedValue({ isInfected: false });
mockMediaService.processImage.mockResolvedValue({
buffer: Buffer.from("processed"),
extension: "webp",
mimeType: "image/webp",
size: 500,
});
mockDb.returning.mockResolvedValue([{ id: "content-id" }]);
const result = await service.uploadAndProcess("user1", file, {
title: "Meme",
type: "meme",
});
expect(mediaService.scanFile).toHaveBeenCalled();
expect(mediaService.processImage).toHaveBeenCalled();
expect(s3Service.uploadFile).toHaveBeenCalled();
expect(result).toEqual({ id: "content-id" });
});
it("should throw if file is infected", async () => {
mockConfigService.get.mockReturnValue(1024);
mockMediaService.scanFile.mockResolvedValue({
isInfected: true,
virusName: "Eicar",
});
await expect(
service.uploadAndProcess("user1", file, { title: "X", type: "meme" }),
).rejects.toThrow(BadRequestException);
});
});
describe("findAll", () => {
it("should return contents and total count", async () => {
mockDb.where.mockResolvedValueOnce([{ count: 10 }]); // for count
mockDb.offset.mockResolvedValueOnce([{ id: "1" }]); // for data
const result = await service.findAll({ limit: 10, offset: 0 });
expect(result.totalCount).toBe(10);
expect(result.data).toHaveLength(1);
});
});
describe("incrementViews", () => {
it("should increment views", async () => {
mockDb.returning.mockResolvedValue([{ id: "1", views: 1 }]);
const result = await service.incrementViews("1");
expect(mockDb.update).toHaveBeenCalled();
expect(result[0].views).toBe(1);
});
});
});

View File

@@ -0,0 +1,100 @@
import { ConflictException, NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { FavoritesService } from "./favorites.service";
describe("FavoritesService", () => {
let service: FavoritesService;
const mockDb = {
select: jest.fn(),
from: jest.fn(),
where: jest.fn(),
limit: jest.fn(),
offset: jest.fn(),
innerJoin: jest.fn(),
insert: jest.fn(),
values: jest.fn(),
delete: jest.fn(),
returning: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
mock.mockImplementation(mockImplementation);
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
FavoritesService,
{ provide: DatabaseService, useValue: { db: mockDb } },
],
}).compile();
service = module.get<FavoritesService>(FavoritesService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("addFavorite", () => {
it("should add a favorite", async () => {
mockDb.limit.mockResolvedValue([{ id: "content1" }]);
mockDb.returning.mockResolvedValue([
{ userId: "u1", contentId: "content1" },
]);
const result = await service.addFavorite("u1", "content1");
expect(result).toEqual([{ userId: "u1", contentId: "content1" }]);
});
it("should throw NotFoundException if content does not exist", async () => {
mockDb.limit.mockResolvedValue([]);
await expect(service.addFavorite("u1", "invalid")).rejects.toThrow(
NotFoundException,
);
});
it("should throw ConflictException on duplicate favorite", async () => {
mockDb.limit.mockResolvedValue([{ id: "content1" }]);
mockDb.returning.mockRejectedValue(new Error("Duplicate"));
await expect(service.addFavorite("u1", "content1")).rejects.toThrow(
ConflictException,
);
});
});
describe("removeFavorite", () => {
it("should remove a favorite", async () => {
mockDb.returning.mockResolvedValue([{ userId: "u1", contentId: "c1" }]);
const result = await service.removeFavorite("u1", "c1");
expect(result).toEqual({ userId: "u1", contentId: "c1" });
});
it("should throw NotFoundException if favorite not found", async () => {
mockDb.returning.mockResolvedValue([]);
await expect(service.removeFavorite("u1", "c1")).rejects.toThrow(
NotFoundException,
);
});
});
});

View File

@@ -0,0 +1,42 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "./database/database.service";
import { HealthController } from "./health.controller";
describe("HealthController", () => {
let controller: HealthController;
const mockDb = {
execute: jest.fn().mockResolvedValue([]),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
providers: [
{
provide: DatabaseService,
useValue: {
db: mockDb,
},
},
],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it("should return ok if database is connected", async () => {
mockDb.execute.mockResolvedValue([]);
const result = await controller.check();
expect(result.status).toBe("ok");
expect(result.database).toBe("connected");
});
it("should return error if database is disconnected", async () => {
mockDb.execute.mockRejectedValue(new Error("DB Error"));
const result = await controller.check();
expect(result.status).toBe("error");
expect(result.database).toBe("disconnected");
expect(result.message).toBe("DB Error");
});
});

View File

@@ -0,0 +1,94 @@
import * as fs from "node:fs/promises";
import { BadRequestException, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import ffmpeg from "fluent-ffmpeg";
import sharp from "sharp";
import { MediaService } from "./media.service";
jest.mock("sharp");
jest.mock("fluent-ffmpeg");
jest.mock("node:fs/promises");
jest.mock("uuid", () => ({ v4: () => "mock-uuid" }));
describe("MediaService", () => {
let service: MediaService;
const mockSharp = {
metadata: jest.fn().mockResolvedValue({ width: 100, height: 100 }),
webp: jest.fn().mockReturnThis(),
toBuffer: jest.fn().mockResolvedValue(Buffer.from("processed")),
};
beforeEach(async () => {
jest.clearAllMocks();
jest.spyOn(Logger.prototype, "error").mockImplementation(() => {});
jest.spyOn(Logger.prototype, "warn").mockImplementation(() => {});
(sharp as unknown as jest.Mock).mockReturnValue(mockSharp);
const module: TestingModule = await Test.createTestingModule({
providers: [
MediaService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key) => {
if (key === "CLAMAV_HOST") return "localhost";
if (key === "CLAMAV_PORT") return 3310;
}),
},
},
],
}).compile();
service = module.get<MediaService>(MediaService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("processImage", () => {
it("should process an image to webp", async () => {
const result = await service.processImage(Buffer.from("input"), "webp");
expect(result.extension).toBe("webp");
expect(result.buffer).toEqual(Buffer.from("processed"));
expect(mockSharp.webp).toHaveBeenCalled();
});
it("should throw BadRequestException on error", async () => {
mockSharp.toBuffer.mockRejectedValue(new Error("Sharp error"));
await expect(service.processImage(Buffer.from("input"))).rejects.toThrow(
BadRequestException,
);
});
});
describe("processVideo", () => {
it("should process a video", async () => {
const mockFfmpegCommand = {
toFormat: jest.fn().mockReturnThis(),
videoCodec: jest.fn().mockReturnThis(),
audioCodec: jest.fn().mockReturnThis(),
outputOptions: jest.fn().mockReturnThis(),
on: jest.fn().mockImplementation(function (event, cb) {
if (event === "end") setTimeout(cb, 0);
return this;
}),
save: jest.fn().mockReturnThis(),
};
(ffmpeg as unknown as jest.Mock).mockReturnValue(mockFfmpegCommand);
(fs.readFile as jest.Mock).mockResolvedValue(Buffer.from("processed-video"));
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
(fs.unlink as jest.Mock).mockResolvedValue(undefined);
const result = await service.processVideo(
Buffer.from("video-input"),
"webm",
);
expect(result.extension).toBe("webm");
expect(result.buffer).toEqual(Buffer.from("processed-video"));
});
});
});

View File

@@ -0,0 +1,88 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { CreateReportDto } from "./dto/create-report.dto";
import { ReportsService } from "./reports.service";
describe("ReportsService", () => {
let service: ReportsService;
const mockDb = {
insert: jest.fn(),
values: jest.fn(),
returning: jest.fn(),
select: jest.fn(),
from: jest.fn(),
orderBy: jest.fn(),
limit: jest.fn(),
offset: jest.fn(),
update: jest.fn(),
set: jest.fn(),
where: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
mock.mockImplementation(mockImplementation);
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
ReportsService,
{ provide: DatabaseService, useValue: { db: mockDb } },
],
}).compile();
service = module.get<ReportsService>(ReportsService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create a report", async () => {
const reporterId = "u1";
const data: CreateReportDto = { contentId: "c1", reason: "spam" };
mockDb.returning.mockResolvedValue([{ id: "r1", ...data, reporterId }]);
const result = await service.create(reporterId, data);
expect(result.id).toBe("r1");
expect(mockDb.insert).toHaveBeenCalled();
});
});
describe("findAll", () => {
it("should return reports", async () => {
mockDb.offset.mockResolvedValue([{ id: "r1" }]);
const result = await service.findAll(10, 0);
expect(result).toHaveLength(1);
});
});
describe("updateStatus", () => {
it("should update report status", async () => {
mockDb.returning.mockResolvedValue([{ id: "r1", status: "resolved" }]);
const result = await service.updateStatus("r1", "resolved");
expect(result[0].status).toBe("resolved");
});
});
});

View File

@@ -0,0 +1,124 @@
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 { UnauthorizedException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { CryptoService } from "../crypto/crypto.service";
import { DatabaseService } from "../database/database.service";
import { SessionsService } from "./sessions.service";
describe("SessionsService", () => {
let service: SessionsService;
const mockDb = {
insert: jest.fn(),
select: jest.fn(),
update: jest.fn(),
};
const mockCryptoService = {
generateJwt: jest.fn().mockResolvedValue("mock-jwt"),
hashIp: jest.fn().mockResolvedValue("mock-ip-hash"),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
then: jest.fn().mockImplementation(function (cb) {
return Promise.resolve(this).then(cb);
}),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
if (mock !== chain.then) {
mock.mockImplementation(mockImplementation);
}
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
SessionsService,
{ provide: DatabaseService, useValue: { db: mockDb } },
{ provide: CryptoService, useValue: mockCryptoService },
],
}).compile();
service = module.get<SessionsService>(SessionsService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("createSession", () => {
it("should create a session", async () => {
mockDb.returning.mockResolvedValue([{ id: "s1", refreshToken: "mock-jwt" }]);
const result = await service.createSession("u1", "agent", "1.2.3.4");
expect(result.id).toBe("s1");
expect(mockCryptoService.generateJwt).toHaveBeenCalledWith(
{ sub: "u1", type: "refresh" },
"7d",
);
});
});
describe("refreshSession", () => {
it("should refresh a valid session", async () => {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 1);
const session = { id: "s1", userId: "u1", expiresAt, isValid: true };
mockDb.where.mockImplementation(() => {
const chain = {
limit: jest.fn().mockReturnThis(),
returning: jest
.fn()
.mockResolvedValue([{ ...session, refreshToken: "new-jwt" }]),
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
then: jest.fn().mockResolvedValue(session),
};
return Object.assign(Promise.resolve([session]), chain);
});
const result = await service.refreshSession("old-jwt");
expect(result.refreshToken).toBe("new-jwt");
});
it("should throw UnauthorizedException if session not found", async () => {
mockDb.where.mockImplementation(() => {
const chain = {
limit: jest.fn().mockReturnThis(),
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
then: jest.fn().mockResolvedValue(null),
};
return Object.assign(Promise.resolve([]), chain);
});
await expect(service.refreshSession("invalid")).rejects.toThrow(
UnauthorizedException,
);
});
});
});

View File

@@ -0,0 +1,76 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { TagsService } from "./tags.service";
describe("TagsService", () => {
let service: TagsService;
const mockDb = {
select: jest.fn(),
from: jest.fn(),
leftJoin: jest.fn(),
where: jest.fn(),
groupBy: jest.fn(),
orderBy: jest.fn(),
limit: jest.fn(),
offset: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
leftJoin: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
mock.mockImplementation(mockImplementation);
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
TagsService,
{ provide: DatabaseService, useValue: { db: mockDb } },
],
}).compile();
service = module.get<TagsService>(TagsService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("findAll", () => {
it("should return tags", async () => {
const mockTags = [{ id: "1", name: "tag1" }];
mockDb.offset.mockResolvedValue(mockTags);
const result = await service.findAll({ limit: 10, offset: 0 });
expect(result).toEqual(mockTags);
expect(mockDb.select).toHaveBeenCalled();
});
it("should handle popular sort", async () => {
mockDb.offset.mockResolvedValue([{ id: "1", name: "tag1", count: 5 }]);
const result = await service.findAll({
limit: 10,
offset: 0,
sortBy: "popular",
});
expect(result[0].count).toBe(5);
expect(mockDb.leftJoin).toHaveBeenCalled();
expect(mockDb.groupBy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,114 @@
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 { Test, TestingModule } from "@nestjs/testing";
import { CryptoService } from "../crypto/crypto.service";
import { DatabaseService } from "../database/database.service";
import { UsersService } from "./users.service";
describe("UsersService", () => {
let service: UsersService;
const mockDb = {
insert: jest.fn(),
select: jest.fn(),
update: jest.fn(),
transaction: jest.fn().mockImplementation((cb) => cb(mockDb)),
};
const mockCryptoService = {
getPgpEncryptionKey: jest.fn().mockReturnValue("mock-pgp-key"),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
then: jest.fn().mockImplementation(function (cb) {
return Promise.resolve(this).then(cb);
}),
};
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
for (const mock of Object.values(chain)) {
if (mock !== chain.then) {
mock.mockImplementation(mockImplementation);
}
}
Object.assign(mockDb, chain);
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: DatabaseService, useValue: { db: mockDb } },
{ provide: CryptoService, useValue: mockCryptoService },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create a user", async () => {
const data = {
username: "u1",
email: "e1",
passwordHash: "p1",
emailHash: "eh1",
};
mockDb.returning.mockResolvedValue([{ uuid: "uuid1", ...data }]);
const result = await service.create(data);
expect(result.uuid).toBe("uuid1");
expect(mockDb.insert).toHaveBeenCalled();
});
});
describe("findOne", () => {
it("should find a user", async () => {
mockDb.limit.mockResolvedValue([{ uuid: "uuid1" }]);
const result = await service.findOne("uuid1");
expect(result.uuid).toBe("uuid1");
});
it("should return null if not found", async () => {
mockDb.limit.mockResolvedValue([]);
const result = await service.findOne("uuid1");
expect(result).toBeNull();
});
});
describe("update", () => {
it("should update a user", async () => {
mockDb.returning.mockResolvedValue([{ uuid: "uuid1", displayName: "New" }]);
const result = await service.update("uuid1", { displayName: "New" });
expect(result[0].displayName).toBe("New");
});
});
});