waffensachkunde

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

/ app tests protokoll.test.ts

6,1 KB Rohdatei
app/tests/protokoll.test.ts — 175 Zeilen
1 // @vitest-environment node
2 /**
3 * Das lokale Fehlerprotokoll.
4 *
5 * **Der Anlass.** Sämtliche Fehler des Hauptprozesses landeten bis 0.22.0
6 * ausschließlich in `console.error` – und die sieht im gepackten Programm
7 * niemand, weil es ohne Konsole startet. Der Rückmeldeweg dieser Anwendung
8 * ist eine E-Mail; wer „es stürzt manchmal ab“ melden wollte, hatte nichts in
9 * der Hand, und wer die Meldung bearbeiten sollte, bekam nichts Greifbares.
10 *
11 * **Was hier vor allem geprüft wird**, ist nicht das Schreiben, sondern das
12 * Weglassen: Eine Datei, die zwar hilft, aber nebenbei den Anmeldenamen
13 * mitschreibt, wäre ein schlechterer Tausch als gar keine. Die Anwendung
14 * verspricht an drei Stellen, dass nichts Personenbezogenes das Gerät
15 * verlässt – diese Datei ist dazu gedacht, von Hand verschickt zu werden.
16 */
17
18 import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
19 import { tmpdir } from 'node:os';
20 import { join } from 'node:path';
21
22 import { afterEach, beforeEach, describe, expect, it } from 'vitest';
23
24 import {
25 entpersoniert,
26 fehlerZeile,
27 MAX_BYTES,
28 protokollEinrichten,
29 protokollieren,
30 protokollPfad,
31 protokollVergessen,
32 PROTOKOLL_DATEI,
33 } from '../src/main/protokoll';
34
35 let verzeichnis = '';
36
37 function inhalt(): string {
38 const pfad = join(verzeichnis, PROTOKOLL_DATEI);
39 return existsSync(pfad) ? readFileSync(pfad, 'utf8') : '';
40 }
41
42 beforeEach(() => {
43 verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-protokoll-'));
44 protokollEinrichten(verzeichnis);
45 });
46
47 afterEach(() => {
48 protokollVergessen();
49 rmSync(verzeichnis, { recursive: true, force: true });
50 });
51
52 describe('Personenbezug entfernen', () => {
53 it('ersetzt das Heimatverzeichnis, egal mit welchem Trennzeichen', () => {
54 /* Node liefert Windows-Pfade mit Rückstrich, viele Meldungen aus
55 Chromium und SQLite mit Schrägstrich – beide enthalten den
56 Anmeldenamen. */
57 const heim = 'C:\\Users\\Erika';
58
59 expect(entpersoniert('EACCES: C:\\Users\\Erika\\AppData\\lernstand.db', heim)).toBe(
60 'EACCES: <Benutzer>\\AppData\\lernstand.db',
61 );
62 expect(entpersoniert('open C:/Users/Erika/AppData/lernstand.db', heim)).toBe(
63 'open <Benutzer>/AppData/lernstand.db',
64 );
65 });
66
67 it('macht aus jeder Meldung genau eine Zeile', () => {
68 /* Sonst ließe sich eine Fehlermeldung so wählen, dass sie im Protokoll
69 wie mehrere Einträge mit eigenen Zeitstempeln aussieht. */
70 expect(entpersoniert('erste Zeile\nzweite Zeile\r\ndritte', 'C:\\Users\\Erika')).toBe(
71 'erste Zeile zweite Zeile dritte',
72 );
73 });
74
75 it('kürzt eine überlange Meldung', () => {
76 const lang = entpersoniert('x'.repeat(10_000), 'C:\\Users\\Erika');
77
78 expect(lang.length).toBeLessThanOrEqual(2000);
79 });
80
81 it('kommt ohne bekanntes Heimatverzeichnis aus', () => {
82 expect(entpersoniert('irgendein Fehler', '')).toBe('irgendein Fehler');
83 });
84 });
85
86 describe('Schreiben', () => {
87 it('schreibt Zeitpunkt, Bereich und Meldung', () => {
88 protokollieren('fenster', 'Renderer beendet (crashed)', new Date('2026-08-29T18:30:00.000Z'));
89
90 expect(inhalt()).toBe('2026-08-29T18:30:00.000Z [fenster] Renderer beendet (crashed)\n');
91 });
92
93 it('hängt weitere Einträge an, statt zu überschreiben', () => {
94 protokollieren('a', 'erster', new Date('2026-08-29T18:30:00.000Z'));
95 protokollieren('b', 'zweiter', new Date('2026-08-29T18:31:00.000Z'));
96
97 expect(inhalt().trimEnd().split('\n')).toHaveLength(2);
98 });
99
100 it('nimmt den Anmeldenamen auch beim Schreiben heraus', () => {
101 /* Die Reinigung sitzt an genau einer Stelle – hier wird gemessen, dass
102 der Schreibweg sie wirklich benutzt und nicht daran vorbeiführt. */
103 protokollieren(
104 'fehler',
105 `EACCES: open ${join(process.env['USERPROFILE'] ?? process.env['HOME'] ?? '', 'geheim.db')}`,
106 );
107
108 expect(inhalt()).toContain('<Benutzer>');
109 });
110
111 it('schweigt, solange kein Ablageort feststeht', () => {
112 /* Ohne Einrichten darf nichts entstehen: Sonst legte ein früh
113 auftretender Fehler eine Datei an einer beliebigen Stelle an. */
114 protokollVergessen();
115 protokollieren('fehler', 'darf nirgends landen');
116
117 expect(existsSync(join(verzeichnis, PROTOKOLL_DATEI))).toBe(false);
118 });
119
120 it('bringt die Anwendung nicht zu Fall, wenn nicht geschrieben werden kann', () => {
121 /* Ein Protokoll, das den Start verhindert, wäre schlimmer als keines.
122 Hier steht ein Verzeichnis an der Stelle der Datei. */
123 protokollVergessen();
124 const eng = join(verzeichnis, 'eng');
125 writeFileSync(join(verzeichnis, 'sperre'), 'x');
126 protokollEinrichten(join(verzeichnis, 'sperre'));
127
128 expect(() => {
129 protokollieren('fehler', 'irgendetwas');
130 }).not.toThrow();
131 expect(existsSync(eng)).toBe(false);
132 });
133
134 it('nennt seinen Ablageort für den Systemzustand', () => {
135 expect(protokollPfad()).toBe(join(verzeichnis, PROTOKOLL_DATEI));
136
137 protokollVergessen();
138 expect(protokollPfad()).toBeNull();
139 });
140 });
141
142 describe('Obergrenze', () => {
143 it('schneidet die ältere Hälfte weg, statt endlos zu wachsen', () => {
144 const pfad = join(verzeichnis, PROTOKOLL_DATEI);
145 /* Eine überfüllte Datei mit erkennbarem Anfang und Ende. */
146 const zeile = `${'a'.repeat(200)}\n`;
147 writeFileSync(
148 pfad,
149 `ALTESTE_ZEILE\n${zeile.repeat(Math.ceil(MAX_BYTES / 200))}JUENGSTE_ZEILE\n`,
150 );
151 expect(statSync(pfad).size).toBeGreaterThan(MAX_BYTES);
152
153 protokollieren('fehler', 'neuer Eintrag');
154
155 const danach = inhalt();
156 expect(statSync(pfad).size).toBeLessThan(MAX_BYTES);
157 expect(danach).not.toContain('ALTESTE_ZEILE');
158 expect(danach).toContain('JUENGSTE_ZEILE');
159 expect(danach).toContain('neuer Eintrag');
160 expect(danach).toContain('[gekürzt]');
161 });
162 });
163
164 describe('Fehler zu einer Zeile machen', () => {
165 it('nennt Meldung und die erste Stelle der Aufrufliste', () => {
166 const zeile = fehlerZeile(new Error('Etwas ging schief'));
167
168 expect(zeile).toContain('Etwas ging schief');
169 });
170
171 it('kommt auch mit einem geworfenen Nicht-Fehler zurecht', () => {
172 expect(fehlerZeile('nur ein Text')).toBe('nur ein Text');
173 expect(fehlerZeile(undefined)).toBe('undefined');
174 });
175 });