import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdtempSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { IPC, type KartenAntwort } from '../../shared/ipc';
/**
* Befund 31: Fremder Antworttext eines Kartendienstes gelangte woertlich in das
* Protokoll, das "keine Projektinhalte" verspricht.
*
* Die Zusage stand in electron/protokoll.ts und - als sichtbarer Anwendertext,
* verbunden mit der Aufforderung, die Datei aus der Hand zu geben - im
* Hilfefenster ("Bitte legen Sie diese Datei einer Fehlermeldung bei - sie
* enthält nur technische Angaben, keine Projektinhalte."). Seit 5.43.0 steht
* sie dort schwaecher, so wie sie belegt ist ("darauf angelegt, keine
* Projektinhalte aufzunehmen"). Durchgesetzt wurde sie damals nirgends: Der Griff `IPC.fetchMapImage` schrieb `antwort.fehler`
* ungefiltert ins Protokoll, und electron/karte.ts klebt dort bei einer
* Nicht-Bild-Antwort bis zu 400 Byte des fremden Antwortkoerpers hinein. Ein WMS
* meldet Fehler nach WMS 1.3.0 als `ServiceExceptionReport` mit Status 200 und
* zitiert darin ueblicherweise die gestellte Anfrage - samt BBOX. Damit standen
* die UTM-Koordinaten des geplanten Knotenpunkts in der Datei, die der Anwender
* herausgibt.
*
* Geprueft wird gegen die echte Protokolldatei; ersetzt ist nur der
* Kartendienst.
*/
/** Was ein ausgelasteter WMS zurueckgibt - mit der zitierten Anfrage darin. */
const FREMDTEXT =
'' +
'Requested ' +
'BBOX=32456789.123,5678901.234,32457089.123,5679201.234 lies outside the layer extent' +
'';
/** Der Ostwert des Knotenpunkts - Projektinhalt, keine technische Angabe. */
const OSTWERT = '32456789.123';
const DIENSTMELDUNG = `Der Kartendienst lieferte kein Bild (text/xml): ${FREMDTEXT}`;
const ABRUFADRESSE =
'https://www.wms.nrw.de/geobasis/wms_nw_dop?SERVICE=WMS&REQUEST=GetMap&LAYERS=nw_dop_rgb' +
`&CRS=EPSG:25832&BBOX=${OSTWERT},5678901.234,32457089.123,5679201.234&WIDTH=1600&HEIGHT=1600`;
const welt = vi.hoisted(() => {
type Horcher = (...werte: unknown[]) => void;
class Ereignisquelle {
private readonly horcher = new Map();
on(name: string, fn: Horcher): this {
const liste = this.horcher.get(name) ?? [];
liste.push(fn);
this.horcher.set(name, liste);
return this;
}
once(name: string, fn: Horcher): this {
return this.on(name, fn);
}
removeListener(name: string, fn: Horcher): this {
this.horcher.set(
name,
(this.horcher.get(name) ?? []).filter((h) => h !== fn),
);
return this;
}
off(name: string, fn: Horcher): this {
return this.removeListener(name, fn);
}
}
class Anzeigeinhalt extends Ereignisquelle {
send(): void {
/* Im Test ohne Wirkung. */
}
setZoomFactor(): void {
/* Im Test ohne Wirkung. */
}
setWindowOpenHandler(): void {
/* Im Test ohne Wirkung. */
}
isCrashed(): boolean {
return false;
}
isDestroyed(): boolean {
return false;
}
}
const zustand = {
fenster: null as Fenster | null,
benutzerdaten: '.',
/** Was der ersetzte Kartendienst zurueckgibt. */
kartenantwort: { ok: false, datenUrl: null, fehler: null } as {
ok: boolean;
datenUrl: string | null;
fehler: string | null;
},
};
class Fenster extends Ereignisquelle {
readonly webContents = new Anzeigeinhalt();
constructor() {
super();
zustand.fenster = this;
}
loadFile(): Promise {
return Promise.resolve();
}
show(): void {
/* Im Test ohne Wirkung. */
}
setBackgroundColor(): void {
/* Im Test ohne Wirkung. */
}
setDocumentEdited(): void {
/* Im Test ohne Wirkung. */
}
isDestroyed(): boolean {
return false;
}
close(): void {
/* Im Test ohne Wirkung. */
}
static getAllWindows(): Fenster[] {
return zustand.fenster === null ? [] : [zustand.fenster];
}
}
const behandler = new Map unknown>();
const electron = {
app: {
isPackaged: false,
requestSingleInstanceLock: (): boolean => true,
on: (): void => undefined,
whenReady: (): Promise => Promise.resolve(),
getPath: (): string => zustand.benutzerdaten,
getVersion: (): string => '5.10.0',
quit: (): void => undefined,
},
BrowserWindow: Fenster,
Menu: {
buildFromTemplate: (): { getMenuItemById: () => null } => ({ getMenuItemById: () => null }),
setApplicationMenu: (): void => undefined,
},
dialog: {
showMessageBoxSync: (): number => 0,
showErrorBox: (): void => undefined,
showSaveDialog: (): Promise<{ canceled: boolean }> => Promise.resolve({ canceled: true }),
showOpenDialog: (): Promise<{ canceled: boolean; filePaths: string[] }> =>
Promise.resolve({ canceled: true, filePaths: [] }),
},
ipcMain: {
handle: (kanal: string, fn: (...werte: unknown[]) => unknown): void => {
behandler.set(kanal, fn);
},
on: (): void => undefined,
},
nativeTheme: {
shouldUseDarkColors: false,
shouldUseHighContrastColors: false,
on: (): void => undefined,
},
systemPreferences: { getColor: (): string => '#000000' },
session: {
defaultSession: {
webRequest: { onHeadersReceived: (): void => undefined },
setPermissionRequestHandler: (): void => undefined,
setPermissionCheckHandler: (): void => undefined,
},
},
shell: { openExternal: (): Promise => Promise.resolve() },
};
return {
electron,
zustand,
behandler,
zuruecksetzen: (): void => {
zustand.fenster = null;
zustand.kartenantwort = { ok: false, datenUrl: null, fehler: null };
behandler.clear();
},
};
});
vi.mock('electron', () => welt.electron);
/**
* Nur der Abruf selbst ist ersetzt. `erlaubteRechner` bleibt das echte - sonst
* pruefte dieser Test die Liste, die er sich selbst geschrieben hat.
*/
vi.mock('../../electron/karte', async (echtesModul) => {
const echt = await echtesModul();
return {
...echt,
holeKartenbild: (): Promise => Promise.resolve(welt.zustand.kartenantwort),
};
});
process.setMaxListeners(64);
async function starteHauptprozess(): Promise {
vi.resetModules();
welt.zuruecksetzen();
await import('../../electron/main');
for (let i = 0; i < 5; i += 1) await Promise.resolve();
}
/** Lageplan -> Kartenbild holen. */
async function holeKachel(adresse: string): Promise {
return (await welt.behandler.get(IPC.fetchMapImage)?.(null, { url: adresse })) as KartenAntwort;
}
function protokolltext(): string {
return readFileSync(path.join(welt.zustand.benutzerdaten, 'fehlerprotokoll.log'), 'utf8');
}
describe('Fehlerprotokoll: kein fremder Antworttext, wie zugesagt', () => {
beforeEach(async () => {
vi.spyOn(console, 'error').mockImplementation(() => undefined);
welt.zustand.benutzerdaten = mkdtempSync(path.join(os.tmpdir(), 'lsa-hauptprozess-protokoll-'));
await starteHauptprozess();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('schreibt die Ausnahmemeldung des WMS nicht in die Datei', async () => {
welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
await holeKachel(ABRUFADRESSE);
const protokoll = protokolltext();
expect(protokoll, 'fremder Antworttext im Protokoll').not.toContain('ServiceExceptionReport');
expect(protokoll, 'der Standort des Knotenpunkts im Protokoll').not.toContain(OSTWERT);
});
it('schreibt auch die Abrufadresse nicht hinein - die BBOX steht in ihrer Abfrage', async () => {
welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
await holeKachel(ABRUFADRESSE);
expect(protokolltext(), 'die Abrufadresse traegt die BBOX').not.toContain('BBOX=');
});
it('vermerkt den Fehlschlag trotzdem und nennt den Dienst', async () => {
welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
await holeKachel(ABRUFADRESSE);
const protokoll = protokolltext();
expect(protokoll).toContain('Kartendienst');
expect(protokoll, 'der Rechnername ist eine technische Angabe').toContain('www.wms.nrw.de');
});
it('reicht die Meldung des Dienstes unveraendert an die Anzeige durch', async () => {
// Der Anwender soll erfahren, woran der Abruf scheiterte - an der
// Oberflaeche, nicht in einer Datei, die er aus der Hand gibt.
welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
const antwort = await holeKachel(ABRUFADRESSE);
expect(antwort.ok).toBe(false);
expect(antwort.fehler).toBe(DIENSTMELDUNG);
});
it('nennt einen nicht freigegebenen Rechnernamen nicht beim Namen', async () => {
// Ausgerechnet der Fall, in dem der Name NICHT aus der festen Liste stammt,
// ist ein Fehlschlagsfall und wird deshalb protokolliert: `holeKartenbild`
// weist eine nicht freigegebene Adresse ab, und der Griff schriebe den vom
// Anzeigeprozess frei gewaehlten Namen woertlich in die Datei, die der
// Anwender laut Hilfefenster aus der Hand geben soll.
welt.zustand.kartenantwort = {
ok: false,
datenUrl: null,
fehler: 'Die Adresse ist nicht freigegeben.',
};
await holeKachel('https://fremder-sammler.beispiel.invalid/ablage?kennung=abc');
const protokoll = protokolltext();
expect(protokoll, 'ein frei gewaehlter Rechnername im Protokoll').not.toContain(
'fremder-sammler.beispiel.invalid',
);
expect(protokoll, 'der Fehlschlag bleibt vermerkt').toContain('Kartendienst');
});
it('nennt bei einer Adresse ohne Rechnernamen keinen Namen', async () => {
welt.zustand.kartenantwort = { ok: false, datenUrl: null, fehler: DIENSTMELDUNG };
await holeKachel('kein Netzort');
expect(protokolltext()).toContain('Kartendienst');
});
it('vermerkt bei einem gelungenen Abruf gar nichts', async () => {
welt.zustand.kartenantwort = {
ok: true,
datenUrl: 'data:image/jpeg;base64,/9j/4AAQ',
fehler: null,
};
await holeKachel(ABRUFADRESSE);
expect(protokolltext()).not.toContain('Kartendienst');
});
});