Files
betreuungs-cockpit/tests/umgebung-durchreichen.test.ts
Florian Wilde 810716e4e4 Fehlende Umgebungsvariablen durchreichen - und dauerhaft absichern
Der Dienst "app" zaehlt seine Umgebungsvariablen einzeln auf; ein
env_file gibt es nicht. Drei Werte, die die Anwendung ausliest, standen
dort nicht: ERSTEINRICHTUNG_GEHEIMNIS, VERTRAUE_PROXY (beide seit v1.11.0)
und S3_SSE (seit v1.11.1).

Wer sie laut .env.example und INSTALLATION.md eintrug, brachte sie damit
nicht in den Behaelter - die Anleitung war an dieser Stelle schlicht
falsch. Besonders unangenehm bei VERTRAUE_PROXY: Der Wert fehlt still,
die Anmeldesperre zaehlt dann nur nach E-Mail-Adresse statt zusaetzlich
nach Anschluss, und im Betrieb faellt nichts auf.

VERTRAUE_PROXY bekommt bewusst KEINE Vorgabe "true". Die strengere
Einstellung bleibt die Voreinstellung; wer einen Proxy davorstellt, sagt
es ausdruecklich. Ohne Proxy ist der weitergereichte Kopf frei erfunden.

Der eigentliche Ertrag ist der Test: Er liest, welche Variablen env.ts
ausliest - ueber process.env ebenso wie ueber pflicht(), zahl(),
schalter() und schluessel32() -, vergleicht das mit dem, was die
Compose-Datei dem Dienst mitgibt, und meldet jede Luecke. Ausnahmen sind
aufgezaehlt und begruendet.

Damit wiederholt sich der Fehler beim naechsten neuen Wert nicht: Wer ihn
einfuehrt und das Durchreichen vergisst, sieht es sofort.

Gegen drei Mutationen geprueft: Wert wieder entfernt, Vorgabe "true"
gesetzt, S3_SSE entfernt - jede wird rot.

Refs #43
2026-09-01 16:12:32 +02:00

114 lines
4.2 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
/**
* Was die Anwendung liest, muss der Behälter auch bekommen.
*
* Die `docker-compose.yml` zählt die Umgebungsvariablen des Dienstes `app`
* einzeln auf — sie hat kein `env_file:`. Wer einen neuen Wert einführt,
* ihn in `.env.example` dokumentiert und in `env.ts` ausliest, hat damit
* noch nichts erreicht: Im Behälter kommt er nicht an.
*
* Genau das ist mit v1.11.0 passiert. `ERSTEINRICHTUNG_GEHEIMNIS` und
* `VERTRAUE_PROXY` standen in der Anleitung, in der Beispieldatei und im
* Code — nur nicht in der Compose-Datei. Die Anleitung war damit schlicht
* falsch, und der Fehler war von außen nicht zu sehen: `VERTRAUE_PROXY`
* fehlt still, die Anmeldesperre zählt dann eine Dimension weniger.
*
* Dieser Test liest beide Seiten und vergleicht sie. Er prüft kein
* Verhalten, sondern die Verpackung — wie `skripte.test.ts`.
*/
const compose = readFileSync('docker-compose.yml', 'utf8')
const envDatei = readFileSync('src/lib/env.ts', 'utf8')
const beispiel = readFileSync('.env.example', 'utf8')
/**
* Alles, was `env.ts` über seine Helfer ausliest.
*
* `process.env.X` direkt wird mitgenommen, ebenso `pflicht('X')`,
* `zahl('X', …)`, `schalter('X', …)` und `schluessel32('X')`.
*/
function gelesen(): Set<string> {
const namen = new Set<string>()
for (const treffer of envDatei.matchAll(/process\.env\.([A-Z_0-9]+)/g)) {
namen.add(treffer[1]!)
}
for (const treffer of envDatei.matchAll(
/(?:pflicht|zahl|schalter|schluessel32)\('([A-Z_0-9]+)'/g,
)) {
namen.add(treffer[1]!)
}
return namen
}
/** Die Variablen, die der Dienst `app` durchreicht. */
function durchgereicht(): Set<string> {
const abschnitt = compose.slice(compose.indexOf('\n app:'), compose.indexOf('\n sicherung:'))
const namen = new Set<string>()
for (const treffer of abschnitt.matchAll(/^ {6}([A-Z_0-9]+):/gm)) {
namen.add(treffer[1]!)
}
return namen
}
/**
* Was absichtlich nicht durchgereicht wird - jeweils mit Grund.
*
* Wer hier etwas ergänzt, nimmt es bewusst aus der Prüfung. Das ist
* erlaubt, aber es soll dabeistehen, warum.
*/
const AUSNAHMEN = new Map<string, string>([
['NODE_ENV', 'wird im Dienst gesetzt, kommt nicht aus der .env'],
['NEXT_PUBLIC_GIT_COMMIT', 'Bauargument, steckt schon im Abbild'],
['NEXT_PUBLIC_BUILD_ZEIT', 'Bauargument, steckt schon im Abbild'],
['DATEI_SPEICHER_PFAD', 'nur fuer den Dateitreiber; im Behaelter laeuft s3'],
['TESSERACT_BEFEHL', 'Standard "tesseract" stimmt im Abbild immer'],
])
describe('Umgebungsvariablen erreichen den Behälter', () => {
it('reicht jede Variable durch, die die Anwendung ausliest', () => {
const fehlend = [...gelesen()]
.filter((name) => !durchgereicht().has(name))
.filter((name) => !AUSNAHMEN.has(name))
// ENC_KEY_V2, V3 ... werden dynamisch gesucht; nur V1 ist Pflicht.
.filter((name) => !/^ENC_KEY_V\d+$/.test(name))
.sort()
expect(
fehlend,
'Diese Werte liest die Anwendung, aber die docker-compose.yml gibt sie ' +
'dem Dienst "app" nicht mit. In der .env stehen sie dann wirkungslos.',
).toEqual([])
})
it('erklärt jede Ausnahme', () => {
for (const [name, grund] of AUSNAHMEN) {
expect(grund.length, `Ausnahme ${name} ohne Begründung`).toBeGreaterThan(10)
}
})
it('dokumentiert die beiden Werte aus v1.11.0 in der Beispieldatei', () => {
// Sie waren der Anlass. Verschwinden sie aus .env.example, merkt es
// sonst niemand, bis jemand eine Neuinstallation versucht.
expect(beispiel).toMatch(/ERSTEINRICHTUNG_GEHEIMNIS=/)
expect(beispiel).toMatch(/VERTRAUE_PROXY=/)
})
it('lässt VERTRAUE_PROXY ohne Vorgabewert', () => {
// Die strengere Einstellung bleibt die Voreinstellung. Ein Vorgabewert
// "true" waere bequem und falsch: Ohne Proxy ist der weitergereichte
// Kopf frei erfunden.
expect(compose).toMatch(/VERTRAUE_PROXY: \$\{VERTRAUE_PROXY:-\}/)
})
it('findet überhaupt etwas - sonst prüft der Test nichts', () => {
// Ein stiller Ausfall der Erkennung waere schlimmer als eine Luecke:
// Der Test staende gruen da und pruefte nichts.
expect(gelesen().size).toBeGreaterThan(15)
expect(durchgereicht().size).toBeGreaterThan(15)
})
})