package restore import ( "encoding/json" "os" "path/filepath" "testing" ) func readLog(t *testing.T, path string) []LogEntry { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("protokolldatei lesen: %v", err) } var entries []LogEntry for _, line := range splitLines(data) { if len(line) == 0 { continue } var e LogEntry if err := json.Unmarshal(line, &e); err != nil { t.Fatalf("protokollzeile dekodieren: %v (%s)", err, line) } entries = append(entries, e) } return entries } func splitLines(data []byte) [][]byte { var out [][]byte start := 0 for i, b := range data { if b == '\n' { out = append(out, data[start:i]) start = i + 1 } } return out } // TestAtomicRestore_EmptyTarget_Succeeds ist Pruefung 1: Restore auf // leerem (nicht vorhandenem) Ziel vollstaendig erfolgreich. func TestAtomicRestore_EmptyTarget_Succeeds(t *testing.T) { root := t.TempDir() target := filepath.Join(root, "ziel") logPath := filepath.Join(root, "restore.log") entry, err := AtomicRestore(KindDatabase, "gen-1", target, false, logPath, func(tempDir string) error { return os.WriteFile(filepath.Join(tempDir, "marker"), []byte("wiederhergestellt"), 0o600) }) if err != nil { t.Fatalf("atomicRestore: %v", err) } if entry.Result != "ok" { t.Fatalf("result = %q, want ok", entry.Result) } content, err := os.ReadFile(filepath.Join(target, "marker")) if err != nil || string(content) != "wiederhergestellt" { t.Fatalf("ziel nicht korrekt befuellt: %v %q", err, content) } } // TestAtomicRestore_NonEmptyTargetWithoutForce_LeavesOriginalUntouched ist // Pruefung 2: Restore auf nicht-leeres Zielverzeichnis laesst bei Abbruch // (keine Bestaetigung) den urspruenglichen Inhalt unveraendert. func TestAtomicRestore_NonEmptyTargetWithoutForce_LeavesOriginalUntouched(t *testing.T) { root := t.TempDir() target := filepath.Join(root, "ziel") logPath := filepath.Join(root, "restore.log") if err := os.MkdirAll(target, 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(target, "bestehend"), []byte("original"), 0o600); err != nil { t.Fatal(err) } restoreFnCalled := false _, err := AtomicRestore(KindObjects, "snap-1", target, false, logPath, func(tempDir string) error { restoreFnCalled = true return nil }) if err != ErrTargetNotEmpty { t.Fatalf("erwartet ErrTargetNotEmpty, habe: %v", err) } if restoreFnCalled { t.Fatal("restoreFn haette bei nicht-leerem ziel ohne force NIE aufgerufen werden duerfen") } content, err := os.ReadFile(filepath.Join(target, "bestehend")) if err != nil || string(content) != "original" { t.Fatalf("urspruenglicher inhalt veraendert: %v %q", err, content) } } // TestAtomicRestore_FailedRestoreFn_LeavesTargetUntouched ist Nachweis, // dass ein Fehler WAEHREND der Wiederherstellung (in tempDir) das // bestehende Ziel nicht beschaedigt, weil erst nach Erfolg umbenannt wird. func TestAtomicRestore_FailedRestoreFn_LeavesTargetUntouched(t *testing.T) { root := t.TempDir() target := filepath.Join(root, "ziel") logPath := filepath.Join(root, "restore.log") if err := os.MkdirAll(target, 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(target, "bestehend"), []byte("original"), 0o600); err != nil { t.Fatal(err) } _, err := AtomicRestore(KindDatabase, "gen-2", target, true, logPath, func(tempDir string) error { return os.ErrInvalid // simuliert fehlgeschlagene wiederherstellung }) if err == nil { t.Fatal("erwartet fehler") } content, err := os.ReadFile(filepath.Join(target, "bestehend")) if err != nil || string(content) != "original" { t.Fatalf("ziel bei fehlgeschlagenem restoreFn veraendert: %v %q", err, content) } } // TestAtomicRestore_ForceOverwritesNonEmptyTarget ist Nachweis, dass eine // BEWUSSTE Bestaetigung (force) bestehenden Inhalt ersetzen darf. func TestAtomicRestore_ForceOverwritesNonEmptyTarget(t *testing.T) { root := t.TempDir() target := filepath.Join(root, "ziel") logPath := filepath.Join(root, "restore.log") if err := os.MkdirAll(target, 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(target, "alt"), []byte("alt"), 0o600); err != nil { t.Fatal(err) } _, err := AtomicRestore(KindObjects, "snap-2", target, true, logPath, func(tempDir string) error { return os.WriteFile(filepath.Join(tempDir, "neu"), []byte("neu"), 0o600) }) if err != nil { t.Fatalf("atomicRestore mit force: %v", err) } if _, err := os.Stat(filepath.Join(target, "alt")); !os.IsNotExist(err) { t.Fatal("alter inhalt haette nach force-restore ersetzt sein muessen") } if _, err := os.Stat(filepath.Join(target, "neu")); err != nil { t.Fatalf("neuer inhalt fehlt: %v", err) } } // TestAtomicRestore_LogsCompleteEntry ist Pruefung 3: Protokolleintrag // ist vollstaendig und nachvollziehbar (Quelle, Zeitpunkt, Ergebnis) - // sowohl fuer Erfolg als auch fuer Abbruch, in derselben Datei. func TestAtomicRestore_LogsCompleteEntry(t *testing.T) { root := t.TempDir() logPath := filepath.Join(root, "restore.log") okTarget := filepath.Join(root, "ok-ziel") if _, err := AtomicRestore(KindDatabase, "gen-3", okTarget, false, logPath, func(tempDir string) error { return os.WriteFile(filepath.Join(tempDir, "x"), []byte("x"), 0o600) }); err != nil { t.Fatalf("erfolgreicher restore: %v", err) } blockedTarget := filepath.Join(root, "blockiert-ziel") if err := os.MkdirAll(blockedTarget, 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(blockedTarget, "bestehend"), []byte("y"), 0o600); err != nil { t.Fatal(err) } _, _ = AtomicRestore(KindObjects, "snap-3", blockedTarget, false, logPath, func(tempDir string) error { return nil }) entries := readLog(t, logPath) if len(entries) != 2 { t.Fatalf("erwartet 2 protokolleintraege, habe %d: %+v", len(entries), entries) } if entries[0].Source != "gen-3" || entries[0].Kind != KindDatabase || entries[0].Result != "ok" || entries[0].Timestamp.IsZero() { t.Fatalf("erster eintrag unvollstaendig: %+v", entries[0]) } if entries[1].Source != "snap-3" || entries[1].Kind != KindObjects || entries[1].Result == "" || entries[1].Timestamp.IsZero() { t.Fatalf("zweiter eintrag unvollstaendig: %+v", entries[1]) } }