lsa-planer

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

/ tests domain lizenzEupl.test.ts

5,9 KB Rohdatei
tests/domain/lizenzEupl.test.ts — 126 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3
4 /*
5 * Die Lizenz des Erzeugnisses: EUPL 1.2.
6 *
7 * Vor Fassung 5.12.0 fuehrte `package.json` "MIT", eine `LICENSE`-Datei
8 * gab es nicht, und die README hatte keinen Lizenzabschnitt - drei Angaben, von
9 * denen zwei fehlten und die dritte nicht gewollt war. Das war ein offener
10 * Punkt. Der Urheber hat entschieden: die Open-Source-Lizenz fuer die
11 * Europaeische Union v. 1.2.
12 *
13 * WARUM DAS EINE WACHE BRAUCHT. Eine Lizenzangabe steht an drei Stellen - im
14 * Bezeichner `license` der `package.json`, im beiliegenden Wortlaut und im
15 * Abschnitt "Rechtliches" der README. Laufen sie auseinander, ist keine davon
16 * mehr etwas wert: Ein Bezeichner ohne Wortlaut belegt nichts, und ein Wortlaut,
17 * den kein Bezeichner nennt, findet niemand. Genau dieser Zustand lag vor.
18 *
19 * DER WORTLAUT IST AMTLICH UEBERNOMMEN, nicht nacherzaehlt: EUR-Lex,
20 * Durchfuehrungsbeschluss (EU) 2017/863 der Kommission vom 18. Mai 2017
21 * (CELEX 32017D0863). Beide Sprachfassungen liegen bei und sind nach Artikel 13
22 * der Lizenz gleichermassen verbindlich. Die Wache prueft deshalb an BEIDEN,
23 * dass sie vollstaendig sind - fuenfzehn Artikel und die Anlage -, und dass sie
24 * dieselbe Lizenz sind.
25 *
26 * Was hier NICHT geprueft wird: ob der Wortlaut Zeichen fuer Zeichen dem
27 * amtlichen entspricht. Dafuer waere die amtliche Quelle abzurufen, und ein Test
28 * dieses Projekts geht nicht ins Netz - es gibt genau einen Netzaufruf im ganzen
29 * Programm, und der gehoert dem Kartendienst. Geprueft ist die Vollstaendigkeit
30 * der Gliederung; wer den Text austauscht, muss ihn wieder aus der amtlichen
31 * Quelle nehmen.
32 */
33
34 const PAKET = JSON.parse(readFileSync('package.json', 'utf8')) as {
35 license: string;
36 private: boolean;
37 };
38 const LIZENZ_EN = readFileSync('LICENSE', 'utf8');
39 const LIZENZ_DE = readFileSync('LICENSE.de.txt', 'utf8');
40 const README = readFileSync('README.md', 'utf8');
41
42 /** Die Artikelueberschriften "1." bis "15." am Zeilenanfang. */
43 function artikel(text: string): number[] {
44 return [...text.matchAll(/^(\d+)\. /gm)].map((t) => Number(t[1]));
45 }
46
47 describe('Die Lizenz des Erzeugnisses steht an allen drei Stellen gleich', () => {
48 it('nennt `package.json` den SPDX-Bezeichner der EUPL 1.2', () => {
49 expect(PAKET.license, 'Bezeichner in package.json').toBe('EUPL-1.2');
50 });
51
52 /*
53 * `private` sperrt allein `npm publish`. Es steht der EUPL nicht entgegen -
54 * dieses Erzeugnis wird als Installationspaket weitergegeben, nicht ueber die
55 * npm-Registratur. Der Fall haelt den Schalter fest, damit die Frage nicht ein
56 * zweites Mal aufgemacht wird; die Begruendung steht in `comment:license`.
57 */
58 it('bleibt gegen eine Veroeffentlichung in der npm-Registratur gesperrt', () => {
59 expect(PAKET.private, 'private in package.json').toBe(true);
60 });
61
62 it('liegt der Wortlaut in beiden amtlichen Sprachfassungen bei', () => {
63 expect(LIZENZ_EN, 'Kopf der englischen Fassung').toContain(
64 'EUROPEAN UNION PUBLIC LICENCE v. 1.2',
65 );
66 expect(LIZENZ_DE, 'Kopf der deutschen Fassung').toContain(
67 'OPEN-SOURCE-LIZENZ FÜR DIE EUROPÄISCHE UNION v. 1.2',
68 );
69 });
70
71 it('sind beide Fassungen vollstaendig und dieselbe Lizenz', () => {
72 const erwartet = Array.from({ length: 15 }, (_, i) => i + 1);
73 expect(artikel(LIZENZ_EN), 'Artikel der englischen Fassung').toEqual(erwartet);
74 expect(artikel(LIZENZ_DE), 'Artikel der deutschen Fassung').toEqual(erwartet);
75 expect(LIZENZ_EN, 'Anlage der englischen Fassung').toContain('Appendix');
76 expect(LIZENZ_DE, 'Anlage der deutschen Fassung').toContain('Anlage');
77 // Die Anlage traegt die kompatiblen Lizenzen - ohne sie waere die
78 // Copyleft-Wirkung eine andere.
79 for (const [name, text] of [
80 ['englisch', LIZENZ_EN],
81 ['deutsch', LIZENZ_DE],
82 ] as const) {
83 expect(text, `kompatible Lizenzen, ${name}`).toContain(
84 'GNU General Public License (GPL) v. 2, v. 3',
85 );
86 expect(text, `kompatible Lizenzen, ${name}`).toContain('Mozilla Public Licence (MPL) v. 2');
87 }
88 });
89
90 /*
91 * Die Lizenz schreibt den Hinweis selbst vor: Das Werk steht unter ihren
92 * Bedingungen, wenn der Lizenzgeber "unmittelbar hinter dem Urhebervermerk"
93 * den Vermerk "Licensed under the EUPL" angebracht hat. Der Wortlaut ist
94 * vorgegeben und wird deshalb woertlich geprueft, nicht sinngemaess.
95 */
96 it('traegt die README den Vermerk, den die Lizenz selbst verlangt', () => {
97 const urheber = README.indexOf('© 2026 Olaf Willerding');
98 const vermerk = README.indexOf('Licensed under the EUPL');
99 expect(urheber, 'Urhebervermerk in der README').toBeGreaterThan(0);
100 expect(vermerk, 'Lizenzvermerk in der README').toBeGreaterThan(urheber);
101 // Unmittelbar dahinter: dazwischen steht nichts als Leerraum.
102 expect(
103 README.slice(urheber + '© 2026 Olaf Willerding'.length, vermerk).trim(),
104 'zwischen Urheber- und Lizenzvermerk',
105 ).toBe('');
106 });
107
108 it('nennt die README beide Fassungen und den Bezeichner', () => {
109 const rechtliches = README.slice(README.indexOf('## Rechtliches'));
110 expect(rechtliches, 'Abschnitt "Rechtliches"').not.toBe('');
111 expect(rechtliches, 'SPDX-Bezeichner in der README').toContain(PAKET.license);
112 expect(rechtliches, 'englische Fassung genannt').toContain('`LICENSE`');
113 expect(rechtliches, 'deutsche Fassung genannt').toContain('`LICENSE.de.txt`');
114 expect(rechtliches, 'Verweis auf die Fremdbestandteile').toContain('docs/fremdbestandteile.md');
115 });
116
117 /*
118 * Die Copyleft-Wirkung ist der Grund, warum diese Wahl fuer einen Weitergebenden
119 * etwas bedeutet. Steht sie nicht in der README, liest sie niemand nach.
120 */
121 it('sagt die README, dass die EUPL Copyleft ist', () => {
122 const rechtliches = README.slice(README.indexOf('## Rechtliches'));
123 expect(rechtliches, 'Copyleft in der README').toMatch(/Copyleft/);
124 expect(rechtliches, 'Pflicht zur Weitergabe des Quelltextes').toMatch(/Quelltext/);
125 });
126 });