// @vitest-environment node /** * Jede aufgenommene Inhaltsdatei braucht eine Lizenzangabe. * * Anlass ist ein selbst verursachter Rückschritt: Mit `content/glossar.json` * kam eine neue Inhaltsdatei ins Projekt, ohne Eintrag in `REUSE.toml`. Damit * war die Zusage aus einem früheren Stand – „reuse lint ist konform“ – nicht * mehr wahr, und niemand hat es bemerkt. Aufgefallen ist es erst, als jemand * für einen ganz anderen Zweck nachgesehen hat. * * `reuse lint` selbst läuft hier nicht: Es ist ein Python-Werkzeug und keine * Abhängigkeit dieses Pakets; ein Test, der es aufruft, wäre auf einem * Rechner ohne Python rot, ohne dass etwas falsch wäre. Geprüft wird deshalb * genau die eine Regel, an der es gescheitert ist – dass keine aufgenommene * Inhaltsdatei ohne Eintrag bleibt. Das ersetzt `reuse lint` nicht, es fängt * den Fall ab, der wirklich passiert ist. * * Gefragt wird `git ls-files`, nicht das Dateisystem: `reuse lint` sieht nur * aufgenommene Dateien, und unter `content/gesetze/` liegen heruntergeladene * Gesetzesarchive, die niemand einchecken will. */ import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const wurzel = join(fileURLToPath(new URL('..', import.meta.url)), '..'); /** Aufgenommene Dateien unter `content/`, oder `null` ohne Git. */ function aufgenommeneInhalte(): string[] | null { try { return execFileSync('git', ['ls-files', 'content'], { cwd: wurzel, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }) .split('\n') .map((zeile) => zeile.trim()) .filter((zeile) => zeile.length > 0); } catch { return null; } } /** * Aufgenommene Dateien unmittelbar im Projektverzeichnis, oder `null` ohne Git. * * `git ls-files` liefert ohne Pfadangabe den ganzen Baum; hier interessieren * nur die Dateien ohne Schrägstrich im Namen. Der Anlass ist derselbe wie * oben, eine Ebene höher: `CHANGELOG.md` kam als neue Datei in die Wurzel, * und dort greift kein `**`-Muster – sie hätte ohne Lizenzangabe bleiben * können, ohne dass es auffällt. */ function aufgenommeneWurzeldateien(): string[] | null { try { return execFileSync('git', ['ls-files'], { cwd: wurzel, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }) .split('\n') .map((zeile) => zeile.trim()) .filter((zeile) => zeile.length > 0 && !zeile.includes('/')); } catch { return null; } } /** * Die Pfadmuster aus den `path = [...]`-Angaben von REUSE.toml. * * Gelesen werden gezielt diese Listen und nicht jede Zeichenkette der Datei. * Der erste Versuch nahm alle Anführungszeichen – und verrutschte an * `SPDX-PackageDownloadLocation = ""`: Die leere Zeichenkette fiel durch das * Muster, danach war jedes Paar um eins verschoben und die Liste bestand aus * Bruchstücken. Aufgefallen ist das nur, weil der Test unten die Ausbeute * gegenprüft. */ function pfadmuster(): string[] { const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8') .split('\n') .filter((zeile) => !zeile.trimStart().startsWith('#')) .join('\n'); return [...toml.matchAll(/path\s*=\s*\[([^\]]*)\]/gu)].flatMap((liste) => [...(liste[1] ?? '').matchAll(/"([^"]*)"/gu)] .map((treffer) => treffer[1] ?? '') .filter((pfad) => pfad.length > 0), ); } /** * Deckt ein Muster aus REUSE.toml diesen Pfad ab? * * Nachgebildet wird nur, was das Projekt tatsächlich verwendet: der genaue * Pfad und ein abschließendes `**`. Ein vollständiger Glob-Vergleich wäre * mehr Code als die Regel, die er prüfen soll. */ function abgedeckt(pfad: string, muster: readonly string[]): boolean { return muster.some((m) => (m.endsWith('/**') ? pfad.startsWith(m.slice(0, -2)) : m === pfad)); } describe('Lizenzauszeichnung der Inhalte', () => { const muster = pfadmuster(); it('liest überhaupt Pfade aus REUSE.toml', () => { /* Ohne diese Gegenprobe wäre der Test unten immer grün und damit wertlos – dieselbe Falle wie eine leere Prüfliste. Genau hier ist der Verschiebefehler des ersten Versuchs aufgefallen. */ expect(muster).toContain('app/**'); expect(muster).toContain('content/katalog/**'); expect(muster.every((m) => !m.includes('\n'))).toBe(true); }); it('zeichnet jede aufgenommene Datei unter content/ aus', () => { const dateien = aufgenommeneInhalte(); if (dateien === null) { /* Kein Git – dann ist nicht feststellbar, was `reuse lint` sähe. */ return; } expect(dateien.length).toBeGreaterThan(3); const ohne = dateien.filter((datei) => !abgedeckt(datei, muster)); expect( ohne, 'Neue Inhaltsdateien gehören in REUSE.toml – sonst ist ihre Herkunft nicht ausgewiesen.', ).toEqual([]); }); it('zeichnet jede aufgenommene Datei im Projektverzeichnis aus', () => { /* Eine Ebene höher als der Test darüber, und aus demselben Anlass: In der Wurzel greift kein `**`-Muster, jede Datei dort braucht einen eigenen Eintrag. Als `CHANGELOG.md` dazukam, fiel das nur auf, weil jemand danach gesucht hat. */ const dateien = aufgenommeneWurzeldateien(); if (dateien === null) { return; } expect(dateien).toContain('README.md'); const ohne = dateien.filter((datei) => !abgedeckt(datei, muster)); expect( ohne, 'Neue Dateien im Projektverzeichnis gehören einzeln in REUSE.toml – ' + 'ein `**`-Muster deckt die Wurzel nicht ab.', ).toEqual([]); }); it('nennt für den amtlichen Katalog eine eigene Lizenzkennung', () => { /* Der Katalog darf nie unter die eigene Lizenz des Projekts geraten: Auf ein amtliches Werk kann niemand eine Lizenz erteilen. */ const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); const abKatalog = toml.slice(toml.indexOf('"content/katalog/**"')); const bisKennung = abKatalog.slice(0, abKatalog.indexOf('SPDX-License-Identifier') + 60); expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA'); expect(bisKennung).not.toContain('EUPL-1.2'); }); /* Die genauere Angabe muss hinter der allgemeinen stehen. Treffen mehrere Einträge auf dieselbe Datei, gilt der **letzte**. Bis Fassung 0.24.1 stand `docs/**` hinter der Aufzählung der Bilder mit amtlichem Wortlaut und überschrieb sie vollständig: Am `reuse lint --json` nachgemessen trugen alle neun Dateien und der Prüfbericht `EUPL-1.2` – also genau die Zuordnung, die die Aufzählung ausschließen sollte. Zwölf Zeilen Begründung ohne jede Wirkung, und niemand hat es gesehen, weil `reuse lint` in keinem Torwächter läuft. Geprüft wird die Reihenfolge und nicht das Ergebnis: `reuse` ist ein Python-Werkzeug und keine Abhängigkeit dieses Pakets – dieselbe Überlegung wie im Kopf dieser Datei. */ it('stellt die Ausnahmen hinter die pauschale docs-Regel', () => { const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); const pauschal = toml.indexOf('path = ["docs/**"]'); const ausnahme = toml.indexOf('"docs/katalog-pruefbericht.html"'); expect(pauschal, 'Die pauschale docs-Regel fehlt.').toBeGreaterThanOrEqual(0); expect(ausnahme, 'Die Aufzählung der Bilder mit amtlichem Wortlaut fehlt.').toBeGreaterThan(0); expect( ausnahme, 'Die Aufzählung steht vor `docs/**` und wird davon überschrieben – ' + 'bei mehreren Treffern gilt der letzte Eintrag.', ).toBeGreaterThan(pauschal); }); it('führt jedes Bildschirmfoto mit amtlichem Wortlaut auf', () => { /* Die Liste hier ist die Gegenprobe zur Liste dort: Wer ein Bild hinzunimmt, auf dem eine Frage im Wortlaut steht, muss beide anfassen. */ const toml = readFileSync(join(wurzel, 'REUSE.toml'), 'utf8'); const block = toml.slice(toml.indexOf('"docs/katalog-pruefbericht.html"')); const bisKennung = block.slice(0, block.indexOf('SPDX-License-Identifier') + 60); for (const datei of [ 'docs/bildschirmfotos/04-lernsitzung.png', 'docs/bildschirmfotos/05-lernsitzung-feedback.png', 'docs/bildschirmfotos/08-pruefungslauf.png', 'docs/bildschirmfotos/10-erklaerung.png', 'docs/bildschirmfotos/13-suche.png', 'docs/store-bilder/01-frage-mit-antwortmoeglichkeiten.png', 'docs/store-bilder/02-rueckmeldung-mit-begruendung.png', 'docs/store-bilder/05-fragen-durchsuchen.png', 'app/v2-frage.png', ]) { expect(bisKennung, `${datei} fehlt in der Aufzählung.`).toContain(`"${datei}"`); } expect(bisKennung).toContain('LicenseRef-Amtliches-Werk-BVA'); }); });