Files
Investidor-V2/tests/frontend/desk-attachments.test.mjs
T

570 lines
19 KiB
JavaScript

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