196 lines
9.6 KiB
TypeScript
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:'));
|
|
});
|
|
});
|