lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests electron protokollFremdtext.test.ts
| 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 | }); |