waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app tests inhaltslader.test.ts

6,9 KB Rohdatei
app/tests/inhaltslader.test.ts — 223 Zeilen
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 });