92 lines
8.8 KiB
TypeScript
92 lines
8.8 KiB
TypeScript
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import { JSDOM } from 'jsdom';
|
||
import { describe, expect, it, vi } from 'vitest';
|
||
import { buildSecurityViewReportHtml, type SecurityViewReadPort } from '../../src/reporting/security-view-report';
|
||
|
||
vi.mock('../../src/styles/ui-tokens.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/ui-tokens.css', 'utf8') }));
|
||
vi.mock('../../src/styles/tokens.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/tokens.css', 'utf8') }));
|
||
vi.mock('../../src/styles/base.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/base.css', 'utf8') }));
|
||
vi.mock('../../src/styles/components.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/components.css', 'utf8') }));
|
||
vi.mock('../../src/styles/datahub.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/datahub.css', 'utf8') }));
|
||
vi.mock('../../src/styles/client-security.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/client-security.css', 'utf8') }));
|
||
vi.mock('../../src/styles/policy-analysis.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/policy-analysis.css', 'utf8') }));
|
||
vi.mock('../../src/styles/security-scoring.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/security-scoring.css', 'utf8') }));
|
||
vi.mock('../../src/styles/functional-icons.css?raw', async () => ({ default: (await import('node:fs')).readFileSync('src/styles/functional-icons.css', 'utf8') }));
|
||
|
||
const snapshot = Object.freeze({ markup: '<div class="client-security-panel"><details><summary>Native finding</summary>Original source text</details></div>',
|
||
width: 1910, theme: 'light' as const, client: 'Synthetic <client>', assessments: 'Synthetic assessment', generatedAt: '12.10.2026',
|
||
versions: [['Datenstand', 'Revision 1']] as const });
|
||
const port: SecurityViewReadPort = { read: () => snapshot };
|
||
describe('Standalone Security view renderer', () => {
|
||
it('formats separate assessments and an exact reconciled German overview without altering published counts', () => {
|
||
const distribution = { total: 10, domains: [{ label: 'System <demo>', count: 7 }, { label: 'Identity', count: 3 }, { label: 'Empty', count: 0 }],
|
||
categories: [{ label: 'Category A', count: 4 }, { label: 'Category B', count: 6 }, { label: 'Empty', count: 0 }] };
|
||
const before = JSON.stringify(distribution);
|
||
const html = buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution,
|
||
assessmentDetails: [{ name: 'Assessment 2026-07-28', effectiveAt: '2026-07-28' }, { name: 'Synthetic comparison', effectiveAt: '2026-10-12T23:00:00Z' }],
|
||
environmentMarkup: '<section class="client-security-hardfacts">Original environment</section>' }) });
|
||
const dom = new JSDOM(html.replace(/<style>[\s\S]*?<\/style>/g, ''));
|
||
const doc = dom.window.document;
|
||
expect(doc.querySelector('h1')?.textContent).toBe('Security Report');
|
||
expect([...doc.querySelectorAll('.security-export-assessment')].map(item => item.textContent)).toEqual(['Assessment · 28.07.2026', 'Assessment: Synthetic comparison · 13.10.2026']);
|
||
expect(doc.querySelector('.security-export-summary')?.textContent).toBe('Die 10 Abweichungen verteilen sich auf 2 Security Domains und 2 Problemfelder. Den größten Anteil trägt System <demo> (7, 70 %); das am stärksten belegte Problemfeld ist Category B (6).');
|
||
expect(html).not.toContain('Vollständige Security-Ansicht');
|
||
expect(html.indexOf('Original environment')).toBeLessThan(html.indexOf(snapshot.markup));
|
||
expect(JSON.stringify(distribution)).toBe(before);
|
||
dom.window.close();
|
||
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution: { ...distribution, total: 11 } }) })).toThrow('reconcile');
|
||
expect(buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution: { total: 0, domains: [], categories: [] } }) })).toContain('keine Abweichungen');
|
||
expect(buildSecurityViewReportHtml({ read: () => ({ ...snapshot, assessmentDetails: [{ name: 'Synthetic invalid date', effectiveAt: '2026-02-31' }] }) })).toContain('Datum nicht verfügbar');
|
||
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, environmentMarkup: '<img onerror="bad()">' }) })).toThrow('active content');
|
||
});
|
||
it.each(['light', 'dark'] as const)('retains native script-free category navigation and independent maximizable popups (%s)', theme => {
|
||
const markup = '<a href="#security-category-synthetic">Show synthetic category</a><div style="height:1400px">Synthetic overview</div><details class="client-security-group" id="security-category-synthetic"><summary>Synthetic category</summary><div class="client-security-group-content">Original synthetic finding</div></details><button popovertarget="synthetic-popup">Results</button><div style="height:1400px">Synthetic footer</div>';
|
||
const popups = '<section popover="auto" id="synthetic-popup" class="security-export-popup" role="dialog" aria-label="Synthetic results"><header><h2>Synthetic results</h2><button class="btn" popovertarget="synthetic-popup" popovertargetaction="hide" aria-label="Close synthetic results">×</button></header><div class="security-export-popup-body"><dl>' + Array.from({ length: 9 }, (_, i) => `<div><dt>Fact ${i}</dt><dd>Original ${i}</dd></div>`).join('') + '</dl><div style="height:1600px">Synthetic long evidence</div><p>Evidence end</p></div></section>';
|
||
const html = buildSecurityViewReportHtml({ read: () => ({ ...snapshot, theme, markup, popups }) });
|
||
expect(html).not.toMatch(/<script|\son[a-z]+=/i);
|
||
expect(html).toContain('href="#security-category-content-synthetic"');
|
||
expect(html).toContain('<div class="client-security-group-content" id="security-category-content-synthetic">');
|
||
expect(html).toContain('overscroll-behavior:contain');
|
||
expect(html).toContain('100vw / 2');
|
||
expect(html).toContain('max-width:100%;margin:0 auto');
|
||
expect(html).not.toContain('zoom:');
|
||
expect(html).toContain('font-size:var(--type-table-body-size)');
|
||
expect(html).toContain('@container security-report (max-width:1800px)');
|
||
expect(html).toContain('.client-security-map-scroll>svg{width:1280px;max-width:none}');
|
||
expect(html).toContain('html,body{background:var(--surface-panel)}');
|
||
expect(html).toContain('grid-template-columns:repeat(5,minmax(0,1fr))');
|
||
expect(html).toContain('aria-label="Maximize popup"');
|
||
expect(html.indexOf('aria-label="Maximize popup"')).toBeLessThan(html.indexOf('aria-label="Close synthetic results"'));
|
||
expect(html).toContain('Original synthetic finding');
|
||
const dom = new JSDOM(html.replace(/<style>[\s\S]*?<\/style>/g, ''));
|
||
const doc = dom.window.document;
|
||
const input = doc.querySelector<HTMLInputElement>('.security-export-maximize input')!;
|
||
input.click(); expect(input.checked).toBe(true);
|
||
input.click(); expect(input.checked).toBe(false);
|
||
const link = doc.querySelector<HTMLAnchorElement>('a')!;
|
||
const target = doc.getElementById(link.hash.slice(1))!;
|
||
expect(target.closest('details')?.id).toBe('security-category-synthetic');
|
||
expect(target.textContent).toBe('Original synthetic finding');
|
||
expect(doc.querySelector('.security-export-popup>header')?.lastElementChild?.textContent).toBe('×');
|
||
dom.window.close();
|
||
if (process.env.ASSESSOR_REPORT_PREVIEW_DIR) writeFileSync(`${process.env.ASSESSOR_REPORT_PREVIEW_DIR}/security-${theme}.html`, html);
|
||
});
|
||
it('embeds the unchanged application styles, local assets and frozen authorized snapshot with no script or external resource', () => {
|
||
const before = JSON.stringify(snapshot); const html = buildSecurityViewReportHtml(port);
|
||
for (const file of ['ui-tokens', 'tokens', 'base', 'components', 'datahub', 'client-security', 'policy-analysis', 'security-scoring', 'functional-icons']) {
|
||
expect(html.includes(readFileSync(`src/styles/${file}.css`, 'utf8'))).toBe(true);
|
||
}
|
||
expect(html).toContain(snapshot.markup); expect(html).toContain('Synthetic <client>');
|
||
expect(html).toContain('width:min(1910px,calc(100vw / 2))'); expect(html).toContain('VERTRAULICH');
|
||
expect(html).toContain('data:font/woff2;base64,'); expect(html).toContain('data:image/png;base64,');
|
||
expect(html).not.toMatch(/<script|<(?:img|link)\b[^>]*(?:src|href)=["']https?:/i);
|
||
expect(JSON.stringify(snapshot)).toBe(before);
|
||
});
|
||
it('rejects an unavailable width and active source markup instead of creating a partial or executable export', () => {
|
||
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, width: 0 }) })).toThrow('measured width');
|
||
for (const markup of ['<script>malicious()</script>', '<img onerror="malicious()">', '<a href="javascript:malicious()">bad</a>']) {
|
||
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, markup }) })).toThrow('active content');
|
||
}
|
||
});
|
||
});
|