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:
179
backend/src/api-keys/api-keys.service.spec.ts
Normal file
179
backend/src/api-keys/api-keys.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
122
backend/src/categories/categories.service.spec.ts
Normal file
122
backend/src/categories/categories.service.spec.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
67
backend/src/common/services/purge.service.spec.ts
Normal file
67
backend/src/common/services/purge.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
174
backend/src/contents/contents.service.spec.ts
Normal file
174
backend/src/contents/contents.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
100
backend/src/favorites/favorites.service.spec.ts
Normal file
100
backend/src/favorites/favorites.service.spec.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
42
backend/src/health.controller.spec.ts
Normal file
42
backend/src/health.controller.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
94
backend/src/media/media.service.spec.ts
Normal file
94
backend/src/media/media.service.spec.ts
Normal 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"));
|
||||
});
|
||||
});
|
||||
});
|
||||
88
backend/src/reports/reports.service.spec.ts
Normal file
88
backend/src/reports/reports.service.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
124
backend/src/sessions/sessions.service.spec.ts
Normal file
124
backend/src/sessions/sessions.service.spec.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
76
backend/src/tags/tags.service.spec.ts
Normal file
76
backend/src/tags/tags.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
114
backend/src/users/users.service.spec.ts
Normal file
114
backend/src/users/users.service.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user