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

238 lines
11 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.0.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: [],
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 without script or remote resources', () => {
const html = buildManagementOverviewHtml(projection());
expect(html.startsWith('<!doctype html>')).toBe(true);
expect(html).not.toMatch(/<script\b/i);
expect(html).not.toMatch(/\son[a-z]+\s*=/i);
expect(html).not.toMatch(/https?:\/\//i);
expect(html).not.toMatch(/<link\b/i);
expect(html).not.toMatch(/url\(/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('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', 'Source Overview'
]);
// Every section is labelled for assistive technology.
for (const section of document.querySelectorAll('section.segment')) {
expect(section.getAttribute('aria-labelledby')).toBeTruthy();
}
});
it('places no table inside a tile and 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('.tile table')).toHaveLength(0);
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);
});
});