syncova-backup/packages/agent/specialfiles_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

153 lines
5.5 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"syscall"
"testing"
)
// TestSpecialFilesAreNotCountedAsDataLoss ist der Kerntest der Phase 6.
//
// Ein Socket oder eine benannte Pipe *fehlt* nicht im Backup — sie *gehoert*
// nicht hinein: Sie hat keinen Inhalt, den man sichern und zurueckschreiben
// koennte. Ein Socket ist der Endpunkt eines laufenden Prozesses.
//
// Zaehlte man sie als uebergangenes Objekt, waere jede Sicherung eines
// Linux-Systems ein Teilfehler: In /var/run und /tmp liegen staendig Sockets.
// Nach einer Woche klickt niemand mehr einen Teilfehler an — und dann faellt
// auch der echte nicht mehr auf.
func TestSpecialFilesAreNotCountedAsDataLoss(testInstance *testing.T) {
sourcePath := testInstance.TempDir()
if writeError := os.WriteFile(filepath.Join(sourcePath, "nutzdaten.txt"),
[]byte("echte Daten"), 0o644); writeError != nil {
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
}
pipePath := filepath.Join(sourcePath, "warteschlange.fifo")
if pipeError := syscall.Mkfifo(pipePath, 0o644); pipeError != nil {
testInstance.Skipf("eine benannte Pipe ließ sich nicht anlegen: %v", pipeError)
}
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
if discoveryError != nil {
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
}
// Die Pipe muss vermerkt sein — verschwiegen werden darf sie nicht.
if !discoveryResult.HasProblems() {
testInstance.Fatal("die benannte Pipe wurde nicht vermerkt")
}
// Aber sie ist kein Datenverlust.
if discoveryResult.DataLossProblemCount() != 0 {
testInstance.Errorf("die benannte Pipe zählt als Datenverlust (%d)",
discoveryResult.DataLossProblemCount())
}
// Die Meldung muss verstaendlich sein: „prw-r--r--" sagt niemandem etwas.
var reasonText string
for _, problem := range discoveryResult.Problems {
if strings.HasSuffix(problem.Path, "warteschlange.fifo") {
reasonText = problem.Reason
}
}
if !strings.Contains(reasonText, "Pipe") {
testInstance.Errorf("die Meldung benennt den Objekttyp nicht verständlich: %q", reasonText)
}
if !strings.Contains(reasonText, "kein Fehler") {
testInstance.Errorf("die Meldung sagt nicht, dass es kein Fehler ist: %q", reasonText)
}
}
// TestPermissionErrorsRemainDataLoss ist die Gegenprobe.
//
// Eine nicht lesbare Datei ist etwas voellig anderes als ein Socket: Dort waren
// Daten, und sie fehlen jetzt im Backup. Der Lauf muss ein Teilfehler sein.
func TestPermissionErrorsRemainDataLoss(testInstance *testing.T) {
if os.Geteuid() == 0 {
testInstance.Skip("als root greifen Dateirechte nicht")
}
sourcePath := testInstance.TempDir()
lockedDirectory := filepath.Join(sourcePath, "gesperrt")
if makeError := os.Mkdir(lockedDirectory, 0o755); makeError != nil {
testInstance.Fatalf("das Verzeichnis ließ sich nicht anlegen: %v", makeError)
}
if writeError := os.WriteFile(filepath.Join(lockedDirectory, "geheim.txt"),
[]byte("Daten"), 0o644); writeError != nil {
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
}
if chmodError := os.Chmod(lockedDirectory, 0o000); chmodError != nil {
testInstance.Fatalf("die Rechte ließen sich nicht setzen: %v", chmodError)
}
// Aufraeumen, sonst laesst sich das Testverzeichnis nicht entfernen.
testInstance.Cleanup(func() { _ = os.Chmod(lockedDirectory, 0o755) })
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
if discoveryError != nil {
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
}
if discoveryResult.DataLossProblemCount() == 0 {
testInstance.Error("ein Rechtefehler zählt nicht als Datenverlust — dort fehlen Daten")
}
if discoveryResult.PermissionProblemCount() == 0 {
testInstance.Error("der Rechtefehler wird nicht als solcher ausgewiesen")
}
}
// TestHardlinksAreBackedUpAsSeparateFiles hält die bekannte Grenze fest.
//
// Der **Inhalt** kommt vollstaendig zurueck, und dank Deduplizierung liegt er
// nur einmal im Repository. Die Verknuepfung selbst geht jedoch verloren: Nach
// einer Wiederherstellung sind es zwei unabhaengige Dateien.
//
// Das ist eine Grenze, keine Ueberraschung — sie steht hier, damit sie
// niemandem erst im Ernstfall auffaellt.
func TestHardlinksAreBackedUpAsSeparateFiles(testInstance *testing.T) {
sourcePath := testInstance.TempDir()
originalPath := filepath.Join(sourcePath, "original.txt")
if writeError := os.WriteFile(originalPath, []byte("gemeinsamer Inhalt"), 0o644); writeError != nil {
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
}
if linkError := os.Link(originalPath, filepath.Join(sourcePath, "verknuepfung.txt")); linkError != nil {
testInstance.Skipf("eine harte Verknüpfung ließ sich nicht anlegen: %v", linkError)
}
discoveryResult, discoveryError := Discover(sourcePath, DiscoveryOptions{})
if discoveryError != nil {
testInstance.Fatalf("die Erfassung schlug fehl: %v", discoveryError)
}
fileCount := 0
for _, discoveredEntry := range discoveryResult.Entries {
if discoveredEntry.EntryType == EntryTypeFile {
fileCount++
}
}
// Beide erscheinen als eigenstaendige Dateien — das ist der dokumentierte
// Zustand. Aendert sich das, gehoert die Dokumentation nachgezogen.
if fileCount != 2 {
testInstance.Errorf("es wurden %d Dateien erfasst, erwartet wurden zwei "+
"(harte Verknüpfungen werden als eigenständige Dateien gesichert)", fileCount)
}
if discoveryResult.DataLossProblemCount() != 0 {
testInstance.Error("eine harte Verknüpfung wurde als Datenverlust gewertet")
}
}