Files
lislgosms/api/src/files/files.service.spec.ts
T

180 lines
7.3 KiB
TypeScript

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();
});
});