/** * Die Lader der mitgelieferten Inhaltsdateien. * * **Warum das geprüft wird.** Normtexte und Themengliederung liegen als JSON * im Ressourcenverzeichnis der Anwendung. Was dort steht, hat die Pipeline * geschrieben – aber der Lader ist die letzte Stelle vor der Oberfläche, und * er muss zwei Dinge leisten: Unbrauchbares **abweisen**, statt es * weiterzureichen, und dabei die Anwendung **nicht mitreißen**. Fehlt die * Datei oder ist sie kaputt, läuft das Programm ohne – genau wie vor 0.22.0. * * Geprüft wird gegen echte Dateien in einem Wegwerfverzeichnis, nicht gegen * eine Attrappe des Dateisystems: Der Weg vom Pfad bis zum geprüften Objekt * ist das, worauf es ankommt. */ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-lader-')); vi.mock('electron', () => ({ app: { isPackaged: true, getAppPath: (): string => verzeichnis, }, })); /* `process.resourcesPath` gibt es nur im gepackten Betrieb; unter Vitest ist es nicht gesetzt. Mit `isPackaged: true` oben suchen die Lader genau dort – das ist der Weg, der beim Anwender läuft. */ const vorher = process.resourcesPath; Object.defineProperty(process, 'resourcesPath', { value: verzeichnis, configurable: true }); const { normtexteLaden, normtexteZuruecksetzen, normtextePfad } = await import('../src/main/normtexte'); const { themenLaden, themenZuruecksetzen, themenPfad } = await import('../src/main/themen'); /** Schreibt eine Datei ins Wegwerfverzeichnis – oder löscht sie. */ function ablegen(name: string, inhalt: string | null): void { const pfad = join(verzeichnis, name); if (inhalt === null) { rmSync(pfad, { force: true }); } else { writeFileSync(pfad, inhalt, 'utf8'); } } const NORMTEXTE_GUELTIG = { meta: { version: 1, stand: '2026-08-29', hinweis: 'Auszug', gesetzesstand: { WaffG: 'Stand' } }, gesetze: { WaffG: { bezeichnung: 'Waffengesetz', stand: 'Stand', quelle: 'https://www.gesetze-im-internet.de/waffg_2002/', normen: { '§ 1': { titel: 'Zweck', istAnlage: false, absaetze: { '1': 'Text' } }, 'Anlage 1': { titel: '(zu § 1 Abs. 4)', istAnlage: true, bloecke: [{ marke: '1.1', pfad: ['Abschnitt 1'], text: '1.1\nText' }], }, }, }, }, }; const THEMEN_GUELTIG = { meta: { version: 1, stand: '2026-08-29', hinweis: 'Gliederung dieser Software' }, gruppen: [{ id: 'II.1', kapitel: 'II', titel: 'Waffenarten', fragen: ['II-01', 'II-02'] }], }; beforeEach(() => { normtexteZuruecksetzen(); themenZuruecksetzen(); vi.spyOn(console, 'error').mockImplementation(() => undefined); }); afterEach(() => { vi.restoreAllMocks(); ablegen('normtexte.json', null); ablegen('themen.json', null); }); afterAll(() => { Object.defineProperty(process, 'resourcesPath', { value: vorher, configurable: true }); rmSync(verzeichnis, { recursive: true, force: true }); }); describe('normtexteLaden', () => { it('sucht im gepackten Betrieb neben der Anwendung', () => { expect(normtextePfad()).toBe(join(verzeichnis, 'normtexte.json')); }); it('liest eine gültige Datei', () => { ablegen('normtexte.json', JSON.stringify(NORMTEXTE_GUELTIG)); const geladen = normtexteLaden(); expect(geladen.gesetze['WaffG']?.normen['§ 1']?.absaetze?.['1']).toBe('Text'); expect(geladen.gesetze['WaffG']?.normen['Anlage 1']?.bloecke?.[0]?.marke).toBe('1.1'); }); it('läuft ohne die Datei weiter', () => { /* Der Lernbetrieb hängt nicht daran: Ohne Normtexte steht die Fundstelle wie vor 0.22.0 als reines Zitat da. */ expect(normtexteLaden().gesetze).toEqual({}); }); it('reißt bei kaputtem JSON nichts mit', () => { ablegen('normtexte.json', '{ das ist kein JSON'); expect(normtexteLaden().gesetze).toEqual({}); expect(console.error).toHaveBeenCalled(); }); it('weist eine Anlage ohne Blöcke ab, statt sie halb zu übernehmen', () => { ablegen( 'normtexte.json', JSON.stringify({ ...NORMTEXTE_GUELTIG, gesetze: { WaffG: { ...NORMTEXTE_GUELTIG.gesetze.WaffG, normen: { 'Anlage 1': { titel: 'T', istAnlage: true } }, }, }, }), ); expect(normtexteLaden().gesetze).toEqual({}); }); it('weist eine Fassung ohne Versionsangabe ab', () => { ablegen( 'normtexte.json', JSON.stringify({ ...NORMTEXTE_GUELTIG, meta: { ...NORMTEXTE_GUELTIG.meta, version: 0 } }), ); expect(normtexteLaden().gesetze).toEqual({}); }); it('liest die Datei nur einmal', () => { ablegen('normtexte.json', JSON.stringify(NORMTEXTE_GUELTIG)); const erste = normtexteLaden(); ablegen('normtexte.json', null); // Ohne Zwischenspeicher käme jetzt der leere Bestand zurück. expect(normtexteLaden()).toBe(erste); }); }); describe('themenLaden', () => { it('sucht im gepackten Betrieb neben der Anwendung', () => { expect(themenPfad()).toBe(join(verzeichnis, 'themen.json')); }); it('liest eine gültige Datei', () => { ablegen('themen.json', JSON.stringify(THEMEN_GUELTIG)); expect(themenLaden().gruppen[0]).toMatchObject({ id: 'II.1', titel: 'Waffenarten' }); }); it('läuft ohne die Datei weiter', () => { // Dann steht in den Kapiteln II bis IV nur der Knopf für das ganze Kapitel. expect(themenLaden().gruppen).toEqual([]); }); it('weist eine leere Gruppe ab', () => { /* Eine leere Gruppe erschiene als Knopf, der eine Sitzung ohne Fragen startet – schlimmer als eine fehlende Gruppe. */ ablegen( 'themen.json', JSON.stringify({ ...THEMEN_GUELTIG, gruppen: [{ id: 'II.1', kapitel: 'II', titel: 'T', fragen: [] }], }), ); expect(themenLaden().gruppen).toEqual([]); }); it('weist eine Frage in zwei Gruppen ab', () => { /* Sonst zöge ein Thema Fragen eines anderen mit, und die Zählung neben dem Knopf stimmte für beide nicht. */ ablegen( 'themen.json', JSON.stringify({ ...THEMEN_GUELTIG, gruppen: [ { id: 'II.1', kapitel: 'II', titel: 'A', fragen: ['II-01'] }, { id: 'II.2', kapitel: 'II', titel: 'B', fragen: ['II-01'] }, ], }), ); expect(themenLaden().gruppen).toEqual([]); }); it('weist doppelte Gruppenkennungen ab', () => { ablegen( 'themen.json', JSON.stringify({ ...THEMEN_GUELTIG, gruppen: [ { id: 'II.1', kapitel: 'II', titel: 'A', fragen: ['II-01'] }, { id: 'II.1', kapitel: 'II', titel: 'B', fragen: ['II-02'] }, ], }), ); expect(themenLaden().gruppen).toEqual([]); }); it('reißt bei kaputtem JSON nichts mit', () => { ablegen('themen.json', 'null'); expect(themenLaden().gruppen).toEqual([]); expect(console.error).toHaveBeenCalled(); }); });