109 lines
7.2 KiB
TypeScript
109 lines
7.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { resolve } from 'node:path';
|
|
import test from 'node:test';
|
|
import { buildInspectionActPdf } from '../../src/inspection-reports/inspection-act-pdf-builder';
|
|
import { buildInspectionReportWord } from '../../src/inspection-reports/inspection-report-word-builder';
|
|
import sharp from 'sharp';
|
|
import { renderableInspectionImage } from '../../src/inspection-reports/inspection-document-images';
|
|
|
|
const digest = (data: Buffer) => createHash('sha256').update(data).digest('hex');
|
|
const photo = readFileSync(resolve(process.cwd(), 'assets/logo-mendoza.png'));
|
|
const locked = {
|
|
act: { code: 'ACT-TEST-01', occurredAt: '2026-09-14T18:30:00Z',
|
|
summary: 'Se inspeccionó un tanque.', inspection: { code: 'INSP-TEST-01' } },
|
|
responsible: { fullName: 'Responsable de prueba', documentNumber: '12345678' },
|
|
inventories: [{ id: 'asset-1', code: 'CAM-TEST-01', name: 'Tanque de prueba' }],
|
|
findings: [{ id: 'finding-1', assetId: 'asset-1', code: 'ACT-TEST-H01',
|
|
title: 'Pérdida', description: 'Se constató una pérdida visible.', severity: 3 }],
|
|
};
|
|
const sealed = { lockedSnapshot: locked, finalSha256: 'a'.repeat(64),
|
|
lockedSha256: 'b'.repeat(64), signatures: [], seal: { serverSealedAt: '2026-09-15T18:30:00Z' } };
|
|
const evidence = { id: 'photo-1', findingId: 'finding-1', sha256: digest(photo), buffer: photo };
|
|
|
|
test('F6.9 consolidated Acta is a hashed PDF 1.4 with actual evidence embedded', async () => {
|
|
const without = await buildInspectionActPdf(sealed);
|
|
const withEvidence = await buildInspectionActPdf(sealed, [evidence]);
|
|
assert.ok(withEvidence.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4')));
|
|
assert.equal(withEvidence.sha256, digest(withEvidence.buffer));
|
|
assert.ok(withEvidence.buffer.length > without.buffer.length + 3_000);
|
|
assert.notEqual(withEvidence.sha256, without.sha256);
|
|
});
|
|
|
|
test('F6.9 technical Informe embeds evidence and its institutional source hash', () => {
|
|
const input = { code: 'INF-TEST-01', title: 'Informe de prueba', generatedAt: new Date('2026-09-15'),
|
|
frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed, source: { actCode: locked.act.code } } };
|
|
const plain = buildInspectionReportWord(input);
|
|
const withEvidence = buildInspectionReportWord({ ...input, photos: [evidence] });
|
|
assert.equal(withEvidence.sha256, digest(withEvidence.buffer));
|
|
assert.ok(withEvidence.buffer.subarray(0, 4).equals(Buffer.from('PK\x03\x04')));
|
|
assert.ok(withEvidence.buffer.includes(Buffer.from('word/media/photo-1.png')));
|
|
assert.ok(withEvidence.buffer.includes(photo));
|
|
assert.ok(withEvidence.buffer.includes(Buffer.from('ACT-TEST-H01')));
|
|
assert.ok(withEvidence.buffer.includes(Buffer.from('SHA-256 de la fuente del Informe')));
|
|
assert.ok(withEvidence.buffer.length > plain.buffer.length + photo.length);
|
|
assert.ok(!withEvidence.buffer.includes(Buffer.from('[Completar')));
|
|
});
|
|
|
|
test('F6.9 keeps prior sealed PDF and prior company responses available for audit', () => {
|
|
const pdf = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-act-pdf.service.ts'), 'utf8');
|
|
const controller = readFileSync(resolve(process.cwd(), 'src/act-administration/act-administration.controller.ts'), 'utf8');
|
|
assert.match(pdf, /inspection_act_consolidated_pdf_revisions/);
|
|
const word = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-report-word.service.ts'), 'utf8');
|
|
assert.match(word, /inspection_report_consolidated_word_revisions/);
|
|
assert.match(pdf, /ON CONFLICT \(act_id,template_version\) DO NOTHING/);
|
|
assert.match(controller, /responseContent\(responseId\)/);
|
|
assert.doesNotMatch(controller, /@Post\('responses'\)/);
|
|
});
|
|
|
|
test('F6.9 renders WebP field photographs in both documents without changing the source hash', async () => {
|
|
const original = await sharp(photo).webp().toBuffer();
|
|
const originalSha = digest(original);
|
|
const display = await renderableInspectionImage(original);
|
|
assert.ok(display.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])));
|
|
assert.equal(digest(original), originalSha);
|
|
const pdf = await buildInspectionActPdf(sealed, [{ ...evidence, sha256: originalSha, buffer: display }]);
|
|
assert.ok(pdf.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4')));
|
|
const word = buildInspectionReportWord({ code: 'INF-WEBP', title: 'Informe', generatedAt: new Date(),
|
|
frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [{ ...evidence, sha256: originalSha, buffer: display }] });
|
|
assert.ok(word.buffer.includes(display));
|
|
assert.ok(word.buffer.includes(Buffer.from(originalSha)));
|
|
});
|
|
|
|
test('F6.9 built CommonJS runtime renders signed PDF and WebP evidence', async () => {
|
|
const builtPdf = resolve(process.cwd(), 'dist/inspection-reports/inspection-act-pdf-builder.js');
|
|
const builtImage = resolve(process.cwd(), 'dist/inspection-reports/inspection-document-images.js');
|
|
if (!existsSync(builtPdf) || !existsSync(builtImage)) return;
|
|
const requireCjs = createRequire(resolve(process.cwd(), 'package.json'));
|
|
const runtimePdf = requireCjs(builtPdf) as { buildInspectionActPdf: typeof buildInspectionActPdf };
|
|
const runtimeImage = requireCjs(builtImage) as { renderableInspectionImage: typeof renderableInspectionImage };
|
|
const webp = await sharp(photo).webp().toBuffer();
|
|
const rendered = await runtimeImage.renderableInspectionImage(webp);
|
|
const pdf = await runtimePdf.buildInspectionActPdf(sealed, [{ ...evidence, buffer: rendered, sha256: digest(webp) }]);
|
|
assert.ok(pdf.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4')));
|
|
});
|
|
|
|
test('F6.9 revision migration archives both first document versions before serving the corrected template', () => {
|
|
const migration = readFileSync(resolve(process.cwd(),
|
|
'src/database/migrations/1790135400000-f6-9-consolidated-document-revisions.ts'), 'utf8');
|
|
assert.match(migration, /SELECT act_id,1,stored_name,original_name,size_bytes,sha256,generated_at/);
|
|
assert.match(migration, /SELECT report_id,1,stored_name,original_name,size_bytes,sha256,generated_at/);
|
|
assert.match(migration, /PRIMARY KEY \(act_id,template_version\)/);
|
|
assert.match(migration, /PRIMARY KEY \(report_id,template_version\)/);
|
|
});
|
|
|
|
test('Acta ignores standalone field photos while the technical Informe remains independent', async () => {
|
|
const other = await sharp(photo).resize(75).png().toBuffer();
|
|
const unlinked = { id: 'photo-other-asset', assetId: 'asset-2', title: 'Otra instalación', sha256: digest(other), buffer: other };
|
|
const withUnlinked = await buildInspectionActPdf(sealed, [evidence, unlinked]);
|
|
const linkedOnly = await buildInspectionActPdf(sealed, [evidence]);
|
|
assert.ok(Math.abs(withUnlinked.buffer.length - linkedOnly.buffer.length) < 500);
|
|
const word = buildInspectionReportWord({ code: 'INF-OTHER', title: 'Informe', generatedAt: new Date(),
|
|
frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [evidence, unlinked] });
|
|
assert.ok(word.buffer.includes(Buffer.from('OTRAS INSTALACIONES INSPECCIONADAS')));
|
|
assert.ok(word.buffer.includes(Buffer.from('word/media/photo-2.png')));
|
|
assert.ok(word.buffer.includes(other));
|
|
});
|