mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
|
|
import { ContractTransitionService } from './contract-transition.service';
|
|
|
|
/**
|
|
* The staff signature seals with the ONE global stamp by REFERENCE: the
|
|
* signature row stores the current global stampFileId instead of re-uploading
|
|
* a copy per contract. That id stays valid after the stamp is replaced
|
|
* (StampSettingsService never deletes retired stamp files), so each contract
|
|
* keeps the exact seal it was signed with. These specs pin the sourcing
|
|
* split: EDR always seals with the global stamp and staff never supply one,
|
|
* while the customer must upload their own.
|
|
*/
|
|
describe('applySignature stamp sourcing', () => {
|
|
const contract = { id: 'c-1', reference: 'CTR-1', status: 'SIGNED_CUSTOMER' };
|
|
const GLOBAL_STAMP = 'data:image/png;base64,RURS';
|
|
const GLOBAL_STAMP_FILE_ID = 'file-global-stamp';
|
|
|
|
const build = (globalStampFileId: string | null = GLOBAL_STAMP_FILE_ID) => {
|
|
const uploads: Array<{ code: string; image: string }> = [];
|
|
const saved: unknown[] = [];
|
|
const service = Object.create(
|
|
ContractTransitionService.prototype,
|
|
) as ContractTransitionService;
|
|
Object.assign(service, {
|
|
logger: { warn: jest.fn(), log: jest.fn() },
|
|
stampSettings: {
|
|
get: jest.fn().mockResolvedValue({
|
|
id: 's-1',
|
|
stampFileId: globalStampFileId,
|
|
}),
|
|
},
|
|
contractsRepository: {
|
|
saveSignature: jest.fn((row: unknown) => {
|
|
saved.push(row);
|
|
return Promise.resolve(undefined);
|
|
}),
|
|
},
|
|
signaturesService: {
|
|
getForUser: jest.fn().mockResolvedValue(null),
|
|
upsertForUser: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
uploadSignatureAsset: jest.fn((_c: unknown, code: string, image: string) => {
|
|
uploads.push({ code, image });
|
|
return Promise.resolve({ id: `file-${code}` });
|
|
}),
|
|
});
|
|
return { service, uploads, saved };
|
|
};
|
|
|
|
const apply = (
|
|
service: ContractTransitionService,
|
|
dto: Record<string, unknown>,
|
|
) =>
|
|
(
|
|
service as unknown as {
|
|
applySignature(
|
|
c: unknown,
|
|
d: unknown,
|
|
o: { signerUserId?: string },
|
|
): Promise<void>;
|
|
}
|
|
).applySignature(contract, dto, { signerUserId: 'u-1' });
|
|
|
|
const staffDto = {
|
|
role: 'STAFF' as const,
|
|
signerDisplayName: 'E. Staff',
|
|
signatureImageBase64: 'data:image/png;base64,U0lH',
|
|
};
|
|
|
|
it('seals the EDR side by referencing the global stamp file, without re-uploading it', async () => {
|
|
const { service, uploads, saved } = build();
|
|
|
|
await apply(service, staffDto);
|
|
|
|
expect(uploads.map((u) => u.code)).toEqual(['signature_staff']);
|
|
expect(saved[0]).toEqual(
|
|
expect.objectContaining({ stampFileId: GLOBAL_STAMP_FILE_ID }),
|
|
);
|
|
});
|
|
|
|
it('ignores a stamp a staff client tries to supply', async () => {
|
|
const { service, uploads, saved } = build();
|
|
|
|
await apply(service, {
|
|
...staffDto,
|
|
stampImageBase64: 'data:image/png;base64,SEFDSw==',
|
|
});
|
|
|
|
expect(uploads.map((u) => u.image)).not.toContain(
|
|
'data:image/png;base64,SEFDSw==',
|
|
);
|
|
expect(saved[0]).toEqual(
|
|
expect.objectContaining({ stampFileId: GLOBAL_STAMP_FILE_ID }),
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Failing loudly matters here: silently executing an unsealed contract
|
|
* would be worse than refusing to counter-sign.
|
|
*/
|
|
it('refuses to counter-sign when no global stamp is configured', async () => {
|
|
const { service, saved } = build(null);
|
|
|
|
await expect(apply(service, staffDto)).rejects.toBeInstanceOf(
|
|
BadRequestException,
|
|
);
|
|
await expect(apply(service, staffDto)).rejects.toThrow(/company stamp is configured/i);
|
|
expect(saved).toHaveLength(0);
|
|
});
|
|
|
|
it('requires the customer to upload their own stamp', async () => {
|
|
const { service, saved } = build();
|
|
|
|
await expect(
|
|
apply(service, {
|
|
role: 'CUSTOMER',
|
|
signerDisplayName: 'C. Customer',
|
|
signatureImageBase64: 'data:image/png;base64,U0lH',
|
|
}),
|
|
).rejects.toThrow(/company stamp is required/i);
|
|
expect(saved).toHaveLength(0);
|
|
});
|
|
|
|
it('snapshots the customer stamp and never substitutes the global one', async () => {
|
|
const { service, uploads } = build();
|
|
const customerStamp = 'data:image/png;base64,Q1VTVA==';
|
|
|
|
await apply(service, {
|
|
role: 'CUSTOMER',
|
|
signerDisplayName: 'C. Customer',
|
|
signatureImageBase64: 'data:image/png;base64,U0lH',
|
|
stampImageBase64: customerStamp,
|
|
});
|
|
|
|
expect(uploads).toContainEqual({
|
|
code: 'stamp_customer',
|
|
image: customerStamp,
|
|
});
|
|
expect(uploads.map((u) => u.image)).not.toContain(GLOBAL_STAMP);
|
|
});
|
|
|
|
/**
|
|
* DIRECTOR/CEO rows are internal approval signatures, not party seals, so
|
|
* they are deliberately exempt from the stamp requirement.
|
|
*/
|
|
it('lets internal approval signatures through without any stamp', async () => {
|
|
const { service, uploads, saved } = build();
|
|
|
|
await apply(service, {
|
|
role: 'DIRECTOR',
|
|
signerDisplayName: 'D. Director',
|
|
signatureImageBase64: 'data:image/png;base64,U0lH',
|
|
});
|
|
|
|
expect(uploads.map((u) => u.code)).toEqual(['signature_director']);
|
|
expect(saved[0]).toEqual(expect.objectContaining({ stampFileId: null }));
|
|
});
|
|
});
|