lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests tools navigationGrenze.test.ts

6,6 KB Rohdatei
tests/tools/navigationGrenze.test.ts — 147 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { entscheideNavigation } from '../../electron/navigation';
4
5 /**
6 * Die Grenze des Fensters - was hinaus darf und was nicht.
7 *
8 * DER BEFUND, DER DIESE DATEI AUSGELOEST HAT, war keine Lücke im Programm,
9 * sondern eine Zusage über eine Prüfung, die es nicht gab:
10 * `vitest.config.ts` begründete die Abdeckungslücke in `electron/main.ts`
11 * damit, `will-navigate` und `setWindowOpenHandler` prüfe der Rauchtest am
12 * gebauten Stand. GEMESSEN: `e2e/rauchtest.spec.ts` hat sechs Fälle, und das
13 * Wort "navigate" kommt darin nicht vor. Eine Abdeckungsausnahme, gedeckt von
14 * einem Test, den es nicht gab - und das an den beiden Wachen, die verhindern,
15 * dass das Programm verlassen wird.
16 *
17 * ZWEI WEITERE ANGABEN WAREN DABEI SCHIEF: Beschrieben war, die beiden
18 * braechen jede Navigation ab und lehnten jedes Fenster ab - dass
19 * sie jede `https:`-Adresse anschliessend an den Standardbrowser
20 * weiterreichen, stand nirgends. Und der Kartenabruf hiess dort die "Einzige
21 * Ausnahme" vom Grundsatz, nicht ins Netz zu gehen.
22 *
23 * WARUM HIER UND NICHT IM RAUCHTEST: Der Rauchtest kann die Entscheidung
24 * "extern" nicht prüfen, ohne wirklich einen Browser zu öffnen - auf dem
25 * Rechner dessen, der die Tests fährt. Er prüft deshalb die Verdrahtung an
26 * einer Adresse, die NICHT hinausgeht; die Entscheidungstafel steht hier.
27 */
28
29 const RAUCHTEST = readFileSync('e2e/rauchtest.spec.ts', 'utf8');
30 const ABDECKUNG = readFileSync('vitest.config.ts', 'utf8');
31
32 /** Die geladene Seite - so, wie `pathToFileURL` sie bildet. */
33 const EIGEN = 'file:///C:/Programme/LSA-Planer/build/renderer/index.html';
34
35 describe('Die eigene Seite darf geladen werden, sonst nichts', () => {
36 it('laedt die geladene index.html', () => {
37 expect(entscheideNavigation(EIGEN, EIGEN)).toBe('laden');
38 });
39
40 it('bricht eine andere Datei desselben Verzeichnisses ab', () => {
41 /*
42 * Der Fall, um den es geht: Ein Renderer, der auf eine Nachbardatei
43 * navigiert, verliesse die geprüfte Seite - mit derselben Herkunft und
44 * denselben Rechten, aber ohne das, was `index.html` an Richtlinien
45 * mitbringt.
46 */
47 expect(
48 entscheideNavigation('file:///C:/Programme/LSA-Planer/build/renderer/andere.html', EIGEN),
49 ).toBe('abbrechen');
50 });
51
52 it('bricht eine Datei ausserhalb ab und gibt sie NICHT nach draussen', () => {
53 // `shell.openExternal` mit einer file-Adresse öffnet ein beliebiges
54 // Programm mit einer beliebigen Datei. Das ist der gefährlichste Fall.
55 expect(entscheideNavigation('file:///C:/Windows/System32/cmd.exe', EIGEN)).toBe('abbrechen');
56 });
57 });
58
59 describe('Nur https geht nach draussen', () => {
60 it('gibt eine https-Adresse an das Betriebssystem', () => {
61 expect(entscheideNavigation('https://beispiel.invalid/', EIGEN)).toBe('extern');
62 });
63
64 it.each([
65 ['http://beispiel.invalid/', 'unverschlüsselt - ein Weg, Daten unbemerkt abfliessen zu lassen'],
66 ['javascript:alert(1)', 'Kode statt Adresse'],
67 ['data:text/html,<h1>x</h1>', 'eingebetteter Inhalt ohne Herkunft'],
68 ['ftp://beispiel.invalid/', 'fremdes Protokoll'],
69 ['mailto:jemand@beispiel.invalid', 'oeffnet das Mailprogramm'],
70 ['ms-msdt:/id', 'Protokollhandler des Betriebssystems'],
71 ])('bricht %s ab (%s)', (adresse) => {
72 expect(entscheideNavigation(adresse, EIGEN)).toBe('abbrechen');
73 });
74
75 it('bricht ab, was sich nicht als Adresse lesen laesst', () => {
76 // Was `new URL` nicht annimmt, geht nicht an das Betriebssystem weiter -
77 // was Windows daraus machte, entschiede dann nicht mehr dieses Programm.
78 for (const unsinn of ['', ' ', 'kein-protokoll', '://', 'https:/']) {
79 expect(entscheideNavigation(unsinn, EIGEN), `Eingabe ${JSON.stringify(unsinn)}`).toBe(
80 'abbrechen',
81 );
82 }
83 });
84
85 it('prueft das ausgewertete Protokoll und nicht den Anfang der Zeichenkette', () => {
86 /*
87 * `setWindowOpenHandler` fragte bis hierher `url.startsWith('https://')`.
88 * Beide Wachen an derselben Grenze, aber verschieden streng - und die
89 * schwächere entschied über neue Fenster. Sie prüfen jetzt dasselbe.
90 */
91 expect(entscheideNavigation('https:example.com', EIGEN), 'ohne Schrägstriche').toBe('extern');
92 expect(
93 entscheideNavigation('httpsx://beispiel.invalid/', EIGEN),
94 'fremdes Protokoll mit ähnlichem Namen',
95 ).toBe('abbrechen');
96 });
97
98 it('gibt die eigene Seite nicht nach draussen, auch wenn sie https waere', () => {
99 // Die Reihenfolge der Prüfungen ist die Sicherheitsrichtung: Gleichheit
100 // zuerst. Wer nach dem Protokoll zuerst fragte, gäbe die eigene Seite an
101 // den Browser.
102 const eigenHttps = 'https://localhost:5273/index.html';
103 expect(entscheideNavigation(eigenHttps, eigenHttps)).toBe('laden');
104 });
105 });
106
107 describe('Die Unterlagen sagen, was wirklich geschieht', () => {
108 /** Die Namen der Faelle des Rauchtests - nicht seine Kommentare. */
109 function rauchtestfaelle(): readonly string[] {
110 return [...RAUCHTEST.matchAll(/^test\('([^']+)'/gm)].map((t) => String(t[1]));
111 }
112
113 it('prueft der Rauchtest die Grenze wirklich, und nicht bloss im Kommentar', () => {
114 /*
115 * DER KERN DES BEFUNDS, und die Wache dagegen musste zweimal geschaerft
116 * werden.
117 *
118 * Der erste Anlauf suchte "will-navigate" IRGENDWO im Rauchtest. Das
119 * genuegte nicht: Ein Kommentar, der das Wort nennt, ist keine Pruefung -
120 * und genau ein solcher Kommentar staende auch dann dort, wenn der Fall
121 * daneben geloescht wuerde. GEMESSEN an der Mutation "den
122 * Navigationsfall aus dem Rauchtest entfernen": Der Fall blieb gruen.
123 *
124 * Gesucht wird deshalb ein FALLNAME. Ein Fall heisst, wie er heisst, weil
125 * er etwas prueft; ein Kommentar heisst gar nichts.
126 */
127 const namen = rauchtestfaelle();
128 expect(namen.length, 'der Rauchtest hat gar keine Faelle mehr').toBeGreaterThan(0);
129 expect(
130 namen.some((n) => /navigier/i.test(n)),
131 `kein Fall des Rauchtests beruehrt die Navigationsgrenze: ${namen.join(' | ')}`,
132 ).toBe(true);
133 });
134
135 it('deckt sich die Begruendung der Abdeckungsluecke mit dem, was wirklich geprueft wird', () => {
136 /*
137 * Die Gegenrichtung. Nennt `vitest.config.ts` die beiden Wachen als vom
138 * Rauchtest geprueft, muss der Rauchtest einen Fall dafuer haben - sonst
139 * steht die Ausnahme wieder ohne Deckung da, so wie sie es tat.
140 */
141 if (!/will-navigate|setWindowOpenHandler/.test(ABDECKUNG)) return;
142 expect(
143 rauchtestfaelle().some((n) => /navigier/i.test(n)),
144 'vitest.config.ts nennt die Wachen als vom Rauchtest geprueft, der Rauchtest hat keinen Fall dafuer',
145 ).toBe(true);
146 });
147 });