Files
aia-website/www/tools/service-portal.test.mjs
T
DeCentN2Madness 8dff6a5ad0 style(portal): refine resource links, list markers and focus styling
Remove forced link underlines and suppress bullets and numbering only on
staff lists whose items contain resource buttons. Keep the responsive
Management Templates grid while allowing its buttons to use their natural
size instead of stretching to fill each cell.

Remove the outline around the unlocked content container while preserving
programmatic focus and visible keyboard focus on inputs and resource links.
Update the presentation regression assertions for these styling choices.

Validation: Astro build (128 pages), link audit (zero broken links), and
10 focused service/portal tests pass.
2026-11-01 14:45:13 -07:00

146 lines
11 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';
test('completion envelope requires exact 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, data: JSON.stringify(data) }, source, true), true);
for (const change of [
{ origin: 'https://form.jotform.com.evil.invalid' }, { 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('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, destinationImplemented: true, destinationVerified: true, deliveryPath: '/staff-resources/example.pdf', classification: 'STAFF-INTERNAL' };
assert.throws(() => rewriteResources(html, [record]));
const result = rewriteResources(html, [{ ...record, destinationAccessControlled: true }]);
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('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 requires every local gate and rejects missing or malformed ciphertext', 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 = { approved: 'true', indexing: 'false', 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', file, mode }), undefined);
}
for (const changed of [{ approved: undefined }, { approved: 'false' }, { indexing: 'true' }, { indexing: undefined }, { file: '/missing-fixture' }]) assert.equal(readPortalInput({ ...options, ...changed }), undefined);
await fs.writeFile(file, '<h1>Not encrypted</h1>'); assert.equal(readPortalInput(options), undefined);
} finally { await fs.rm(dir, { recursive: true }); }
});