diff --git a/backend/src/comments/comments.service.spec.ts b/backend/src/comments/comments.service.spec.ts new file mode 100644 index 0000000..e8781ba --- /dev/null +++ b/backend/src/comments/comments.service.spec.ts @@ -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); + repository = module.get(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); + }); + }); +}); diff --git a/backend/src/contents/contents.service.spec.ts b/backend/src/contents/contents.service.spec.ts index 7e1afb0..0059c42 100644 --- a/backend/src/contents/contents.service.spec.ts +++ b/backend/src/contents/contents.service.spec.ts @@ -7,6 +7,7 @@ import { BadRequestException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Test, TestingModule } from "@nestjs/testing"; import { MediaService } from "../media/media.service"; +import { EventsGateway } from "../realtime/events.gateway"; import { S3Service } from "../s3/s3.service"; import { ContentsService } from "./contents.service"; import { ContentsRepository } from "./repositories/contents.repository"; @@ -49,6 +50,10 @@ describe("ContentsService", () => { del: jest.fn(), }; + const mockEventsGateway = { + sendToUser: jest.fn(), + }; + beforeEach(async () => { jest.clearAllMocks(); @@ -60,6 +65,7 @@ describe("ContentsService", () => { { provide: MediaService, useValue: mockMediaService }, { provide: ConfigService, useValue: mockConfigService }, { provide: CACHE_MANAGER, useValue: mockCacheManager }, + { provide: EventsGateway, useValue: mockEventsGateway }, ], }).compile(); diff --git a/backend/src/messages/messages.service.spec.ts b/backend/src/messages/messages.service.spec.ts new file mode 100644 index 0000000..7ea9a00 --- /dev/null +++ b/backend/src/messages/messages.service.spec.ts @@ -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); + repository = module.get(MessagesRepository); + eventsGateway = module.get(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); + }); + }); +}); diff --git a/backend/src/realtime/events.gateway.spec.ts b/backend/src/realtime/events.gateway.spec.ts new file mode 100644 index 0000000..32e5bdb --- /dev/null +++ b/backend/src/realtime/events.gateway.spec.ts @@ -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); + jwtService = module.get(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); + }); + }); +}); diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts index 3c9b134..fdf2461 100644 --- a/backend/src/users/users.service.spec.ts +++ b/backend/src/users/users.service.spec.ts @@ -108,6 +108,7 @@ describe("UsersService", () => { describe("findOne", () => { it("should find a user", async () => { mockUsersRepository.findOne.mockResolvedValue({ uuid: "uuid1" }); + mockRbacService.getUserRoles.mockResolvedValue([]); const result = await service.findOne("uuid1"); expect(result.uuid).toBe("uuid1"); }); @@ -139,6 +140,7 @@ describe("UsersService", () => { describe("findByEmailHash", () => { it("should call repository.findByEmailHash", async () => { mockUsersRepository.findByEmailHash.mockResolvedValue({ uuid: "u1" }); + mockRbacService.getUserRoles.mockResolvedValue([]); const result = await service.findByEmailHash("hash"); expect(result.uuid).toBe("u1"); expect(mockUsersRepository.findByEmailHash).toHaveBeenCalledWith("hash");