275 lines
8.3 KiB
JavaScript
275 lines
8.3 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
const base = 'http://127.0.0.1:8005';
|
|
function client() {
|
|
const cookies = new Map();
|
|
let csrf;
|
|
let version;
|
|
async function request(path, method = 'GET', data, inertia = true) {
|
|
const response = await fetch(base + path, {
|
|
method,
|
|
redirect: 'manual',
|
|
headers: {
|
|
Accept: inertia ? 'application/json' : 'text/html',
|
|
Cookie: [...cookies].map(([k, v]) => `${k}=${v}`).join('; '),
|
|
...(inertia
|
|
? {
|
|
'X-Inertia': 'true',
|
|
'X-Inertia-Version': version ?? '',
|
|
}
|
|
: {}),
|
|
...(data
|
|
? {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrf,
|
|
}
|
|
: {}),
|
|
},
|
|
...(data ? { body: JSON.stringify(data) } : {}),
|
|
});
|
|
for (const cookie of response.headers.getSetCookie()) {
|
|
const pair = cookie.split(';', 1)[0],
|
|
i = pair.indexOf('=');
|
|
cookies.set(pair.slice(0, i), pair.slice(i + 1));
|
|
}
|
|
const text = await response.text();
|
|
return {
|
|
status: response.status,
|
|
location: response.headers.get('location'),
|
|
text,
|
|
page: response.headers
|
|
.get('content-type')
|
|
?.includes('application/json')
|
|
? JSON.parse(text)
|
|
: null,
|
|
};
|
|
}
|
|
return {
|
|
request,
|
|
async enter(role) {
|
|
let page = await request('/avaliar', 'GET', undefined, false);
|
|
assert.equal(page.status, 200);
|
|
csrf = page.text.match(/name="_token" value="([^"]+)"/)[1];
|
|
assert.equal(
|
|
(await request('/avaliar/entrar', 'POST', { role }, false))
|
|
.status,
|
|
302,
|
|
);
|
|
page = await request('/avaliar', 'GET', undefined, false);
|
|
csrf = page.text.match(/name="_token" value="([^"]+)"/)[1];
|
|
const dashboard = await request(
|
|
'/dashboard',
|
|
'GET',
|
|
undefined,
|
|
false,
|
|
);
|
|
const encoded = dashboard.text.match(
|
|
/<script data-page="app" type="application\/json">([\s\S]*?)<\/script>/,
|
|
);
|
|
assert.ok(encoded, 'Expected Inertia bootstrap data');
|
|
version = JSON.parse(encoded[1]).version;
|
|
},
|
|
};
|
|
}
|
|
|
|
const planner = client(),
|
|
responsible = client(),
|
|
reviewer = client();
|
|
await planner.enter('admin');
|
|
await responsible.enter('procurement');
|
|
await reviewer.enter('finance');
|
|
const path = '/fornecedores/avaliacoes/1';
|
|
const parent = await planner.request(path);
|
|
assert.equal(parent.status, 200);
|
|
assert.equal(parent.page.props.evaluation.status, 'approved');
|
|
assert.equal(parent.page.props.improvements.ready, true);
|
|
const original = JSON.stringify(parent.page.props.evaluation);
|
|
const responsiblePage = await responsible.request(path);
|
|
const responsibleId = responsiblePage.page.props.auth.user.id;
|
|
const title = 'QA — medida de melhoria do percurso integrado';
|
|
const plan = {
|
|
title,
|
|
description:
|
|
'Exemplo fictício: completar a referência documental da prestação avaliada.',
|
|
responsible_id: responsibleId,
|
|
due_on: '2026-10-29',
|
|
operation_key: randomUUID(),
|
|
};
|
|
const created = await planner.request(path + '/medidas', 'POST', plan);
|
|
assert.equal(created.status, 302);
|
|
assert.equal(
|
|
(await planner.request(path + '/medidas', 'POST', plan)).status,
|
|
302,
|
|
);
|
|
let page = await planner.request(path);
|
|
const measure = page.page.props.improvements.records
|
|
.filter((m) => m.title === title)
|
|
.sort((a, b) => b.id - a.id)[0];
|
|
assert.ok(measure);
|
|
const url = path + '/medidas/' + measure.id;
|
|
assert.equal(
|
|
page.page.props.improvements.records.filter((m) => m.id === measure.id)
|
|
.length,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await responsible.request(url + '/submit', 'POST', {
|
|
version: 1,
|
|
result: 'Resultado sem evidência.',
|
|
})
|
|
).status,
|
|
422,
|
|
);
|
|
let evidence = {
|
|
result: 'Resultado fictício preparado para conferência.',
|
|
evidence:
|
|
'Referência QA scripts/supplier-improvement-check.mjs; sem prestação real.',
|
|
};
|
|
assert.equal(
|
|
(
|
|
await responsible.request(url + '/submit', 'POST', {
|
|
version: 1,
|
|
...evidence,
|
|
})
|
|
).status,
|
|
302,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await responsible.request(url + '/close', 'POST', {
|
|
version: 2,
|
|
reason: 'Tentativa do próprio responsável.',
|
|
})
|
|
).status,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await planner.request(url + '/close', 'POST', {
|
|
version: 2,
|
|
reason: 'Tentativa do autor do plano.',
|
|
})
|
|
).status,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await reviewer.request(url + '/return', 'POST', {
|
|
version: 2,
|
|
reason: 'Precisar a referência da evidência do exemplo.',
|
|
})
|
|
).status,
|
|
302,
|
|
);
|
|
const replanned = {
|
|
...plan,
|
|
version: 3,
|
|
due_on: '2026-11-15',
|
|
reason: 'Reagendamento fictício documentado após a devolução.',
|
|
};
|
|
delete replanned.operation_key;
|
|
assert.equal(
|
|
(await planner.request(url + '/plan', 'POST', replanned)).status,
|
|
302,
|
|
);
|
|
assert.equal(
|
|
(await planner.request(url + '/plan', 'POST', replanned)).status,
|
|
422,
|
|
);
|
|
evidence = {
|
|
...evidence,
|
|
evidence: 'Referência QA completada; exemplo sem documento comercial real.',
|
|
};
|
|
assert.equal(
|
|
(
|
|
await responsible.request(url + '/progress', 'POST', {
|
|
version: 4,
|
|
...evidence,
|
|
})
|
|
).status,
|
|
302,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await responsible.request(url + '/submit', 'POST', {
|
|
version: 5,
|
|
...evidence,
|
|
})
|
|
).status,
|
|
302,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await reviewer.request(url + '/close', 'POST', {
|
|
version: 6,
|
|
reason: 'Resultado e evidência do exemplo conferidos independentemente.',
|
|
})
|
|
).status,
|
|
302,
|
|
);
|
|
assert.equal(
|
|
(await planner.request(url + '/plan', 'POST', { ...replanned, version: 7 }))
|
|
.status,
|
|
409,
|
|
);
|
|
page = await planner.request(path);
|
|
const closed = page.page.props.improvements.records.find(
|
|
(m) => m.id === measure.id,
|
|
);
|
|
assert.equal(closed.status, 'closed');
|
|
assert.equal(closed.version, 7);
|
|
assert.equal(closed.due_on, '2026-11-15');
|
|
assert.equal(JSON.stringify(page.page.props.evaluation), original);
|
|
const actions = page.page.props.improvements.history.data.filter(
|
|
(e) => e.after.id === measure.id,
|
|
);
|
|
assert.equal(actions.length, 7);
|
|
assert.ok(
|
|
actions
|
|
.find((e) => e.action === 'supplier_improvement.plan')
|
|
.after.change_reason.includes('Reagendamento'),
|
|
);
|
|
const report = await planner.request(path + '/relatorio');
|
|
assert.equal(report.status, 200);
|
|
const printed = report.page.props.improvements.records.find(
|
|
(m) => m.id === measure.id,
|
|
);
|
|
assert.equal(printed.status, 'closed');
|
|
assert.equal(printed.canPlan, false);
|
|
assert.equal(printed.canExecute, false);
|
|
assert.equal(printed.canReview, false);
|
|
for (const c of [planner, responsible, reviewer])
|
|
assert.equal((await c.request('/logout', 'POST', {}, false)).status, 302);
|
|
await fs.writeFile(
|
|
'docs/proofs/supplier-improvements-http.json',
|
|
JSON.stringify(
|
|
{
|
|
checked_at: new Date().toISOString(),
|
|
base,
|
|
evaluation_path: path,
|
|
measure_id: measure.id,
|
|
create_retry_idempotent: true,
|
|
missing_evidence_refused: true,
|
|
self_closure_refused: true,
|
|
planner_closure_refused: true,
|
|
return_replan_correct_submit_close: true,
|
|
stale_plan_refused: true,
|
|
closed_edit_refused: true,
|
|
parent_evaluation_unchanged: true,
|
|
history_events: actions.length,
|
|
report_includes_closed_measure: true,
|
|
synthetic_example_only: true,
|
|
financial_commands: 0,
|
|
visual_review: false,
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
);
|
|
console.log(
|
|
'Supplier improvement HTTP flow passed, including independent closure and preserved approved evaluation.',
|
|
);
|