syncova-backup/scripts/release_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

140 lines
5.2 KiB
Go

// Package scripts prüft die Vollständigkeit der Auslieferung (Phase 23).
//
// Der Test läuft im gewöhnlichen Testlauf mit und braucht kein gebautes Paket:
// Er prüft die **Bestandteile**, aus denen es entsteht. Ein Paket zu bauen,
// nur um es zu prüfen, dauerte Minuten und liefe deshalb bald nicht mehr mit.
package scripts
import (
"os"
"path/filepath"
"strings"
"testing"
)
// repositoryRoot ist der Pfad zum Wurzelverzeichnis.
const repositoryRoot = ".."
// TestReleaseDocumentationIsComplete prueft die Liste aus §25.
//
// Der Umsetzungsplan nennt zwoelf Bestandteile, die ein Auslieferungspaket
// enthalten muss. Fuenf davon sind Dokumente, und die schreibt man einmal —
// danach faellt es niemandem auf, wenn eines beim Umbenennen verlorengeht.
// Genau deshalb steht die Liste hier und nicht nur in einer Aufzaehlung.
func TestReleaseDocumentationIsComplete(testInstance *testing.T) {
requiredDocuments := map[string]string{
"docs/installation.md": "Installationsanleitung",
"docs/recovery-runbook.md": "Wiederherstellungsdokumentation",
"docs/security-guide.md": "Sicherheitsleitfaden",
"docs/api.md": "API-Dokumentation",
"docs/troubleshooting.md": "Stoerungsleitfaden",
"docs/agent-installation.md": "Installationsanleitung der Agenten",
"CHANGELOG.md": "Aenderungsliste",
"README.md": "Ueberblick",
}
for documentPath, documentPurpose := range requiredDocuments {
fileInformation, statError := os.Stat(filepath.Join(repositoryRoot, documentPath))
if statError != nil {
testInstance.Errorf("%s fehlt (%s): %v", documentPath, documentPurpose, statError)
continue
}
// Eine leere Datei erfüllt die Liste formal und hilft niemandem. Die
// Grenze ist bewusst niedrig — sie fängt die versehentlich angelegte
// Hülle, nicht ein knappes Dokument.
if fileInformation.Size() < 500 {
testInstance.Errorf("%s ist mit %d Byte zu klein für %s",
documentPath, fileInformation.Size(), documentPurpose)
}
}
}
// TestReleaseScriptCoversEveryCommand prueft, dass kein Programm fehlt.
//
// Ein Programm, das gebaut, aber nicht ausgeliefert wird, faellt erst beim
// Kunden auf — und dann fehlt genau das Werkzeug, das er gerade braucht.
func TestReleaseScriptCoversEveryCommand(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
commandDirectories := []string{"apps/api/cmd", "apps/agent/cmd"}
for _, commandDirectory := range commandDirectories {
directoryEntries, readError := os.ReadDir(filepath.Join(repositoryRoot, commandDirectory))
if readError != nil {
testInstance.Fatalf("%s ließ sich nicht lesen: %v", commandDirectory, readError)
}
for _, singleEntry := range directoryEntries {
if !singleEntry.IsDir() {
continue
}
commandPath := commandDirectory + "/" + singleEntry.Name()
if !strings.Contains(buildScript, commandPath) {
testInstance.Errorf("das Programm %s wird gebaut, aber nicht ausgeliefert.\n"+
" Entweder in scripts/build-release.sh aufnehmen oder begründen, "+
"warum es nicht ins Paket gehört.", singleEntry.Name())
}
}
}
}
// TestReleaseScriptShipsEveryTargetPlatform prueft die Zielplattformen.
//
// Dieselben drei, die `make cross-build` bei jedem Lauf uebersetzt. Eine
// Plattform, die uebersetzt, aber nicht ausgeliefert wird, ist eine Zusage ohne
// Gegenstueck.
func TestReleaseScriptShipsEveryTargetPlatform(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
requiredPlatforms := []string{"linux/amd64", "linux/arm64", "windows/amd64"}
for _, platformName := range requiredPlatforms {
if !strings.Contains(buildScript, platformName) {
testInstance.Errorf("die Zielplattform %s wird nicht ausgeliefert", platformName)
}
}
}
// TestReleaseScriptIncludesMigrationsAndInterface prueft die uebrigen Bestandteile.
func TestReleaseScriptIncludesMigrationsAndInterface(testInstance *testing.T) {
buildScript := readReleaseScript(testInstance)
requiredParts := map[string]string{
"migrations/*.sql": "Migrationsdateien",
"migrations/checksums.txt": "Prüfsummen der Migrationen",
"apps/web/dist": "Weboberfläche",
"docs/": "Dokumentation",
"CHANGELOG.md": "Änderungsliste",
"SHA256SUMS": "Prüfsummen des Pakets",
}
for partPattern, partPurpose := range requiredParts {
if !strings.Contains(buildScript, partPattern) {
testInstance.Errorf("im Auslieferungsskript fehlt %s (%s)", partPattern, partPurpose)
}
}
// Ohne diesen Schalter bindet Go gegen die libc des Bausystems. Der Start
// scheitert dann auf einer älteren Distribution mit einer Meldung über
// GLIBC — die niemand einem Backupprogramm zuordnet.
if !strings.Contains(buildScript, "CGO_ENABLED=0") {
testInstance.Error("die Programme werden nicht statisch gebunden (CGO_ENABLED=0 fehlt)")
}
}
// readReleaseScript liest das Auslieferungsskript.
func readReleaseScript(testInstance *testing.T) string {
testInstance.Helper()
scriptContent, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts/build-release.sh"))
if readError != nil {
testInstance.Fatalf("das Auslieferungsskript ließ sich nicht lesen: %v", readError)
}
return string(scriptContent)
}