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