waffensachkunde

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

/ app tests reuse-abdeckung.test.ts

8,6 KB Rohdatei
app/tests/reuse-abdeckung.test.ts — 221 Zeilen
1 // @vitest-environment node
2 /**
3 * Jede aufgenommene Inhaltsdatei braucht eine Lizenzangabe.
4 *
5 * Anlass ist ein selbst verursachter Rückschritt: Mit `content/glossar.json`
6 * kam eine neue Inhaltsdatei ins Projekt, ohne Eintrag in `REUSE.toml`. Damit
7 * war die Zusage aus einem früheren Stand – „reuse lint ist konform“ – nicht
8 * mehr wahr, und niemand hat es bemerkt. Aufgefallen ist es erst, als jemand
9 * für einen ganz anderen Zweck nachgesehen hat.
10 *
11 * `reuse lint` selbst läuft hier nicht: Es ist ein Python-Werkzeug und keine
12 * Abhängigkeit dieses Pakets; ein Test, der es aufruft, wäre auf einem
13 * Rechner ohne Python rot, ohne dass etwas falsch wäre. Geprüft wird deshalb
14 * genau die eine Regel, an der es gescheitert ist – dass keine aufgenommene
15 * Inhaltsdatei ohne Eintrag bleibt. Das ersetzt `reuse lint` nicht, es fängt
16 * den Fall ab, der wirklich passiert ist.
17 *
18 * Gefragt wird `git ls-files`, nicht das Dateisystem: `reuse lint` sieht nur
19 * aufgenommene Dateien, und unter `content/gesetze/` liegen heruntergeladene
20 * Gesetzesarchive, die niemand einchecken will.
21 */
22
23 import { execFileSync } from 'node:child_process';
24 import { readFileSync } from 'node:fs';
25 import { join } from 'node:path';
26 import { fileURLToPath } from 'node:url';
27
28 import { describe, expect, it } from 'vitest';
29
30 const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..');
31
32 /** Aufgenommene Dateien unter `content/`, oder `null` ohne Git. */
33 function aufgenommeneInhalte(): string[] | null {
34 try {
35 return execFileSync('git', ['ls-files', 'content'], {
36 cwd: wurzel,
37 encoding: 'utf8',
38 stdio: ['ignore', 'pipe', 'ignore'],
39 })
40 .split('\n')
41 .map((zeile) => zeile.trim())
42 .filter((zeile) => zeile.length > 0);
43 } catch {
44 return null;
45 }
46 }
47
48 /**
49 * Aufgenommene Dateien unmittelbar im Projektverzeichnis, oder `null` ohne Git.
50 *
51 * `git ls-files` liefert ohne Pfadangabe den ganzen Baum; hier interessieren
52 * nur die Dateien ohne Schrägstrich im Namen. Der Anlass ist derselbe wie
53 * oben, eine Ebene höher: `CHANGELOG.md` kam als neue Datei in die Wurzel,
54 * und dort greift kein `**`-Muster – sie hätte ohne Lizenzangabe bleiben
55 * können, ohne dass es auffällt.
56 */
57 function aufgenommeneWurzeldateien(): string[] | null {
58 try {
59 return execFileSync('git', ['ls-files'], {
60 cwd: wurzel,
61 encoding: 'utf8',
62 stdio: ['ignore', 'pipe', 'ignore'],
63 })
64 .split('\n')
65 .map((zeile) => zeile.trim())
66 .filter((zeile) => zeile.length > 0 && !zeile.includes('/'));
67 } catch {
68 return null;
69 }
70 }
71
72 /**
73 * Die Pfadmuster aus den `path = [...]`-Angaben von REUSE.toml.
74 *
75 * Gelesen werden gezielt diese Listen und nicht jede Zeichenkette der Datei.
76 * Der erste Versuch nahm alle Anführungszeichen – und verrutschte an
77 * `SPDX-PackageDownloadLocation = ""`: Die leere Zeichenkette fiel durch das
78 * Muster, danach war jedes Paar um eins verschoben und die Liste bestand aus
79 * Bruchstücken. Aufgefallen ist das nur, weil der Test unten die Ausbeute
80 * gegenprüft.
81 */
82 function pfadmuster(): string[] {
83 const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8')
84 .split('\n')
85 .filter((zeile) => !zeile.trimStart().startsWith('#'))
86 .join('\n');
87
88 return [...toml.matchAll(/path\s*=\s*\[([^\]]*)\]/gu)].flatMap((liste) =>
89 [...(liste[1] ?? '').matchAll(/"([^"]*)"/gu)]
90 .map((treffer) => treffer[1] ?? '')
91 .filter((pfad) => pfad.length > 0),
92 );
93 }
94
95 /**
96 * Deckt ein Muster aus REUSE.toml diesen Pfad ab?
97 *
98 * Nachgebildet wird nur, was das Projekt tatsächlich verwendet: der genaue
99 * Pfad und ein abschließendes `**`. Ein vollständiger Glob-Vergleich wäre
100 * mehr Code als die Regel, die er prüfen soll.
101 */
102 function abgedeckt(pfad: string, muster: readonly string[]): boolean {
103 return muster.some((m) => (m.endsWith('/**') ? pfad.startsWith(m.slice(0, -2)) : m === pfad));
104 }
105
106 describe('Lizenzauszeichnung der Inhalte', () => {
107 const muster = pfadmuster();
108
109 it('liest überhaupt Pfade aus REUSE.toml', () => {
110 /* Ohne diese Gegenprobe wäre der Test unten immer grün und damit wertlos
111 – dieselbe Falle wie eine leere Prüfliste. Genau hier ist der
112 Verschiebefehler des ersten Versuchs aufgefallen. */
113 expect(muster).toContain('app/**');
114 expect(muster).toContain('content/katalog/**');
115 expect(muster.every((m) => !m.includes('\n'))).toBe(true);
116 });
117
118 it('zeichnet jede aufgenommene Datei unter content/ aus', () => {
119 const dateien = aufgenommeneInhalte();
120 if (dateien === null) {
121 /* Kein Git – dann ist nicht feststellbar, was `reuse lint` sähe. */
122 return;
123 }
124
125 expect(dateien.length).toBeGreaterThan(3);
126
127 const ohne = dateien.filter((datei) => !abgedeckt(datei, muster));
128
129 expect(
130 ohne,
131 'Neue Inhaltsdateien gehören in REUSE.toml – sonst ist ihre Herkunft nicht ausgewiesen.',
132 ).toEqual([]);
133 });
134
135 it('zeichnet jede aufgenommene Datei im Projektverzeichnis aus', () => {
136 /*
137 Eine Ebene höher als der Test darüber, und aus demselben Anlass: In der
138 Wurzel greift kein `**`-Muster, jede Datei dort braucht einen eigenen
139 Eintrag. Als `CHANGELOG.md` dazukam, fiel das nur auf, weil jemand
140 danach gesucht hat.
141 */
142 const dateien = aufgenommeneWurzeldateien();
143 if (dateien === null) {
144 return;
145 }
146
147 expect(dateien).toContain('README.md');
148
149 const ohne = dateien.filter((datei) => !abgedeckt(datei, muster));
150
151 expect(
152 ohne,
153 'Neue Dateien im Projektverzeichnis gehören einzeln in REUSE.toml – ' +
154 'ein `**`-Muster deckt die Wurzel nicht ab.',
155 ).toEqual([]);
156 });
157
158 it('nennt für den amtlichen Katalog eine eigene Lizenzkennung', () => {
159 /* Der Katalog darf nie unter die eigene Lizenz des Projekts geraten: Auf
160 ein amtliches Werk kann niemand eine Lizenz erteilen. */
161 const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8');
162 const abKatalog = toml.slice(toml.indexOf('"content/katalog/**"'));
163 const bisKennung = abKatalog.slice(0, abKatalog.indexOf('SPDX-License-Identifier') + 60);
164
165 expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA');
166 expect(bisKennung).not.toContain('EUPL-1.2');
167 });
168
169 /*
170 Die genauere Angabe muss hinter der allgemeinen stehen.
171
172 Treffen mehrere Einträge auf dieselbe Datei, gilt der **letzte**. Bis
173 Fassung 0.24.1 stand `docs/**` hinter der Aufzählung der Bilder mit
174 amtlichem Wortlaut und überschrieb sie vollständig: Am `reuse lint --json`
175 nachgemessen trugen alle neun Dateien und der Prüfbericht `EUPL-1.2` –
176 also genau die Zuordnung, die die Aufzählung ausschließen sollte. Zwölf
177 Zeilen Begründung ohne jede Wirkung, und niemand hat es gesehen, weil
178 `reuse lint` in keinem Torwächter läuft.
179
180 Geprüft wird die Reihenfolge und nicht das Ergebnis: `reuse` ist ein
181 Python-Werkzeug und keine Abhängigkeit dieses Pakets – dieselbe Überlegung
182 wie im Kopf dieser Datei.
183 */
184 it('stellt die Ausnahmen hinter die pauschale docs-Regel', () => {
185 const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8');
186
187 const pauschal = toml.indexOf('path = ["docs/**"]');
188 const ausnahme = toml.indexOf('"docs/katalog-pruefbericht.html"');
189
190 expect(pauschal, 'Die pauschale docs-Regel fehlt.').toBeGreaterThanOrEqual(0);
191 expect(ausnahme, 'Die Aufzählung der Bilder mit amtlichem Wortlaut fehlt.').toBeGreaterThan(0);
192 expect(
193 ausnahme,
194 'Die Aufzählung steht vor `docs/**` und wird davon überschrieben – ' +
195 'bei mehreren Treffern gilt der letzte Eintrag.',
196 ).toBeGreaterThan(pauschal);
197 });
198
199 it('führt jedes Bildschirmfoto mit amtlichem Wortlaut auf', () => {
200 /* Die Liste hier ist die Gegenprobe zur Liste dort: Wer ein Bild
201 hinzunimmt, auf dem eine Frage im Wortlaut steht, muss beide anfassen. */
202 const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8');
203 const block = toml.slice(toml.indexOf('"docs/katalog-pruefbericht.html"'));
204 const bisKennung = block.slice(0, block.indexOf('SPDX-License-Identifier') + 60);
205
206 for (const datei of [
207 'docs/bildschirmfotos/04-lernsitzung.png',
208 'docs/bildschirmfotos/05-lernsitzung-feedback.png',
209 'docs/bildschirmfotos/08-pruefungslauf.png',
210 'docs/bildschirmfotos/10-erklaerung.png',
211 'docs/bildschirmfotos/13-suche.png',
212 'docs/store-bilder/01-frage-mit-antwortmoeglichkeiten.png',
213 'docs/store-bilder/02-rueckmeldung-mit-begruendung.png',
214 'docs/store-bilder/05-fragen-durchsuchen.png',
215 'app/v2-frage.png',
216 ]) {
217 expect(bisKennung, `${datei} fehlt in der Aufzählung.`).toContain(`"${datei}"`);
218 }
219 expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA');
220 });
221 });