import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { entscheideNavigation } from '../../electron/navigation'; /** * Die Grenze des Fensters - was hinaus darf und was nicht. * * DER BEFUND, DER DIESE DATEI AUSGELOEST HAT, war keine Lücke im Programm, * sondern eine Zusage über eine Prüfung, die es nicht gab: * `vitest.config.ts` begründete die Abdeckungslücke in `electron/main.ts` * damit, `will-navigate` und `setWindowOpenHandler` prüfe der Rauchtest am * gebauten Stand. GEMESSEN: `e2e/rauchtest.spec.ts` hat sechs Fälle, und das * Wort "navigate" kommt darin nicht vor. Eine Abdeckungsausnahme, gedeckt von * einem Test, den es nicht gab - und das an den beiden Wachen, die verhindern, * dass das Programm verlassen wird. * * ZWEI WEITERE ANGABEN WAREN DABEI SCHIEF: Beschrieben war, die beiden * braechen jede Navigation ab und lehnten jedes Fenster ab - dass * sie jede `https:`-Adresse anschliessend an den Standardbrowser * weiterreichen, stand nirgends. Und der Kartenabruf hiess dort die "Einzige * Ausnahme" vom Grundsatz, nicht ins Netz zu gehen. * * WARUM HIER UND NICHT IM RAUCHTEST: Der Rauchtest kann die Entscheidung * "extern" nicht prüfen, ohne wirklich einen Browser zu öffnen - auf dem * Rechner dessen, der die Tests fährt. Er prüft deshalb die Verdrahtung an * einer Adresse, die NICHT hinausgeht; die Entscheidungstafel steht hier. */ const RAUCHTEST = readFileSync('e2e/rauchtest.spec.ts', 'utf8'); const ABDECKUNG = readFileSync('vitest.config.ts', 'utf8'); /** Die geladene Seite - so, wie `pathToFileURL` sie bildet. */ const EIGEN = 'file:///C:/Programme/LSA-Planer/build/renderer/index.html'; describe('Die eigene Seite darf geladen werden, sonst nichts', () => { it('laedt die geladene index.html', () => { expect(entscheideNavigation(EIGEN, EIGEN)).toBe('laden'); }); it('bricht eine andere Datei desselben Verzeichnisses ab', () => { /* * Der Fall, um den es geht: Ein Renderer, der auf eine Nachbardatei * navigiert, verliesse die geprüfte Seite - mit derselben Herkunft und * denselben Rechten, aber ohne das, was `index.html` an Richtlinien * mitbringt. */ expect( entscheideNavigation('file:///C:/Programme/LSA-Planer/build/renderer/andere.html', EIGEN), ).toBe('abbrechen'); }); it('bricht eine Datei ausserhalb ab und gibt sie NICHT nach draussen', () => { // `shell.openExternal` mit einer file-Adresse öffnet ein beliebiges // Programm mit einer beliebigen Datei. Das ist der gefährlichste Fall. expect(entscheideNavigation('file:///C:/Windows/System32/cmd.exe', EIGEN)).toBe('abbrechen'); }); }); describe('Nur https geht nach draussen', () => { it('gibt eine https-Adresse an das Betriebssystem', () => { expect(entscheideNavigation('https://beispiel.invalid/', EIGEN)).toBe('extern'); }); it.each([ ['http://beispiel.invalid/', 'unverschlüsselt - ein Weg, Daten unbemerkt abfliessen zu lassen'], ['javascript:alert(1)', 'Kode statt Adresse'], ['data:text/html,

x

', 'eingebetteter Inhalt ohne Herkunft'], ['ftp://beispiel.invalid/', 'fremdes Protokoll'], ['mailto:jemand@beispiel.invalid', 'oeffnet das Mailprogramm'], ['ms-msdt:/id', 'Protokollhandler des Betriebssystems'], ])('bricht %s ab (%s)', (adresse) => { expect(entscheideNavigation(adresse, EIGEN)).toBe('abbrechen'); }); it('bricht ab, was sich nicht als Adresse lesen laesst', () => { // Was `new URL` nicht annimmt, geht nicht an das Betriebssystem weiter - // was Windows daraus machte, entschiede dann nicht mehr dieses Programm. for (const unsinn of ['', ' ', 'kein-protokoll', '://', 'https:/']) { expect(entscheideNavigation(unsinn, EIGEN), `Eingabe ${JSON.stringify(unsinn)}`).toBe( 'abbrechen', ); } }); it('prueft das ausgewertete Protokoll und nicht den Anfang der Zeichenkette', () => { /* * `setWindowOpenHandler` fragte bis hierher `url.startsWith('https://')`. * Beide Wachen an derselben Grenze, aber verschieden streng - und die * schwächere entschied über neue Fenster. Sie prüfen jetzt dasselbe. */ expect(entscheideNavigation('https:example.com', EIGEN), 'ohne Schrägstriche').toBe('extern'); expect( entscheideNavigation('httpsx://beispiel.invalid/', EIGEN), 'fremdes Protokoll mit ähnlichem Namen', ).toBe('abbrechen'); }); it('gibt die eigene Seite nicht nach draussen, auch wenn sie https waere', () => { // Die Reihenfolge der Prüfungen ist die Sicherheitsrichtung: Gleichheit // zuerst. Wer nach dem Protokoll zuerst fragte, gäbe die eigene Seite an // den Browser. const eigenHttps = 'https://localhost:5273/index.html'; expect(entscheideNavigation(eigenHttps, eigenHttps)).toBe('laden'); }); }); describe('Die Unterlagen sagen, was wirklich geschieht', () => { /** Die Namen der Faelle des Rauchtests - nicht seine Kommentare. */ function rauchtestfaelle(): readonly string[] { return [...RAUCHTEST.matchAll(/^test\('([^']+)'/gm)].map((t) => String(t[1])); } it('prueft der Rauchtest die Grenze wirklich, und nicht bloss im Kommentar', () => { /* * DER KERN DES BEFUNDS, und die Wache dagegen musste zweimal geschaerft * werden. * * Der erste Anlauf suchte "will-navigate" IRGENDWO im Rauchtest. Das * genuegte nicht: Ein Kommentar, der das Wort nennt, ist keine Pruefung - * und genau ein solcher Kommentar staende auch dann dort, wenn der Fall * daneben geloescht wuerde. GEMESSEN an der Mutation "den * Navigationsfall aus dem Rauchtest entfernen": Der Fall blieb gruen. * * Gesucht wird deshalb ein FALLNAME. Ein Fall heisst, wie er heisst, weil * er etwas prueft; ein Kommentar heisst gar nichts. */ const namen = rauchtestfaelle(); expect(namen.length, 'der Rauchtest hat gar keine Faelle mehr').toBeGreaterThan(0); expect( namen.some((n) => /navigier/i.test(n)), `kein Fall des Rauchtests beruehrt die Navigationsgrenze: ${namen.join(' | ')}`, ).toBe(true); }); it('deckt sich die Begruendung der Abdeckungsluecke mit dem, was wirklich geprueft wird', () => { /* * Die Gegenrichtung. Nennt `vitest.config.ts` die beiden Wachen als vom * Rauchtest geprueft, muss der Rauchtest einen Fall dafuer haben - sonst * steht die Ausnahme wieder ohne Deckung da, so wie sie es tat. */ if (!/will-navigate|setWindowOpenHandler/.test(ABDECKUNG)) return; expect( rauchtestfaelle().some((n) => /navigier/i.test(n)), 'vitest.config.ts nennt die Wachen als vom Rauchtest geprueft, der Rauchtest hat keinen Fall dafuer', ).toBe(true); }); });