test: add unit tests for messaging, comments, events, and user services

- Added comprehensive unit tests for `MessagesService`, `CommentsService`, `EventsGateway`, and enhancements in `UsersService`.
- Ensured proper mocking and test coverage for newly introduced dependencies like `EventsGateway` and `RBACService`.
This commit is contained in:
Mathis HERRIOT
2026-01-29 15:06:12 +01:00
parent 1be8571f26
commit 7b76942795
5 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import { ForbiddenException, NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { CommentsService } from "./comments.service";
import { CommentsRepository } from "./repositories/comments.repository";
describe("CommentsService", () => {
let service: CommentsService;
let repository: CommentsRepository;
const mockCommentsRepository = {
create: jest.fn(),
findAllByContentId: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommentsService,
{ provide: CommentsRepository, useValue: mockCommentsRepository },
],
}).compile();
service = module.get<CommentsService>(CommentsService);
repository = module.get<CommentsRepository>(CommentsRepository);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create a comment", async () => {
const userId = "user1";
const contentId = "content1";
const dto = { text: "Nice meme" };
mockCommentsRepository.create.mockResolvedValue({ id: "c1", ...dto });
const result = await service.create(userId, contentId, dto);
expect(result.id).toBe("c1");
expect(repository.create).toHaveBeenCalledWith({
userId,
contentId,
text: dto.text,
});
});
});
describe("findAllByContentId", () => {
it("should return comments for a content", async () => {
mockCommentsRepository.findAllByContentId.mockResolvedValue([{ id: "c1" }]);
const result = await service.findAllByContentId("content1");
expect(result).toHaveLength(1);
expect(repository.findAllByContentId).toHaveBeenCalledWith("content1");
});
});
describe("remove", () => {
it("should remove comment if owner", async () => {
mockCommentsRepository.findOne.mockResolvedValue({ userId: "u1" });
await service.remove("u1", "c1");
expect(repository.delete).toHaveBeenCalledWith("c1");
});
it("should remove comment if admin", async () => {
mockCommentsRepository.findOne.mockResolvedValue({ userId: "u1" });
await service.remove("other", "c1", true);
expect(repository.delete).toHaveBeenCalledWith("c1");
});
it("should throw NotFoundException if comment does not exist", async () => {
mockCommentsRepository.findOne.mockResolvedValue(null);
await expect(service.remove("u1", "c1")).rejects.toThrow(NotFoundException);
});
it("should throw ForbiddenException if not owner and not admin", async () => {
mockCommentsRepository.findOne.mockResolvedValue({ userId: "u1" });
await expect(service.remove("other", "c1")).rejects.toThrow(ForbiddenException);
});
});
});

View File

@@ -7,6 +7,7 @@ import { BadRequestException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from "@nestjs/testing";
import { MediaService } from "../media/media.service"; import { MediaService } from "../media/media.service";
import { EventsGateway } from "../realtime/events.gateway";
import { S3Service } from "../s3/s3.service"; import { S3Service } from "../s3/s3.service";
import { ContentsService } from "./contents.service"; import { ContentsService } from "./contents.service";
import { ContentsRepository } from "./repositories/contents.repository"; import { ContentsRepository } from "./repositories/contents.repository";
@@ -49,6 +50,10 @@ describe("ContentsService", () => {
del: jest.fn(), del: jest.fn(),
}; };
const mockEventsGateway = {
sendToUser: jest.fn(),
};
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -60,6 +65,7 @@ describe("ContentsService", () => {
{ provide: MediaService, useValue: mockMediaService }, { provide: MediaService, useValue: mockMediaService },
{ provide: ConfigService, useValue: mockConfigService }, { provide: ConfigService, useValue: mockConfigService },
{ provide: CACHE_MANAGER, useValue: mockCacheManager }, { provide: CACHE_MANAGER, useValue: mockCacheManager },
{ provide: EventsGateway, useValue: mockEventsGateway },
], ],
}).compile(); }).compile();

View File

@@ -0,0 +1,81 @@
import { ForbiddenException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { EventsGateway } from "../realtime/events.gateway";
import { MessagesService } from "./messages.service";
import { MessagesRepository } from "./repositories/messages.repository";
describe("MessagesService", () => {
let service: MessagesService;
let repository: MessagesRepository;
let eventsGateway: EventsGateway;
const mockMessagesRepository = {
findConversationBetweenUsers: jest.fn(),
createConversation: jest.fn(),
addParticipant: jest.fn(),
createMessage: jest.fn(),
findAllConversations: jest.fn(),
isParticipant: jest.fn(),
findMessagesByConversationId: jest.fn(),
};
const mockEventsGateway = {
sendToUser: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MessagesService,
{ provide: MessagesRepository, useValue: mockMessagesRepository },
{ provide: EventsGateway, useValue: mockEventsGateway },
],
}).compile();
service = module.get<MessagesService>(MessagesService);
repository = module.get<MessagesRepository>(MessagesRepository);
eventsGateway = module.get<EventsGateway>(EventsGateway);
});
describe("sendMessage", () => {
it("should send message to existing conversation", async () => {
const senderId = "s1";
const dto = { recipientId: "r1", text: "hello" };
mockMessagesRepository.findConversationBetweenUsers.mockResolvedValue({ id: "conv1" });
mockMessagesRepository.createMessage.mockResolvedValue({ id: "m1", text: "hello" });
const result = await service.sendMessage(senderId, dto);
expect(result.id).toBe("m1");
expect(mockEventsGateway.sendToUser).toHaveBeenCalledWith("r1", "new_message", expect.anything());
});
it("should create new conversation if not exists", async () => {
const senderId = "s1";
const dto = { recipientId: "r1", text: "hello" };
mockMessagesRepository.findConversationBetweenUsers.mockResolvedValue(null);
mockMessagesRepository.createConversation.mockResolvedValue({ id: "new_conv" });
mockMessagesRepository.createMessage.mockResolvedValue({ id: "m1" });
await service.sendMessage(senderId, dto);
expect(mockMessagesRepository.createConversation).toHaveBeenCalled();
expect(mockMessagesRepository.addParticipant).toHaveBeenCalledTimes(2);
});
});
describe("getMessages", () => {
it("should return messages if user is participant", async () => {
mockMessagesRepository.isParticipant.mockResolvedValue(true);
mockMessagesRepository.findMessagesByConversationId.mockResolvedValue([{ id: "m1" }]);
const result = await service.getMessages("u1", "conv1");
expect(result).toHaveLength(1);
});
it("should throw ForbiddenException if user is not participant", async () => {
mockMessagesRepository.isParticipant.mockResolvedValue(false);
await expect(service.getMessages("u1", "conv1")).rejects.toThrow(ForbiddenException);
});
});
});

View File

@@ -0,0 +1,52 @@
import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import { Server } from "socket.io";
import { JwtService } from "../crypto/services/jwt.service";
import { EventsGateway } from "./events.gateway";
describe("EventsGateway", () => {
let gateway: EventsGateway;
let jwtService: JwtService;
const mockJwtService = {
verifyJwt: jest.fn(),
};
const mockConfigService = {
get: jest.fn().mockReturnValue("secret-password-32-chars-long-!!!"),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EventsGateway,
{ provide: JwtService, useValue: mockJwtService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();
gateway = module.get<EventsGateway>(EventsGateway);
jwtService = module.get<JwtService>(JwtService);
gateway.server = {
to: jest.fn().mockReturnThis(),
emit: jest.fn(),
} as any;
});
it("should be defined", () => {
expect(gateway).toBeDefined();
});
describe("sendToUser", () => {
it("should emit event to user room", () => {
const userId = "user123";
const event = "test_event";
const data = { foo: "bar" };
gateway.sendToUser(userId, event, data);
expect(gateway.server.to).toHaveBeenCalledWith(`user:${userId}`);
expect(gateway.server.to(`user:${userId}`).emit).toHaveBeenCalledWith(event, data);
});
});
});

View File

@@ -108,6 +108,7 @@ describe("UsersService", () => {
describe("findOne", () => { describe("findOne", () => {
it("should find a user", async () => { it("should find a user", async () => {
mockUsersRepository.findOne.mockResolvedValue({ uuid: "uuid1" }); mockUsersRepository.findOne.mockResolvedValue({ uuid: "uuid1" });
mockRbacService.getUserRoles.mockResolvedValue([]);
const result = await service.findOne("uuid1"); const result = await service.findOne("uuid1");
expect(result.uuid).toBe("uuid1"); expect(result.uuid).toBe("uuid1");
}); });
@@ -139,6 +140,7 @@ describe("UsersService", () => {
describe("findByEmailHash", () => { describe("findByEmailHash", () => {
it("should call repository.findByEmailHash", async () => { it("should call repository.findByEmailHash", async () => {
mockUsersRepository.findByEmailHash.mockResolvedValue({ uuid: "u1" }); mockUsersRepository.findByEmailHash.mockResolvedValue({ uuid: "u1" });
mockRbacService.getUserRoles.mockResolvedValue([]);
const result = await service.findByEmailHash("hash"); const result = await service.findByEmailHash("hash");
expect(result.uuid).toBe("u1"); expect(result.uuid).toBe("u1");
expect(mockUsersRepository.findByEmailHash).toHaveBeenCalledWith("hash"); expect(mockUsersRepository.findByEmailHash).toHaveBeenCalledWith("hash");