import { BadRequestException, NotFoundException } from '@nestjs/common'; import * as fs from 'node:fs/promises'; import { DocumentsService } from './documents.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; jest.mock('node:fs/promises'); describe('DocumentsService', () => { let service: DocumentsService; let prisma: { document: { findMany: jest.Mock; findUnique: jest.Mock; create: jest.Mock; delete: jest.Mock; }; apiary: { findUnique: jest.Mock }; colony: { findUnique: jest.Mock }; queen: { findUnique: jest.Mock }; inspection: { findUnique: jest.Mock }; honeyBatch: { findUnique: jest.Mock }; }; let auditLog: { record: jest.Mock }; const document = { id: 'doc-1', entityType: 'COLONY', entityId: 'colony-1', fileName: 'bild.jpg', mimeType: 'image/jpeg', sizeBytes: 1234, storagePath: '/tmp/uploads/colony/abc.jpg', uploadedByUserId: 'user-1', createdAt: new Date('2026-01-01'), }; beforeEach(() => { prisma = { document: { findMany: jest.fn().mockResolvedValue([]), findUnique: jest.fn(), create: jest.fn(), delete: jest.fn(), }, apiary: { findUnique: jest.fn() }, colony: { findUnique: jest.fn() }, queen: { findUnique: jest.fn() }, inspection: { findUnique: jest.fn() }, honeyBatch: { findUnique: jest.fn() }, }; auditLog = { record: jest.fn().mockResolvedValue(undefined) }; service = new DocumentsService( prisma as unknown as PrismaService, auditLog as unknown as AuditLogService, ); (fs.mkdir as jest.Mock).mockResolvedValue(undefined); (fs.writeFile as jest.Mock).mockResolvedValue(undefined); (fs.readFile as jest.Mock).mockResolvedValue(Buffer.from('data')); (fs.unlink as jest.Mock).mockResolvedValue(undefined); }); it('findAll strips storagePath from results', async () => { prisma.document.findMany.mockResolvedValueOnce([document]); const result = await service.findAll('COLONY', 'colony-1'); expect(result).toEqual([ { id: 'doc-1', entityType: 'COLONY', entityId: 'colony-1', fileName: 'bild.jpg', mimeType: 'image/jpeg', sizeBytes: 1234, uploadedByUserId: 'user-1', createdAt: document.createdAt, }, ]); }); describe('create', () => { const file = { buffer: Buffer.from([0xff, 0xd8, 0xff, 0x00, 0x01, 0x02]), originalname: 'bild.jpg', mimetype: 'image/jpeg', size: 1234, } as Express.Multer.File; it('rejects an unsupported mime type', async () => { const badFile = { ...file, mimetype: 'application/zip', }; await expect( service.create(badFile, 'COLONY', 'colony-1', 'user-1'), ).rejects.toBeInstanceOf(BadRequestException); }); it('rejects a file whose bytes do not match the declared mime type', async () => { const mislabeled = { ...file, buffer: Buffer.from('not-a-jpeg') }; await expect( service.create(mislabeled, 'COLONY', 'colony-1', 'user-1'), ).rejects.toBeInstanceOf(BadRequestException); expect(fs.writeFile).not.toHaveBeenCalled(); }); it('throws NotFoundException when the referenced entity does not exist', async () => { prisma.colony.findUnique.mockResolvedValue(null); await expect( service.create(file, 'COLONY', 'missing', 'user-1'), ).rejects.toBeInstanceOf(NotFoundException); }); it('writes the file to disk and persists a document row', async () => { prisma.colony.findUnique.mockResolvedValue({ id: 'colony-1' }); prisma.document.create.mockResolvedValue(document); const result = await service.create(file, 'COLONY', 'colony-1', 'user-1'); expect(fs.mkdir).toHaveBeenCalled(); expect(fs.writeFile).toHaveBeenCalled(); const [[createArg]] = prisma.document.create.mock.calls as [ [ { data: { entityType: string; entityId: string; fileName: string; uploadedByUserId: string; }; }, ], ]; expect(createArg.data.entityType).toBe('COLONY'); expect(createArg.data.entityId).toBe('colony-1'); expect(createArg.data.fileName).toBe('bild.jpg'); expect(createArg.data.uploadedByUserId).toBe('user-1'); expect(result).not.toHaveProperty('storagePath'); }); }); describe('readFile', () => { it('throws NotFoundException when the document does not exist', async () => { prisma.document.findUnique.mockResolvedValue(null); await expect(service.readFile('missing')).rejects.toBeInstanceOf( NotFoundException, ); }); it('reads the stored file from disk', async () => { prisma.document.findUnique.mockResolvedValue(document); const result = await service.readFile('doc-1'); expect(fs.readFile).toHaveBeenCalledWith(document.storagePath); expect(result.fileName).toBe('bild.jpg'); expect(result.mimeType).toBe('image/jpeg'); }); }); describe('delete', () => { it('throws NotFoundException when the document does not exist', async () => { prisma.document.findUnique.mockResolvedValue(null); await expect(service.delete('missing', 'user-1')).rejects.toBeInstanceOf( NotFoundException, ); }); it('deletes the db row and unlinks the file', async () => { prisma.document.findUnique.mockResolvedValue(document); await service.delete('doc-1', 'user-1'); expect(prisma.document.delete).toHaveBeenCalledWith({ where: { id: 'doc-1' }, }); expect(fs.unlink).toHaveBeenCalledWith(document.storagePath); expect(auditLog.record).toHaveBeenCalledWith( expect.objectContaining({ entityType: 'Document', action: 'DELETE' }), ); }); }); });