import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import { compileFunction } from 'node:vm'; import { File } from 'node:buffer'; import { renderToStaticMarkup } from 'react-dom/server'; import ts from 'typescript'; const require = createRequire(import.meta.url); function load(file, mocks = {}, globals = {}, suffix = '') { const output = ts.transpileModule( fs.readFileSync(new URL(file, import.meta.url), 'utf8') + suffix, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, }, }, ).outputText; const module = { exports: {} }; compileFunction(output, [ 'require', 'module', 'exports', ...Object.keys(globals), ])( (name) => mocks[name] ?? require(name), module, module.exports, ...Object.values(globals), ); return module.exports; } const values = load('../../resources/js/lib/desk-field-values.ts'); function nodes(tree) { if (Array.isArray(tree)) return tree.flatMap(nodes); if (!tree || typeof tree !== 'object') return []; return [tree, ...nodes(tree.props?.children)]; } const field = { type: 'attachment', label: 'SYNTHETIC support', required: true, mime_types: ['text/plain'], max_size_kb: 2, max_files: 1, }; const document = { id: 'link-one', document_id: 'document-one', file_name: 'SYNTHETIC proof.txt', category: 'SYNTHETIC', source_reference: 'SYNTHETIC source', status: 'quarantined', malware_scan_status: 'pending', media_type: 'text/plain', size_bytes: 100, version_no: 1, }; function fieldHarness( handler = async () => ({ documents: [document], case_version: 2, link_id: document.id, }), ) { const cells = [], calls = [], inventory = [], busy = [], changes = []; let cursor = 0, props, tree; const react = { useState(initial) { const i = cursor++; if (!(i in cells)) cells[i] = initial; return [ cells[i], (value) => (cells[i] = typeof value === 'function' ? value(cells[i]) : value), ]; }, useRef(initial) { const i = cursor++; cells[i] ??= { current: initial }; return cells[i]; }, }; class ValidationError extends Error {} const Component = load( '../../resources/js/components/desk-attachment-field.tsx', { react, '@/lib/profile-json': { ProfileValidationError: ValidationError, async profileJson(url, body = {}, method = 'POST') { calls.push({ url, body, method }); return handler(url, body, method); }, }, '@/lib/desk-field-values': values, './desk-document-picker': { default: () => null }, }, ).default; props = { caseId: 'synthetic-case', requestId: 'synthetic-request', caseVersion: 1, fieldKey: 'support', field, value: [], documents: [document], disabled: false, uploading: false, onChange: (ids) => changes.push(ids), onBusy: (value) => busy.push(value), onInventory: (data, select) => inventory.push({ data, select }), }; const render = (patch = {}) => { props = { ...props, ...patch }; cursor = 0; tree = Component(props); return tree; }; render(); return { render, calls, inventory, busy, changes, async settle() { for (let i = 0; i < 8; i++) await Promise.resolve(); render(); }, get tree() { return tree; }, button(label) { return nodes(tree).find( (node) => node.type === 'button' && node.props.children === label, ); }, prepare( file = new File(['SYNTHETIC bytes'], 'synthetic-proof.txt', { type: 'text/plain', }), ) { nodes(tree) .find( (node) => node.type === 'input' && node.props.type === 'file', ) .props.onChange({ target: { files: [file] } }); render(); nodes(tree) .find( (node) => node.type === 'input' && node.props.maxLength === 3000, ) .props.onChange({ target: { value: 'SYNTHETIC submitted source' }, }); render(); nodes(tree) .filter( (node) => node.type === 'input' && node.props.type === 'checkbox', ) .at(-1) .props.onChange({ target: { checked: true } }); render(); }, }; } await test('cross-dossier picker uses the selected origin identity and does not re-enter existing provenance', async () => { const h = fieldHarness(); const picker = nodes(h.tree).find( (node) => typeof node.type === 'function' && 'onSelect' in node.props, ); picker.props.onSelect({ ...document, id: 'other-dossier-link', document_id: 'other-original', origin_label: 'SYNTHETIC previous dossier', }); h.render(); assert.ok( !nodes(h.tree).some( (node) => node.type === 'input' && node.props.type === 'file', ), ); assert.ok( !nodes(h.tree).some( (node) => node.type === 'input' && node.props.maxLength === 3000, ), ); nodes(h.tree) .filter( (node) => node.type === 'input' && node.props.type === 'checkbox', ) .at(-1) .props.onChange({ target: { checked: true } }); h.render(); h.button('Reutilizar documento nesta pergunta').props.onClick(); await h.settle(); assert.equal(h.calls[0].body.get('document_id'), 'other-original'); assert.equal(h.calls[0].body.get('reuse_link_id'), 'other-dossier-link'); assert.equal(h.calls[0].body.has('source_reference'), false); assert.equal(h.calls[0].body.has('file'), false); assert.equal(h.inventory[0].select, true); }); await test('attachment question reuses labelled documents, preserves pending drafts and enforces configured choice/format limits', () => { const h = fieldHarness(); let tree = h.tree; assert.equal( nodes(tree).filter( (node) => node.type === 'input' && node.props.type === 'checkbox', )[0].props.disabled, false, ); nodes(tree) .filter( (node) => node.type === 'input' && node.props.type === 'checkbox', )[0] .props.onChange({ target: { checked: true } }); assert.deepEqual(h.changes[0], ['link-one']); const html = renderToStaticMarkup(tree); assert.match(html, /SYNTHETIC proof.txt/); assert.match(html, /Análise ou disponibilidade pendente/); assert.match(html, /2 KB por ficheiro/); assert.ok( !nodes(tree).some( (node) => node.type === 'input' && node.props.name === 'document_id', ), ); tree = h.render({ value: ['link-one'], documents: [ document, { ...document, id: 'link-two', document_id: 'document-two', file_name: 'SYNTHETIC other.txt', }, ], }); assert.equal( nodes(tree).filter( (node) => node.type === 'input' && node.props.type === 'checkbox', )[1].props.disabled, true, ); tree = h.render({ value: [], documents: [{ ...document, media_type: 'application/pdf' }], }); assert.equal( nodes(tree).find( (node) => node.type === 'input' && node.props.type === 'checkbox', ).props.disabled, true, ); tree = h.render({ value: [], documents: [{ ...document, media_type: null, size_bytes: null }], }); assert.equal( nodes(tree).find( (node) => node.type === 'input' && node.props.type === 'checkbox', ).props.disabled, false, ); assert.match(renderToStaticMarkup(tree), /Formato ou tamanho por conferir/); assert.equal(h.render({ disabled: true }).props.disabled, true); }); await test('document aliases render once and removing a choice removes duplicate aliases without changing another question', () => { const h = fieldHarness(); const tree = h.render({ value: ['link-one', 'link-alias'], documents: [document, { ...document, id: 'link-alias' }], }); const checkboxes = nodes(tree).filter( (node) => node.type === 'input' && node.props.type === 'checkbox', ); assert.equal(checkboxes.length, 2); // One document and upload consent. checkboxes[0].props.onChange({ target: { checked: false } }); assert.deepEqual(h.changes[0], []); const inherited = values.mergeDeskPrefill( { support: [] }, { support: ['link-one'] }, { support: field }, true, ); assert.deepEqual(inherited.support, []); }); await test('new support uses multipart with request, question and case version and never submits the request', async () => { const h = fieldHarness(); h.prepare(); const fileInput = nodes(h.tree).find( (node) => node.type === 'input' && node.props.type === 'file', ); fileInput.props.ref.current = { value: 'selected' }; await h.button('Associar ficheiro a esta pergunta').props.onClick(); await h.settle(); assert.equal(h.calls.length, 1); const data = h.calls[0].body; assert.ok(data instanceof FormData); assert.equal(data.get('field_key'), 'support'); assert.equal(data.get('request_id'), 'synthetic-request'); assert.equal(data.get('version'), '1'); assert.equal(data.get('source_reference'), 'SYNTHETIC submitted source'); assert.equal(data.get('submit'), null); assert.equal(data.get('context_reviewed'), null); assert.equal(h.inventory[0].select, true); assert.deepEqual(h.busy, [true, false]); assert.equal(fileInput.props.ref.current.value, ''); assert.equal( h.button('Associar ficheiro a esta pergunta').props.disabled, true, ); }); await test('oversized file stays selected without a write and a failed upload can be retried with the original data', async () => { const oversized = fieldHarness(); oversized.prepare( new File(['X'.repeat(2049)], 'oversized.txt', { type: 'text/plain' }), ); await oversized.button('Associar ficheiro a esta pergunta').props.onClick(); oversized.render(); assert.equal(oversized.calls.length, 0); assert.match( renderToStaticMarkup(oversized.tree), /excede o tamanho máximo/, ); let failed = true; const h = fieldHarness(async () => { if (failed) throw Error('SYNTHETIC network failure'); return { documents: [document], case_version: 2, link_id: document.id, reused_link: true, }; }); h.prepare(); await h.button('Associar ficheiro a esta pergunta').props.onClick(); await h.settle(); assert.equal( h.button('Associar ficheiro a esta pergunta').props.disabled, false, ); failed = false; await h.button('Associar ficheiro a esta pergunta').props.onClick(); await h.settle(); assert.equal(h.calls.length, 2); assert.equal( h.calls[0].body.get('file').size, h.calls[1].body.get('file').size, ); assert.match( renderToStaticMarkup(h.tree), /Conservou-se a referência registada/, ); }); await test('document refresh keeps answers on the page and a stale upload response is not treated as a confirmed selection', async () => { const h = fieldHarness(async (url, body, method) => method === 'GET' ? { documents: [document], caseVersion: 8 } : { ok: true }, ); await h.button('Actualizar documentos disponíveis').props.onClick(); await h.settle(); assert.equal(h.inventory[0].select, false); assert.equal(h.inventory[0].data.case_version, 8); assert.equal(h.changes.length, 0); h.prepare(); await h.button('Associar ficheiro a esta pergunta').props.onClick(); await h.settle(); assert.equal(h.inventory.length, 1); assert.match(renderToStaticMarkup(h.tree), /não confirmou o documento/); assert.equal( h.button('Associar ficheiro a esta pergunta').props.disabled, false, ); }); await test('the common transport keeps multipart boundaries with the browser and preserves existing JSON and CSRF behaviour', async () => { const sent = []; const api = load( '../../resources/js/lib/profile-json.ts', {}, { document: { cookie: 'XSRF-TOKEN=synthetic%3Dtoken' }, fetch: async (url, options) => { sent.push(options); return new Response(JSON.stringify({ ok: true }), { status: 200, }); }, }, ); const data = new FormData(); data.set( 'file', new File(['SYNTHETIC'], 'proof.txt', { type: 'text/plain' }), ); await api.profileJson('/synthetic-upload', data); assert.equal(sent[0].body, data); assert.equal(sent[0].headers['Content-Type'], undefined); assert.equal(sent[0].headers['X-XSRF-TOKEN'], 'synthetic=token'); assert.equal(sent[0].credentials, 'same-origin'); await api.profileJson('/synthetic-json', { answer: 0 }); assert.equal(sent[1].body, '{"answer":0}'); assert.equal(sent[1].headers['Content-Type'], 'application/json'); }); await test('owner reuse chooses the precise registered source without asking for its reference again or sending another file', async () => { const sourceB = { ...document, id: 'source-b', document_id: 'shared-original', source_reference: 'SYNTHETIC ref B', stage: 'E1', valid_until: '2027-01-02', }; const sourceC = { ...sourceB, id: 'source-c', source_reference: 'SYNTHETIC ref C', stage: 'E2', }; const target = { ...sourceB, id: 'new-use' }; const h = fieldHarness(async () => ({ documents: [target], case_version: 4, link_id: target.id, reusable_documents: [sourceB, sourceC, target], })); h.render({ documents: [], reusableDocuments: [sourceB, sourceC] }); let select = nodes(h.tree).find((node) => node.type === 'select'); assert.equal( nodes(select).filter((node) => node.type === 'option').length, 3, ); select.props.onChange({ target: { value: 'source-b' } }); h.render(); assert.ok( !nodes(h.tree).some( (node) => node.type === 'input' && node.props.type === 'file', ), ); assert.ok( !nodes(h.tree).some( (node) => node.type === 'input' && node.props.maxLength === 3000, ), ); assert.match( renderToStaticMarkup(h.tree), /Referência já registada: SYNTHETIC ref B/, ); nodes(h.tree) .find((node) => node.type === 'input' && node.props.type === 'checkbox') .props.onChange({ target: { checked: true } }); h.render(); await h.button('Reutilizar documento nesta pergunta').props.onClick(); await h.settle(); const data = h.calls[0].body; assert.equal(data.get('document_id'), 'shared-original'); assert.equal(data.get('reuse_link_id'), 'source-b'); assert.equal(data.get('source_reference'), null); assert.equal(data.get('file'), null); assert.equal(h.inventory[0].select, true); }); await test('editor publishes explicit attachment limits in the proposed version and clears unrelated metadata when type changes', () => { let form; const Editor = load( '../../resources/js/pages/desk/service-editor.tsx', { react: { useState: (value) => [value, () => {}] }, '@inertiajs/react': { Link: 'a', useForm(key, initial) { form ??= { data: initial, errors: {}, processing: false, setData(value) { this.data = value(this.data); }, }; return form; }, }, '@/pages/investment/shared': { Frame: 'main', card: '', input: '', button: '', }, '@/components/desk-service-history': { default: () => null }, }, {}, '\nexport { Editor };', ).Editor; const service = { code: 'synthetic', version: 1, definition: { title: 'SYNTHETIC', theme: 'Synthetic', profile: 'Investor', entity: 'Internal', result: 'Internal preparation', stage: 'E10', fields: { question: { label: 'SYNTHETIC support', type: 'text', required: false, }, }, }, }; const render = () => Editor({ service }); let tree = render(); const type = () => nodes(tree).find( (node) => node.type === 'select' && nodes(node).some( (child) => child.type === 'option' && child.props.value === 'attachment', ), ); type().props.onChange({ target: { value: 'attachment' } }); tree = render(); assert.equal(form.data.definition.fields.question.max_files, 1); assert.equal(form.data.definition.fields.question.max_size_kb, 10240); assert.equal(form.data.definition.fields.question.mime_types.length, 4); assert.equal(form.data.definition.fields.question.required, false); const size = nodes(tree).find( (node) => node.type === 'input' && node.props.max === 10240, ); size.props.onChange({ target: { value: '256' } }); tree = render(); assert.equal(form.data.definition.fields.question.max_size_kb, 256); assert.equal(form.data.confirm, false); assert.match(renderToStaticMarkup(tree), /Tamanho por ficheiro: 256 KiB/); type().props.onChange({ target: { value: 'text' } }); assert.equal(form.data.definition.fields.question.mime_types, undefined); assert.equal(form.data.definition.fields.question.max_files, undefined); assert.equal(service.definition.fields.question.type, 'text'); });