// @vitest-environment node // Reiner Dateisystem-Test – jsdom wird hier nicht gebraucht und würde // `import.meta.url` auf eine http-URL setzen. import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { istGleicheDatei } from '../src/main/oberflaeche'; import { cspRichtlinie } from '../src/shared/csp'; /** * Schutzgitter für die nicht verhandelbaren Sicherheitsschalter. * * Electron 43 bietet keine öffentliche API mehr, um die tatsächlich * angewandten `webPreferences` zur Laufzeit auszulesen. Dieser Test liest * deshalb die Quelle und schlägt fehl, sobald jemand einen der drei harten * Schalter umstellt oder entfernt. Das beobachtbare Verhalten prüfen * zusätzlich die E2E-Tests in `e2e/anwendung.spec.ts`. */ const wurzel = fileURLToPath(new URL('..', import.meta.url)); function quelle(relativerPfad: string): string { return readFileSync(join(wurzel, relativerPfad), 'utf8'); } describe('webPreferences des Hauptfensters', () => { const fensterQuelle = quelle('src/main/fenster.ts'); it.each([ ['contextIsolation', 'true'], ['nodeIntegration', 'false'], ['sandbox', 'true'], ['nodeIntegrationInWorker', 'false'], ['nodeIntegrationInSubFrames', 'false'], ['webSecurity', 'true'], ['allowRunningInsecureContent', 'false'], ['webviewTag', 'false'], ])('setzt %s auf %s', (schalter, wert) => { expect(fensterQuelle).toMatch(new RegExp(`\\b${schalter}\\s*:\\s*${wert}\\s*,`, 'u')); }); it('lädt das Preload-Skript aus dem gebauten Ausgabeverzeichnis', () => { expect(fensterQuelle).toContain("join(__dirname, '../preload/index.js')"); }); }); describe('Content-Security-Policy', () => { /* * Was hier gemessen wird – und was ausdrücklich nicht. * * In dieser Datei läuft kein Browser. Geprüft wird der **Wortlaut** der * Richtlinie, nicht ihre Wirkung. Genau daran hat dieser Block schon einmal * grün gemeldet, während nichts galt: `toContain("frame-ancestors 'none'")` * war erfüllt, obwohl Chromium die Direktive im ``-Element verwirft * und bei jedem Start einen Konsolenfehler darüber schrieb. Eine Zusicherung, * die mehr verspricht als sie hält, ist schlimmer als keine – deshalb steht * der Satz hier und nicht im Nachhinein. * * Die Wirkung misst `e2e/anwendung.spec.ts` im echten Fenster: dort werden * die tatsächlich angewandten Richtlinien über `securitypolicyviolation` * ausgelesen und die Konsole beim Laden mitgehört. * * Hier bleibt, was ohne Browser beantwortbar ist: ob der Wortlaut stimmt und * ob die beiden Fassungen (`` und HTTP-Kopf) zueinander passen. */ const produktionKopf = cspRichtlinie('production', 'kopf'); const produktionMeta = cspRichtlinie('production', 'meta'); const entwicklungKopf = cspRichtlinie('development', 'kopf'); const entwicklungMeta = cspRichtlinie('development', 'meta'); /** Beide Fassungen des Produktionsbuilds, für die gemeinsamen Zusicherungen. */ const produktion: readonly [string, string][] = [ ['HTTP-Kopf', produktionKopf], ['', produktionMeta], ]; it.each(produktion)('sperrt im Produktionsbuild alles per Vorgabe (%s)', (_weg, richtlinie) => { expect(richtlinie).toContain("default-src 'none'"); }); it.each(produktion)( 'erlaubt im Produktionsbuild weder unsafe-inline noch unsafe-eval (%s)', (_weg, richtlinie) => { expect(richtlinie).not.toContain('unsafe-inline'); expect(richtlinie).not.toContain('unsafe-eval'); }, ); it.each(produktion)( 'verbietet Formularversand, fremde Basis-URLs, Objekte und eigene Rahmen (%s)', (_weg, richtlinie) => { expect(richtlinie).toContain("form-action 'none'"); expect(richtlinie).toContain("base-uri 'none'"); expect(richtlinie).toContain("object-src 'none'"); // `frame-src` wirkt auch im ``: Die eigene Seite darf nichts einbetten. expect(richtlinie).toContain("frame-src 'none'"); }, ); it.each(produktion)( 'beschränkt Netzwerkverbindungen auf die eigene Herkunft (%s)', (_weg, richtlinie) => { expect(richtlinie).toContain("connect-src 'self'"); expect(richtlinie).not.toMatch(/connect-src[^;]*https?:/u); }, ); /** * Direktiven, die jeder Browser im ``-Element ignoriert (CSP Level 3). * Sie gehören ausschließlich in den HTTP-Kopf. */ const NUR_IM_KOPF = ['frame-ancestors', 'report-uri', 'sandbox'] as const; /** Namen der gesetzten Direktiven, in der Reihenfolge der Richtlinie. */ function direktivnamen(richtlinie: string): string[] { return richtlinie.split('; ').map((teil) => teil.split(' ')[0] ?? ''); } it('setzt frame-ancestors dort, wo die Direktive gilt: im HTTP-Kopf', () => { expect(produktionKopf).toContain("frame-ancestors 'none'"); expect(entwicklungKopf).toContain("frame-ancestors 'none'"); }); it.each([ ['Produktion', produktionMeta], ['Entwicklung', entwicklungMeta], ])( 'lässt aus der -Fassung jede Direktive weg, die dort ignoriert würde (%s)', (_modus, richtlinie) => { /* Nicht nur `frame-ancestors`: Wer später `sandbox` oder `report-uri` ergänzt, soll hier auffallen und nicht erst am Konsolenfehler beim Start. Ein ``, das eine ignorierte Direktive nennt, kostet nichts – behauptet aber Schutz, den es nicht gibt. */ for (const direktive of NUR_IM_KOPF) { expect(direktivnamen(richtlinie)).not.toContain(direktive); } }, ); it.each([ ['Produktion', produktionKopf, produktionMeta], ['Entwicklung', entwicklungKopf, entwicklungMeta], ])( 'hält beide Fassungen deckungsgleich – bis auf die Nur-Kopf-Direktiven (%s)', (_modus, kopf, meta) => { /* Wäre die Kopf-Fassung an anderer Stelle strenger als die ``-Fassung, bräche der Dev-Modus unbemerkt an der Schnittmenge beider Richtlinien. Verglichen wird deshalb Zeichen für Zeichen. */ const erwartet = kopf .split('; ') .filter((teil) => !NUR_IM_KOPF.some((d) => teil.startsWith(`${d} `))) .join('; '); expect(meta).toBe(erwartet); }, ); it('lockert im Dev-Modus nur genau das, was Vite braucht', () => { expect(entwicklungKopf).toContain("script-src 'self' 'unsafe-inline'"); expect(entwicklungKopf).toContain('ws:'); // Auch im Dev-Modus bleibt eval verboten. expect(entwicklungKopf).not.toContain('unsafe-eval'); }); }); describe('Navigationsziel', () => { /* * Das Protokoll `file:` allein darf nicht genügen. Sonst käme jede Datei auf * der Platte in Frage – und das Preload-Skript hängt am `webContents`, wird * also auch für die neue Seite wieder eingespielt. Eine heruntergeladene * HTML-Datei hätte damit die volle Bridge in der Hand. */ const eigene = 'file:///C:/Programme/Waffensachkunde/resources/app/out/renderer/index.html'; it('lässt genau die eigene Oberfläche zu', () => { expect(istGleicheDatei(eigene, eigene)).toBe(true); }); it('lässt Sprungziele und Parameter innerhalb der eigenen Seite zu', () => { expect(istGleicheDatei(`${eigene}#inhalt`, eigene)).toBe(true); expect(istGleicheDatei(`${eigene}?x=1`, eigene)).toBe(true); }); it('weist jede andere Datei ab', () => { for (const fremd of [ 'file:///C:/Users/Test/Downloads/beliebig.html', 'file:///C:/Windows/win.ini', 'file:///C:/Programme/Waffensachkunde/resources/app/out/renderer/andere.html', 'file:///C:/Programme/Waffensachkunde/resources/app/out/index.html', ]) { expect(istGleicheDatei(fremd, eigene)).toBe(false); } }); it('nimmt Groß- und Kleinschreibung des Pfades hin', () => { // Windows liefert den Laufwerksbuchstaben nicht immer gleich geschrieben. expect(istGleicheDatei(eigene.replace('C:', 'c:'), eigene)).toBe(true); }); it('behandelt prozentkodierte Pfade wie ihre Klartextform', () => { const mitLeerzeichen = 'file:///C:/Mein%20Ordner/index.html'; expect(istGleicheDatei('file:///C:/Mein Ordner/index.html', mitLeerzeichen)).toBe(true); }); it('weist andere Protokolle und Unsinn ab', () => { for (const fremd of ['http://example.com/', 'javascript:alert(1)', 'nichts', '']) { expect(istGleicheDatei(fremd, eigene)).toBe(false); } }); });