import { execFileSync } from 'node:child_process' import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' /** * Die Betriebsskripte müssen im Repository so liegen, dass sie auf dem * Server auch laufen. * * Anlass sind zwei Fehler, die beide erst im Betrieb aufgefallen sind und * beide nichts mit dem Programm zu tun hatten, sondern mit der Verpackung: * * 1. `scripts/` fehlte im Docker-Abbild — `node scripts/totp-geheimnis.mjs` * lief ins Leere (behoben in 1.0.1). * 2. Die `.sh`-Dateien lagen mit Modus 644 im Repository. Die Anleitung * sagt `./scripts/update.sh`, auf Linux kam „Permission denied". * * Beide Male hat kein Test angeschlagen, weil beide Male der Code richtig * war. Diese Datei prüft deshalb nicht Verhalten, sondern Verpackung. * * Der Ausführbarkeitsbit wird aus dem Git-Index gelesen, nicht vom * Dateisystem: Unter Windows steht `core.filemode=false`, dort sagt das * Dateisystem nichts aus. Maßgeblich ist, was ausgecheckt wird. */ function gitVerfuegbar(): boolean { try { execFileSync('git', ['rev-parse', '--git-dir'], { stdio: 'pipe' }) return true } catch { return false } } /** Liefert [Modus, Pfad] je Datei, so wie sie im Index steht. */ function eintraegeImIndex(muster: string): [string, string][] { const ausgabe = execFileSync('git', ['ls-files', '-s', muster], { encoding: 'utf8' }) return ausgabe .split('\n') .filter((zeile) => zeile.trim() !== '') .map((zeile) => { const [angaben, pfad] = zeile.split('\t') return [angaben!.split(' ')[0]!, pfad!] }) } const mitGit = gitVerfuegbar() describe.skipIf(!mitGit)('Betriebsskripte', () => { it('legt jedes Shell-Skript ausführbar ab', () => { const skripte = eintraegeImIndex('scripts/*.sh') // Wenn hier nichts steht, stimmt das Muster nicht mehr - das waere // ein stiller Ausfall der Pruefung. expect(skripte.length).toBeGreaterThan(0) for (const [modus, pfad] of skripte) { expect(modus, `${pfad} muss ausführbar sein (100755, ist ${modus})`).toBe('100755') } }) it('gibt jedem Shell-Skript eine Shebang-Zeile', () => { for (const [, pfad] of eintraegeImIndex('scripts/*.sh')) { const ersteZeile = readFileSync(pfad, 'utf8').split('\n')[0] ?? '' expect(ersteZeile, `${pfad} braucht eine Shebang-Zeile`).toMatch(/^#!/) } }) it('nimmt scripts/ ins Docker-Abbild auf', () => { // Der Fehler aus 1.0.1: Die Wartungsskripte lagen im Repository, aber // nicht im Behaelter. Aufgefallen ist es erst, als sie gebraucht wurden. const dockerfile = readFileSync('Dockerfile', 'utf8') // Die Zeile traegt zusaetzliche Schalter (--from, --chown) - deshalb // nicht auf eine feste Reihenfolge festlegen, sondern nur darauf, dass // scripts/ ins Abbild kopiert wird. expect(dockerfile).toMatch(/^COPY.*\/scripts\s+\.\/scripts\s*$/m) }) it('bindet die Sicherungsskripte aus scripts/ in die Compose-Datei ein', () => { // Sie werden als Volume in den Sicherungsdienst gehaengt. Ein // umbenanntes Skript faellt sonst erst um 03:15 Uhr auf - im Log, // das niemand liest. const compose = readFileSync('docker-compose.yml', 'utf8') for (const name of ['sicherung.sh', 'wiederherstellung.sh']) { expect(compose, `${name} wird nicht eingebunden`).toContain(`./scripts/${name}:`) } }) })