import { BadRequestException } from '@nestjs/common'; import { FilesService } from './files.service'; describe('FilesService', () => { it('uploads file content to object storage before creating FileObject metadata', async () => { const prisma = { fileObject: { create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'file-1', ...data })), }, }; const objectStorage = { getBucket: jest.fn().mockReturnValue('cmpp-platform'), putObject: jest.fn().mockResolvedValue({ etag: 'etag-1' }), presignedPutObject: jest.fn(), }; const service = new FilesService(prisma as never, objectStorage as never); const file = { originalname: '营业执照.png', mimetype: 'image/png', size: 12, buffer: Buffer.from('file-content'), }; await expect(service.upload({ tenantId: 'tenant-1', purpose: 'signature_material', prefix: 'signature-materials/sig-1' }, file)) .resolves.toEqual(expect.objectContaining({ id: 'file-1', tenantId: 'tenant-1', bucket: 'cmpp-platform', fileName: '营业执照.png', contentType: 'image/png', sizeBytes: '12', purpose: 'signature_material', })); expect(objectStorage.putObject).toHaveBeenCalledWith( expect.stringMatching(/^signature-materials\/sig-1\/\d+-[a-f0-9-]+-营业执照\.png$/), file.buffer, 12, 'image/png', ); expect(prisma.fileObject.create).toHaveBeenCalledWith({ data: expect.objectContaining({ tenantId: 'tenant-1', bucket: 'cmpp-platform', fileName: '营业执照.png', contentType: 'image/png', sizeBytes: BigInt(12), purpose: 'signature_material', }), }); }); it('restores UTF-8 filenames that multipart parsing exposed as Latin-1', async () => { const prisma = { fileObject: { create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'file-1', ...data })), }, }; const objectStorage = { getBucket: jest.fn().mockReturnValue('cmpp-platform'), putObject: jest.fn().mockResolvedValue({ etag: 'etag-1' }), presignedPutObject: jest.fn(), }; const service = new FilesService(prisma as never, objectStorage as never); const originalname = Buffer.from('营业执照.png', 'utf8').toString('latin1'); await service.upload({ purpose: 'enterprise_photo' }, { originalname, mimetype: 'image/png', size: 12, buffer: Buffer.from('file-content'), }); expect(prisma.fileObject.create).toHaveBeenCalledWith({ data: expect.objectContaining({ fileName: '营业执照.png' }), }); expect(objectStorage.putObject).toHaveBeenCalledWith( expect.stringMatching(/营业执照\.png$/), expect.any(Buffer), 12, 'image/png', ); }); it('rejects images over 2MB and other files over 10MB before object storage writes', async () => { const prisma = { fileObject: { create: jest.fn() } }; const objectStorage = { putObject: jest.fn(), getBucket: jest.fn().mockReturnValue('bucket') }; const service = new FilesService(prisma as never, objectStorage as never); await expect(service.upload({ purpose: 'test' }, { originalname: 'large.png', mimetype: 'image/png', size: 2 * 1024 * 1024 + 1, buffer: Buffer.alloc(0), })).rejects.toBeInstanceOf(BadRequestException); await expect(service.upload({ purpose: 'test' }, { originalname: 'large.pdf', mimetype: 'application/pdf', size: 10 * 1024 * 1024 + 1, buffer: Buffer.alloc(0), })).rejects.toBeInstanceOf(BadRequestException); expect(objectStorage.putObject).not.toHaveBeenCalled(); }); it('downloads file content from object storage by FileObject id', async () => { const fileObject = { id: 'file-1', tenantId: 'tenant-1', bucket: 'cmpp-platform', objectKey: 'signature-materials/sig-1/file.png', fileName: 'file.png', contentType: 'image/png', sizeBytes: BigInt(12), checksum: null, purpose: 'signature_material', createdAt: new Date('2026-07-06T00:00:00.000Z'), }; const prisma = { fileObject: { findUnique: jest.fn().mockResolvedValue(fileObject), }, }; const objectStorage = { getObject: jest.fn().mockResolvedValue(Buffer.from('file-content')), }; const service = new FilesService(prisma as never, objectStorage as never); await expect(service.getDownload('file-1')).resolves.toEqual({ fileObject: expect.objectContaining({ id: 'file-1', fileName: 'file.png', sizeBytes: '12', }), content: Buffer.from('file-content'), }); expect(prisma.fileObject.findUnique).toHaveBeenCalledWith({ where: { id: 'file-1' } }); expect(objectStorage.getObject).toHaveBeenCalledWith('signature-materials/sig-1/file.png'); }); it('derives client upload tenant from the authenticated user and ignores tenant headers', async () => { const prisma = { user: { findFirst: jest.fn().mockResolvedValue({ tenantId: 'tenant-real' }) }, fileObject: { create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'file-client', ...data })) }, }; const objectStorage = { getBucket: jest.fn().mockReturnValue('cmpp-platform'), putObject: jest.fn().mockResolvedValue({ etag: 'etag' }), }; const service = new FilesService(prisma as never, objectStorage as never); await expect(service.uploadForClient('user-1', { purpose: 'enterprise_certification', prefix: 'enterprise-certifications/license', }, { originalname: 'license.pdf', mimetype: 'application/pdf', size: 4, buffer: Buffer.from('test') })) .resolves.toEqual(expect.objectContaining({ tenantId: 'tenant-real', purpose: 'enterprise_certification' })); expect(prisma.user.findFirst).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ id: 'user-1' }) })); }); it('rejects arbitrary client upload purposes and prefixes before object storage writes', async () => { const prisma = { user: { findFirst: jest.fn().mockResolvedValue({ tenantId: 'tenant-1' }) } }; const objectStorage = { putObject: jest.fn() }; const service = new FilesService(prisma as never, objectStorage as never); await expect(service.uploadForClient('user-1', { purpose: 'enterprise_certification', prefix: '../admin' }, { originalname: 'file.pdf', mimetype: 'application/pdf', size: 4, buffer: Buffer.from('test'), })).rejects.toBeInstanceOf(BadRequestException); expect(objectStorage.putObject).not.toHaveBeenCalled(); }); it('prevents client users from downloading another tenant file', async () => { const prisma = { user: { findFirst: jest.fn().mockResolvedValue({ tenantId: 'tenant-1' }) }, fileObject: { findFirst: jest.fn().mockResolvedValue(null) }, }; const objectStorage = { getObject: jest.fn() }; const service = new FilesService(prisma as never, objectStorage as never); await expect(service.getClientDownload('user-1', 'foreign-file')).rejects.toThrow('File object not found'); expect(prisma.fileObject.findFirst).toHaveBeenCalledWith({ where: { id: 'foreign-file', tenantId: 'tenant-1' } }); expect(objectStorage.getObject).not.toHaveBeenCalled(); }); });