Assessor_2.0/tests/exports/management-overview-report....

315 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest';
import { buildManagementOverviewHtml } from '../../src/exports/management-overview-report';
import type { ReportProjection } from '../../src/domain/report-projection';
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 }],
priorities: [{
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 1,
sources: [{ toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, shown: [finding()], total: 1 }]
}]
}));
expect(html).toContain('Priority Findings');
expect(html).toContain('Overall Score');
expect(html).toContain('Security Domains');
expect(html).toContain('Deviations');
expect(html).toContain('>Critical<');
expect(html).toContain('GPO &amp; Baseline');
// German carries the prose around the English terms.
expect(html).toContain('Gruppiert nach Security Domain');
});
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('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('names the ordering basis of every source and never merges them', () => {
const html = buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'hardening', domainLabel: 'System Hardening', deviations: 2,
sources: [
{ toolId: 'cis-cat', basis: { kind: 'equal-weight', reason: 'CIS benchmarks state no criticality.' }, ranked: false, shown: [finding({ severity: 'unknown', toolId: 'cis-cat' })], total: 1 },
{ toolId: 'pingcastle', basis: { kind: 'numeric', property: 'rule.points', scale: '0-50+', direction: 'higher-is-worse' }, ranked: true, shown: [finding({ toolId: 'pingcastle', severity: 'medium', rankValue: 35, rankBasis: { kind: 'numeric', property: 'rule.points', scale: '0-50+', direction: 'higher-is-worse' } })], total: 1 }
]
}]
}));
expect(html).toContain('Ohne Rangfolge: CIS benchmarks state no criticality.');
expect(html).toContain('Sortiert nach rule.points (0-50+, höher ist kritischer)');
expect(html).toContain('quellenübergreifende Rangfolge');
});
it('points at the technical report instead of silently truncating a domain', () => {
const html = buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 9,
sources: [{ toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, shown: [finding()], total: 9 }]
}]
}));
expect(html).toContain('8 weitere Deviations dieser Quelle stehen im Technical Report.');
});
it('keeps GPO equal-weight and marks the score-neutral verdicts as such', () => {
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).toContain('A baseline deviation carries no native criticality.');
expect(html).toContain('gleichgewichtig');
expect(html).toContain('sind score-neutral und begründen keine Maßnahme');
expect(html).toContain('trifft keine Aussage über die gesamte Domäne');
});
it('shows GPO deviations once in the GPO block instead of per comparison in Priority Findings', () => {
const html = buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'gpo', domainLabel: 'GPO & Baseline', deviations: 4,
sources: [
{ toolId: 'pingcastle', basis: { kind: 'severity' }, ranked: true, shown: [finding({ toolId: 'pingcastle', title: 'Synthetic scanner item' })], total: 1 },
{ toolId: 'GPOs', basis: { kind: 'equal-weight', reason: 'A baseline deviation carries no native criticality.' }, ranked: false,
shown: [1, 2, 3].map(index => finding({ toolId: 'GPOs', severity: 'unknown', title: `Synthetic comparison ${index}` })), total: 3 }
]
}],
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('Synthetic scanner item');
expect(html).not.toContain('Synthetic comparison');
expect(html).toContain('1 Deviations</span>');
// One scanner finding plus one GPO policy setting, not three comparisons.
expect(html).toContain('1 aus Scans · 1 GPO Policy Settings');
});
it('omits the remediation section entirely when no reviews exist', () => {
expect(buildManagementOverviewHtml(projection())).not.toContain('Remediation-Status');
});
it('marks remediation as self reported and not as technical proof', () => {
const html = buildManagementOverviewHtml(projection({
remediation: {
selfReported: true, items: 4, byStatus: { Actionable: 4 },
withoutOwner: 1, withoutDue: 2, riskAccepted: 0
}
}));
expect(html).toContain('Remediation-Status');
expect(html).toContain('Selbstauskunft');
expect(html).toContain('kein technischer Nachweis');
expect(html).toContain('Rescan');
});
it('names the score method per source and explains an inverted native risk', () => {
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('Nativer Risk, invertiert als 100 − Risk');
expect(html).toContain('100 − Risk</code> in den Index ein');
expect(html).toContain('22.07.2026');
});
it('escapes hostile content from a finding title', () => {
const html = buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 1,
sources: [{
toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, total: 1,
shown: [finding({ title: '<img src=x onerror=alert(1)>' })]
}]
}]
}));
expect(html).toContain('&lt;img src=x onerror=alert(1)&gt;');
expect(html).not.toMatch(/<img\b/i);
});
});
describe('management overview structure', () => {
it('keeps the agreed section order and heading levels', async () => {
const { JSDOM } = await import('jsdom');
const html = buildManagementOverviewHtml(projection({
remediation: {
selfReported: true, items: 1, byStatus: {}, withoutOwner: 0, withoutDue: 0, riskAccepted: 0
}
}));
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', 'Priority Findings', 'Remediation-Status', 'Abdeckung und Lücken',
'Konvergenz der Quellen', 'Maßnahmenbündel', 'Source Overview'
]);
// 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('filters Priority Findings by severity chip and search through the inline script', async () => {
const { JSDOM } = await import('jsdom');
const html = buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 2,
sources: [{
toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, total: 2,
shown: [finding({ title: 'Synthetic critical' }), finding({ resultProjectionId: 'synthetic-2', title: 'Synthetic high', severity: 'high' })]
}]
}],
findings: [finding(), finding({ resultProjectionId: 'synthetic-2', severity: 'high' })]
}));
const { document } = new JSDOM(html, { runScripts: 'dangerously' }).window;
const rows = [...document.querySelectorAll<HTMLElement>('[data-filter-item]')];
const visible = () => rows.filter(row => !row.hidden).map(row => row.querySelector('.finding__title')!.textContent);
expect(document.querySelector<HTMLElement>('[data-filter-toolbar]')!.hidden).toBe(false);
expect(visible()).toEqual(['Synthetic critical', 'Synthetic high']);
document.querySelector<HTMLButtonElement>('[data-filter-value="critical"]')!.click();
expect(visible()).toEqual(['Synthetic critical']);
expect(document.querySelector('[data-filter-reset]')!.getAttribute('aria-pressed')).toBe('false');
document.querySelector<HTMLButtonElement>('[data-filter-reset]')!.click();
const search = document.querySelector<HTMLInputElement>('[data-filter-search]')!;
search.value = 'high';
search.dispatchEvent(new document.defaultView!.Event('input'));
expect(visible()).toEqual(['Synthetic high']);
expect(document.querySelector('[data-filter-count]')!.textContent).toBe('1 / 2');
});
it('keeps every finding readable when the script does not run', async () => {
const { JSDOM } = await import('jsdom');
const { document } = new JSDOM(buildManagementOverviewHtml(projection({
priorities: [{
domainId: 'identity', domainLabel: 'Identity & Account Hygiene', deviations: 1,
sources: [{ toolId: 'purple-knight', basis: { kind: 'severity' }, ranked: true, shown: [finding()], total: 1 }]
}],
findings: [finding(), finding({ resultProjectionId: 'synthetic-2', severity: 'high' })]
}))).window;
expect(document.querySelector<HTMLElement>('[data-filter-toolbar]')!.hidden).toBe(true);
expect([...document.querySelectorAll<HTMLElement>('[data-filter-item]')].every(row => !row.hidden)).toBe(true);
});
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);
});
});