import { ConfigService } from "@nestjs/config"; import { Test, TestingModule } from "@nestjs/testing"; import * as Minio from "minio"; import { S3Service } from "./s3.service"; jest.mock("minio"); describe("S3Service", () => { let service: S3Service; let configService: ConfigService; let minioClient: any; beforeEach(async () => { minioClient = { bucketExists: jest.fn().mockResolvedValue(true), makeBucket: jest.fn().mockResolvedValue(undefined), putObject: jest.fn().mockResolvedValue(undefined), getObject: jest.fn().mockResolvedValue({}), presignedUrl: jest.fn().mockResolvedValue("http://localhost/url"), removeObject: jest.fn().mockResolvedValue(undefined), statObject: jest.fn().mockResolvedValue({ size: 100, etag: "123" }), copyObject: jest.fn().mockResolvedValue(undefined), }; (Minio.Client as jest.Mock).mockImplementation(() => minioClient); const module: TestingModule = await Test.createTestingModule({ providers: [ S3Service, { provide: ConfigService, useValue: { get: jest.fn((key: string, defaultValue?: string) => { if (key === "S3_PORT") return "9000"; if (key === "S3_USE_SSL") return "false"; return defaultValue || key; }), }, }, ], }).compile(); service = module.get(S3Service); configService = module.get(ConfigService); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("onModuleInit", () => { it("should create bucket if it does not exist", async () => { minioClient.bucketExists.mockResolvedValue(false); await service.onModuleInit(); expect(minioClient.makeBucket).toHaveBeenCalledWith("memegoat"); }); it("should not create bucket if it exists", async () => { minioClient.bucketExists.mockResolvedValue(true); await service.onModuleInit(); expect(minioClient.makeBucket).not.toHaveBeenCalled(); }); }); describe("uploadFile", () => { it("should upload a file to default bucket", async () => { const fileName = "test.txt"; const file = Buffer.from("test content"); const mimeType = "text/plain"; const result = await service.uploadFile(fileName, file, mimeType); expect(minioClient.putObject).toHaveBeenCalledWith( "memegoat", fileName, file, file.length, { "Content-Type": mimeType }, ); expect(result).toBe(fileName); }); it("should upload a file to specific bucket", async () => { const fileName = "test.txt"; const file = Buffer.from("test content"); const mimeType = "text/plain"; const bucketName = "other-bucket"; await service.uploadFile(fileName, file, mimeType, {}, bucketName); expect(minioClient.putObject).toHaveBeenCalledWith( bucketName, fileName, file, file.length, { "Content-Type": mimeType }, ); }); }); describe("getFile", () => { it("should get a file from default bucket", async () => { const fileName = "test.txt"; await service.getFile(fileName); expect(minioClient.getObject).toHaveBeenCalledWith("memegoat", fileName); }); it("should get a file from specific bucket", async () => { const fileName = "test.txt"; const bucketName = "other-bucket"; await service.getFile(fileName, bucketName); expect(minioClient.getObject).toHaveBeenCalledWith(bucketName, fileName); }); }); describe("getFileUrl", () => { it("should get a presigned URL from default bucket", async () => { const fileName = "test.txt"; const url = await service.getFileUrl(fileName); expect(minioClient.presignedUrl).toHaveBeenCalledWith( "GET", "memegoat", fileName, 3600, ); expect(url).toBe("http://localhost/url"); }); it("should get a presigned URL from specific bucket", async () => { const fileName = "test.txt"; const bucketName = "other-bucket"; await service.getFileUrl(fileName, 3600, bucketName); expect(minioClient.presignedUrl).toHaveBeenCalledWith( "GET", bucketName, fileName, 3600, ); }); }); describe("deleteFile", () => { it("should delete a file from default bucket", async () => { const fileName = "test.txt"; await service.deleteFile(fileName); expect(minioClient.removeObject).toHaveBeenCalledWith("memegoat", fileName); }); it("should delete a file from specific bucket", async () => { const fileName = "test.txt"; const bucketName = "other-bucket"; await service.deleteFile(fileName, bucketName); expect(minioClient.removeObject).toHaveBeenCalledWith(bucketName, fileName); }); }); describe("ensureBucketExists", () => { it("should create bucket if it does not exist", async () => { minioClient.bucketExists.mockResolvedValue(false); await service.ensureBucketExists("new-bucket"); expect(minioClient.makeBucket).toHaveBeenCalledWith("new-bucket"); }); it("should not create bucket if it exists", async () => { minioClient.bucketExists.mockResolvedValue(true); await service.ensureBucketExists("existing-bucket"); expect(minioClient.makeBucket).not.toHaveBeenCalled(); }); }); describe("getFileInfo", () => { it("should return file info from default bucket", async () => { const fileName = "test.txt"; const info = await service.getFileInfo(fileName); expect(minioClient.statObject).toHaveBeenCalledWith("memegoat", fileName); expect(info).toEqual({ size: 100, etag: "123" }); }); it("should return file info from specific bucket", async () => { const fileName = "test.txt"; const bucketName = "other-bucket"; await service.getFileInfo(fileName, bucketName); expect(minioClient.statObject).toHaveBeenCalledWith(bucketName, fileName); }); }); describe("getPublicUrl", () => { it("should use API_URL if provided", () => { (configService.get as jest.Mock).mockImplementation((key: string) => { if (key === "API_URL") return "https://api.test.com"; return null; }); const url = service.getPublicUrl("test.webp"); expect(url).toBe("https://api.test.com/media?path=test.webp"); }); it("should use DOMAIN_NAME and PORT for localhost", () => { (configService.get as jest.Mock).mockImplementation( (key: string, def: unknown) => { if (key === "API_URL") return null; if (key === "DOMAIN_NAME") return "localhost"; if (key === "PORT") return 3000; return def; }, ); const url = service.getPublicUrl("test.webp"); expect(url).toBe("http://localhost:3000/media?path=test.webp"); }); it("should use api.DOMAIN_NAME for production", () => { (configService.get as jest.Mock).mockImplementation( (key: string, def: unknown) => { if (key === "API_URL") return null; if (key === "DOMAIN_NAME") return "memegoat.fr"; return def; }, ); const url = service.getPublicUrl("test.webp"); expect(url).toBe("https://api.memegoat.fr/media?path=test.webp"); }); }); });