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