Files
Converge/scripts/test-report-leak.mjs
Chris Olson d5c77543e8 Initial commit: Converge portfolio rationalization app
Next.js app for mapping a customer's competitive tool landscape onto our
portfolio and building the consolidation business case: assessment
workbench, customer report and Word export, product catalog, competitive
knowledge base, dossier import, shareable landscape questionnaires with a
review inbox, tool-footprint and business-unit value analysis, and an
implementation timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 11:48:42 -04:00

80 lines
4.0 KiB
JavaScript

/**
* The account dossier holds internal pricing strategy, competitive risk and
* personal data. None of it may reach a customer deliverable.
*
* Self-contained: builds its own assessment, imports the sample dossier through
* the real endpoint, asserts on the redacted projection and the generated Word
* file, then removes itself.
*
* (The rendered report page is verified in a browser; its server-rendered shell
* carries no data, so asserting on that HTML would pass vacuously.)
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
const B = 'http://127.0.0.1:3500';
const json = (u, o) => fetch(B + u, o).then(r => r.json());
const send = (u, m, b) => json(u, { method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
let pass = 0, fail = 0;
const t = (n, ok, x = '') => { ok ? (pass++, console.log(' PASS', n)) : (fail++, console.log(' FAIL', n, x)); };
const text = readFileSync(new URL('./fixtures/dossier-sample.txt', import.meta.url), 'utf8');
const SOURCE = 'https://docs.google.com/document/d/LEAKTEST/edit';
const a = await send('/api/assessments', 'POST', { customerName: 'Redaction Test Co' });
try {
const imported = await send('/api/assessments/x/intel', 'POST', { text, sourceUrl: SOURCE });
t('dossier parsed for the test', imported.proposals.length > 0 && !!imported.intel.company);
a.accountIntel = imported.intel;
a.candidates = imported.proposals.slice(0, 3).map((p, i) => ({
id: `c-${i}`, incumbentVendor: p.vendor, incumbentProduct: p.product, domain: p.domain,
productIds: p.productIds, annualSpend: 500000, units: 0, unitLabel: '', renewalDate: '',
capability: p.capability, switchEase: p.switchEase, timeToRoiMonths: p.timeToRoiMonths,
savingsPct: p.savingsPct, migrationCost: 50000, strategicValue: 3,
risks: '', notes: `From account dossier — ${p.evidence}`, included: true,
}));
await send(`/api/assessments/${a.id}`, 'PUT', a);
const full = await json(`/api/assessments/${a.id}`);
t('stored record keeps the dossier', !!full.accountIntel);
t('stored record keeps internal notes', full.candidates.every(c => c.notes));
const safe = await json(`/api/assessments/${a.id}/report`);
t('projection drops the dossier', safe.accountIntel === null);
t('projection drops internal notes', safe.candidates.every(c => c.notes === ''));
t('projection keeps every candidate', safe.candidates.length === full.candidates.length);
t('projection keeps the mappings', safe.candidates.every((c, i) => c.productIds.join() === full.candidates[i].productIds.join()));
t('projection keeps the spend figures', safe.candidates.every((c, i) => c.annualSpend === full.candidates[i].annualSpend));
// A .docx is a ZIP: the bytes must be inflated or a text search passes vacuously.
const buf = Buffer.from(await (await fetch(`${B}/api/assessments/${a.id}/docx`)).arrayBuffer());
const tmp = new URL('./.leak-test.docx', import.meta.url);
writeFileSync(tmp, buf);
const docx = execFileSync('unzip', ['-p', tmp.pathname, 'word/document.xml'], { maxBuffer: 32e6 }).toString('utf8');
t('Word file is a readable docx', docx.includes('<w:document') && docx.length > 2000);
const safeText = JSON.stringify(safe);
const secrets = [
SOURCE, 'docs.google.com', 'Tier 1 / Strategic', 'Renewal Risk', 'Intelligence Score',
'TechDirector', 'Shelfware', 'Tech Debt Signals', 'AM-SOUTHEAST', 'From account dossier',
'Everpure', 'Workload Distribution',
];
for (const s of secrets) {
const label = s.length > 30 ? s.slice(0, 30) + '…' : s;
t(`projection omits "${label}"`, !safeText.includes(s));
t(`Word omits "${label}"`, !docx.includes(s));
}
t('Word still names the customer', docx.includes('Redaction Test Co'));
t('Word still carries a mapped product', /Avi Load Balancer|VCF Networking|Layer7/.test(docx));
execFileSync('rm', ['-f', tmp.pathname]);
} finally {
await fetch(`${B}/api/assessments/${a.id}`, { method: 'DELETE' });
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);