waffensachkunde

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

/ app tests paketstand.test.ts

14,6 KB Rohdatei
app/tests/paketstand.test.ts — 390 Zeilen
1 // @vitest-environment node
2 // Reiner Dateisystem-Test – jsdom wird hier nicht gebraucht und würde
3 // `import.meta.url` auf eine http-URL setzen.
4
5 /**
6 * Der Altersvergleich für das gepackte Paket.
7 *
8 * Er entscheidet, ob die Prüfungen aus `e2e/gepackt.spec.ts` laufen.
9 * Sagt er fälschlich „aktuell“, prüfen sie die vorige Fassung und melden
10 * grün – genau das ist in diesem Projekt zweimal geschehen.
11 *
12 * Ein drittes Mal wäre um ein Haar dazugekommen: Der Vergleich sah nur
13 * `app/src` an, die ausgelieferten **Inhalte** liegen aber unter `content/`
14 * und kommen über `extraResources` ins Paket. Eine geänderte Erklärung ließ
15 * das Paket „so jung wie der Quelltext“ aussehen. Die Fälle unten halten
16 * beide Hälften fest.
17 */
18
19 import { mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
20 import { tmpdir } from 'node:os';
21 import { join, resolve } from 'node:path';
22 import { fileURLToPath } from 'node:url';
23
24 import { afterEach, beforeEach, describe, expect, it } from 'vitest';
25
26 import {
27 ausgelieferteQuellen,
28 bauressourcenAusBauplan,
29 musterAusBauplan,
30 paketstand,
31 } from '../tools/paketstand.mjs';
32
33 /** Das echte `app/`-Verzeichnis dieses Projekts. */
34 const echteAppWurzel = fileURLToPath(new URL('..', import.meta.url));
35
36 /*
37 Ein Prüfbaum, der dem echten in der Form gleicht: `app/` neben `content/`,
38 weil `extraResources` mit `../content/...` genau über diese Grenze greift.
39 Läge `content/` unterhalb von `app/`, prüfte der Test eine Anordnung, die
40 es im Projekt nicht gibt.
41 */
42 let wurzel: string;
43 let appWurzel: string;
44
45 /** Bauplan in der Form der echten Datei – der Ausschnitt, der etwas liefert. */
46 const BAUPLAN = `appId: de.willerding.waffensachkunde
47
48 directories:
49 output: release
50 buildResources: build-resources
51
52 # Nur die gebauten Bundles paketieren.
53 files:
54 - out/**/*
55 - package.json
56 - '!**/*.map'
57 - '!**/{test,tests,__tests__,e2e}/**'
58
59 extraResources:
60 - from: ../content/katalog
61 to: katalog
62 filter:
63 - '**/*'
64 - from: ../content/erklaerungen.json
65 to: erklaerungen.json
66
67 asar: true
68 `;
69
70 /** Setzt das Änderungsdatum einer Datei auf jetzt plus Versatz. */
71 function datieren(pfad: string, sekundenVersatz: number): void {
72 const zeit = Date.now() / 1000 + sekundenVersatz;
73 utimesSync(pfad, zeit, zeit);
74 }
75
76 /** Schreibt eine Datei samt Elternverzeichnissen und datiert sie. */
77 function schreiben(pfad: string, inhalt: string, sekundenVersatz: number): void {
78 mkdirSync(join(pfad, '..'), { recursive: true });
79 writeFileSync(pfad, inhalt, 'utf8');
80 datieren(pfad, sekundenVersatz);
81 }
82
83 /** Legt eine Quelldatei unter `app/src/` an. */
84 function quelle(sekundenVersatz: number): void {
85 schreiben(join(appWurzel, 'src', 'datei.ts'), 'const a = 1;\n', sekundenVersatz);
86 }
87
88 /** Legt einen ausgelieferten Inhalt unter `content/` an. */
89 function inhalt(sekundenVersatz: number): void {
90 schreiben(join(wurzel, 'content', 'katalog', 'katalog.json'), '{}\n', sekundenVersatz);
91 schreiben(join(wurzel, 'content', 'erklaerungen.json'), '{}\n', sekundenVersatz);
92 }
93
94 /** Legt ein gepacktes Paket an und setzt sein Änderungsdatum. */
95 function paket(sekundenVersatz: number): void {
96 const verzeichnis = join(appWurzel, 'release', 'win-unpacked');
97 mkdirSync(verzeichnis, { recursive: true });
98 const pfad = join(verzeichnis, 'Waffensachkunde Lernsoftware.exe');
99 writeFileSync(pfad, 'MZ', 'utf8');
100 datieren(pfad, sekundenVersatz);
101 }
102
103 beforeEach(() => {
104 wurzel = mkdtempSync(join(tmpdir(), 'wsk-paketstand-'));
105 appWurzel = join(wurzel, 'app');
106 mkdirSync(join(appWurzel, 'src'), { recursive: true });
107 // Alles, was der Bauplan nennt, muss dastehen – sonst wirft die Auflösung.
108 schreiben(join(appWurzel, 'electron-builder.yml'), BAUPLAN, -7200);
109 schreiben(join(appWurzel, 'package.json'), '{"version":"0.0.0"}\n', -7200);
110 // Das Verzeichnis mit dem Programmsymbol. Es zählt zum Programm, weil das
111 // Symbol in die gebaute exe eingebaut wird.
112 schreiben(join(appWurzel, 'build-resources', 'icon.ico'), 'ICO', -7200);
113 quelle(-7200);
114 inhalt(-7200);
115 });
116
117 afterEach(() => {
118 rmSync(wurzel, { recursive: true, force: true });
119 });
120
121 describe('paketstand', () => {
122 it('meldet ein fehlendes Paket als nicht vorhanden', () => {
123 const stand = paketstand(appWurzel);
124
125 expect(stand.vorhanden).toBe(false);
126 expect(stand.veraltet).toBe(false);
127 expect(stand.grund).toContain('dist:win');
128 });
129
130 it('meldet ein Paket als veraltet, das älter ist als der Quelltext', () => {
131 /* Der Fall, der zweimal einen Fehler verdeckt hat: Das Paket liegt da,
132 ist aber von vor der Änderung. */
133 paket(-3600);
134 quelle(0);
135
136 const stand = paketstand(appWurzel);
137
138 expect(stand.vorhanden).toBe(true);
139 expect(stand.veraltet).toBe(true);
140 expect(stand.alterSekunden).toBeGreaterThan(3000);
141 expect(stand.grund).toContain('älter als das Programm');
142 });
143
144 it('meldet ein Paket als veraltet, dessen Inhalte weitergewandert sind', () => {
145 /*
146 Die Lücke, um die es hier geht. Der Quelltext bleibt alt, geändert wird
147 allein `content/` – also das, was über `extraResources` ins Paket geht.
148 Die vorige Fassung sah nur `app/src` an und hätte hier „so jung wie der
149 Quelltext“ gemeldet; die Prüfungen aus `e2e/gepackt.spec.ts` hätten
150 die vorige Fassung des Katalogs gemessen und grün gemeldet.
151 */
152 paket(-3600);
153 inhalt(0);
154
155 const stand = paketstand(appWurzel);
156
157 expect(stand.veraltet).toBe(true);
158 expect(stand.ursache).toBe('inhalte');
159 expect(stand.grund).toContain('älter als die mitgelieferten Inhalte');
160 });
161
162 it('benennt die Datei, die den Ausschlag gibt', () => {
163 /* „Irgendetwas ist neuer“ zwingt zum Suchen. Der Name der Datei macht aus
164 der Meldung eine nachprüfbare Aussage – und sagt zugleich, ob neu zu
165 bauen oder nur neu zu packen ist. */
166 paket(-3600);
167 schreiben(join(wurzel, 'content', 'erklaerungen.json'), '{"a":1}\n', 0);
168
169 const stand = paketstand(appWurzel);
170
171 expect(stand.juengsteQuelle).toBe('content/erklaerungen.json');
172 expect(stand.grund).toContain('content/erklaerungen.json');
173 });
174
175 it('nennt beides, wenn Quelltext und Inhalte weitergewandert sind', () => {
176 paket(-3600);
177 quelle(-60);
178 inhalt(0);
179
180 const stand = paketstand(appWurzel);
181
182 expect(stand.ursache).toBe('beides');
183 expect(stand.grund).toContain('Programm und mitgelieferte Inhalte');
184 });
185
186 it('lässt ein Paket gelten, das jünger ist als Quelltext und Inhalte', () => {
187 quelle(-3600);
188 inhalt(-3600);
189 paket(0);
190
191 const stand = paketstand(appWurzel);
192
193 expect(stand.veraltet).toBe(false);
194 expect(stand.alterSekunden).toBe(0);
195 expect(stand.ursache).toBeNull();
196 expect(stand.grund).toContain('Inhalte');
197 });
198
199 it('verzeiht eine Sekunde Unterschied', () => {
200 /* Ein Paketbau schreibt seine Ausgaben nicht in derselben Millisekunde,
201 in der er die Quellen liest. Ohne Toleranz schlüge die Prüfung
202 unmittelbar nach einem erfolgreichen Bau an. */
203 paket(-1);
204 quelle(0);
205 inhalt(0);
206
207 expect(paketstand(appWurzel).veraltet).toBe(false);
208 });
209
210 it('sieht auch in Unterverzeichnisse des Quelltextes', () => {
211 /* Die meisten Änderungen liegen nicht direkt unter src/, sondern tief
212 darunter. Ein Vergleich nur der obersten Ebene ginge fast immer gut
213 aus – und wäre damit wertlos. */
214 paket(-3600);
215 schreiben(join(appWurzel, 'src', 'renderer', 'components', 'Tief.tsx'), 'export {};\n', 0);
216
217 expect(paketstand(appWurzel).veraltet).toBe(true);
218 });
219
220 it('sieht auch in Unterverzeichnisse der Inhalte', () => {
221 /* Dasselbe Argument für die andere Hälfte: Die Prüfzeichen liegen unter
222 `content/katalog/assets/`, nicht daneben. */
223 paket(-3600);
224 schreiben(join(wurzel, 'content', 'katalog', 'assets', 'zeichen.png'), 'PNG', 0);
225
226 expect(paketstand(appWurzel).veraltet).toBe(true);
227 });
228
229 it('lässt `out/` selbst außer Betracht', () => {
230 /*
231 `files:` nennt `out/**\/*`, das ist aber das Erzeugnis von
232 `electron-vite build` aus `src/`. `npm run gate` baut es in jedem Lauf
233 neu. Zählte sein Alter mit, sähe das Paket unmittelbar nach jedem Gate
234 veraltet aus – und die Prüfungen übersprängen sich für immer.
235 */
236 paket(-3600);
237 schreiben(join(appWurzel, 'out', 'main', 'index.js'), 'console.log(1);\n', 0);
238
239 expect(paketstand(appWurzel).veraltet).toBe(false);
240 });
241
242 it('wirft, wenn der Bauplan auf etwas verweist, das es nicht gibt', () => {
243 /* Der stille Fehlschlag wäre der schlimmere: Ein Bauplan, den der
244 Vergleich nicht auflösen kann, hieße wieder „geprüft“ ohne Prüfung. */
245 paket(0);
246 schreiben(
247 join(appWurzel, 'electron-builder.yml'),
248 `${BAUPLAN}\nextraResources:\n - from: ../content/gibtesnicht.json\n to: x.json\n`,
249 -7200,
250 );
251
252 expect(() => paketstand(appWurzel)).toThrow(/gibtesnicht\.json/u);
253 });
254
255 it('wirft ohne Bauplan', () => {
256 paket(0);
257 rmSync(join(appWurzel, 'electron-builder.yml'));
258
259 expect(() => paketstand(appWurzel)).toThrow(/Bauplan/u);
260 });
261 });
262
263 describe('bauressourcenAusBauplan', () => {
264 /*
265 Das Verzeichnis mit dem Programmsymbol steht in keiner der beiden Listen
266 des Bauplans, sondern unter `directories`. Es zählt trotzdem: Das Symbol
267 wird in die exe eingebaut. Wer es ändert, ohne neu zu bauen, trägt das
268 alte Bild weiter – und ohne diese Auflösung meldete der Vergleich dazu
269 „so jung wie der Quelltext“.
270 */
271 it('findet das Verzeichnis unter directories', () => {
272 expect(bauressourcenAusBauplan(BAUPLAN)).toBe('build-resources');
273 });
274
275 it('meldet null, wenn keines eingetragen ist', () => {
276 expect(bauressourcenAusBauplan('appId: x\nfiles:\n - out/**/*\n')).toBeNull();
277 });
278
279 it('greift nicht auf einen gleichnamigen Schlüssel außerhalb des Blocks', () => {
280 /* Ein blankes Suchen nach „buildResources“ fände auch einen Eintrag, der
281 woanders steht – und bewachte dann ein Verzeichnis, aus dem gar nichts
282 gebaut wird. */
283 const fremd = 'appx:\n buildResources: irgendwo\ndirectories:\n output: release\n';
284 expect(bauressourcenAusBauplan(fremd)).toBeNull();
285 });
286
287 it('überliest Kommentare', () => {
288 const mitKommentar =
289 'directories:\n # buildResources: alt-und-falsch\n buildResources: build-resources\n';
290 expect(bauressourcenAusBauplan(mitKommentar)).toBe('build-resources');
291 });
292 });
293
294 describe('musterAusBauplan', () => {
295 it('trennt ausgeliefertes Programm von ausgelieferten Inhalten', () => {
296 const muster = musterAusBauplan(BAUPLAN);
297
298 expect(muster.programm).toEqual(['out/**/*', 'package.json']);
299 expect(muster.inhalte).toEqual(['../content/katalog', '../content/erklaerungen.json']);
300 });
301
302 it('übergeht Ausschlussmuster', () => {
303 // Ein Muster mit führendem Ausrufezeichen liefert nichts aus; sein Alter
304 // zu messen wäre sinnlos, und der Pfad ließe sich gar nicht auflösen.
305 expect(musterAusBauplan(BAUPLAN).programm).not.toContain('!**/*.map');
306 });
307
308 it('wirft bei einer Schreibweise, die es nicht versteht', () => {
309 /*
310 Der Kern dieser Wache: Käme im Bauplan eine Form dazu, die der
311 Ausschnitt still überliest, prüfte der Altersvergleich wieder weniger,
312 als er behauptet – und niemand merkte es. Lieber laut scheitern.
313 */
314 const fremd = 'extraResources:\n ? seltsam\n';
315
316 expect(() => musterAusBauplan(fremd)).toThrow(/unverstandene Zeile/u);
317 });
318
319 it('liest den echten Bauplan dieses Projekts vollständig', () => {
320 /*
321 Die eigentliche Absicherung gegen Auseinanderlaufen: nicht ein Abbild
322 des Bauplans, sondern der Bauplan selbst. Kommt eine Ressource dazu und
323 versteht der Ausschnitt sie nicht, fällt dieser Fall um – nicht erst der
324 nächste Paketbau.
325
326 Die erwarteten Werte stehen hier ausgeschrieben, damit ein stilles
327 Verschwinden auffällt: Ein Vergleich gegen „irgendetwas Nichtleeres“
328 wäre auch dann grün, wenn der Katalog aus dem Paket fiele.
329 */
330 const muster = musterAusBauplan(
331 readFileSync(join(echteAppWurzel, 'electron-builder.yml'), 'utf8'),
332 );
333
334 expect(muster.programm).toEqual(['out/**/*', 'package.json']);
335 expect(muster.inhalte).toEqual([
336 '../content/katalog',
337 '../content/erklaerungen.json',
338 '../content/glossar.json',
339 '../content/normtexte.json',
340 '../content/themen.json',
341 '../LICENSE.de.txt',
342 '../content/drittlizenzen.json',
343 /* Seit 0.25.0: Die Datenschutzerklärung liegt in der Anwendung. Wer
344 sie ändert, entwertet damit den Beleg der Paketprüfungen – genau
345 das soll diese Liste sicherstellen. */
346 '../content/datenschutz.json',
347 ]);
348 });
349 });
350
351 describe('ausgelieferteQuellen gegen den echten Projektbaum', () => {
352 it('löst jeden Pfad des echten Bauplans auf einen vorhandenen Ort auf', () => {
353 /*
354 Ohne diesen Fall bliebe die Herleitung Theorie. Er darf nicht werfen –
355 ein Wurf hieße, dass Bauplan und Auflösung auseinanderliegen.
356
357 Bewusst über `ausgelieferteQuellen()` und nicht über `paketstand()`:
358 Letzteres kehrt ohne gebautes Paket sofort um und käme an der Auflösung
359 vorbei. Der Test prüfte dann an jedem Arbeitsplatz ohne `release/`
360 nichts und meldete trotzdem grün – dieselbe Unwahrheit, gegen die diese
361 ganze Datei angelegt ist.
362 */
363 const quellen = ausgelieferteQuellen(echteAppWurzel);
364
365 // `out/**/*` zählt als `src/`: das Erzeugnis wird über seine Quelle datiert.
366 expect(quellen.quelltext).toEqual([
367 resolve(echteAppWurzel, 'src'),
368 resolve(echteAppWurzel, 'package.json'),
369 // Der Bauplan selbst: Er bestimmt nicht nur, was ausgeliefert wird,
370 // sondern auch wie – Ziele, Dateilisten, ASAR-Entpackung, die Angaben
371 // des Store-Pakets. Bis 0.24.1 sah die Wache jeden Pfad an, den er
372 // nennt, nur nicht ihn.
373 resolve(echteAppWurzel, 'electron-builder.yml'),
374 // Das Programmsymbol steckt in der gebauten exe, steht aber in keiner
375 // der beiden Listen des Bauplans - es kommt aus directories.buildResources.
376 resolve(echteAppWurzel, 'build-resources'),
377 ]);
378 // Der Inhalt, den `e2e/gepackt.spec.ts` im Paket misst.
379 expect(quellen.inhalte).toEqual([
380 resolve(echteAppWurzel, '..', 'content', 'katalog'),
381 resolve(echteAppWurzel, '..', 'content', 'erklaerungen.json'),
382 resolve(echteAppWurzel, '..', 'content', 'glossar.json'),
383 resolve(echteAppWurzel, '..', 'content', 'normtexte.json'),
384 resolve(echteAppWurzel, '..', 'content', 'themen.json'),
385 resolve(echteAppWurzel, '..', 'LICENSE.de.txt'),
386 resolve(echteAppWurzel, '..', 'content', 'drittlizenzen.json'),
387 resolve(echteAppWurzel, '..', 'content', 'datenschutz.json'),
388 ]);
389 });
390 });