diff --git a/backend/src/contents/contents.controller.spec.ts b/backend/src/contents/contents.controller.spec.ts new file mode 100644 index 0000000..a4399c3 --- /dev/null +++ b/backend/src/contents/contents.controller.spec.ts @@ -0,0 +1,230 @@ +jest.mock("uuid", () => ({ + v4: jest.fn(() => "mocked-uuid"), +})); + +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().mockReturnValue({ + setProtectedHeader: jest.fn().mockReturnThis(), + setIssuedAt: jest.fn().mockReturnThis(), + setExpirationTime: jest.fn().mockReturnThis(), + sign: jest.fn().mockResolvedValue("mocked-jwt"), + }), + jwtVerify: jest.fn(), +})); + +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard"; +import { RolesGuard } from "../auth/guards/roles.guard"; +import { AuthenticatedRequest } from "../common/interfaces/request.interface"; +import { ContentsController } from "./contents.controller"; +import { ContentsService } from "./contents.service"; + +describe("ContentsController", () => { + let controller: ContentsController; + let service: ContentsService; + + const mockContentsService = { + create: jest.fn(), + getUploadUrl: jest.fn(), + uploadAndProcess: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + incrementViews: jest.fn(), + incrementUsage: jest.fn(), + remove: jest.fn(), + removeAdmin: jest.fn(), + generateBotHtml: jest.fn(), + }; + + const mockCacheManager = { + get: jest.fn(), + set: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ContentsController], + providers: [ + { provide: ContentsService, useValue: mockContentsService }, + { provide: CACHE_MANAGER, useValue: mockCacheManager }, + ], + }) + .overrideGuard(AuthGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(RolesGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(OptionalAuthGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(ContentsController); + service = module.get(ContentsService); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); + + describe("create", () => { + it("should call service.create", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + const dto = { title: "Title", type: "image" as any }; + await controller.create(req, dto as any); + expect(service.create).toHaveBeenCalledWith("user-uuid", dto); + }); + }); + + describe("getUploadUrl", () => { + it("should call service.getUploadUrl", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.getUploadUrl(req, "test.jpg"); + expect(service.getUploadUrl).toHaveBeenCalledWith("user-uuid", "test.jpg"); + }); + }); + + describe("upload", () => { + it("should call service.uploadAndProcess", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + const file = {} as Express.Multer.File; + const dto = { title: "Title" }; + await controller.upload(req, file, dto as any); + expect(service.uploadAndProcess).toHaveBeenCalledWith( + "user-uuid", + file, + dto, + ); + }); + }); + + describe("explore", () => { + it("should call service.findAll", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.explore( + req, + 10, + 0, + "trend", + "tag", + "cat", + "auth", + "query", + false, + undefined, + ); + expect(service.findAll).toHaveBeenCalledWith({ + limit: 10, + offset: 0, + sortBy: "trend", + tag: "tag", + category: "cat", + author: "auth", + query: "query", + favoritesOnly: false, + userId: "user-uuid", + }); + }); + }); + + describe("trends", () => { + it("should call service.findAll with trend sort", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.trends(req, 10, 0); + expect(service.findAll).toHaveBeenCalledWith({ + limit: 10, + offset: 0, + sortBy: "trend", + userId: "user-uuid", + }); + }); + }); + + describe("recent", () => { + it("should call service.findAll with recent sort", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.recent(req, 10, 0); + expect(service.findAll).toHaveBeenCalledWith({ + limit: 10, + offset: 0, + sortBy: "recent", + userId: "user-uuid", + }); + }); + }); + + describe("findOne", () => { + it("should return json for normal user", async () => { + const req = { user: { sub: "user-uuid" }, headers: {} } as any; + const res = { json: jest.fn(), send: jest.fn() } as any; + const content = { id: "1" }; + mockContentsService.findOne.mockResolvedValue(content); + + await controller.findOne("1", req, res); + + expect(res.json).toHaveBeenCalledWith(content); + }); + + it("should return html for bot", async () => { + const req = { + user: { sub: "user-uuid" }, + headers: { "user-agent": "Googlebot" }, + } as any; + const res = { json: jest.fn(), send: jest.fn() } as any; + const content = { id: "1" }; + mockContentsService.findOne.mockResolvedValue(content); + mockContentsService.generateBotHtml.mockReturnValue(""); + + await controller.findOne("1", req, res); + + expect(res.send).toHaveBeenCalledWith(""); + }); + + it("should throw NotFoundException if not found", async () => { + const req = { user: { sub: "user-uuid" }, headers: {} } as any; + const res = { json: jest.fn(), send: jest.fn() } as any; + mockContentsService.findOne.mockResolvedValue(null); + + await expect(controller.findOne("1", req, res)).rejects.toThrow( + "Contenu non trouvé", + ); + }); + }); + + describe("incrementViews", () => { + it("should call service.incrementViews", async () => { + await controller.incrementViews("1"); + expect(service.incrementViews).toHaveBeenCalledWith("1"); + }); + }); + + describe("incrementUsage", () => { + it("should call service.incrementUsage", async () => { + await controller.incrementUsage("1"); + expect(service.incrementUsage).toHaveBeenCalledWith("1"); + }); + }); + + describe("remove", () => { + it("should call service.remove", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.remove("1", req); + expect(service.remove).toHaveBeenCalledWith("1", "user-uuid"); + }); + }); + + describe("removeAdmin", () => { + it("should call service.removeAdmin", async () => { + await controller.removeAdmin("1"); + expect(service.removeAdmin).toHaveBeenCalledWith("1"); + }); + }); +}); diff --git a/backend/src/contents/contents.service.spec.ts b/backend/src/contents/contents.service.spec.ts index 75307bb..7e1afb0 100644 --- a/backend/src/contents/contents.service.spec.ts +++ b/backend/src/contents/contents.service.spec.ts @@ -23,6 +23,7 @@ describe("ContentsService", () => { incrementViews: jest.fn(), incrementUsage: jest.fn(), softDelete: jest.fn(), + softDeleteAdmin: jest.fn(), findOne: jest.fn(), findBySlug: jest.fn(), }; @@ -147,4 +148,81 @@ describe("ContentsService", () => { expect(result[0].views).toBe(1); }); }); + + describe("incrementUsage", () => { + it("should increment usage", async () => { + mockContentsRepository.incrementUsage.mockResolvedValue([ + { id: "1", usageCount: 1 }, + ]); + await service.incrementUsage("1"); + expect(mockContentsRepository.incrementUsage).toHaveBeenCalledWith("1"); + }); + }); + + describe("remove", () => { + it("should soft delete content", async () => { + mockContentsRepository.softDelete.mockResolvedValue({ id: "1" }); + await service.remove("1", "u1"); + expect(mockContentsRepository.softDelete).toHaveBeenCalledWith("1", "u1"); + }); + }); + + describe("removeAdmin", () => { + it("should soft delete content without checking owner", async () => { + mockContentsRepository.softDeleteAdmin.mockResolvedValue({ id: "1" }); + await service.removeAdmin("1"); + expect(mockContentsRepository.softDeleteAdmin).toHaveBeenCalledWith("1"); + }); + }); + + describe("findOne", () => { + it("should return content by id", async () => { + mockContentsRepository.findOne.mockResolvedValue({ + id: "1", + storageKey: "k", + author: { avatarUrl: "a" }, + }); + mockS3Service.getPublicUrl.mockReturnValue("url"); + const result = await service.findOne("1"); + expect(result.id).toBe("1"); + expect(result.url).toBe("url"); + }); + + it("should return content by slug", async () => { + mockContentsRepository.findOne.mockResolvedValue({ + id: "1", + slug: "s", + storageKey: "k", + }); + const result = await service.findOne("s"); + expect(result.slug).toBe("s"); + }); + }); + + describe("generateBotHtml", () => { + it("should generate html with og tags", () => { + const content = { title: "Title", storageKey: "k" }; + mockS3Service.getPublicUrl.mockReturnValue("url"); + const html = service.generateBotHtml(content as any); + expect(html).toContain("Title"); + expect(html).toContain('content="Title"'); + expect(html).toContain('content="url"'); + }); + }); + + describe("ensureUniqueSlug", () => { + it("should return original slug if unique", async () => { + mockContentsRepository.findBySlug.mockResolvedValue(null); + const slug = (service as any).ensureUniqueSlug("My Title"); + await expect(slug).resolves.toBe("my-title"); + }); + + it("should append counter if not unique", async () => { + mockContentsRepository.findBySlug + .mockResolvedValueOnce({ id: "1" }) + .mockResolvedValueOnce(null); + const slug = await (service as any).ensureUniqueSlug("My Title"); + expect(slug).toBe("my-title-1"); + }); + }); });