From a2c5403d399d90c2558cd5672b2d4a69d241ac9d Mon Sep 17 00:00:00 2001 From: StarAppeal Date: Fri, 26 Sep 2025 05:26:06 +0200 Subject: [PATCH] add unit tests for FileService methods and refactor file model to remove userId index --- src/db/models/file.ts | 1 - src/rest/restStorage.ts | 1 - tests/services/db/fileService.test.ts | 210 ++++++++++++++++++++++++++ tests/services/s3Service.test.ts | 180 ++++++++-------------- 4 files changed, 275 insertions(+), 117 deletions(-) create mode 100644 tests/services/db/fileService.test.ts diff --git a/src/db/models/file.ts b/src/db/models/file.ts index baf94af..145b651 100644 --- a/src/db/models/file.ts +++ b/src/db/models/file.ts @@ -15,7 +15,6 @@ const fileSchema = new mongoose.Schema( userId: { type: String, required: true, - index: true, }, objectKey: { type: String, diff --git a/src/rest/restStorage.ts b/src/rest/restStorage.ts index a2e9821..1e4f0c3 100644 --- a/src/rest/restStorage.ts +++ b/src/rest/restStorage.ts @@ -77,7 +77,6 @@ export class RestStorage { const userId = req.payload.uuid; const objectKey = req.params[0]; - console.log(objectKey); if (!objectKey.startsWith(`user-${userId}/`)) { return forbidden(res); } diff --git a/tests/services/db/fileService.test.ts b/tests/services/db/fileService.test.ts new file mode 100644 index 0000000..131049f --- /dev/null +++ b/tests/services/db/fileService.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { FileModel } from "../../../src/db/models/file"; +import { FileService } from "../../../src/services/db/fileService"; + +vi.mock("../../../src/db/models/file"); + +const mockedFileModel = vi.mocked(FileModel); + +describe("FileService", () => { + let fileService: FileService; + + beforeEach(() => { + vi.clearAllMocks(); + (FileService as any).instance = undefined; + fileService = FileService.getInstance(); + }); + + describe("getInstance (singleton)", () => { + it("should create a singleton instance", () => { + const instance1 = fileService; + const instance2 = FileService.getInstance(); + + expect(instance1).toBe(instance2); + }); + }); + + describe("createFileRecord", () => { + it("should create a file record and return it", async () => { + const mockDate = new Date(); + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + + const mockFile = { + userId: "user123", + objectKey: "object-key-123", + originalName: "test-file.txt", + mimeType: "text/plain", + size: 1024, + uploadedAt: mockDate, + save: vi.fn().mockResolvedValue({ + userId: "user123", + objectKey: "object-key-123", + originalName: "test-file.txt", + mimeType: "text/plain", + size: 1024, + uploadedAt: mockDate, + }), + }; + + mockedFileModel.mockImplementation(() => mockFile as any); + + const result = await fileService.createFileRecord( + "user123", + "object-key-123", + "test-file.txt", + "text/plain", + 1024 + ); + + expect(mockedFileModel).toHaveBeenCalledWith({ + userId: "user123", + objectKey: "object-key-123", + originalName: "test-file.txt", + mimeType: "text/plain", + size: 1024, + uploadedAt: mockDate, + }); + + expect(mockFile.save).toHaveBeenCalled(); + expect(result).toEqual({ + userId: "user123", + objectKey: "object-key-123", + originalName: "test-file.txt", + mimeType: "text/plain", + size: 1024, + uploadedAt: mockDate, + }); + + vi.useRealTimers(); + }); + }); + + describe("getFilesByUserId", () => { + it("should return files for a given userId", async () => { + const mockFiles = [ + { objectKey: "object1", originalName: "file1.txt" }, + { objectKey: "object2", originalName: "file2.txt" }, + ]; + + const mockExec = vi.fn().mockResolvedValue(mockFiles); + const mockSort = vi.fn().mockReturnValue({ exec: mockExec }); + mockedFileModel.find.mockReturnValue({ sort: mockSort } as any); + + const result = await fileService.getFilesByUserId("user123"); + + expect(mockedFileModel.find).toHaveBeenCalledWith({ userId: "user123" }); + expect(mockSort).toHaveBeenCalledWith({ uploadedAt: -1 }); + expect(result).toEqual(mockFiles); + }); + }); + + describe("getFileByObjectKey", () => { + it("should return a file for a given objectKey", async () => { + const mockFile = { objectKey: "object-key-123", originalName: "file1.txt" }; + + const mockExec = vi.fn().mockResolvedValue(mockFile); + mockedFileModel.findOne.mockReturnValue({ exec: mockExec } as any); + + const result = await fileService.getFileByObjectKey("object-key-123"); + + expect(mockedFileModel.findOne).toHaveBeenCalledWith({ objectKey: "object-key-123" }); + expect(result).toEqual(mockFile); + }); + + it("should return null if file not found", async () => { + const mockExec = vi.fn().mockResolvedValue(null); + mockedFileModel.findOne.mockReturnValue({ exec: mockExec } as any); + + const result = await fileService.getFileByObjectKey("non-existent-key"); + + expect(result).toBeNull(); + }); + }); + + describe("deleteFileRecord", () => { + it("should delete a file record and return true on success", async () => { + mockedFileModel.deleteOne.mockResolvedValue({ deletedCount: 1 } as any); + + const result = await fileService.deleteFileRecord("object-key-123"); + + expect(mockedFileModel.deleteOne).toHaveBeenCalledWith({ objectKey: "object-key-123" }); + expect(result).toBe(true); + }); + + it("should return false if no file was deleted", async () => { + mockedFileModel.deleteOne.mockResolvedValue({ deletedCount: 0 } as any); + + const result = await fileService.deleteFileRecord("non-existent-key"); + + expect(result).toBe(false); + }); + }); + + describe("isFileDuplicate", () => { + it("should return true if a file with the same name exists for the user", async () => { + mockedFileModel.countDocuments.mockResolvedValue(1); + + const result = await fileService.isFileDuplicate("duplicate-file.txt", "user123"); + + expect(mockedFileModel.countDocuments).toHaveBeenCalledWith({ + userId: "user123", + originalName: "duplicate-file.txt", + }); + expect(result).toBe(true); + }); + + it("should return false if no duplicate file exists", async () => { + mockedFileModel.countDocuments.mockResolvedValue(0); + + const result = await fileService.isFileDuplicate("unique-file.txt", "user123"); + + expect(result).toBe(false); + }); + + it("should return false if an error occurs", async () => { + mockedFileModel.countDocuments.mockRejectedValue(new Error("Database error")); + + // Mock console.error to prevent test output cluttering + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const result = await fileService.isFileDuplicate("error-file.txt", "user123"); + + expect(result).toBe(false); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Error in isFileDuplicate")); + + consoleSpy.mockRestore(); + }); + }); + + describe("updateObjectKey", () => { + it("should update object key and return the updated file", async () => { + const mockFile = { + _id: "file123", + objectKey: "new-object-key", + originalName: "test.txt", + }; + + const mockExec = vi.fn().mockResolvedValue(mockFile); + mockedFileModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any); + + const result = await fileService.updateObjectKey("file123", "new-object-key"); + + expect(mockedFileModel.findByIdAndUpdate).toHaveBeenCalledWith( + "file123", + { objectKey: "new-object-key" }, + { new: true } + ); + expect(result).toEqual(mockFile); + }); + + it("should return null if file not found", async () => { + const mockExec = vi.fn().mockResolvedValue(null); + mockedFileModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any); + + const result = await fileService.updateObjectKey("non-existent-id", "new-object-key"); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/tests/services/s3Service.test.ts b/tests/services/s3Service.test.ts index 3dcf845..8b4a4e7 100644 --- a/tests/services/s3Service.test.ts +++ b/tests/services/s3Service.test.ts @@ -10,15 +10,17 @@ vi.mock("@aws-sdk/client-s3", async (importOriginal) => { }; }); +vi.mock("../../src/services/db/fileService", () => ({ + FileService: { + getInstance: vi.fn(), + }, +})); + import { S3Service, S3ClientConfig } from "../../src/services/s3Service"; -import { - S3Client, - CreateBucketCommand, - PutObjectCommand, - ListObjectsV2Command, - DeleteObjectCommand, -} from "@aws-sdk/client-s3"; +import { S3Client, CreateBucketCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { FileService } from "../../src/services/db/fileService"; +import { File } from "../../src/db/models/file"; const testConfig: S3ClientConfig = { endpoint: "http://test-minio", @@ -32,6 +34,13 @@ const testConfig: S3ClientConfig = { const MockS3Client = vi.mocked(S3Client); const mockSend = vi.fn(); const mockGetSignedUrl = vi.mocked(getSignedUrl); +const mockFileService = { + createFileRecord: vi.fn(), + getFilesByUserId: vi.fn(), + isFileDuplicate: vi.fn(), + deleteFileRecord: vi.fn(), + updateObjectKey: vi.fn(), +}; describe("S3Service", () => { let s3Service: S3Service; @@ -53,10 +62,12 @@ describe("S3Service", () => { }) as never ); + vi.mocked(FileService.getInstance).mockReturnValue(mockFileService as any); + // @ts-ignore S3Service.instance = undefined; - s3Service = S3Service.getInstance(testConfig); + s3Service = S3Service.getInstance(testConfig, mockFileService as any); }); describe("Initialization and Bucket Creation", () => { @@ -90,9 +101,12 @@ describe("S3Service", () => { originalname: "test-image.jpg", buffer: Buffer.from("test-data"), mimetype: "image/jpeg", + size: 1024, }; const userId = "user-123"; + mockSend.mockResolvedValue({}); + mockFileService.createFileRecord.mockResolvedValue({}); const objectKey = await s3Service.uploadFile(mockFile as never, userId); @@ -106,6 +120,14 @@ describe("S3Service", () => { expect(sentCommand.Bucket).toBe("test-bucket"); expect(sentCommand.Key).toBe(objectKey); expect(sentCommand.Body).toBe(mockFile.buffer); + + expect(mockFileService.createFileRecord).toHaveBeenCalledWith( + userId, + objectKey, + mockFile.originalname, + mockFile.mimetype, + mockFile.size + ); }); }); @@ -113,69 +135,60 @@ describe("S3Service", () => { const userId = "user-123"; it("should return a correctly formatted list of files for a user", async () => { - const mockS3Response = { - Contents: [ - { - Key: `user-${userId}/uuid1_file1.txt`, - LastModified: new Date("2023-01-01"), - }, - { - Key: `user-${userId}/uuid2_image.jpg`, - LastModified: new Date("2023-01-02"), - }, - ], - }; - mockSend.mockResolvedValue(mockS3Response); + const mockDbFiles: Partial[] = [ + { + objectKey: `user-${userId}/uuid1_file1.txt`, + originalName: "file1.txt", + mimeType: "text/plain", + size: 100, + uploadedAt: new Date("2023-01-01"), + }, + { + objectKey: `user-${userId}/uuid2_image.jpg`, + originalName: "image.jpg", + mimeType: "image/jpeg", + size: 1024, + uploadedAt: new Date("2023-01-02"), + }, + ]; + mockFileService.getFilesByUserId.mockResolvedValue(mockDbFiles); const files = await s3Service.listFilesForUser(userId); - expect(mockSend).toHaveBeenCalledOnce(); - expect(mockSend).toHaveBeenCalledWith(expect.any(ListObjectsV2Command)); - - const sentCommand = (mockSend.mock.calls[0][0] as ListObjectsV2Command).input; - expect(sentCommand.Bucket).toBe("test-bucket"); - expect(sentCommand.Prefix).toBe(`user-${userId}/`); + expect(mockFileService.getFilesByUserId).toHaveBeenCalledWith(userId); expect(files).toHaveLength(2); expect(files).toContainEqual({ key: `user-${userId}/uuid1_file1.txt`, lastModified: new Date("2023-01-01"), originalName: "file1.txt", + mimeType: "text/plain", + size: 100, }); expect(files).toContainEqual({ key: `user-${userId}/uuid2_image.jpg`, lastModified: new Date("2023-01-02"), originalName: "image.jpg", + mimeType: "image/jpeg", + size: 1024, }); }); it("should return an empty array if the user has no files", async () => { - const mockS3Response = { - Contents: [], - }; - mockSend.mockResolvedValue(mockS3Response); + mockFileService.getFilesByUserId.mockResolvedValue([]); const files = await s3Service.listFilesForUser(userId); - expect(mockSend).toHaveBeenCalledOnce(); - expect(files).toEqual([]); - }); - - it("should return an empty array if the S3 response has no 'Contents' property", async () => { - const mockS3Response = {}; - mockSend.mockResolvedValue(mockS3Response); - - const files = await s3Service.listFilesForUser(userId); - - expect(mockSend).toHaveBeenCalledOnce(); + expect(mockFileService.getFilesByUserId).toHaveBeenCalledWith(userId); expect(files).toEqual([]); }); }); describe("deleteFile", () => { - it("should call the S3 client with the correct DeleteObjectCommand", async () => { + it("should call the S3 client with the correct DeleteObjectCommand and delete the file record", async () => { const objectKey = "user-123/some-file-to-delete.txt"; mockSend.mockResolvedValue({}); + mockFileService.deleteFileRecord.mockResolvedValue(true); await expect(s3Service.deleteFile(objectKey)).resolves.toBeUndefined(); @@ -185,6 +198,8 @@ describe("S3Service", () => { const sentCommand = (mockSend.mock.calls[0][0] as DeleteObjectCommand).input; expect(sentCommand.Bucket).toBe("test-bucket"); expect(sentCommand.Key).toBe(objectKey); + + expect(mockFileService.deleteFileRecord).toHaveBeenCalledWith(objectKey); }); it("should throw an error if the S3 client fails to delete the object", async () => { @@ -193,11 +208,13 @@ describe("S3Service", () => { mockSend.mockRejectedValue(s3Error); await expect(s3Service.deleteFile(objectKey)).rejects.toThrow("Access Denied"); + + expect(mockFileService.deleteFileRecord).not.toHaveBeenCalled(); }); }); describe("isFileDuplicate", () => { - it("should correctly identify duplicate files", async () => { + it("should use FileService to check for duplicate files", async () => { const userId = "user-123"; const mockFile = { originalname: "duplicate-image.jpg", @@ -205,30 +222,12 @@ describe("S3Service", () => { mimetype: "image/jpeg", }; - const mockFiles = [ - { - key: `user-${userId}/file1_original-file.txt`, - lastModified: new Date("2023-01-01"), - originalName: "original-file.txt", - }, - { - key: `user-${userId}/file2_duplicate-image.jpg`, - lastModified: new Date("2023-01-02"), - originalName: "duplicate-image.jpg", - }, - ]; - - mockSend.mockResolvedValueOnce({ - Contents: mockFiles.map((file) => ({ - Key: file.key, - LastModified: file.lastModified, - })), - }); + mockFileService.isFileDuplicate.mockResolvedValue(true); const isDuplicate = await s3Service.isFileDuplicate(mockFile as never, userId); expect(isDuplicate).toBe(true); - expect(mockSend).toHaveBeenCalledWith(expect.any(ListObjectsV2Command)); + expect(mockFileService.isFileDuplicate).toHaveBeenCalledWith(mockFile.originalname, userId); }); it("should correctly identify non-duplicate files", async () => { @@ -239,60 +238,12 @@ describe("S3Service", () => { mimetype: "image/jpeg", }; - const mockFiles = [ - { - key: `user-${userId}/file1_existing-file.txt`, - lastModified: new Date("2023-01-01"), - originalName: "existing-file.txt", - }, - { - key: `user-${userId}/file2_another-image.jpg`, - lastModified: new Date("2023-01-02"), - originalName: "another-image.jpg", - }, - ]; - - mockSend.mockResolvedValueOnce({ - Contents: mockFiles.map((file) => ({ - Key: file.key, - LastModified: file.lastModified, - })), - }); + mockFileService.isFileDuplicate.mockResolvedValue(false); const isDuplicate = await s3Service.isFileDuplicate(mockFile as never, userId); expect(isDuplicate).toBe(false); - expect(mockSend).toHaveBeenCalledWith(expect.any(ListObjectsV2Command)); - }); - - it("should handle empty file lists correctly", async () => { - const userId = "user-123"; - const mockFile = { - originalname: "test-image.jpg", - buffer: Buffer.from("test-data"), - mimetype: "image/jpeg", - }; - - mockSend.mockResolvedValueOnce({ - Contents: [], - }); - - const isDuplicate = await s3Service.isFileDuplicate(mockFile as never, userId); - - expect(isDuplicate).toBe(false); - expect(mockSend).toHaveBeenCalledWith(expect.any(ListObjectsV2Command)); - }); - }); - - describe("extractOriginalNameFromKey", () => { - it("should correctly extract the original filename from an object key", () => { - const originalName = (s3Service as any).extractOriginalNameFromKey("user-123/abc123_original-file.jpg"); - expect(originalName).toBe("original-file.jpg"); - }); - - it("should return undefined for invalid object keys", () => { - const originalName = (s3Service as any).extractOriginalNameFromKey("invalid-key"); - expect(originalName).toBeUndefined(); + expect(mockFileService.isFileDuplicate).toHaveBeenCalledWith(mockFile.originalname, userId); }); }); @@ -307,7 +258,6 @@ describe("S3Service", () => { expect(signedUrl).toBe(fakeSignedUrl); - // Prüfung, dass getSignedUrl korrekt aufgerufen wurde expect(mockGetSignedUrl).toHaveBeenCalledOnce(); expect(mockGetSignedUrl).toHaveBeenCalledWith( expect.anything(),