import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { VERWEISZIELE, verweisadresse } from '../../electron/verweise'; /** * Die Verweise, die aus dem Programm herausführen. * * DREI ZIELE, UND DER RENDERER KENNT KEINE ADRESSE. Er schickt einen Schlüssel * über `IPC.oeffneVerweis`; die Adresse dahinter steht im Hauptprozess. Das ist * der Kern: `shell.openExternal` gibt eine Zeichenkette an das Betriebssystem, * und nähme der Kanal eine Adresse entgegen, bestimmte am Ende jeder, der etwas * in die Oberfläche trägt, wohin sie zeigt. Über genau dieses Muster ließ sich * im Altbestand mit einer präparierten Projektdatei beliebiger Code ausführen. * * WAS HIER GEPRÜFT WIRD: * - Jeder Schlüssel führt auf eine Adresse, und zwar auf https ohne Anhängsel. * - Was nicht in der Liste steht, öffnet nichts - auch nicht mit Tricks. * - Die Adresse des Quellcodes ist dieselbe wie in Beilage und README. * Mehrere Stellen für eine Adresse laufen auseinander, und eine falsche wäre * schlimmer als keine (Artikel 3 der EUPL). */ const UEBER = readFileSync('src/ui/ueber.ts', 'utf8'); const MENUE = readFileSync('electron/menu.ts', 'utf8'); const BEILAGE = readFileSync('QUELLCODE.txt', 'utf8'); const LIESMICH = readFileSync('README.md', 'utf8'); describe('Jeder Schluessel fuehrt auf genau eine saubere Adresse', () => { it('gibt es ueberhaupt Ziele', () => { // Ohne diesen Fall liefen die beiden darunter über eine leere Liste. expect(VERWEISZIELE.length, 'Verweisziele').toBeGreaterThan(0); }); it.each([...VERWEISZIELE])('fuehrt "%s" auf eine https-Adresse ohne Anhaengsel', (ziel) => { const adresse = verweisadresse(ziel); expect(adresse, `Adresse zu ${ziel}`).not.toBeNull(); const url = new URL(String(adresse)); expect(url.protocol, `Protokoll von ${ziel}`).toBe('https:'); /* * KEINE VERFOLGUNGSPARAMETER. Wer einen dieser Verweise anklickt, soll dem * Ziel nichts mitteilen, das er nicht ohnehin mitteilte, indem er die * Adresse eintippt - kein `?ref=`, keine Kennung, kein Anker. */ expect(url.search, `${ziel} traegt eine Abfragezeichenkette`).toBe(''); expect(url.hash, `${ziel} traegt einen Anker`).toBe(''); expect(url.username + url.password, `${ziel} traegt Anmeldedaten`).toBe(''); }); }); describe('Was nicht in der Liste steht, oeffnet nichts', () => { it.each([ ['unbekannt', 'ein Wort, das es nicht gibt'], ['', 'die leere Zeichenkette'], ['https://boese.invalid/', 'eine Adresse statt eines Schluessels'], ['__proto__', 'ein Eigenname des Prototyps'], ['constructor', 'ein zweiter'], ['toString', 'ein dritter'], ])('weist %s ab (%s)', (ziel, _grund) => { expect(verweisadresse(ziel)).toBeNull(); }); it.each([ [null, 'null'], [undefined, 'undefined'], [42, 'eine Zahl'], [{}, 'ein Objekt'], [['quellcode'], 'ein Feld mit gueltigem Inhalt'], ])('weist %s ab (%s)', (ziel, _grund) => { expect(verweisadresse(ziel)).toBeNull(); }); it('gibt kein Ergebnis zurueck, das nicht aus der Liste stammt', () => { /* * Die Gegenprobe zu allem darüber: Was auch immer hineingeht, es kommt * entweder `null` heraus oder eine der hinterlegten Adressen. Ein * Rückfall auf irgendetwas gäbe es nicht. */ const erlaubt = new Set(VERWEISZIELE.map((z) => verweisadresse(z))); for (const unsinn of ['unbekannt', '', 'https://boese.invalid/', '__proto__', 'valueOf']) { const ergebnis = verweisadresse(unsinn); expect(ergebnis === null || erlaubt.has(ergebnis), `Ergebnis zu ${unsinn}`).toBe(true); } }); }); describe('Die Adressen decken sich mit den Unterlagen', () => { it('nennt jede Unterlage denselben Quellcode-Ort wie der Hauptprozess', () => { /* * Artikel 3 der EUPL: Der Vermerk verweist auf einen Speicherort, an dem * der Quellcode unentgeltlich zu bekommen ist. Dieser Fall haelt die * Beilage und die README gegen den Hauptprozess. Die Schaltflaeche im * "Über"-Fenster fuehrt jetzt dorthin - eine abweichende Adresse waere * schlimmer als keine Schaltflaeche. */ const ort = verweisadresse('quellcode'); expect(ort, 'Adresse zum Quellcode').not.toBeNull(); for (const [name, text] of [ ['QUELLCODE.txt', BEILAGE], ['README.md', LIESMICH], ] as const) { expect(text, `${name} nennt einen anderen Ort`).toContain(String(ort)); } }); it('haelt das "Über"-Fenster keine eigene Fassung der Adresse', () => { /* * Die Kehrseite des Falles darueber, und der wichtigere: Das Fenster ZEIGT * die Adresse an - es darf sie aber nicht ein zweites Mal fuehren. Bis zum * Zusammenlegen stand sie dort als eigene Konstante und im Hauptprozess * noch einmal; zwei Staende fuer eine Adresse, und Artikel 3 der EUPL * haengt daran. */ expect(UEBER, 'das Fenster fuehrt die Adresse selbst').not.toContain('olaf-willerding.de'); expect(UEBER, 'das Fenster holt die Adresse nicht aus der Tafel').toContain( 'VERWEISADRESSEN.quellcode', ); }); it('oeffnet das Hilfemenue dieselbe Unterstuetzungsseite', () => { // Zwei Wege zu einer Seite - der Menüeintrag und die Schaltfläche im // "Über"-Fenster. Zwei Adressen wären eine zu viel. const ort = verweisadresse('unterstuetzung'); expect(ort, 'Adresse zur Unterstuetzung').not.toBeNull(); expect(MENUE, 'das Hilfemenue nennt eine andere Adresse').toContain(String(ort)); }); it('sagt das "Über"-Fenster vorher, dass der Browser aufgeht', () => { /* * Eine Schaltfläche, die das Programm verlässt, kündigt das an - im Text * und nicht in der Beschriftung. Das ist die Gepflogenheit dieses * Programms; der Luftbild-Dialog nennt ebenso im Fließtext, was den * Rechner verlässt. */ const einzeilig = UEBER.replace(/\s+/g, ' '); expect(einzeilig, 'das Fenster kuendigt den Browser nicht an').toContain('in Ihrem Browser'); expect(einzeilig, 'das Fenster verschweigt, dass es selbst nichts abruft').toContain( 'keine Verbindung auf', ); }); it('nennt das "Über"-Fenster die Unterstuetzung als freiwillig', () => { // Der Ton ist Teil der Sache: Das Programm ist frei und kostet nichts. const einzeilig = UEBER.replace(/\s+/g, ' '); expect(einzeilig, 'die Freiwilligkeit fehlt').toMatch(/freiwillig/); expect(einzeilig, 'der Hinweis auf die Nutzung fehlt').toContain( 'auf die Nutzung hat das keinen Einfluss', ); }); });