lsa-planer

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

/ tests services fremdeDateiAbweisen.test.ts

5,5 KB Rohdatei
tests/services/fremdeDateiAbweisen.test.ts — 152 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { openProjectFromFile, serializeProject } from '@/services/storage';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { CURRENT_SCHEMA_VERSION } from '@/domain/model/project';
5 import type { DesktopBridge, OpenFileResult } from '@/platform/bridge';
6
7 /**
8 * Was das Einlesen einer Datei mit dem Dateipfad tun darf.
9 *
10 * Befunde 41 und 15 zur Fassung 5.10.0. Beide enden im selben Schaden: Der Pfad
11 * einer Datei, deren Inhalt das Programm gerade NICHT vollstaendig uebernommen
12 * hat, wird als Ziel des naechsten Speicherns gefuehrt. Weil `store.replace`
13 * mit Pfad die Aenderungsmarke loescht, meldet die Kopfzeile "gespeichert", und
14 * Strg+S laeuft ohne Dialog ueber `bridge.writeFile` auf genau diese Datei -
15 * der Hauptprozess hat den Pfad beim Oeffnen mit `approvePath`
16 * schreibberechtigt gemacht.
17 *
18 * 41: Jede beliebige JSON-Datei wurde angenommen. Die Zerlegehelfer in
19 * schema.ts melden nur vorhandene, unpassende Werte; fehlende Schluessel
20 * erzeugen null Meldungen, `showImportIssues` unterdrueckt den Hinweiskasten
21 * daraufhin ganz. Der Anwender bekam "Projekt geladen.", sah ein leeres
22 * Projekt namens "Importiertes Projekt" - und sein naechstes Strg+S ersetzte
23 * die fremde Datei durch dieses leere Projekt. Die Wache `looksLikeProject`
24 * gab es, sie hatte nur keinen Aufrufer.
25 *
26 * 15: Eine Datei aus einer neueren Programmversion erzeugte nur eine
27 * Hinweiszeile. Danach galt sie als heil: Pfad uebernommen, Aenderungsmarke
28 * geloescht. Das naechste Speichern schrieb sie mit der eigenen Schemaversion
29 * zurueck, und jedes Feld, das diese Fassung nicht kennt, war fort. Beide
30 * Sitzungswege haben laengst eine Wache vor `loadProject` (siehe UNBRAUCHBAR
31 * in src/services/storage.ts); der Dateiweg hatte keine.
32 */
33
34 function mitBruecke(antwort: OpenFileResult): void {
35 const bruecke = {
36 openFile: () => Promise.resolve(antwort),
37 } as unknown as DesktopBridge;
38 (globalThis as unknown as { window?: { lsaPlaner?: DesktopBridge } }).window = {
39 lsaPlaner: bruecke,
40 };
41 }
42
43 function antwort(teil: Partial<OpenFileResult>): OpenFileResult {
44 return {
45 ok: true,
46 canceled: false,
47 filePath: 'C:/Plaene/Fremd.json',
48 content: null,
49 error: null,
50 ...teil,
51 };
52 }
53
54 afterEach(() => {
55 delete (globalThis as unknown as { window?: unknown }).window;
56 });
57
58 describe('Fremde JSON-Datei (Befund 41)', () => {
59 it('weist eine Datei ab, die kein Projekt ist, statt ein leeres zu bauen', async () => {
60 mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' }));
61
62 const ergebnis = await openProjectFromFile();
63
64 expect(ergebnis.ok).toBe(false);
65 expect(ergebnis.result).toBeNull();
66 expect(ergebnis.message).toContain('kein Projekt');
67 });
68
69 it('bindet den Pfad der fremden Datei nicht an das Programm', async () => {
70 mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' }));
71
72 const ergebnis = await openProjectFromFile();
73
74 // Ohne Pfad kein rueckfragefreies Ueberschreiben: Das ist der eigentliche
75 // Schaden des Befundes.
76 expect(ergebnis.filePath).toBeNull();
77 });
78
79 it('nimmt eine 4.x-Datei weiterhin an - sie traegt keine Schemaversion', async () => {
80 mitBruecke(
81 antwort({
82 content: JSON.stringify({
83 projektdaten: { name: 'Altbestand' },
84 signalgruppen: [{ name: 'K1', typ: 'kfz' }],
85 }),
86 }),
87 );
88
89 const ergebnis = await openProjectFromFile();
90
91 expect(ergebnis.ok).toBe(true);
92 expect(ergebnis.result?.migrated).toBe(true);
93 });
94
95 it('nimmt eine selbst geschriebene Datei weiterhin an', async () => {
96 mitBruecke(antwort({ content: serializeProject(createStandardIntersectionProject()) }));
97
98 const ergebnis = await openProjectFromFile();
99
100 expect(ergebnis.ok).toBe(true);
101 expect(ergebnis.filePath).toBe('C:/Plaene/Fremd.json');
102 });
103 });
104
105 describe('Datei aus einer neueren Programmversion (Befund 15)', () => {
106 function neuereDatei(): string {
107 return JSON.stringify({
108 ...createStandardIntersectionProject(),
109 schemaVersion: CURRENT_SCHEMA_VERSION + 1,
110 });
111 }
112
113 it('uebernimmt den Dateipfad nicht - Speichern muss nach einem Ziel fragen', async () => {
114 mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' }));
115
116 const ergebnis = await openProjectFromFile();
117
118 expect(ergebnis.ok).toBe(true);
119 expect(ergebnis.filePath).toBeNull();
120 });
121
122 it('sagt in der Meldung, dass der Pfad nicht uebernommen wurde', async () => {
123 mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' }));
124
125 const ergebnis = await openProjectFromFile();
126
127 expect(ergebnis.message).toContain('neueren Programmversion');
128 expect(ergebnis.message).toContain('nicht überschrieben');
129 });
130
131 it('liest die Datei trotzdem ein und behaelt die Hinweiszeile', async () => {
132 mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' }));
133
134 const ergebnis = await openProjectFromFile();
135
136 expect(ergebnis.result?.project.signalGroups.length).toBeGreaterThan(0);
137 expect(ergebnis.result?.issues.some((i) => i.path === 'schemaVersion')).toBe(true);
138 });
139
140 it('laesst den Pfad einer Datei der eigenen Fassung unangetastet', async () => {
141 mitBruecke(
142 antwort({
143 content: serializeProject(createStandardIntersectionProject()),
144 filePath: 'C:/Plaene/Eigen.lsap',
145 }),
146 );
147
148 const ergebnis = await openProjectFromFile();
149
150 expect(ergebnis.filePath).toBe('C:/Plaene/Eigen.lsap');
151 });
152 });