Assessor_2.0/tests/features/linked-results.test.ts

196 lines
9.6 KiB
TypeScript

import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { LinkedResults } from '../../src/features/linked-results/LinkedResults';
import { makeSheet } from '../../src/importers/sheet-factory';
describe('LinkedResults', () => {
it('verwendet getrennte Soll- und Verstossfarben fuer Belegzeilen', () => {
const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
expect(styles).toMatch(/\.rich-text-table tr\.rich-text-table-row--criterion td\s*\{[^}]*background:\s*var\(--criterion-highlight-bg\);/s);
expect(styles).toMatch(/\.rich-text-table tr\.rich-text-table-row--violation td\s*\{[^}]*background:\s*var\(--violation-highlight-bg\);/s);
});
it('haelt Ergebnisabschnitte ohne platzraubenden Aussenkasten', () => {
const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
const rule = /\.result-block\s*\{([^}]*)\}/.exec(styles)?.[1] ?? '';
expect(rule).toContain('padding: 0');
expect(rule).toContain('border: 0');
expect(rule).toContain('background: transparent');
});
// Maester liefert statt einer verknuepfbaren Ergebnisdatei nur einen
// Ergebnistext (siehe `resultMessageFor`). Ohne eigene Darstellung erschien
// hier bislang der irrefuehrende Hinweis "Keine passende Ergebnisdatei
// automatisch gefunden.", obwohl der eigentliche Befund vorliegt.
it('zeigt den werkzeuggebundenen Ergebnistext, wenn keine Ergebnisdatei verknuepft ist', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [],
sections: [{ title: 'Test result', value: 'No conditional access policy securing security info registration.' }]
}));
expect(html).toContain('No conditional access policy securing security info registration.');
expect(html).not.toContain('Keine passende Ergebnisdatei automatisch gefunden.');
});
it('zeigt weiterhin den Nicht-gefunden-Hinweis, wenn weder Ergebnisdatei noch Ergebnistext vorliegen', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, { sheets: [] }));
expect(html).toContain('No matching result file found automatically.');
});
/**
* Regression: Der Ergebnistext wurde als reiner Text ausgegeben. Maester
* liefert darin Ueberschriften, Aufzaehlungen, Verweise und ganze Tabellen -
* sie standen dadurch woertlich als Rauten, Sternchen und Pipe-Ketten in der
* Oberflaeche.
*/
it('wertet den Ergebnistext als Markdown aus statt ihn woertlich auszugeben', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [],
sections: [{
title: 'Test result',
value: 'You have 2 users without MFA. #### Impacted resources '
+ '| Status | Name | | --- | --- | | Fail | [tyler@example.invalid](https://example.invalid/a) | '
+ '| Fail | ann@example.invalid |'
}]
}));
expect(html).toContain('<table');
expect(html).toContain('<th>Status</th>');
expect(html).toContain('href="https://example.invalid/a"');
expect(html).not.toContain('####');
expect(html).not.toContain('| --- |');
});
it('wertet eine Aufzaehlung im Ergebnistext als Liste aus', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [],
sections: [{
title: 'Test result',
value: 'The percentage of global administrators is high. '
+ '- Damien Bowden (damien.bowden@example.invalid) '
+ '- Ravi Kalwani (ravi.kalwani@example.invalid) '
+ '- Gael Colas (gael@example.invalid)'
}]
}));
expect(html).toContain('<ul>');
expect((html.match(/<li>/g) ?? [])).toHaveLength(3);
});
/**
* Die Ergebnisdarstellung ist werkzeugunabhaengig verbindlich (siehe
* Abschnitt 17 der Komponentenvertraege im UI-Skill). Ein verknuepftes Blatt
* verwendete zuvor die allgemeine Tabellenform mit klebendem Kopf und sah
* dadurch anders aus als derselbe Befund aus einem Werkzeugtext.
*/
const sheet = makeSheet(
'Privileged Users with Weak Password Policy',
[['DistinguishedName', 'MinLength'], ['CN=Administrator,DC=example,DC=invalid', '5']],
'',
'assessment.xlsx'
);
it('stellt ein verknuepftes Blatt im selben Ergebnisblock dar wie einen Werkzeugtext', () => {
const sheetHtml = renderToStaticMarkup(createElement(LinkedResults, { sheets: [sheet] }));
const messageHtml = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [],
sections: [{ title: 'Test result', value: 'Intro. | A | B | | --- | --- | | 1 | 2 |' }]
}));
for (const html of [sheetHtml, messageHtml]) {
expect(html).toContain('class="result-block"');
expect(html).toContain('class="result-block__title"');
expect(html).toContain('class="rich-text-table-wrap"');
expect(html).toContain('class="rich-text-table"');
}
});
it('kann Blattüberschriften im Result-Fenster ausblenden', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, { sheets: [sheet], showSheetTitles: false }));
expect(html).not.toContain('>Privileged Users with Weak Password Policy</h4>');
expect(html).toContain('class="rich-text-table"');
});
it('zeigt den Finding-Titel zusätzlich im Result-Fenster', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [sheet],
heading: 'Synthetic result',
showSheetTitles: false
}));
expect(html).toContain('<h3 class="linked-results-heading">Synthetic result</h3>');
expect(html).not.toContain('>Privileged Users with Weak Password Policy</h4>');
});
it('benennt den Ergebnisblock eines Blattes nach dem Blatt', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, { sheets: [sheet] }));
expect(html).toContain('>Privileged Users with Weak Password Policy</h4>');
});
/**
* Die Ergebnistexte sind werkzeugunabhaengig: das Fenster zeigt genau die
* Abschnitte, die ihm das Toolprofil uebergibt (siehe `resultSectionsFor`),
* unter deren eigenem Namen. Zuvor konnte es nur einen einzigen, fest
* "Test result" genannten Text darstellen.
*/
it('zeigt mehrere benannte Ergebnisabschnitte unter ihrem eigenen Namen', () => {
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [],
sections: [
{ title: 'Control Mappings', value: '### CIS Controls V7.0\n\n- Control 1: Synthetic Inventory' },
{ title: 'Test result', value: 'No conditional access policy securing security info registration.' }
]
}));
expect(html).toContain('>Control Mappings</h4>');
expect(html).toContain('>Test result</h4>');
expect(html).toContain('CIS Controls V7.0');
expect((html.match(/class="result-block"/g) ?? [])).toHaveLength(2);
});
it('schaltet Spaltentrenner ueber den Fensterkontext fuer Result-Tabellen frei', () => {
const docked = renderToStaticMarkup(createElement(LinkedResults, { sheets: [sheet] }));
const floating = renderToStaticMarkup(createElement(LinkedResults, { sheets: [sheet], resizableTables: true }));
expect(docked).not.toContain('result-column-resizer');
expect(docked).not.toContain('is-column-resizable');
expect(floating).toContain('rich-text-table is-column-resizable');
expect(floating).toContain('role="separator"');
expect(floating).toContain('Resize column 1');
expect(floating).toContain('double-clicking it');
});
it('haelt maximierte Tabellen vor manueller Anpassung kompakt und umbruchfrei', () => {
const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
expect(styles).toMatch(/\.results-window\.is-maximized \.rich-text-table\.is-column-resizable:not\(\.has-custom-column-widths\)\s*\{[^}]*width:\s*max-content;[^}]*min-width:\s*max-content;[^}]*table-layout:\s*auto;/s);
expect(styles).toMatch(/\.results-window\.is-maximized \.rich-text-table\.is-column-resizable:not\(\.has-custom-column-widths\) td\s*\{[^}]*white-space:\s*nowrap;/s);
expect(styles).toMatch(/\.rich-text-table-wrap\s*\{[^}]*overflow-x:\s*auto;/s);
});
it('zeigt den erklaerenden Quelltext unmittelbar vor der zugehoerigen Tabelle', () => {
const withPreamble = { ...sheet, resultPreamble: 'Synthetic explanation for this evidence table.' };
const html = renderToStaticMarkup(createElement(LinkedResults, { sheets: [withPreamble] }));
expect(html).toContain('<p class="result-block__preamble">Synthetic explanation for this evidence table.</p>');
expect(html.indexOf('result-block__preamble')).toBeLessThan(html.indexOf('rich-text-table-wrap'));
});
it('gruppiert mehrere Tabellen unter einer Hauptueberschrift mit je eigener Tabellenueberschrift', () => {
const first = { ...sheet, id: 'v7-control-9', name: 'V7 table 9' };
const second = { ...sheet, id: 'v7-control-11', name: 'V7 table 11' };
const subtitles = new Map([
[first.id, 'Control 9: Limitation and Control of Network Ports, Protocols, and Services'],
[second.id, 'Control 11: Secure Configuration for Network Devices']
]);
const html = renderToStaticMarkup(createElement(LinkedResults, {
sheets: [first, second],
sheetTitle: () => 'CIS Controls V7.0',
sheetSubtitle: current => subtitles.get(current.id)
}));
expect(html.match(/>CIS Controls V7.0<\/h4>/g)).toHaveLength(1);
expect(html).toContain('>Control 9: Limitation and Control of Network Ports, Protocols, and Services</h5>');
expect(html).toContain('>Control 11: Secure Configuration for Network Devices</h5>');
expect(html.indexOf('Control 9:')).toBeLessThan(html.indexOf('Control 11:'));
});
});