lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests electron protokollFremdtext.test.ts

10,3 KB Rohdatei
tests/electron/protokollFremdtext.test.ts — 315 Zeilen
1 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2 import { mkdtempSync, readFileSync } from 'node:fs';
3 import os from 'node:os';
4 import path from 'node:path';
5 import { IPC, type KartenAntwort } from '../../shared/ipc';
6
7 /**
8 * Befund 31: Fremder Antworttext eines Kartendienstes gelangte woertlich in das
9 * Protokoll, das "keine Projektinhalte" verspricht.
10 *
11 * Die Zusage stand in electron/protokoll.ts und - als sichtbarer Anwendertext,
12 * verbunden mit der Aufforderung, die Datei aus der Hand zu geben - im
13 * Hilfefenster ("Bitte legen Sie diese Datei einer Fehlermeldung bei - sie
14 * enthält nur technische Angaben, keine Projektinhalte."). Seit 5.43.0 steht
15 * sie dort schwaecher, so wie sie belegt ist ("darauf angelegt, keine
16 * Projektinhalte aufzunehmen"). Durchgesetzt wurde sie damals nirgends: Der Griff `IPC.fetchMapImage` schrieb `antwort.fehler`
17 * ungefiltert ins Protokoll, und electron/karte.ts klebt dort bei einer
18 * Nicht-Bild-Antwort bis zu 400 Byte des fremden Antwortkoerpers hinein. Ein WMS
19 * meldet Fehler nach WMS 1.3.0 als `ServiceExceptionReport` mit Status 200 und
20 * zitiert darin ueblicherweise die gestellte Anfrage - samt BBOX. Damit standen
21 * die UTM-Koordinaten des geplanten Knotenpunkts in der Datei, die der Anwender
22 * herausgibt.
23 *
24 * Geprueft wird gegen die echte Protokolldatei; ersetzt ist nur der
25 * Kartendienst.
26 */
27
28 /** Was ein ausgelasteter WMS zurueckgibt - mit der zitierten Anfrage darin. */
29 const FREMDTEXT =
30 '<?xml version="1.0"?><ServiceExceptionReport version="1.3.0">' +
31 '<ServiceException code="InvalidBBOX">Requested ' +
32 'BBOX=32456789.123,5678901.234,32457089.123,5679201.234 lies outside the layer extent' +
33 '</ServiceException></ServiceExceptionReport>';
34
35 /** Der Ostwert des Knotenpunkts - Projektinhalt, keine technische Angabe. */
36 const OSTWERT = '32456789.123';
37
38 const DIENSTMELDUNG = `Der Kartendienst lieferte kein Bild (text/xml): ${FREMDTEXT}`;
39
40 const ABRUFADRESSE =
41 'https://www.wms.nrw.de/geobasis/wms_nw_dop?SERVICE=WMS&REQUEST=GetMap&LAYERS=nw_dop_rgb' +
42 `&CRS=EPSG:25832&BBOX=${OSTWERT},5678901.234,32457089.123,5679201.234&WIDTH=1600&HEIGHT=1600`;
43
44 const welt = vi.hoisted(() => {
45 type Horcher = (...werte: unknown[]) => void;
46
47 class Ereignisquelle {
48 private readonly horcher = new Map<string, Horcher[]>();
49
50 on(name: string, fn: Horcher): this {
51 const liste = this.horcher.get(name) ?? [];
52 liste.push(fn);
53 this.horcher.set(name, liste);
54 return this;
55 }
56
57 once(name: string, fn: Horcher): this {
58 return this.on(name, fn);
59 }
60
61 removeListener(name: string, fn: Horcher): this {
62 this.horcher.set(
63 name,
64 (this.horcher.get(name) ?? []).filter((h) => h !== fn),
65 );
66 return this;
67 }
68
69 off(name: string, fn: Horcher): this {
70 return this.removeListener(name, fn);
71 }
72 }
73
74 class Anzeigeinhalt extends Ereignisquelle {
75 send(): void {
76 /* Im Test ohne Wirkung. */
77 }
78 setZoomFactor(): void {
79 /* Im Test ohne Wirkung. */
80 }
81 setWindowOpenHandler(): void {
82 /* Im Test ohne Wirkung. */
83 }
84 isCrashed(): boolean {
85 return false;
86 }
87 isDestroyed(): boolean {
88 return false;
89 }
90 }
91
92 const zustand = {
93 fenster: null as Fenster | null,
94 benutzerdaten: '.',
95 /** Was der ersetzte Kartendienst zurueckgibt. */
96 kartenantwort: { ok: false, datenUrl: null, fehler: null } as {
97 ok: boolean;
98 datenUrl: string | null;
99 fehler: string | null;
100 },
101 };
102
103 class Fenster extends Ereignisquelle {
104 readonly webContents = new Anzeigeinhalt();
105
106 constructor() {
107 super();
108 zustand.fenster = this;
109 }
110
111 loadFile(): Promise<void> {
112 return Promise.resolve();
113 }
114 show(): void {
115 /* Im Test ohne Wirkung. */
116 }
117 setBackgroundColor(): void {
118 /* Im Test ohne Wirkung. */
119 }
120 setDocumentEdited(): void {
121 /* Im Test ohne Wirkung. */
122 }
123 isDestroyed(): boolean {
124 return false;
125 }
126 close(): void {
127 /* Im Test ohne Wirkung. */
128 }
129
130 static getAllWindows(): Fenster[] {
131 return zustand.fenster === null ? [] : [zustand.fenster];
132 }
133 }
134
135 const behandler = new Map<string, (...werte: unknown[]) => unknown>();
136
137 const electron = {
138 app: {
139 isPackaged: false,
140 requestSingleInstanceLock: (): boolean => true,
141 on: (): void => undefined,
142 whenReady: (): Promise<void> => Promise.resolve(),
143 getPath: (): string => zustand.benutzerdaten,
144 getVersion: (): string => '5.10.0',
145 quit: (): void => undefined,
146 },
147 BrowserWindow: Fenster,
148 Menu: {
149 buildFromTemplate: (): { getMenuItemById: () => null } => ({ getMenuItemById: () => null }),
150 setApplicationMenu: (): void => undefined,
151 },
152 dialog: {
153 showMessageBoxSync: (): number => 0,
154 showErrorBox: (): void => undefined,
155 showSaveDialog: (): Promise<{ canceled: boolean }> => Promise.resolve({ canceled: true }),
156 showOpenDialog: (): Promise<{ canceled: boolean; filePaths: string[] }> =>
157 Promise.resolve({ canceled: true, filePaths: [] }),
158 },
159 ipcMain: {
160 handle: (kanal: string, fn: (...werte: unknown[]) => unknown): void => {
161 behandler.set(kanal, fn);
162 },
163 on: (): void => undefined,
164 },
165 nativeTheme: {
166 shouldUseDarkColors: false,
167 shouldUseHighContrastColors: false,
168 on: (): void => undefined,
169 },
170 systemPreferences: { getColor: (): string => '#000000' },
171 session: {
172 defaultSession: {
173 webRequest: { onHeadersReceived: (): void => undefined },
174 setPermissionRequestHandler: (): void => undefined,
175 setPermissionCheckHandler: (): void => undefined,
176 },
177 },
178 shell: { openExternal: (): Promise<void> => Promise.resolve() },
179 };
180
181 return {
182 electron,
183 zustand,
184 behandler,
185 zuruecksetzen: (): void => {
186 zustand.fenster = null;
187 zustand.kartenantwort = { ok: false, datenUrl: null, fehler: null };
188 behandler.clear();
189 },
190 };
191 });
192
193 vi.mock('electron', () => welt.electron);
194
195 /**
196 * Nur der Abruf selbst ist ersetzt. `erlaubteRechner` bleibt das echte - sonst
197 * pruefte dieser Test die Liste, die er sich selbst geschrieben hat.
198 */
199 vi.mock('../../electron/karte', async (echtesModul) => {
200 const echt = await echtesModul<typeof import('../../electron/karte')>();
201 return {
202 ...echt,
203 holeKartenbild: (): Promise<unknown> => Promise.resolve(welt.zustand.kartenantwort),
204 };
205 });
206
207 process.setMaxListeners(64);
208
209 async function starteHauptprozess(): Promise<void> {
210 vi.resetModules();
211 welt.zuruecksetzen();
212 await import('../../electron/main');
213 for (let i = 0; i < 5; i += 1) await Promise.resolve();
214 }
215
216 /** Lageplan -> Kartenbild holen. */
217 async function holeKachel(adresse: string): Promise<KartenAntwort> {
218 return (await welt.behandler.get(IPC.fetchMapImage)?.(null, { url: adresse })) as KartenAntwort;
219 }
220
221 function protokolltext(): string {
222 return readFileSync(path.join(welt.zustand.benutzerdaten, 'fehlerprotokoll.log'), 'utf8');
223 }
224
225 describe('Fehlerprotokoll: kein fremder Antworttext, wie zugesagt', () => {
226 beforeEach(async () => {
227 vi.spyOn(console, 'error').mockImplementation(() => undefined);
228 welt.zustand.benutzerdaten = mkdtempSync(path.join(os.tmpdir(), 'lsa-hauptprozess-protokoll-'));
229 await starteHauptprozess();
230 });
231
232 afterEach(() => {
233 vi.restoreAllMocks();
234 });
235
236 it('schreibt die Ausnahmemeldung des WMS nicht in die Datei', async () => {
237 welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
238
239 await holeKachel(ABRUFADRESSE);
240
241 const protokoll = protokolltext();
242 expect(protokoll, 'fremder Antworttext im Protokoll').not.toContain('ServiceExceptionReport');
243 expect(protokoll, 'der Standort des Knotenpunkts im Protokoll').not.toContain(OSTWERT);
244 });
245
246 it('schreibt auch die Abrufadresse nicht hinein - die BBOX steht in ihrer Abfrage', async () => {
247 welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
248
249 await holeKachel(ABRUFADRESSE);
250
251 expect(protokolltext(), 'die Abrufadresse traegt die BBOX').not.toContain('BBOX=');
252 });
253
254 it('vermerkt den Fehlschlag trotzdem und nennt den Dienst', async () => {
255 welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
256
257 await holeKachel(ABRUFADRESSE);
258
259 const protokoll = protokolltext();
260 expect(protokoll).toContain('Kartendienst');
261 expect(protokoll, 'der Rechnername ist eine technische Angabe').toContain('www.wms.nrw.de');
262 });
263
264 it('reicht die Meldung des Dienstes unveraendert an die Anzeige durch', async () => {
265 // Der Anwender soll erfahren, woran der Abruf scheiterte - an der
266 // Oberflaeche, nicht in einer Datei, die er aus der Hand gibt.
267 welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
268
269 const antwort = await holeKachel(ABRUFADRESSE);
270
271 expect(antwort.ok).toBe(false);
272 expect(antwort.fehler).toBe(DIENSTMELDUNG);
273 });
274
275 it('nennt einen nicht freigegebenen Rechnernamen nicht beim Namen', async () => {
276 // Ausgerechnet der Fall, in dem der Name NICHT aus der festen Liste stammt,
277 // ist ein Fehlschlagsfall und wird deshalb protokolliert: `holeKartenbild`
278 // weist eine nicht freigegebene Adresse ab, und der Griff schriebe den vom
279 // Anzeigeprozess frei gewaehlten Namen woertlich in die Datei, die der
280 // Anwender laut Hilfefenster aus der Hand geben soll.
281 welt.zustand.kartenantwort = {
282 ok: false,
283 datenUrl: null,
284 fehler: 'Die Adresse ist nicht freigegeben.',
285 };
286
287 await holeKachel('https://fremder-sammler.beispiel.invalid/ablage?kennung=abc');
288
289 const protokoll = protokolltext();
290 expect(protokoll, 'ein frei gewaehlter Rechnername im Protokoll').not.toContain(
291 'fremder-sammler.beispiel.invalid',
292 );
293 expect(protokoll, 'der Fehlschlag bleibt vermerkt').toContain('Kartendienst');
294 });
295
296 it('nennt bei einer Adresse ohne Rechnernamen keinen Namen', async () => {
297 welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
298
299 await holeKachel('kein Netzort');
300
301 expect(protokolltext()).toContain('Kartendienst');
302 });
303
304 it('vermerkt bei einem gelungenen Abruf gar nichts', async () => {
305 welt.zustand.kartenantwort = {
306 ok: true,
307 datenUrl: 'data:image/jpeg;base64,/9j/4AAQ',
308 fehler: null,
309 };
310
311 await holeKachel(ABRUFADRESSE);
312
313 expect(protokolltext()).not.toContain('Kartendienst');
314 });
315 });