waffensachkunde

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

/ app tests lizenzen.test.ts

8,5 KB Rohdatei
app/tests/lizenzen.test.ts — 268 Zeilen
1 // @vitest-environment node
2 // Die Lizenzangaben werden im Main-Prozess von der Platte gelesen. Getestet
3 // wird gegen echte Dateien in einem temporären Verzeichnis – und einmal gegen
4 // die echten Dateien des Projekts, damit auch die Pfadauflösung geprüft ist.
5
6 import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7 import { tmpdir } from 'node:os';
8 import { join } from 'node:path';
9
10 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
12 import type { Katalog } from '../src/shared/katalog';
13 import { EUPL_HINWEIS, EUPL_KENNUNG, KATALOG_HINWEIS } from '../src/shared/lizenzen';
14
15 /**
16 * `app.getAppPath()` liefert im Entwicklungsmodus das Verzeichnis mit der
17 * package.json – unter Vitest ist das genau das Arbeitsverzeichnis `app/`.
18 * Von dort aus findet die Aufwärtssuche die Dateien im Projektwurzelverzeichnis.
19 */
20 vi.mock('electron', () => ({
21 app: {
22 isPackaged: false,
23 getAppPath: (): string => process.cwd(),
24 },
25 }));
26
27 const { lizenzenBilden, lizenzenLesen, lizenzenZuruecksetzen } =
28 await import('../src/main/lizenzen');
29
30 const KATALOG: Katalog = {
31 meta: {
32 titel: 'Prüfkatalog',
33 herausgeber: 'Bundesverwaltungsamt',
34 stand: '2024-12-16',
35 quellenangabe: 'Amtlicher Fragenkatalog, Stand 16.12.2024.',
36 quelle_url: 'https://www.bva.bund.de/',
37 quelldatei_sha256: 'a'.repeat(64),
38 fragen_gesamt: 1,
39 },
40 kapitel: [{ id: 'I', titel: 'Waffenrecht', abschnitte: [] }],
41 bilder: [],
42 fragen: [
43 {
44 id: 'I-01',
45 amtliche_nummer: '1.01',
46 kapitel: 'I',
47 abschnitt: null,
48 typ: 'freitext',
49 seite: 1,
50 frage: { text: 'Frage', segmente: [{ t: 'Frage' }] },
51 bilder: [],
52 musterantwort: { text: 'Antwort', segmente: [{ t: 'Antwort', h: true }] },
53 },
54 ],
55 };
56
57 const DRITTLIZENZEN = {
58 stand: 'automatisch erzeugt',
59 pakete: [
60 {
61 name: 'electron',
62 version: '43.4.1',
63 lizenz: 'MIT',
64 url: 'https://github.com/electron/electron',
65 lizenztext: 'Copyright (c) Electron contributors …',
66 },
67 {
68 name: 'SQLite',
69 version: null,
70 lizenz: 'Public Domain',
71 url: 'https://www.sqlite.org/copyright.html',
72 lizenztext: null,
73 hinweis: 'SQLite ist gemeinfrei.',
74 },
75 ],
76 };
77
78 let verzeichnis: string;
79 let lizenzPfad: string;
80 let drittPfad: string;
81 let warnungen: string[];
82
83 function quellen(
84 aenderung: Partial<Parameters<typeof lizenzenBilden>[0]> = {},
85 ): Parameters<typeof lizenzenBilden>[0] {
86 return {
87 lizenzPfad,
88 drittPfad,
89 katalog: KATALOG,
90 warnen: (meldung: string) => warnungen.push(meldung),
91 ...aenderung,
92 };
93 }
94
95 beforeEach(() => {
96 verzeichnis = mkdtempSync(join(tmpdir(), 'waffensachkunde-lizenzen-'));
97 lizenzPfad = join(verzeichnis, 'LICENSE.de.txt');
98 drittPfad = join(verzeichnis, 'drittlizenzen.json');
99 writeFileSync(lizenzPfad, 'EUROPÄISCHE UNION\nÖFFENTLICHE LIZENZ v. 1.2\n', 'utf8');
100 writeFileSync(drittPfad, JSON.stringify(DRITTLIZENZEN), 'utf8');
101 warnungen = [];
102 lizenzenZuruecksetzen();
103 });
104
105 afterEach(() => {
106 lizenzenZuruecksetzen();
107 rmSync(verzeichnis, { recursive: true, force: true });
108 });
109
110 // ─── Vollständige Angaben ───────────────────────────────────────────────────
111
112 describe('lizenzenBilden', () => {
113 it('liefert die eigene Lizenz mit deutschem Volltext', () => {
114 const angaben = lizenzenBilden(quellen());
115
116 expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG);
117 expect(angaben.eigene.hinweis).toBe(EUPL_HINWEIS);
118 expect(angaben.eigene.name).toContain('1.2');
119 expect(angaben.eigene.text).toContain('ÖFFENTLICHE LIZENZ');
120 expect(warnungen).toEqual([]);
121 });
122
123 it('führt den Katalog mit Quellenangabe und Rechtshinweis', () => {
124 const angaben = lizenzenBilden(quellen());
125
126 expect(angaben.katalog).toEqual({
127 quellenangabe: KATALOG.meta.quellenangabe,
128 quelleUrl: KATALOG.meta.quelle_url,
129 hinweis: KATALOG_HINWEIS,
130 });
131 });
132
133 it('übernimmt die Fremdkomponenten samt Sammelposten-Hinweis', () => {
134 const angaben = lizenzenBilden(quellen());
135
136 expect(angaben.dritte).toHaveLength(2);
137 expect(angaben.dritte[0]).toEqual({
138 name: 'electron',
139 version: '43.4.1',
140 lizenz: 'MIT',
141 url: 'https://github.com/electron/electron',
142 lizenztext: 'Copyright (c) Electron contributors …',
143 });
144 expect(angaben.dritte[1]).toMatchObject({
145 name: 'SQLite',
146 version: null,
147 lizenztext: null,
148 hinweis: 'SQLite ist gemeinfrei.',
149 });
150 });
151 });
152
153 // ─── Fehlende und beschädigte Dateien ───────────────────────────────────────
154
155 describe('Fehlende Angaben', () => {
156 it('kommt ohne Lizenzdatei aus und protokolliert das verständlich', () => {
157 const angaben = lizenzenBilden(quellen({ lizenzPfad: null }));
158
159 expect(angaben.eigene.text).toBeNull();
160 // Die Lizenz selbst gilt weiter – Kennung und Hinweis bleiben stehen.
161 expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG);
162 expect(angaben.dritte).toHaveLength(2);
163 expect(warnungen).toHaveLength(1);
164 expect(warnungen[0]).toContain('LICENSE.de.txt');
165 expect(warnungen[0]).toMatch(/nicht gefunden/u);
166 });
167
168 it('kommt ohne Datei der Fremdkomponenten aus', () => {
169 const angaben = lizenzenBilden(quellen({ drittPfad: null }));
170
171 expect(angaben.dritte).toEqual([]);
172 expect(angaben.eigene.text).not.toBeNull();
173 expect(warnungen[0]).toContain('drittlizenzen.json');
174 });
175
176 it('stürzt bei einer nicht vorhandenen Datei nicht ab', () => {
177 const angaben = lizenzenBilden(
178 quellen({
179 lizenzPfad: join(verzeichnis, 'gibt-es-nicht.txt'),
180 drittPfad: join(verzeichnis, 'auch-nicht.json'),
181 }),
182 );
183
184 expect(angaben.eigene.text).toBeNull();
185 expect(angaben.dritte).toEqual([]);
186 expect(warnungen).toHaveLength(2);
187 expect(warnungen.every((w) => w.startsWith('Die Datei'))).toBe(true);
188 });
189
190 it('überliest beschädigtes JSON, ohne die übrigen Angaben zu verlieren', () => {
191 writeFileSync(drittPfad, '{ das ist kein JSON', 'utf8');
192
193 const angaben = lizenzenBilden(quellen());
194
195 expect(angaben.dritte).toEqual([]);
196 expect(angaben.eigene.text).not.toBeNull();
197 expect(warnungen).toHaveLength(1);
198 });
199
200 it('übergeht einzelne unbrauchbare Einträge und behält die übrigen', () => {
201 writeFileSync(
202 drittPfad,
203 JSON.stringify({
204 pakete: [
205 { name: 'ohne Lizenzangabe' },
206 'gar kein Objekt',
207 { name: 'react', version: '19.2.8', lizenz: 'MIT', url: null, lizenztext: null },
208 ],
209 }),
210 'utf8',
211 );
212
213 const angaben = lizenzenBilden(quellen());
214
215 expect(angaben.dritte.map((d) => d.name)).toEqual(['react']);
216 expect(warnungen).toHaveLength(2);
217 });
218
219 it('lässt eine Datei ohne „pakete“ nicht durchgehen', () => {
220 writeFileSync(drittPfad, JSON.stringify({ stand: 'leer' }), 'utf8');
221
222 const angaben = lizenzenBilden(quellen());
223
224 expect(angaben.dritte).toEqual([]);
225 expect(warnungen[0]).toContain('pakete');
226 });
227
228 it('lässt die Katalogangaben leer, wenn der Katalog fehlt', () => {
229 const angaben = lizenzenBilden(quellen({ katalog: null }));
230
231 expect(angaben.katalog.quellenangabe).toBe('');
232 expect(angaben.katalog.quelleUrl).toBe('');
233 // Der Rechtshinweis zum amtlichen Werk gilt unabhängig davon.
234 expect(angaben.katalog.hinweis).toBe(KATALOG_HINWEIS);
235 expect(warnungen.some((w) => w.includes('Fragenkatalog'))).toBe(true);
236 });
237 });
238
239 // ─── Pfadauflösung und Zwischenspeicher ─────────────────────────────────────
240
241 describe('lizenzenLesen', () => {
242 it('findet die echten Dateien des Projektwurzelverzeichnisses', () => {
243 const angaben = lizenzenLesen();
244
245 expect(angaben.eigene.kennung).toBe(EUPL_KENNUNG);
246 expect(angaben.eigene.text).toMatch(/EUPL/u);
247 expect(angaben.dritte.length).toBeGreaterThan(0);
248 expect(angaben.dritte.map((d) => d.name)).toContain('electron');
249 expect(angaben.katalog.quellenangabe).toContain('Bundesverwaltungsamt');
250 expect(angaben.katalog.quelleUrl).toMatch(/^https:\/\//u);
251 });
252
253 it('liest die Dateien nur einmal', () => {
254 const erste = lizenzenLesen();
255 const zweite = lizenzenLesen();
256
257 expect(zweite).toBe(erste);
258 });
259
260 it('liest nach dem Zurücksetzen erneut', () => {
261 const erste = lizenzenLesen();
262 lizenzenZuruecksetzen();
263 const zweite = lizenzenLesen();
264
265 expect(zweite).not.toBe(erste);
266 expect(zweite).toEqual(erste);
267 });
268 });