waffensachkunde

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

/ app tests meldetext.test.ts

5,4 KB Rohdatei
app/tests/meldetext.test.ts — 137 Zeilen
1 /**
2 * Der Meldetext zu einer Frage.
3 *
4 * Zwei Dinge stehen hier auf dem Spiel: dass die Meldung **eindeutig** ist –
5 * die amtliche Nummer allein ist es nicht – und dass **nichts** über die
6 * meldende Person hineingerät. Der Text verlässt später das Gerät.
7 */
8
9 import { readFileSync } from 'node:fs';
10 import { join } from 'node:path';
11
12 import { describe, expect, it } from 'vitest';
13
14 import { KONTAKT } from '../src/shared/kontakt';
15 import type { Katalog } from '../src/shared/katalog';
16 import { meldetextBauen } from '../src/shared/meldetext';
17
18 const FRAGE = { id: 'I.1-01', amtliche_nummer: '1.01', seite: 6 };
19 const KATALOG = {
20 herausgeber: 'Bundesverwaltungsamt',
21 stand: '2024-12-16',
22 quelldatei_sha256: '7e5e41faee0a9198b6ba3e2fe3b46ef313448b06753516579596f98212b1d108',
23 };
24 const PROGRAMM = { fassung: '0.12.0', baukennung: '0218791', baustand: '2026-08-22' };
25
26 /** Der echte Katalog – für die Aussagen, die nur an ihm zu prüfen sind. */
27 function echterKatalog(): Katalog {
28 const pfad = join(__dirname, '..', '..', 'content', 'katalog', 'katalog.json');
29 return JSON.parse(readFileSync(pfad, 'utf-8')) as Katalog;
30 }
31
32 describe('Der Meldetext', () => {
33 it('nennt Frage, Katalogstand und Programmfassung', () => {
34 const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM);
35
36 expect(text).toContain('1.01');
37 expect(text).toContain('I.1-01');
38 expect(text).toContain('Seite 6');
39 expect(text).toContain('Bundesverwaltungsamt');
40 expect(text).toContain('16.12.2024');
41 expect(text).toContain('0.12.0');
42 expect(text).toContain('0218791');
43 expect(text).toContain(KONTAKT.epost);
44 });
45
46 it('kürzt die Prüfsumme und sagt, dass sie gekürzt ist', () => {
47 /* Die volle Summe hat 64 Zeichen und spränge die Zeile. Gekürzt genügt
48 sie, um zwei Katalogfassungen auseinanderzuhalten – aber das muss
49 dastehen, sonst hält der Empfänger sie für die ganze. */
50 const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM);
51
52 expect(text).toContain('7e5e41faee0a');
53 expect(text).toContain('(gekürzt)');
54 expect(text).not.toContain(KATALOG.quelldatei_sha256);
55 });
56
57 it('kommt ohne Baukennung aus', () => {
58 /* Ohne Git zur Bauzeit gibt es keine. Dann steht dort nichts statt eines
59 nichtssagenden „unbekannt" – dasselbe Verhalten wie im Systemzustand. */
60 const text = meldetextBauen(FRAGE, KATALOG, { ...PROGRAMM, baukennung: '', baustand: '' });
61
62 expect(text).toContain('Fassung 0.12.0');
63 expect(text).not.toContain('Bau ');
64 });
65
66 it('führt zu jeder der 575 Fragen ihre eindeutige Kennung', () => {
67 /*
68 Der eigentliche Prüfstein. Nachgezählt am echten Katalog teilen 227 der
69 575 Fragen ihre amtliche Nummer mit mindestens einer anderen – „01" gibt
70 es als II-01, III-01 und IV-01. Wer nur die Nummer meldet, meldet
71 dreideutig.
72 */
73 const katalog = echterKatalog();
74
75 const mehrdeutig = katalog.fragen.filter(
76 (frage) =>
77 katalog.fragen.filter((andere) => andere.amtliche_nummer === frage.amtliche_nummer).length >
78 1,
79 );
80 expect(mehrdeutig.length).toBeGreaterThan(200);
81
82 for (const frage of katalog.fragen) {
83 const text = meldetextBauen(frage, katalog.meta, PROGRAMM);
84 expect(text).toContain(frage.id);
85 }
86 });
87
88 it('steht Zeichen für Zeichen fest – nichts kann versehentlich mitreisen', () => {
89 /*
90 Der stärkste Wächter, den es hier geben kann, und die eigentliche Sperre
91 gegen den Fehler, der beim nächsten Umbau naheliegt: schnell noch den
92 Profilnamen oder die gegebene Antwort mit hineinnehmen, weil es bei der
93 Bearbeitung hülfe.
94
95 Ein Wortfilter fängt das nicht – er kennt nur die Wörter, an die jemand
96 vorher gedacht hat. Der vollständige Vergleich fängt jede Ergänzung,
97 auch die, die niemand vorhergesehen hat. Ändert sich der Wortlaut
98 absichtlich, ist diese Zeile mitzuändern; das ist der Sinn.
99 */
100 expect(meldetextBauen(FRAGE, KATALOG, PROGRAMM)).toBe(
101 [
102 'Fehlermeldung zur Waffensachkunde-Lernsoftware',
103 `Senden an: ${KONTAKT.epost}`,
104 '',
105 'Frage: 1.01 (Kennung I.1-01), Seite 6 der Vorlage',
106 'Fragenkatalog: Bundesverwaltungsamt, Stand 16.12.2024, Prüfsumme 7e5e41faee0a (gekürzt)',
107 'Programm: Fassung 0.12.0, Bau 0218791 vom 22.08.2026',
108 '',
109 'Diese Angaben hat die Lernsoftware zusammengestellt. Sie enthalten nichts über die',
110 'meldende Person und nichts über ihren Lernstand.',
111 '',
112 'Was nicht stimmt (bitte hier beschreiben):',
113 '',
114 ].join('\n'),
115 );
116 });
117
118 it('nennt keinen Pfad und keinen Rechnernamen', () => {
119 /* Die Angaben kommen aus vier Skalaren, nicht aus `AnwendungsInfo` – dort
120 hängt `datenbank.meldung` mit dran, die im Fehlerfall einen Dateipfad
121 enthalten kann. Diese Prüfung hält fest, dass die Trennung wirkt. */
122 const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM).toLowerCase();
123
124 for (const verboten of ['appdata', 'users', '.db', 'sqlite']) {
125 expect(text).not.toContain(verboten);
126 }
127 });
128
129 it('trennt Zeilen einheitlich und richtet nichts mit Leerzeichen aus', () => {
130 /* Der Text soll in einer Nur-Text-Mail genauso aussehen wie in einem
131 Forum. Ausgerichtete Spalten halten nur in Festbreitenschrift. */
132 const text = meldetextBauen(FRAGE, KATALOG, PROGRAMM);
133
134 expect(text).not.toContain('\r');
135 expect(text).not.toMatch(/\S {2,}\S/u);
136 });
137 });