// @vitest-environment node // Die Lizenzangaben werden im Main-Prozess von der Platte gelesen. Getestet // wird gegen echte Dateien in einem temporären Verzeichnis – und einmal gegen // die echten Dateien des Projekts, damit auch die Pfadauflösung geprüft ist. import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Katalog } from '../src/shared/katalog'; import { EUPL_HINWEIS, EUPL_KENNUNG, KATALOG_HINWEIS } from '../src/shared/lizenzen'; /** * `app.getAppPath()` liefert im Entwicklungsmodus das Verzeichnis mit der * package.json – unter Vitest ist das genau das Arbeitsverzeichnis `app/`. * Von dort aus findet die Aufwärtssuche die Dateien im Projektwurzelverzeichnis. */ vi.mock('electron', () => ({ app: { isPackaged: false, getAppPath: (): string => process.cwd(), }, })); const { lizenzenBilden, lizenzenLesen, lizenzenZuruecksetzen } = await import('../src/main/lizenzen'); const KATALOG: Katalog = { meta: { titel: 'Prüfkatalog', herausgeber: 'Bundesverwaltungsamt', stand: '2024-12-16', quellenangabe: 'Amtlicher Fragenkatalog, Stand 16.12.2024.', quelle_url: 'https://www.bva.bund.de/', quelldatei_sha256: 'a'.repeat(64), fragen_gesamt: 1, }, kapitel: [{ id: 'I', titel: 'Waffenrecht', abschnitte: [] }], bilder: [], fragen: [ { id: 'I-01', amtliche_nummer: '1.01', kapitel: 'I', abschnitt: null, typ: 'freitext', seite: 1, frage: { text: 'Frage', segmente: [{ t: 'Frage' }] }, bilder: [], musterantwort: { text: 'Antwort', segmente: [{ t: 'Antwort', h: true }] }, }, ], }; const DRITTLIZENZEN = { stand: 'automatisch erzeugt', pakete: [ { name: 'electron', version: '43.4.1', lizenz: 'MIT', url: 'https://github.com/electron/electron', lizenztext: 'Copyright (c) Electron contributors …', }, { name: 'SQLite', version: null, lizenz: 'Public Domain', url: 'https://www.sqlite.org/copyright.html', lizenztext: null, hinweis: 'SQLite ist gemeinfrei.', }, ], }; let verzeichnis: string; let lizenzPfad: string; let drittPfad: string; let warnungen: string[]; function quellen( aenderung: Partial[0]> = {}, ): Parameters[0] { return { lizenzPfad, drittPfad, katalog: KATALOG, warnen: (meldung: string) => warnungen.push(meldung), ...aenderung, }; } beforeEach(() => { verzeichnis = mkdtempSync(join(tmpdir(), 'waffensachkunde-lizenzen-')); lizenzPfad = join(verzeichnis, 'LICENSE.de.txt'); drittPfad = join(verzeichnis, 'drittlizenzen.json'); writeFileSync(lizenzPfad, 'EUROPÄISCHE UNION\nÖFFENTLICHE LIZENZ v. 1.2\n', 'utf8'); writeFileSync(drittPfad, JSON.stringify(DRITTLIZENZEN), 'utf8'); warnungen = []; lizenzenZuruecksetzen(); }); afterEach(() => { lizenzenZuruecksetzen(); rmSync(verzeichnis, { recursive: true, force: true }); }); // ─── Vollständige Angaben ─────────────────────────────────────────────────── describe('lizenzenBilden', () => { it('liefert die eigene Lizenz mit deutschem Volltext', () => { const angaben = lizenzenBilden(quellen()); expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG); expect(angaben.eigene.hinweis).toBe(EUPL_HINWEIS); expect(angaben.eigene.name).toContain('1.2'); expect(angaben.eigene.text).toContain('ÖFFENTLICHE LIZENZ'); expect(warnungen).toEqual([]); }); it('führt den Katalog mit Quellenangabe und Rechtshinweis', () => { const angaben = lizenzenBilden(quellen()); expect(angaben.katalog).toEqual({ quellenangabe: KATALOG.meta.quellenangabe, quelleUrl: KATALOG.meta.quelle_url, hinweis: KATALOG_HINWEIS, }); }); it('übernimmt die Fremdkomponenten samt Sammelposten-Hinweis', () => { const angaben = lizenzenBilden(quellen()); expect(angaben.dritte).toHaveLength(2); expect(angaben.dritte[0]).toEqual({ name: 'electron', version: '43.4.1', lizenz: 'MIT', url: 'https://github.com/electron/electron', lizenztext: 'Copyright (c) Electron contributors …', }); expect(angaben.dritte[1]).toMatchObject({ name: 'SQLite', version: null, lizenztext: null, hinweis: 'SQLite ist gemeinfrei.', }); }); }); // ─── Fehlende und beschädigte Dateien ─────────────────────────────────────── describe('Fehlende Angaben', () => { it('kommt ohne Lizenzdatei aus und protokolliert das verständlich', () => { const angaben = lizenzenBilden(quellen({ lizenzPfad: null })); expect(angaben.eigene.text).toBeNull(); // Die Lizenz selbst gilt weiter – Kennung und Hinweis bleiben stehen. expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG); expect(angaben.dritte).toHaveLength(2); expect(warnungen).toHaveLength(1); expect(warnungen[0]).toContain('LICENSE.de.txt'); expect(warnungen[0]).toMatch(/nicht gefunden/u); }); it('kommt ohne Datei der Fremdkomponenten aus', () => { const angaben = lizenzenBilden(quellen({ drittPfad: null })); expect(angaben.dritte).toEqual([]); expect(angaben.eigene.text).not.toBeNull(); expect(warnungen[0]).toContain('drittlizenzen.json'); }); it('stürzt bei einer nicht vorhandenen Datei nicht ab', () => { const angaben = lizenzenBilden( quellen({ lizenzPfad: join(verzeichnis, 'gibt-es-nicht.txt'), drittPfad: join(verzeichnis, 'auch-nicht.json'), }), ); expect(angaben.eigene.text).toBeNull(); expect(angaben.dritte).toEqual([]); expect(warnungen).toHaveLength(2); expect(warnungen.every((w) => w.startsWith('Die Datei'))).toBe(true); }); it('überliest beschädigtes JSON, ohne die übrigen Angaben zu verlieren', () => { writeFileSync(drittPfad, '{ das ist kein JSON', 'utf8'); const angaben = lizenzenBilden(quellen()); expect(angaben.dritte).toEqual([]); expect(angaben.eigene.text).not.toBeNull(); expect(warnungen).toHaveLength(1); }); it('übergeht einzelne unbrauchbare Einträge und behält die übrigen', () => { writeFileSync( drittPfad, JSON.stringify({ pakete: [ { name: 'ohne Lizenzangabe' }, 'gar kein Objekt', { name: 'react', version: '19.2.8', lizenz: 'MIT', url: null, lizenztext: null }, ], }), 'utf8', ); const angaben = lizenzenBilden(quellen()); expect(angaben.dritte.map((d) => d.name)).toEqual(['react']); expect(warnungen).toHaveLength(2); }); it('lässt eine Datei ohne „pakete“ nicht durchgehen', () => { writeFileSync(drittPfad, JSON.stringify({ stand: 'leer' }), 'utf8'); const angaben = lizenzenBilden(quellen()); expect(angaben.dritte).toEqual([]); expect(warnungen[0]).toContain('pakete'); }); it('lässt die Katalogangaben leer, wenn der Katalog fehlt', () => { const angaben = lizenzenBilden(quellen({ katalog: null })); expect(angaben.katalog.quellenangabe).toBe(''); expect(angaben.katalog.quelleUrl).toBe(''); // Der Rechtshinweis zum amtlichen Werk gilt unabhängig davon. expect(angaben.katalog.hinweis).toBe(KATALOG_HINWEIS); expect(warnungen.some((w) => w.includes('Fragenkatalog'))).toBe(true); }); }); // ─── Pfadauflösung und Zwischenspeicher ───────────────────────────────────── describe('lizenzenLesen', () => { it('findet die echten Dateien des Projektwurzelverzeichnisses', () => { const angaben = lizenzenLesen(); expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG); expect(angaben.eigene.text).toMatch(/EUPL/u); expect(angaben.dritte.length).toBeGreaterThan(0); expect(angaben.dritte.map((d) => d.name)).toContain('electron'); expect(angaben.katalog.quellenangabe).toContain('Bundesverwaltungsamt'); expect(angaben.katalog.quelleUrl).toMatch(/^https:\/\//u); }); it('liest die Dateien nur einmal', () => { const erste = lizenzenLesen(); const zweite = lizenzenLesen(); expect(zweite).toBe(erste); }); it('liest nach dem Zurücksetzen erneut', () => { const erste = lizenzenLesen(); lizenzenZuruecksetzen(); const zweite = lizenzenLesen(); expect(zweite).not.toBe(erste); expect(zweite).toEqual(erste); }); });