mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import { FilesService } from './files.service';
|
|
|
|
/**
|
|
* Replacing a stored document must never destroy the previous one: the customer
|
|
* uploaded it, and a staff correction has to stay auditable against it. The old
|
|
* row is soft-deleted (so every normal read still returns exactly the current
|
|
* version) and stamped with who replaced it and why.
|
|
*/
|
|
describe('FilesService — document versions', () => {
|
|
const file = {
|
|
originalname: 'bill-of-lading.pdf',
|
|
size: 1234,
|
|
mimetype: 'application/pdf',
|
|
buffer: Buffer.from('x'),
|
|
} as Express.Multer.File;
|
|
|
|
let filesRepository: {
|
|
deleteByCode: jest.Mock;
|
|
create: jest.Mock;
|
|
findVersionHistory: jest.Mock;
|
|
};
|
|
let service: FilesService;
|
|
|
|
beforeEach(() => {
|
|
filesRepository = {
|
|
deleteByCode: jest.fn().mockResolvedValue(undefined),
|
|
create: jest.fn(async (row) => ({ id: 'file-new', ...row })),
|
|
findVersionHistory: jest.fn().mockResolvedValue([]),
|
|
};
|
|
service = new FilesService(
|
|
filesRepository as never,
|
|
{
|
|
uploadFile: jest.fn().mockResolvedValue('https://minio/bucket/new.pdf'),
|
|
getObjectNameFromUrl: (u: string) => u,
|
|
getSignedUrl: jest.fn(),
|
|
} as never,
|
|
);
|
|
});
|
|
|
|
it('stamps the retired version with who replaced it and why', async () => {
|
|
await service.upsertByCode(
|
|
{ resourceId: 'ctr-1', resource: 'contracts', code: 'bill_of_lading', file },
|
|
{ userId: 'gl-user-1', reason: 'Customer sent page 2 only' },
|
|
);
|
|
|
|
expect(filesRepository.deleteByCode).toHaveBeenCalledWith(
|
|
'ctr-1',
|
|
'contracts',
|
|
'bill_of_lading',
|
|
{ userId: 'gl-user-1', reason: 'Customer sent page 2 only' },
|
|
);
|
|
});
|
|
|
|
it('still replaces silently when no replacer is given (system overwrites)', async () => {
|
|
await service.upsertByCode({
|
|
resourceId: 'ctr-1',
|
|
resource: 'contracts',
|
|
code: 'contract_pdf',
|
|
file,
|
|
});
|
|
|
|
expect(filesRepository.deleteByCode).toHaveBeenCalledWith(
|
|
'ctr-1',
|
|
'contracts',
|
|
'contract_pdf',
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('marks the live row current and the soft-deleted ones superseded', async () => {
|
|
filesRepository.findVersionHistory.mockResolvedValue([
|
|
{
|
|
id: 'v2',
|
|
name: 'corrected.pdf',
|
|
url: 'u2',
|
|
size: 2,
|
|
mimeType: 'application/pdf',
|
|
createdAt: new Date('2026-07-20T10:00:00Z'),
|
|
deletedAt: null,
|
|
replacedByUserId: null,
|
|
replaceReason: null,
|
|
},
|
|
{
|
|
id: 'v1',
|
|
name: 'original.pdf',
|
|
url: 'u1',
|
|
size: 1,
|
|
mimeType: 'application/pdf',
|
|
createdAt: new Date('2026-07-18T10:00:00Z'),
|
|
deletedAt: new Date('2026-07-20T10:00:00Z'),
|
|
replacedByUserId: 'gl-user-1',
|
|
replaceReason: 'Wrong page order',
|
|
},
|
|
]);
|
|
|
|
const versions = await service.versionHistory(
|
|
'ctr-1',
|
|
'contracts',
|
|
'bill_of_lading',
|
|
);
|
|
|
|
expect(versions[0]).toMatchObject({ id: 'v2', isCurrent: true, replacedAt: null });
|
|
expect(versions[1]).toMatchObject({
|
|
id: 'v1',
|
|
isCurrent: false,
|
|
replacedByUserId: 'gl-user-1',
|
|
replaceReason: 'Wrong page order',
|
|
});
|
|
// The customer's original is still readable — that is the whole point.
|
|
expect(versions[1].url).toBe('u1');
|
|
});
|
|
});
|