// @vitest-environment node /** * Die Offline-Zusage – als Wache, nicht als Beobachtung. * * ## Warum es diese Datei gibt * * „Offline und ohne Konto. Keine Anmeldung, keine Cloud, keine Telemetrie“ * steht in `README.md`, in „Über diese Software“, im Handbuch und in * Abschnitt 2 der Datenschutzerklärung. Es ist die weitreichendste Zusage * dieses Programms, und sie war bis Fassung 0.24.1 die einzige große, die * **niemand bewachte**: Dass in `app/src` kein `fetch`, kein `net.request` * und kein `https.get` steht, war ein von Hand nachgezählter Befund. Beim * nächsten Umbau wäre niemand rot geworden — `docs/stand.md` führt genau das * unter „Als Nächstes“ (Abschnitt 8.2, Punkt 9), und die * Datenschutzerklärung sagt in Abschnitt 2 ausdrücklich „nachgezählt, nicht * bewacht“. * * ## Was hier geprüft wird * * Gelesen wird, was `git ls-files` unter `app/src` führt — nur das wird * ausgeliefert. Gesucht wird nach den Aufrufen, mit denen eine Verbindung * überhaupt entstehen kann. Nicht nach Zeichenketten wie „http“: Adressen * stehen an mehreren Stellen als **Text** da (die Quellenangabe des * Katalogs, die Adresse der Unterstützungsseite, `gesetze-im-internet.de` in * Kommentaren), und ein Test, der daran anschlägt, wird nach dem zweiten * Fehlalarm abgeschaltet. * * ## Die zwei erlaubten Wege nach außen * * Beide liegen in `main/sicherheit.ts`, beide öffnen nur auf ausdrücklichen * Klick den **Systembrowser** — die Anwendung selbst verbindet sich dabei * nicht. `shell.openExternal` ist deshalb erlaubt, aber nur dort; taucht es * anderswo auf, ist das ein neuer Weg nach außen und gehört in die * Datenschutzerklärung. */ import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..'); /** * Aufrufe, mit denen eine Verbindung nach außen entsteht. * * Jeder mit Wortgrenze und öffnender Klammer: `fetch(` trifft, `prefetch(` * nicht, und das Wort „fetch“ in einem Kommentar auch nicht. */ const NETZAUFRUFE: readonly (readonly [RegExp, string])[] = Object.freeze([ [/(? zeile.trim()) .filter((zeile) => zeile.endsWith('.ts') || zeile.endsWith('.tsx')); } catch { return null; } return liste.map((pfad) => ({ pfad, inhalt: readFileSync(join(wurzel, pfad), 'utf8') })); } describe('Die Offline-Zusage', () => { const dateien = quelldateien(); it('findet überhaupt Quelldateien', () => { /* Ohne diese Gegenprobe wären die Prüfungen unten immer grün und damit wertlos – dieselbe Überlegung wie in `reuse-abdeckung.test.ts`. */ expect( dateien, 'Ohne Git lässt sich der ausgelieferte Quelltext nicht bestimmen.', ).not.toBeNull(); expect(dateien?.length ?? 0).toBeGreaterThan(150); }); it('enthält keinen einzigen Netzaufruf', () => { const befunde: string[] = []; for (const { pfad, inhalt } of dateien ?? []) { for (const [muster, name] of NETZAUFRUFE) { if (muster.test(inhalt)) { befunde.push(`${pfad}: ${name}`); } } } expect( befunde, 'Diese Anwendung sagt zu, offline zu arbeiten – in README, in „Über diese ' + 'Software“, im Handbuch und in der Datenschutzerklärung. Ein neuer Weg ' + 'nach außen bricht diese Zusage und gehört in alle vier Texte.', ).toEqual([]); }); it('stößt den Systembrowser nur aus der einen dafür vorgesehenen Stelle an', () => { /* Nur `openExternal`: `shell.openPath` und `shell.showItemInFolder` öffnen eine **lokale** Datei im zuständigen Programm und führen nicht nach außen. Sie stehen in `main/dateizugriff.ts` und sind dort mit Grund. Gesucht wird der Aufruf, nicht die Erwähnung – der Name kommt auch in Kommentaren vor, die genau das erklären. */ const anderswo = (dateien ?? []) .filter( ({ pfad, inhalt }) => !pfad.endsWith(AUSSENWEG) && /\bshell\.openExternal\s*\(/u.test(inhalt), ) .map(({ pfad }) => pfad); expect( anderswo, `Der Weg nach außen gehört in ${AUSSENWEG}: Dort wird die Adresse mit der ` + 'einen eingetragenen verglichen, und dort steht die Begründung.', ).toEqual([]); }); /* Die Zusage muss auch richtig formuliert sein. Bis Fassung 0.24.2 stand in „Über diese Software“ – ausgerechnet im Absatz, der um Unterstützung bittet – der Satz „sie sammelt keine Daten“. Das ist unrichtig: Die Anwendung legt einen Lernstand an, protokolliert jede Antwort mit Zeitpunkt, Dauer und Freitext, schreibt drei Selbstsicherungen und merkt sich die Fensterlage. Abschnitt 3 der Datenschutzerklärung sagt es selbst — „Beim Benutzen der Anwendung entstehen Daten“ — und nennt sie in Abschnitt 4 ausdrücklich personenbezogen im Sinne von Art. 4 Nr. 1 DSGVO. Richtig ist „überträgt“, und sechs andere Stellen sagten es schon so. Die eine falsche wog am schwersten: Wer um Geld bittet, wird beim Wort genommen. */ it('verspricht nirgends, keine Daten zu sammeln – nur, keine zu übertragen', () => { const befunde: string[] = []; for (const { pfad, inhalt } of dateien ?? []) { for (const treffer of inhalt.matchAll(/sammelt\s+kein/gu)) { befunde.push(`${pfad}: ${treffer[0]}`); } } expect( befunde, 'Die Anwendung sammelt sehr wohl Daten – sie überträgt sie nur nicht. ' + 'Die Datenschutzerklärung sagt das in Abschnitt 3 ausdrücklich.', ).toEqual([]); }); it('erkennt die falsche Zusage, wenn eine dasteht', () => { /* Gegenprobe: ohne sie bliebe unbemerkt, wenn das Muster nie mehr trifft. */ expect(/sammelt\s+kein/u.test('sie sammelt keine Daten')).toBe(true); expect(/sammelt\s+kein/u.test('sie überträgt keine Daten')).toBe(false); }); it('lädt keine Schrift und kein Stilblatt aus dem Netz', () => { /* Eine eingebundene Webschrift wäre eine Verbindung, die niemand als solche liest – und sie stünde in keinem der vier Texte. */ const befunde = (dateien ?? []) .filter(({ inhalt }) => /@import\s+url\(\s*['"]?https?:/u.test(inhalt)) .map(({ pfad }) => pfad); expect(befunde).toEqual([]); }); });