waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app tests inhaltslader.test.ts
| 1 | /** |
| 2 | * Die Lader der mitgelieferten Inhaltsdateien. |
| 3 | * |
| 4 | * **Warum das geprüft wird.** Normtexte und Themengliederung liegen als JSON |
| 5 | * im Ressourcenverzeichnis der Anwendung. Was dort steht, hat die Pipeline |
| 6 | * geschrieben – aber der Lader ist die letzte Stelle vor der Oberfläche, und |
| 7 | * er muss zwei Dinge leisten: Unbrauchbares **abweisen**, statt es |
| 8 | * weiterzureichen, und dabei die Anwendung **nicht mitreißen**. Fehlt die |
| 9 | * Datei oder ist sie kaputt, läuft das Programm ohne – genau wie vor 0.22.0. |
| 10 | * |
| 11 | * Geprüft wird gegen echte Dateien in einem Wegwerfverzeichnis, nicht gegen |
| 12 | * eine Attrappe des Dateisystems: Der Weg vom Pfad bis zum geprüften Objekt |
| 13 | * ist das, worauf es ankommt. |
| 14 | */ |
| 15 | |
| 16 | import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; |
| 17 | import { tmpdir } from 'node:os'; |
| 18 | import { join } from 'node:path'; |
| 19 | |
| 20 | import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 21 | |
| 22 | const verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-lader-')); |
| 23 | |
| 24 | vi.mock('electron', () => ({ |
| 25 | app: { |
| 26 | isPackaged: true, |
| 27 | getAppPath: (): string => verzeichnis, |
| 28 | }, |
| 29 | })); |
| 30 | |
| 31 | /* `process.resourcesPath` gibt es nur im gepackten Betrieb; unter Vitest ist |
| 32 | es nicht gesetzt. Mit `isPackaged: true` oben suchen die Lader genau dort – |
| 33 | das ist der Weg, der beim Anwender läuft. */ |
| 34 | const vorher = process.resourcesPath; |
| 35 | Object.defineProperty(process, 'resourcesPath', { value: verzeichnis, configurable: true }); |
| 36 | |
| 37 | const { normtexteLaden, normtexteZuruecksetzen, normtextePfad } = |
| 38 | await import('../src/main/normtexte'); |
| 39 | const { themenLaden, themenZuruecksetzen, themenPfad } = await import('../src/main/themen'); |
| 40 | |
| 41 | /** Schreibt eine Datei ins Wegwerfverzeichnis – oder löscht sie. */ |
| 42 | function ablegen(name: string, inhalt: string | null): void { |
| 43 | const pfad = join(verzeichnis, name); |
| 44 | if (inhalt === null) { |
| 45 | rmSync(pfad, { force: true }); |
| 46 | } else { |
| 47 | writeFileSync(pfad, inhalt, 'utf8'); |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | const NORMTEXTE_GUELTIG = { |
| 52 | meta: { version: 1, stand: '2026-08-29', hinweis: 'Auszug', gesetzesstand: { WaffG: 'Stand' } }, |
| 53 | gesetze: { |
| 54 | WaffG: { |
| 55 | bezeichnung: 'Waffengesetz', |
| 56 | stand: 'Stand', |
| 57 | quelle: 'https://www.gesetze-im-internet.de/waffg_2002/', |
| 58 | normen: { |
| 59 | '§ 1': { titel: 'Zweck', istAnlage: false, absaetze: { '1': 'Text' } }, |
| 60 | 'Anlage 1': { |
| 61 | titel: '(zu § 1 Abs. 4)', |
| 62 | istAnlage: true, |
| 63 | bloecke: [{ marke: '1.1', pfad: ['Abschnitt 1'], text: '1.1\nText' }], |
| 64 | }, |
| 65 | }, |
| 66 | }, |
| 67 | }, |
| 68 | }; |
| 69 | |
| 70 | const THEMEN_GUELTIG = { |
| 71 | meta: { version: 1, stand: '2026-08-29', hinweis: 'Gliederung dieser Software' }, |
| 72 | gruppen: [{ id: 'II.1', kapitel: 'II', titel: 'Waffenarten', fragen: ['II-01', 'II-02'] }], |
| 73 | }; |
| 74 | |
| 75 | beforeEach(() => { |
| 76 | normtexteZuruecksetzen(); |
| 77 | themenZuruecksetzen(); |
| 78 | vi.spyOn(console, 'error').mockImplementation(() => undefined); |
| 79 | }); |
| 80 | |
| 81 | afterEach(() => { |
| 82 | vi.restoreAllMocks(); |
| 83 | ablegen('normtexte.json', null); |
| 84 | ablegen('themen.json', null); |
| 85 | }); |
| 86 | |
| 87 | afterAll(() => { |
| 88 | Object.defineProperty(process, 'resourcesPath', { value: vorher, configurable: true }); |
| 89 | rmSync(verzeichnis, { recursive: true, force: true }); |
| 90 | }); |
| 91 | |
| 92 | describe('normtexteLaden', () => { |
| 93 | it('sucht im gepackten Betrieb neben der Anwendung', () => { |
| 94 | expect(normtextePfad()).toBe(join(verzeichnis, 'normtexte.json')); |
| 95 | }); |
| 96 | |
| 97 | it('liest eine gültige Datei', () => { |
| 98 | ablegen('normtexte.json', JSON.stringify(NORMTEXTE_GUELTIG)); |
| 99 | |
| 100 | const geladen = normtexteLaden(); |
| 101 | |
| 102 | expect(geladen.gesetze['WaffG']?.normen['§ 1']?.absaetze?.['1']).toBe('Text'); |
| 103 | expect(geladen.gesetze['WaffG']?.normen['Anlage 1']?.bloecke?.[0]?.marke).toBe('1.1'); |
| 104 | }); |
| 105 | |
| 106 | it('läuft ohne die Datei weiter', () => { |
| 107 | /* Der Lernbetrieb hängt nicht daran: Ohne Normtexte steht die Fundstelle |
| 108 | wie vor 0.22.0 als reines Zitat da. */ |
| 109 | expect(normtexteLaden().gesetze).toEqual({}); |
| 110 | }); |
| 111 | |
| 112 | it('reißt bei kaputtem JSON nichts mit', () => { |
| 113 | ablegen('normtexte.json', '{ das ist kein JSON'); |
| 114 | |
| 115 | expect(normtexteLaden().gesetze).toEqual({}); |
| 116 | expect(console.error).toHaveBeenCalled(); |
| 117 | }); |
| 118 | |
| 119 | it('weist eine Anlage ohne Blöcke ab, statt sie halb zu übernehmen', () => { |
| 120 | ablegen( |
| 121 | 'normtexte.json', |
| 122 | JSON.stringify({ |
| 123 | ...NORMTEXTE_GUELTIG, |
| 124 | gesetze: { |
| 125 | WaffG: { |
| 126 | ...NORMTEXTE_GUELTIG.gesetze.WaffG, |
| 127 | normen: { 'Anlage 1': { titel: 'T', istAnlage: true } }, |
| 128 | }, |
| 129 | }, |
| 130 | }), |
| 131 | ); |
| 132 | |
| 133 | expect(normtexteLaden().gesetze).toEqual({}); |
| 134 | }); |
| 135 | |
| 136 | it('weist eine Fassung ohne Versionsangabe ab', () => { |
| 137 | ablegen( |
| 138 | 'normtexte.json', |
| 139 | JSON.stringify({ ...NORMTEXTE_GUELTIG, meta: { ...NORMTEXTE_GUELTIG.meta, version: 0 } }), |
| 140 | ); |
| 141 | |
| 142 | expect(normtexteLaden().gesetze).toEqual({}); |
| 143 | }); |
| 144 | |
| 145 | it('liest die Datei nur einmal', () => { |
| 146 | ablegen('normtexte.json', JSON.stringify(NORMTEXTE_GUELTIG)); |
| 147 | const erste = normtexteLaden(); |
| 148 | ablegen('normtexte.json', null); |
| 149 | |
| 150 | // Ohne Zwischenspeicher käme jetzt der leere Bestand zurück. |
| 151 | expect(normtexteLaden()).toBe(erste); |
| 152 | }); |
| 153 | }); |
| 154 | |
| 155 | describe('themenLaden', () => { |
| 156 | it('sucht im gepackten Betrieb neben der Anwendung', () => { |
| 157 | expect(themenPfad()).toBe(join(verzeichnis, 'themen.json')); |
| 158 | }); |
| 159 | |
| 160 | it('liest eine gültige Datei', () => { |
| 161 | ablegen('themen.json', JSON.stringify(THEMEN_GUELTIG)); |
| 162 | |
| 163 | expect(themenLaden().gruppen[0]).toMatchObject({ id: 'II.1', titel: 'Waffenarten' }); |
| 164 | }); |
| 165 | |
| 166 | it('läuft ohne die Datei weiter', () => { |
| 167 | // Dann steht in den Kapiteln II bis IV nur der Knopf für das ganze Kapitel. |
| 168 | expect(themenLaden().gruppen).toEqual([]); |
| 169 | }); |
| 170 | |
| 171 | it('weist eine leere Gruppe ab', () => { |
| 172 | /* Eine leere Gruppe erschiene als Knopf, der eine Sitzung ohne Fragen |
| 173 | startet – schlimmer als eine fehlende Gruppe. */ |
| 174 | ablegen( |
| 175 | 'themen.json', |
| 176 | JSON.stringify({ |
| 177 | ...THEMEN_GUELTIG, |
| 178 | gruppen: [{ id: 'II.1', kapitel: 'II', titel: 'T', fragen: [] }], |
| 179 | }), |
| 180 | ); |
| 181 | |
| 182 | expect(themenLaden().gruppen).toEqual([]); |
| 183 | }); |
| 184 | |
| 185 | it('weist eine Frage in zwei Gruppen ab', () => { |
| 186 | /* Sonst zöge ein Thema Fragen eines anderen mit, und die Zählung neben |
| 187 | dem Knopf stimmte für beide nicht. */ |
| 188 | ablegen( |
| 189 | 'themen.json', |
| 190 | JSON.stringify({ |
| 191 | ...THEMEN_GUELTIG, |
| 192 | gruppen: [ |
| 193 | { id: 'II.1', kapitel: 'II', titel: 'A', fragen: ['II-01'] }, |
| 194 | { id: 'II.2', kapitel: 'II', titel: 'B', fragen: ['II-01'] }, |
| 195 | ], |
| 196 | }), |
| 197 | ); |
| 198 | |
| 199 | expect(themenLaden().gruppen).toEqual([]); |
| 200 | }); |
| 201 | |
| 202 | it('weist doppelte Gruppenkennungen ab', () => { |
| 203 | ablegen( |
| 204 | 'themen.json', |
| 205 | JSON.stringify({ |
| 206 | ...THEMEN_GUELTIG, |
| 207 | gruppen: [ |
| 208 | { id: 'II.1', kapitel: 'II', titel: 'A', fragen: ['II-01'] }, |
| 209 | { id: 'II.1', kapitel: 'II', titel: 'B', fragen: ['II-02'] }, |
| 210 | ], |
| 211 | }), |
| 212 | ); |
| 213 | |
| 214 | expect(themenLaden().gruppen).toEqual([]); |
| 215 | }); |
| 216 | |
| 217 | it('reißt bei kaputtem JSON nichts mit', () => { |
| 218 | ablegen('themen.json', 'null'); |
| 219 | |
| 220 | expect(themenLaden().gruppen).toEqual([]); |
| 221 | expect(console.error).toHaveBeenCalled(); |
| 222 | }); |
| 223 | }); |