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);
});
});