import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' /** * Die Installationsanleitung muss ausführbar sein, nicht nur lesbar. * * Anlass ist eine Fehlersuche vom 31.08.2026. Drei Stellen der Anleitung * haben dabei Zeit gekostet — keine davon war falsch formuliert, alle drei * waren schlicht nicht durchdacht: * * 1. `mc alias set` wurde einmalig gesetzt. Der Alias liegt in `/root/.mc` * **im Behälter** und ist nach jedem `docker compose up -d minio` weg. * `mc ls lokal` liefert dann kommentarlos nichts — man hält seine * Dokumente für verschwunden. * 2. Der Bucket-Name stand fest im Befehl, die Anwendung liest ihn aus * `S3_BUCKET`. Weichen sie ab, zeigen beide aneinander vorbei. * 3. `set -a && source .env` exportiert alle Werte in die Shell — und die * haben danach Vorrang vor der `.env`-Datei. Wer später etwas ändert * und neu startet, sieht keine Wirkung. * * Diese Prüfungen lesen den Text. Sie können die Anleitung nicht * ausführen, aber sie schlagen an, sobald jemand in eine der Fallen * zurückfällt. */ const anleitung = readFileSync('docs/INSTALLATION.md', 'utf8') /** Alle Zeilen mit einem mc-Aufruf, ohne Fließtext und Zitate. */ function mcZeilen(): string[] { return anleitung .split('\n') .filter((zeile) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(zeile)) .filter((zeile) => !zeile.trim().startsWith('>')) } describe('Installationsanleitung', () => { it('setzt bei jedem mc-Aufruf den Alias selbst', () => { // Sonst funktioniert der Befehl nur, solange der Behaelter nicht neu // erstellt wurde - und das sagt einem niemand. const abschnitte = anleitung.split('```') const ohneAlias = abschnitte .filter((block) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(block)) .filter((block) => !block.includes('mc alias set')) expect( ohneAlias.length, 'Ein mc-Befehl ohne "mc alias set" im selben Block läuft nur, solange ' + 'der MinIO-Behälter nicht neu erstellt wurde.', ).toBe(0) }) it('verdrahtet den Namen des Ablagebereichs nicht fest', () => { const festVerdrahtet = mcZeilen().filter((zeile) => /lokal\/betreuungs-cockpit/.test(zeile)) expect( festVerdrahtet, 'Der Name gehört aus $S3_BUCKET - sonst zeigen Anwendung und ' + 'Ablagebereich aneinander vorbei, sobald jemand ihn ändert.', ).toEqual([]) }) it('empfiehlt kein "source .env" mehr', () => { // Exportierte Werte ueberstimmen danach jedes "docker compose up". const zeilen = anleitung .split('\n') .filter((zeile) => !zeile.trim().startsWith('>')) .filter((zeile) => !zeile.trim().startsWith('#')) .filter((zeile) => /source \.env/.test(zeile)) expect(zeilen).toEqual([]) }) it('erklärt trotzdem, warum es nicht empfohlen wird', () => { // Weglassen allein hilft nicht: Wer den Kniff von woanders kennt, // benutzt ihn weiter. expect(anleitung).toMatch(/Vorrang vor der/) }) it('sagt, wo die Daten liegen', () => { expect(anleitung).toMatch(/## Wo deine Daten liegen/) expect(anleitung).toMatch(/docker volume inspect/) expect(anleitung).toMatch(/minio-daten/) expect(anleitung).toMatch(/postgres-daten/) }) it('zeigt, wie man Akte und Speicher abgleicht', () => { // Die wichtigste Kontrolle: Zu jedem Akteneintrag muss eine Datei // gehoeren, sonst belegt eine Pruefsumme ein Dokument, das es nicht // mehr gibt. expect(anleitung).toMatch(/objektSchluessel/) expect(anleitung).toMatch(/comm -23/) }) it('nennt die Schlüssel, ohne die nichts mehr lesbar ist', () => { const abschnitt = anleitung.slice(anleitung.indexOf('## Wo deine Daten liegen')) for (const schluessel of ['ENC_KEY_V1', 'SICHERUNG_PASSWORT', 'MINIO_KMS_SECRET_KEY']) { expect(abschnitt, `${schluessel} fehlt in der Aufzählung`).toContain(schluessel) } }) it('weist docker-compose.override.yml als Ort für eigene Anpassungen aus', () => { expect(anleitung).toMatch(/docker-compose\.override\.yml/) expect(readFileSync('.gitignore', 'utf8')).toMatch(/docker-compose\.override\.yml/) }) it('behauptet nicht mehr, die Dokumente seien ungesichert', () => { // Stand bis v1.12.0 so drin und stimmt seit der Sicherung nicht mehr. expect(anleitung).not.toMatch(/Was das Backup nicht enthält:.*Dateien im Objektspeicher/s) }) }) /** * Die Bedienungsanleitung muss die Fragen beantworten, die tatsächlich * gestellt werden. * * Zwei davon kamen aus der Praxis: * * 1. „Wie mache ich den Jahresabschluss?" — die Anleitung beschrieb, was * in jedem Bericht steht, aber nicht die Reihenfolge und nicht, was man * vorher besorgen muss. Rückkaufswerte brauchen Wochen. * 2. „Das Gericht will einen Kontoauszug für die Genossenschaftsanteile — * den kann ich nicht am Automaten holen." Richtig: Es ist kein * Zahlungskonto, sondern eine Forderung, und der Nachweis ist eine * Bescheinigung, die man anfordert. * * Der zweite Fall ist der wichtigere, weil er zu einem stillen Fehler * führt: Wer die Anteile als Konto anlegt, kann die Bescheinigung nicht am * Wert hinterlegen — genau das Dokument, das das Gericht sehen will. */ describe('Bedienungsanleitung', () => { const bedienung = readFileSync('docs/ANLEITUNG.md', 'utf8') it('erklärt den Jahresabschluss mit Reihenfolge', () => { expect(bedienung).toMatch(/## Der Jahresabschluss, Schritt für Schritt/) }) it('sagt, was vor dem Stichtag zu besorgen ist', () => { // Wer erst am Stichtag daran denkt, wartet Wochen auf die Auskunft. const abschnitt = bedienung.slice(bedienung.indexOf('## Der Jahresabschluss')) expect(abschnitt).toMatch(/Sechs Wochen vorher/) expect(abschnitt).toMatch(/Rückkaufswerte/) expect(abschnitt).toMatch(/Wiedervorlage/) }) it('besteht auf der Quellenangabe beim Kontostand', () => { // Ein Betrag ohne Herkunft ist gegenueber dem Gericht wenig wert. const abschnitt = bedienung.slice(bedienung.indexOf('## Der Jahresabschluss')) expect(abschnitt).toMatch(/Quelle/) expect(abschnitt).toMatch(/Kontoauszug 11\/2026|Online-Banking/) }) describe('Was aussieht wie ein Konto', () => { const abschnitt = bedienung.slice(bedienung.indexOf('## Was aussieht wie ein Konto')) it('gibt es überhaupt', () => { expect(abschnitt.length).toBeGreaterThan(500) }) it('führt Genossenschaftsanteile als Forderung, nicht als Konto', () => { expect(abschnitt).toMatch(/Genossenschaftsanteile/) expect(abschnitt).toMatch(/Art \*\*Forderung\*\*/) }) it('nennt den Grund, statt ihn vorauszusetzen', () => { // Ohne Begruendung legt der naechste Leser es wieder als Konto an. expect(abschnitt).toMatch(/Nachweis anhängen|Nachweis anhaengen/) expect(abschnitt).toMatch(/kein Dokument/) }) it('warnt davor, den Vorjahreswert einfach fortzuschreiben', () => { // "Der Wert bleibt doch gleich" stimmt meistens - aber eine // gutgeschriebene Dividende erhoeht das Geschaeftsguthaben. expect(abschnitt).toMatch(/Dividende/) }) it('sagt, wie man den Nachweis bekommt', () => { expect(abschnitt).toMatch(/Bescheinigung über das\s+Geschäftsguthaben/) expect(abschnitt).toMatch(/Auskunftsersuchen/) }) it('behandelt die verwandten Fälle mit', () => { for (const fall of ['Bausparvertrag', 'Depot', 'Verbindlichkeit']) { expect(abschnitt, `${fall} fehlt in der Übersicht`).toContain(fall) } }) it('schickt Versicherungen zum Vertrag, nicht ins Vermögen', () => { // Der Rueckkaufswert gehoert an den Vertrag, samt Historie. expect(abschnitt).toMatch(/nicht\*\* den Vermögensbereich|Vertrag selbst/) }) }) })