295 lines
14 KiB
TypeScript
295 lines
14 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { buildManagementOverviewHtml } from '../../src/exports/management-overview-report';
|
||
import type { ReportProjection } from '../../src/domain/report-projection';
|
||
import { validAiOverviewResult } from '../fixtures/ai-overview';
|
||
|
||
const ai = () => ({ brief: validAiOverviewResult().managementBrief!, generatedAt: '2026-09-27T09:00:00.000Z' });
|
||
|
||
function projection(overrides: Partial<ReportProjection> = {}): ReportProjection {
|
||
return {
|
||
version: '1.1.0',
|
||
generatedAt: '2026-09-18T10:00:00.000Z',
|
||
project: { id: 'synthetic-project', name: 'Synthetic GmbH' },
|
||
assessmentId: 'synthetic-assessment',
|
||
environment: [{ key: 'Domain', value: 'synthetic.example' }, { key: 'Domain Controller', value: undefined }],
|
||
overallScore: 62,
|
||
scoreSources: [],
|
||
domains: [],
|
||
priorities: [],
|
||
coverage: {
|
||
total: 0, assessed: 0, deviations: 0, passed: 0, notRun: 0,
|
||
notApplicable: 0, executionErrors: 0, unknown: 0, perSource: []
|
||
},
|
||
inventory: { bySeverity: {} as never, byOutcome: {} as never, byDomain: {}, bySource: {} },
|
||
findings: [],
|
||
policyEntries: [],
|
||
workItems: [],
|
||
sourcePriorities: [],
|
||
convergence: [],
|
||
bundles: [],
|
||
singleSourcePolicyActions: [],
|
||
diagnostics: [],
|
||
provenance: [],
|
||
...overrides
|
||
} as ReportProjection;
|
||
}
|
||
|
||
const finding = (overrides = {}) => ({
|
||
resultProjectionId: 'synthetic-1',
|
||
domainId: 'identity',
|
||
toolId: 'purple-knight',
|
||
title: 'Synthetic indicator',
|
||
severity: 'critical' as const,
|
||
outcome: 'fail' as const,
|
||
rankBasis: { kind: 'severity' as const },
|
||
ranked: true,
|
||
...overrides
|
||
});
|
||
|
||
describe('management overview renderer', () => {
|
||
it('renders a self-contained document with only inline script and no remote resources', () => {
|
||
const html = buildManagementOverviewHtml(projection());
|
||
expect(html.startsWith('<!doctype html>')).toBe(true);
|
||
expect(html).not.toMatch(/<script\b[^>]*\bsrc\s*=/i);
|
||
expect(html).not.toMatch(/\son[a-z]+\s*=/i);
|
||
expect(html).not.toMatch(/https?:\/\//i);
|
||
expect(html).not.toMatch(/<link\b/i);
|
||
// The only url() is the embedded font; nothing is fetched when opened.
|
||
expect(html.match(/url\(/gi)).toHaveLength(1);
|
||
expect(html).toContain("url('data:font/woff2;base64,");
|
||
});
|
||
|
||
it('renders for customers in light mode only', () => {
|
||
const html = buildManagementOverviewHtml(projection());
|
||
expect(html).toContain('color-scheme:light');
|
||
expect(html).not.toMatch(/prefers-color-scheme\s*:\s*dark/i);
|
||
});
|
||
|
||
it('declares German as the document language', () => {
|
||
expect(buildManagementOverviewHtml(projection())).toContain('<html lang="de">');
|
||
});
|
||
|
||
it('keeps the binding score wording and never claims baseline compliance', () => {
|
||
const html = buildManagementOverviewHtml(projection());
|
||
expect(html).toContain('Konformität der konfigurierten, vergleichbaren');
|
||
expect(html).not.toMatch(/Baseline-?Compliance/i);
|
||
expect(html).not.toMatch(/Baseline-?Abdeckung/i);
|
||
});
|
||
|
||
it('keeps established English technical terms untranslated', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
domains: [{ domainId: 'gpo', domainLabel: 'GPO & Baseline', passed: 1, deviations: 1, assessed: 2, passRate: 0.5 }],
|
||
findings: [finding()]
|
||
}));
|
||
expect(html).toContain('Overall Score');
|
||
expect(html).toContain('Security Domains');
|
||
expect(html).toContain('Deviations');
|
||
expect(html).toContain('GPO & Baseline');
|
||
// German carries the prose around the English terms.
|
||
expect(html).toContain('Pass Rate je Domain');
|
||
});
|
||
|
||
it('shows no severity, because not every tool provides one', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
findings: [finding(), finding({ resultProjectionId: 'synthetic-2', severity: 'high' }), finding({ resultProjectionId: 'synthetic-3', toolId: 'cis-cat', severity: 'unknown' })],
|
||
gpo: {
|
||
basis: { kind: 'equal-weight', reason: 'A baseline deviation carries no native criticality.' },
|
||
counts: { Fail: 1, Pass: 0, 'Not configured': 0, 'No baseline rule': 0, 'Not comparable': 0, 'Review required': 0 },
|
||
conflicts: 0, duplicates: 0, affectedSettings: 1, affectedBackups: 1, scopeLimited: false
|
||
}
|
||
}), ai());
|
||
const visible = html.slice(html.indexOf('<main'), html.indexOf('<script>'));
|
||
expect(visible).not.toMatch(/severity|critical|kritikalität/i);
|
||
expect(visible).not.toMatch(/>(High|Medium|Low|Info)</);
|
||
// All deviations still count, whatever severity their tool provides.
|
||
expect(visible).toContain('3 aus Scans · 1 GPO Policy Settings');
|
||
});
|
||
|
||
it('gives a brief standing without listing single findings', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
priorities: [{
|
||
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 2,
|
||
sources: [{ toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, shown: [finding({ title: 'Synthetic single finding' })], total: 2 }]
|
||
}],
|
||
findings: [finding({ title: 'Synthetic single finding' }), finding({ resultProjectionId: 'synthetic-2', severity: 'high' })]
|
||
}));
|
||
expect(html).not.toContain('Synthetic single finding');
|
||
expect(html).not.toContain('Priority Findings');
|
||
expect(html).not.toContain('<li class="finding"');
|
||
// The counts still carry the standing.
|
||
expect(html).toContain('2 aus Scans');
|
||
});
|
||
|
||
it('shows each domain on one meter row with singular and plural deviations', async () => {
|
||
const { JSDOM } = await import('jsdom');
|
||
const { document } = new JSDOM(buildManagementOverviewHtml(projection({
|
||
domains: [
|
||
{ domainId: 'identity', domainLabel: 'Identity & Account Hygiene', passed: 13, deviations: 1, assessed: 14, passRate: 13 / 14 },
|
||
{ domainId: 'hardening', domainLabel: 'System Hardening', passed: 0, deviations: 13, assessed: 13, passRate: 0 },
|
||
{ domainId: 'cloud', domainLabel: 'Cloud & Applications', passed: 0, deviations: 0, assessed: 0 }
|
||
]
|
||
}))).window;
|
||
const rows = [...document.querySelectorAll('.meters--detail li')].map(row => [...row.children].map(cell => cell.textContent!.trim()));
|
||
expect(rows).toEqual([
|
||
['Identity & Account Hygiene', '', '93 %', '1 Deviation von 14 bewertet'],
|
||
['System Hardening', '', '0 %', '13 Deviations von 13 bewertet'],
|
||
['Cloud & Applications', '', '—', 'nicht bewertet']
|
||
]);
|
||
});
|
||
|
||
it('keeps explanatory text to the single binding score line', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
coverage: {
|
||
total: 10, assessed: 7, deviations: 3, passed: 4, notRun: 2,
|
||
notApplicable: 1, executionErrors: 0, unknown: 0, perSource: [{ toolId: 'alpha', total: 10, assessed: 7, notRun: 2 }]
|
||
}
|
||
}));
|
||
const visible = html.slice(html.indexOf('<main'), html.indexOf('<script>'));
|
||
expect(visible.match(/class="(?:note|section__lead)"/g)).toHaveLength(1);
|
||
for (const filler of ['Ergebnissen wurden', 'nicht ausgeführt', 'Zerlegung des Index', 'importierten GPO-Backup-Satz', 'datenschutzbereinigten']) {
|
||
expect(visible).not.toContain(filler);
|
||
}
|
||
expect(html).not.toContain('<table');
|
||
});
|
||
|
||
it('renders an unknown value as an em dash and never as a zero', () => {
|
||
const html = buildManagementOverviewHtml(projection({ overallScore: undefined }));
|
||
expect(html).toContain('—');
|
||
// No zero-length arc is drawn, and the readout shows no fabricated value.
|
||
expect(html).not.toContain('<circle class="donut__value"');
|
||
expect(html).not.toContain('<span class="donut__unit">');
|
||
expect(html).toContain('nicht verfügbar');
|
||
expect(html).toContain('<span class="donut__number">—</span>');
|
||
});
|
||
|
||
it('makes a positive score below two percent legible without changing its proportional arc', () => {
|
||
const html = buildManagementOverviewHtml(projection({ overallScore: 1 }));
|
||
expect(html).toContain('class="donut__small-arc"');
|
||
expect(html).toContain('stroke-dasharray="3.393 335.899"');
|
||
expect(buildManagementOverviewHtml(projection({ overallScore: 0.2 }))).toContain('<1<span class="donut__unit">/100</span>');
|
||
expect(buildManagementOverviewHtml(projection({ overallScore: 0 }))).not.toContain('class="donut__small-arc"');
|
||
});
|
||
|
||
it('draws the donut as a full circle starting at twelve o\'clock with flat caps', () => {
|
||
const html = buildManagementOverviewHtml(projection({ overallScore: 50 }));
|
||
expect(html).toContain('rotate(-90 64 64)');
|
||
expect(html).toContain('stroke-linecap:butt');
|
||
expect(html).toContain('donut__track');
|
||
});
|
||
|
||
it('has no separate GPO section but keeps the GPO counts', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
gpo: {
|
||
basis: { kind: 'equal-weight', reason: 'A baseline deviation carries no native criticality.' },
|
||
counts: {
|
||
Fail: 3, Pass: 10, 'Not configured': 40, 'No baseline rule': 5,
|
||
'Not comparable': 2, 'Review required': 1
|
||
},
|
||
conflicts: 1, duplicates: 2, affectedSettings: 3, affectedBackups: 2, scopeLimited: true
|
||
}
|
||
}));
|
||
expect(html).not.toContain('<h2 id="gpo"');
|
||
expect(html).not.toContain('Betroffene GPO-Objekte');
|
||
expect(html).toContain('0 aus Scans · 3 GPO Policy Settings');
|
||
});
|
||
|
||
it('counts GPO deviations once per policy setting, not per comparison', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
findings: [finding({ toolId: 'pingcastle' }), ...[1, 2, 3].map(() => finding({ toolId: 'GPOs', severity: 'unknown' }))],
|
||
gpo: {
|
||
basis: { kind: 'equal-weight', reason: 'A baseline deviation carries no native criticality.' },
|
||
counts: { Fail: 1, Pass: 0, 'Not configured': 0, 'No baseline rule': 0, 'Not comparable': 0, 'Review required': 0 },
|
||
conflicts: 0, duplicates: 0, affectedSettings: 1, affectedBackups: 1, scopeLimited: false
|
||
}
|
||
}));
|
||
expect(html).toContain('1 aus Scans · 1 GPO Policy Settings');
|
||
});
|
||
|
||
it('dates each source', () => {
|
||
const html = buildManagementOverviewHtml(projection({
|
||
scoreSources: [{
|
||
toolId: 'pingcastle', score: 55, method: 'inverted-native', weight: 1,
|
||
passed: 0, deviations: 20, assessed: 20, observedAt: '2026-07-22T00:00:00.000Z', issues: []
|
||
}]
|
||
}));
|
||
expect(html).toContain('22.07.2026');
|
||
});
|
||
|
||
it('adds the German AI brief as marked advisory content', () => {
|
||
const html = buildManagementOverviewHtml(projection(), ai());
|
||
expect(html).toContain('Empfohlene Maßnahmen');
|
||
expect(html).toContain('KI-gestützt · 27.09.2026');
|
||
expect(html).toContain('Synthetische Maßnahme');
|
||
expect(html).toContain('Sofort (0–7 Tage) · IT-Sicherheit');
|
||
expect(html).toContain('Synthetisches Ziel.');
|
||
});
|
||
|
||
it('omits the AI section when no current brief is supplied', () => {
|
||
expect(buildManagementOverviewHtml(projection())).not.toContain('Empfohlene Maßnahmen');
|
||
});
|
||
|
||
it('escapes hostile content from the AI brief', () => {
|
||
const brief = ai();
|
||
brief.brief.priorities[0].title = '<img src=x onerror=alert(1)>';
|
||
const html = buildManagementOverviewHtml(projection(), brief);
|
||
expect(html).toContain('<img src=x onerror=alert(1)>');
|
||
expect(html).not.toMatch(/<img/i);
|
||
});
|
||
});
|
||
|
||
describe('management overview structure', () => {
|
||
it('keeps the agreed section order and heading levels', async () => {
|
||
const { JSDOM } = await import('jsdom');
|
||
const html = buildManagementOverviewHtml(projection({
|
||
gpo: {
|
||
basis: { kind: 'equal-weight', reason: 'A baseline deviation carries no native criticality.' },
|
||
counts: { Fail: 0, Pass: 1, 'Not configured': 0, 'No baseline rule': 0, 'Not comparable': 0, 'Review required': 0 },
|
||
conflicts: 0, duplicates: 0, affectedSettings: 0, affectedBackups: 0, scopeLimited: false
|
||
},
|
||
remediation: {
|
||
selfReported: true, items: 1, byStatus: {}, withoutOwner: 0, withoutDue: 0, riskAccepted: 0
|
||
}
|
||
}), ai());
|
||
const { document } = new JSDOM(html).window;
|
||
expect([...document.querySelectorAll('h1')].map(node => node.textContent))
|
||
.toEqual(['Synthetic GmbH']);
|
||
expect([...document.querySelectorAll('h2')].map(node => node.textContent)).toEqual([
|
||
'Overall Posture', 'Empfohlene Maßnahmen'
|
||
]);
|
||
// Every section is labelled for assistive technology.
|
||
for (const section of document.querySelectorAll('section.segment')) {
|
||
expect(section.getAttribute('aria-labelledby')).toBeTruthy();
|
||
}
|
||
});
|
||
|
||
it('never nests tables', async () => {
|
||
const { JSDOM } = await import('jsdom');
|
||
const { document } = new JSDOM(buildManagementOverviewHtml(projection({
|
||
coverage: {
|
||
total: 1, assessed: 1, deviations: 1, passed: 0, notRun: 0, notApplicable: 0,
|
||
executionErrors: 0, unknown: 0, perSource: [{ toolId: 'alpha', total: 1, assessed: 1, notRun: 0 }]
|
||
}
|
||
}))).window;
|
||
expect(document.querySelectorAll('table table')).toHaveLength(0);
|
||
});
|
||
|
||
it('draws the value arc proportional to the score on a full circle', async () => {
|
||
const { JSDOM } = await import('jsdom');
|
||
const { document } = new JSDOM(buildManagementOverviewHtml(projection({ overallScore: 25 }))).window;
|
||
const arc = document.querySelector('circle.donut__value')!;
|
||
const [drawn, gap] = arc.getAttribute('stroke-dasharray')!.split(' ').map(Number);
|
||
const circumference = 2 * Math.PI * 54;
|
||
expect(drawn + gap).toBeCloseTo(circumference, 2);
|
||
expect(drawn / circumference).toBeCloseTo(0.25, 4);
|
||
});
|
||
|
||
it('clamps an out-of-range score instead of drawing past the ring', async () => {
|
||
const { JSDOM } = await import('jsdom');
|
||
const { document } = new JSDOM(buildManagementOverviewHtml(projection({ overallScore: 140 }))).window;
|
||
const [drawn, gap] = document.querySelector('circle.donut__value')!
|
||
.getAttribute('stroke-dasharray')!.split(' ').map(Number);
|
||
expect(gap).toBeCloseTo(0, 3);
|
||
expect(drawn).toBeCloseTo(2 * Math.PI * 54, 2);
|
||
});
|
||
});
|