import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; /* * Die Lizenz des Erzeugnisses: EUPL 1.2. * * Vor Fassung 5.12.0 fuehrte `package.json` "MIT", eine `LICENSE`-Datei * gab es nicht, und die README hatte keinen Lizenzabschnitt - drei Angaben, von * denen zwei fehlten und die dritte nicht gewollt war. Das war ein offener * Punkt. Der Urheber hat entschieden: die Open-Source-Lizenz fuer die * Europaeische Union v. 1.2. * * WARUM DAS EINE WACHE BRAUCHT. Eine Lizenzangabe steht an drei Stellen - im * Bezeichner `license` der `package.json`, im beiliegenden Wortlaut und im * Abschnitt "Rechtliches" der README. Laufen sie auseinander, ist keine davon * mehr etwas wert: Ein Bezeichner ohne Wortlaut belegt nichts, und ein Wortlaut, * den kein Bezeichner nennt, findet niemand. Genau dieser Zustand lag vor. * * DER WORTLAUT IST AMTLICH UEBERNOMMEN, nicht nacherzaehlt: EUR-Lex, * Durchfuehrungsbeschluss (EU) 2017/863 der Kommission vom 18. Mai 2017 * (CELEX 32017D0863). Beide Sprachfassungen liegen bei und sind nach Artikel 13 * der Lizenz gleichermassen verbindlich. Die Wache prueft deshalb an BEIDEN, * dass sie vollstaendig sind - fuenfzehn Artikel und die Anlage -, und dass sie * dieselbe Lizenz sind. * * Was hier NICHT geprueft wird: ob der Wortlaut Zeichen fuer Zeichen dem * amtlichen entspricht. Dafuer waere die amtliche Quelle abzurufen, und ein Test * dieses Projekts geht nicht ins Netz - es gibt genau einen Netzaufruf im ganzen * Programm, und der gehoert dem Kartendienst. Geprueft ist die Vollstaendigkeit * der Gliederung; wer den Text austauscht, muss ihn wieder aus der amtlichen * Quelle nehmen. */ const PAKET = JSON.parse(readFileSync('package.json', 'utf8')) as { license: string; private: boolean; }; const LIZENZ_EN = readFileSync('LICENSE', 'utf8'); const LIZENZ_DE = readFileSync('LICENSE.de.txt', 'utf8'); const README = readFileSync('README.md', 'utf8'); /** Die Artikelueberschriften "1." bis "15." am Zeilenanfang. */ function artikel(text: string): number[] { return [...text.matchAll(/^(\d+)\. /gm)].map((t) => Number(t[1])); } describe('Die Lizenz des Erzeugnisses steht an allen drei Stellen gleich', () => { it('nennt `package.json` den SPDX-Bezeichner der EUPL 1.2', () => { expect(PAKET.license, 'Bezeichner in package.json').toBe('EUPL-1.2'); }); /* * `private` sperrt allein `npm publish`. Es steht der EUPL nicht entgegen - * dieses Erzeugnis wird als Installationspaket weitergegeben, nicht ueber die * npm-Registratur. Der Fall haelt den Schalter fest, damit die Frage nicht ein * zweites Mal aufgemacht wird; die Begruendung steht in `comment:license`. */ it('bleibt gegen eine Veroeffentlichung in der npm-Registratur gesperrt', () => { expect(PAKET.private, 'private in package.json').toBe(true); }); it('liegt der Wortlaut in beiden amtlichen Sprachfassungen bei', () => { expect(LIZENZ_EN, 'Kopf der englischen Fassung').toContain( 'EUROPEAN UNION PUBLIC LICENCE v. 1.2', ); expect(LIZENZ_DE, 'Kopf der deutschen Fassung').toContain( 'OPEN-SOURCE-LIZENZ FÜR DIE EUROPÄISCHE UNION v. 1.2', ); }); it('sind beide Fassungen vollstaendig und dieselbe Lizenz', () => { const erwartet = Array.from({ length: 15 }, (_, i) => i + 1); expect(artikel(LIZENZ_EN), 'Artikel der englischen Fassung').toEqual(erwartet); expect(artikel(LIZENZ_DE), 'Artikel der deutschen Fassung').toEqual(erwartet); expect(LIZENZ_EN, 'Anlage der englischen Fassung').toContain('Appendix'); expect(LIZENZ_DE, 'Anlage der deutschen Fassung').toContain('Anlage'); // Die Anlage traegt die kompatiblen Lizenzen - ohne sie waere die // Copyleft-Wirkung eine andere. for (const [name, text] of [ ['englisch', LIZENZ_EN], ['deutsch', LIZENZ_DE], ] as const) { expect(text, `kompatible Lizenzen, ${name}`).toContain( 'GNU General Public License (GPL) v. 2, v. 3', ); expect(text, `kompatible Lizenzen, ${name}`).toContain('Mozilla Public Licence (MPL) v. 2'); } }); /* * Die Lizenz schreibt den Hinweis selbst vor: Das Werk steht unter ihren * Bedingungen, wenn der Lizenzgeber "unmittelbar hinter dem Urhebervermerk" * den Vermerk "Licensed under the EUPL" angebracht hat. Der Wortlaut ist * vorgegeben und wird deshalb woertlich geprueft, nicht sinngemaess. */ it('traegt die README den Vermerk, den die Lizenz selbst verlangt', () => { const urheber = README.indexOf('© 2026 Olaf Willerding'); const vermerk = README.indexOf('Licensed under the EUPL'); expect(urheber, 'Urhebervermerk in der README').toBeGreaterThan(0); expect(vermerk, 'Lizenzvermerk in der README').toBeGreaterThan(urheber); // Unmittelbar dahinter: dazwischen steht nichts als Leerraum. expect( README.slice(urheber + '© 2026 Olaf Willerding'.length, vermerk).trim(), 'zwischen Urheber- und Lizenzvermerk', ).toBe(''); }); it('nennt die README beide Fassungen und den Bezeichner', () => { const rechtliches = README.slice(README.indexOf('## Rechtliches')); expect(rechtliches, 'Abschnitt "Rechtliches"').not.toBe(''); expect(rechtliches, 'SPDX-Bezeichner in der README').toContain(PAKET.license); expect(rechtliches, 'englische Fassung genannt').toContain('`LICENSE`'); expect(rechtliches, 'deutsche Fassung genannt').toContain('`LICENSE.de.txt`'); expect(rechtliches, 'Verweis auf die Fremdbestandteile').toContain('docs/fremdbestandteile.md'); }); /* * Die Copyleft-Wirkung ist der Grund, warum diese Wahl fuer einen Weitergebenden * etwas bedeutet. Steht sie nicht in der README, liest sie niemand nach. */ it('sagt die README, dass die EUPL Copyleft ist', () => { const rechtliches = README.slice(README.indexOf('## Rechtliches')); expect(rechtliches, 'Copyleft in der README').toMatch(/Copyleft/); expect(rechtliches, 'Pflicht zur Weitergabe des Quelltextes').toMatch(/Quelltext/); }); });