lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain lizenzEupl.test.ts
| 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 | }); |