237 lines
16 KiB
JavaScript
237 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import fs from 'node:fs/promises';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { randomBytes, pbkdf2Sync, createCipheriv, createHmac } from 'node:crypto';
|
||
import { isSuppressed, reachedHalf, suppressionDuration } from '../src/utils/drawer-preference.ts';
|
||
import { serviceInquiryCopy } from '../src/data/service-inquiry.ts';
|
||
import { decryptPortalStream, encryptPortalStream, validatePortalPayload, portalAlgorithm } from '../src/utils/portal-crypto.mjs';
|
||
import { collectResource, resourceReferences, rewriteResources } from './portal-resources.mjs';
|
||
import { isFormACompletion } from '../src/utils/jotform-completion.ts';
|
||
import { portalLinkPresentation } from '../src/utils/portal-presentation.ts';
|
||
import {
|
||
PortalDownloadError,
|
||
fetchProtectedPortalResource,
|
||
portalDownloadErrorMessage,
|
||
portalRoleForStreamId,
|
||
protectedPortalResource,
|
||
recognizableDownloadFilename,
|
||
} from '../src/utils/portal-download.mjs';
|
||
import { applyRoleMappings, approvedMapping, summarizeMappings } from './portal-production.mjs';
|
||
|
||
test('completion envelope requires an exact approved origin, active frame window and string Form A ID', () => {
|
||
const source = {};
|
||
const check = (event, frame, active) => isFormACompletion(event, frame, active,
|
||
'https://form.jotform.com/262946078314059?formContext=consultation&language=en');
|
||
const data = { action: 'submission-completed', formID: '262946078314059' };
|
||
const event = { origin: 'https://form.jotform.com', source, data };
|
||
assert.equal(check(event, source, true), true);
|
||
assert.equal(check({ ...event, origin: 'https://hipaa-submit.jotform.com' }, source, true), true);
|
||
assert.equal(check({ ...event, data: JSON.stringify(data) }, source, true), true);
|
||
for (const change of [
|
||
{ origin: 'https://form.jotform.com.evil.invalid' }, { origin: 'https://hipaa-submit.jotform.com.evil.invalid' },
|
||
{ origin: 'https://other.jotform.com' }, { origin: 'null' }, { origin: 'http://form.jotform.com' },
|
||
{ source: {} }, { source: null }, { data: { ...data, formID: 'other' } },
|
||
{ data: { ...data, formID: 262946078314059 } }, { data: { ...data, action: 'load' } },
|
||
{ data: '{broken' }, { data: [] }, { data: null }, { data: 'x'.repeat(4097) },
|
||
]) assert.equal(check({ ...event, ...change }, source, true), false);
|
||
assert.equal(check(event, source, false), false);
|
||
assert.equal(check(event, null, true), false);
|
||
assert.equal(isFormACompletion(event, source, true, undefined), false);
|
||
assert.equal(isFormACompletion(event, source, true, 'broken'), false);
|
||
});
|
||
|
||
test('portal presentation translates only explicit button tokens/brand fills and contact protocols', () => {
|
||
assert.equal(portalLinkPresentation(['hs-button', 'untrusted'], '', 'https:'), 'portal-resource-button');
|
||
assert.equal(portalLinkPresentation([], 'rgb(255, 189, 89)', 'https:'), 'portal-resource-button');
|
||
assert.equal(portalLinkPresentation(['arbitrary'], 'url(https://example.invalid)', 'https:'), undefined);
|
||
assert.equal(portalLinkPresentation(['unrecognized-legacy'], '', 'https:', 'Operations Monthly Lineup Report - PM'), 'portal-resource-button');
|
||
assert.equal(portalLinkPresentation([], '', 'https:', 'Human Resources Daily Lineup Report - PM'), 'portal-resource-button');
|
||
assert.equal(portalLinkPresentation([], '', 'https:', 'Billing Daily Lineup Report - PM'), 'portal-resource-button');
|
||
assert.equal(portalLinkPresentation([], '', 'https:', 'Generic reports and documents'), undefined);
|
||
assert.equal(portalLinkPresentation([], '', 'https:', 'Monthly report'), undefined);
|
||
assert.equal(portalLinkPresentation([], '', 'mailto:', 'Operations Monthly Lineup Report - PM'), 'portal-contact-link');
|
||
assert.equal(portalLinkPresentation([], '', 'mailto:'), 'portal-contact-link');
|
||
assert.equal(portalLinkPresentation([], '', 'tel:'), 'portal-contact-link');
|
||
});
|
||
|
||
test('portal downloads use only the validated role and an in-memory authorization header', async () => {
|
||
assert.equal(portalRoleForStreamId('b'), 'staff');
|
||
assert.equal(portalRoleForStreamId('a'), 'management');
|
||
assert.equal(portalRoleForStreamId('fixture-staff'), undefined);
|
||
const origin = 'https://www.azinstitute4autism.com';
|
||
const staff = protectedPortalResource('/staff-resources/' + 'a'.repeat(64) + '.pdf', origin, "Staff's Safety & Training: Résumé (2026)!");
|
||
const management = protectedPortalResource('/management-resources/' + 'b'.repeat(64) + '.docx', origin, 'Management – Policies / Q&A #1');
|
||
assert.deepEqual(staff, {
|
||
role: 'staff', filename: 'a'.repeat(64) + '.pdf',
|
||
downloadFilename: 'Staffs-Safety-and-Training-Resume-2026.pdf',
|
||
url: origin + '/staff-resources/' + 'a'.repeat(64) + '.pdf',
|
||
});
|
||
assert.equal(management.downloadFilename, 'Management-Policies-Q-and-A-1.docx');
|
||
assert.equal(recognizableDownloadFilename('Client Check-In & Check-Out.xlsx', 'c'.repeat(64) + '.xlsx'), 'Client-Check-In-and-Check-Out.xlsx');
|
||
assert.equal(recognizableDownloadFilename('../../', 'd'.repeat(64) + '.pdf'), 'AIA-document.pdf');
|
||
assert.equal(recognizableDownloadFilename('No server extension', 'e'.repeat(64)), undefined);
|
||
assert.equal(protectedPortalResource('/staff-resources/nested/file.pdf', origin), undefined);
|
||
assert.equal(protectedPortalResource('/staff-resources/file.pdf?code=forbidden', origin), undefined);
|
||
assert.equal(protectedPortalResource('https://other.invalid/staff-resources/file.pdf', origin), undefined);
|
||
|
||
const code = 'synthetic browser memory only';
|
||
let captured;
|
||
const result = await fetchProtectedPortalResource(staff, { role: 'staff', code }, async (url, options) => {
|
||
captured = { url, options };
|
||
return new Response('synthetic document', {
|
||
// nginx may expose the hash name; the approved label remains authoritative.
|
||
headers: { 'content-disposition': `attachment; filename="${staff.filename}"` },
|
||
});
|
||
});
|
||
assert.equal(captured.url, staff.url);
|
||
assert.equal(captured.url.includes(code), false);
|
||
assert.equal(captured.options.method, 'GET');
|
||
assert.equal(captured.options.credentials, 'omit');
|
||
assert.equal(captured.options.cache, 'no-store');
|
||
assert.equal(captured.options.redirect, 'error');
|
||
assert.equal(captured.options.referrerPolicy, 'no-referrer');
|
||
assert.match(captured.options.headers.Authorization, /^Basic /);
|
||
assert.equal(result.filename, 'Staffs-Safety-and-Training-Resume-2026.pdf');
|
||
assert.equal(await result.blob.text(), 'synthetic document');
|
||
|
||
for (const [resource, credential] of [
|
||
[management, { role: 'staff', code }],
|
||
[staff, { role: 'management', code }],
|
||
]) {
|
||
let called = false;
|
||
await assert.rejects(
|
||
fetchProtectedPortalResource(resource, credential, async () => { called = true; return new Response(); }),
|
||
(error) => error instanceof PortalDownloadError && error.code === 'role',
|
||
);
|
||
assert.equal(called, false, 'cross-role credentials are never transmitted');
|
||
}
|
||
await assert.rejects(
|
||
fetchProtectedPortalResource(staff, { role: 'staff', code }, async () => new Response('', { status: 401 })),
|
||
(error) => error instanceof PortalDownloadError && error.code === 'unauthorized',
|
||
);
|
||
assert.match(portalDownloadErrorMessage(new PortalDownloadError('unauthorized')), /session could not authorize/);
|
||
assert.match(portalDownloadErrorMessage(new PortalDownloadError('role')), /not available from this portal/);
|
||
});
|
||
|
||
test('staff template presentation follows sanitized headings, not original HubSpot classes', async () => {
|
||
const renderer = await fs.readFile('src/scripts/employee-portal.ts', 'utf8');
|
||
const css = await fs.readFile('src/components/pages/EmployeePortal.astro', 'utf8');
|
||
assert.match(renderer, /enhanceStaffPortalPresentation\(output\)/);
|
||
assert.match(renderer, /'management templates'/);
|
||
assert.match(renderer, /fragment\.querySelectorAll<HTMLAnchorElement>\('a\[href\]'\)/);
|
||
assert.match(renderer, /templatesList\?\.classList\.add\('portal-template-grid'\)/);
|
||
assert.match(css, /grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
|
||
assert.match(css, /@media \(max-width: 767px\)/);
|
||
assert.doesNotMatch(css, /ul\.portal-template-grid a\.portal-resource-button/);
|
||
assert.match(renderer, /items\.every\(\(item\) => item\.querySelector\('a\.portal-resource-button'\)\)/);
|
||
assert.match(renderer, /list\.classList\.add\('portal-resource-list'\)/);
|
||
assert.match(css, /\.portal-resource-list > li::marker\)\s*\{\s*content: none;/);
|
||
assert.match(css, /\.employee-portal__content:focus\s*\{ outline: none;/);
|
||
assert.match(css, /input:focus-visible\s*\{ outline: 3px solid/);
|
||
assert.match(css, /:global\(a:focus-visible\)\s*\{ outline: 3px solid/);
|
||
});
|
||
|
||
test('global preference validates expiry, corrupted values and clock anomalies', () => {
|
||
const now = 10000000000;
|
||
for (const raw of [null, '', 'NaN', '-1', '1.5', 'Infinity', String(now), String(now - 1), String(now + suppressionDuration + 1)]) assert.equal(isSuppressed(raw, now), false);
|
||
assert.equal(isSuppressed(String(now + suppressionDuration), now), true);
|
||
assert.equal(isSuppressed(String(now + 1), now), true);
|
||
});
|
||
test('half-scroll handles short, restored, resized and overscrolled pages', () => {
|
||
assert.equal(reachedHalf(0, 500, 800), false);
|
||
assert.equal(reachedHalf(100, 800, 800), false);
|
||
assert.equal(reachedHalf(499, 1800, 800), false);
|
||
assert.equal(reachedHalf(500, 1800, 800), true);
|
||
assert.equal(reachedHalf(-10, 1800, 800), false);
|
||
assert.equal(reachedHalf(500, 2800, 800), false);
|
||
});
|
||
test('four service configurations are bilingual without adding an Academy route', async () => {
|
||
assert.equal(Object.keys(serviceInquiryCopy).length, 4);
|
||
for (const variants of Object.values(serviceInquiryCopy)) for (const copy of Object.values(variants)) {
|
||
assert.ok(copy.heading && copy.introduction); assert.ok(!copy.introduction.includes('—'));
|
||
}
|
||
const { formAIframePlacements } = await import('../src/utils/form-integration.ts');
|
||
assert.equal(formAIframePlacements.filter(p => p.context === 'consultation').length, 9);
|
||
assert.ok(!formAIframePlacements.some((p) => p.route.includes('preparatory')));
|
||
});
|
||
test('legacy independent crypto fixture and two-view separation round-trip', async () => {
|
||
const pass = randomBytes(32).toString('base64'), other = randomBytes(32).toString('base64');
|
||
const salt = randomBytes(16), iv = randomBytes(16), keys = pbkdf2Sync(pass, salt, 200000, 64, 'sha256');
|
||
const cipher = createCipheriv('aes-256-cbc', keys.subarray(0, 32), iv);
|
||
const ct = Buffer.concat([cipher.update('<h2>Synthetic staff</h2>'), cipher.final()]);
|
||
const mac = createHmac('sha256', keys.subarray(32)).update(Buffer.concat([iv, ct])).digest();
|
||
const stream = { id: 'synthetic-staff', iter: 200000, salt: salt.toString('base64'), iv: iv.toString('base64'), hmac: mac.toString('base64'), ct: ct.toString('base64') };
|
||
const second = await encryptPortalStream('synthetic-management', '<h2>Synthetic management</h2>', other);
|
||
validatePortalPayload({ alg: portalAlgorithm, streams: [stream, second] });
|
||
assert.equal(await decryptPortalStream(stream, pass), '<h2>Synthetic staff</h2>');
|
||
assert.equal(await decryptPortalStream(second, pass), null);
|
||
assert.equal(await decryptPortalStream(stream, other), null);
|
||
assert.equal(await decryptPortalStream(second, other), '<h2>Synthetic management</h2>');
|
||
assert.equal(await decryptPortalStream({ ...stream, hmac: randomBytes(32).toString('base64') }, pass), null);
|
||
assert.throws(() => validatePortalPayload({ alg: portalAlgorithm, streams: [stream, stream] }));
|
||
});
|
||
test('resource extraction reconciles duplicate views and rewrites only approved implemented links', () => {
|
||
const html = '<h2>Synthetic</h2><a href="/hubfs/example.pdf">Example</a><a href="/hubfs/example.pdf">Again</a><a href="https://unapproved.invalid/form">Other</a>';
|
||
const refs = resourceReferences(html, 'synthetic');
|
||
assert.equal(refs.length, 2); assert.equal(new Set(refs.map((r) => r.sourceUrl)).size, 1);
|
||
const record = { references: refs, rewriteApproved: true, mappingApproved: true, deliveryPath: '/staff-resources/example.pdf', classification: 'STAFF-INTERNAL' };
|
||
assert.equal(rewriteResources(html, [record], 'staff').html, html);
|
||
const result = rewriteResources(html, [{ ...record, accessPolicyApproved: true }], 'staff');
|
||
assert.equal(result.count, 2); assert.ok(result.html.includes('>Again</a>')); assert.ok(result.html.includes('https://unapproved.invalid/form'));
|
||
assert.equal(rewriteResources(html, []).html, html);
|
||
});
|
||
test('role-based resource mapping separates source approval from later server verification', () => {
|
||
const record = {
|
||
sourceUrl: 'https://www.azinstitute4autism.com/hubfs/guide.pdf', originalPath: '/hubfs/guide.pdf',
|
||
localFile: 'a'.repeat(64), references: [{ view: 'staff-stream' }, { view: 'management-stream' }],
|
||
};
|
||
const roles = { staff: 'staff-stream', management: 'management-stream' };
|
||
const summary = summarizeMappings({ resources: [record] }, roles);
|
||
assert.equal(summary.shared, 1); assert.equal(summary.ambiguous, 0);
|
||
const mapped = applyRoleMappings({ resources: [record] }, roles, { approveShared: true });
|
||
assert.equal(mapped.resources[0].installMappings.length, 2);
|
||
for (const role of ['staff', 'management']) {
|
||
const mapping = approvedMapping(mapped.resources[0], role);
|
||
assert.ok(mapping); assert.equal(mapping.destinationInstalled, false);
|
||
assert.equal(mapping.destinationAccessVerified, false);
|
||
}
|
||
});
|
||
test('private collector validates binary, reuses hash, rejects errors and unapproved redirects', async () => {
|
||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-private-test-'));
|
||
try {
|
||
const resource = { sourceUrl: 'https://www.azinstitute4autism.com/hubfs/synthetic.pdf' };
|
||
const good = await collectResource(resource, dir, undefined, async () => new Response('%PDF-1.7\nSynthetic', { headers: { 'content-type': 'application/pdf' } }));
|
||
assert.equal(good.downloadStatus, 'downloaded');
|
||
const again = await collectResource(good, dir, undefined, () => { throw new Error('must not fetch'); });
|
||
assert.equal(again.downloadStatus, 'verified-existing');
|
||
await assert.rejects(collectResource(resource, dir, undefined, async () => new Response('<html>Error</html>', { headers: { 'content-type': 'application/pdf' } })));
|
||
await assert.rejects(collectResource(resource, dir, undefined, async () => new Response('', { status: 302, headers: { location: 'https://unapproved.invalid/hubfs/file' } })));
|
||
} finally { await fs.rm(dir, { recursive: true }); }
|
||
});
|
||
|
||
test('portal input isolates local preview from explicit indexing production payloads', async () => {
|
||
const { readPortalInput } = await import('../src/utils/portal-input.mjs');
|
||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-portal-input-'));
|
||
try {
|
||
const pass = randomBytes(24).toString('base64');
|
||
const streams = await Promise.all(['one','two'].map((id) => encryptPortalStream(id, '<p>Synthetic</p>', pass)));
|
||
const file = path.join(dir, 'fixture.json'); await fs.writeFile(file, JSON.stringify({ alg: portalAlgorithm, streams }), { mode: 0o600 });
|
||
const options = { localApproved: 'true', indexing: 'false', localFile: file };
|
||
assert.ok(readPortalInput(options));
|
||
// Form modes cannot substitute for the independent private-input approval.
|
||
for (const mode of ['live', 'off', 'mock']) {
|
||
assert.ok(readPortalInput({ ...options, mode }));
|
||
assert.equal(readPortalInput({ indexing: 'false', localFile: file, mode }), undefined);
|
||
}
|
||
for (const changed of [{ localApproved: undefined }, { localApproved: 'false' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
|
||
assert.throws(() => readPortalInput({ ...options, indexing: 'true' }), /publication mode/);
|
||
assert.ok(readPortalInput({ productionApproved: 'true', indexing: 'true', productionFile: file }));
|
||
assert.throws(() => readPortalInput({ productionApproved: 'true', indexing: 'true' }), /required/);
|
||
assert.throws(() => readPortalInput({ localApproved: 'true', productionApproved: 'true', indexing: 'false', localFile: file, productionFile: file }), /either/);
|
||
await fs.writeFile(file, '<h1>Not encrypted</h1>');
|
||
assert.throws(() => readPortalInput(options), /missing or malformed/);
|
||
} finally { await fs.rm(dir, { recursive: true }); }
|
||
});
|