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>
499 lines
17 KiB
Go
499 lines
17 KiB
Go
package verification
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/syncova/syncova/packages/backupengine"
|
|
"github.com/syncova/syncova/packages/platform/crypto"
|
|
"github.com/syncova/syncova/packages/repository"
|
|
)
|
|
|
|
// discardLogger unterdrueckt die Protokollausgabe der Tests.
|
|
func discardLogger() *slog.Logger {
|
|
return slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
}
|
|
|
|
// buildTestSecretStore erzeugt einen Schluesselspeicher.
|
|
func buildTestSecretStore(testInstance *testing.T) crypto.SecretStore {
|
|
testInstance.Helper()
|
|
|
|
builtStore, storeError := crypto.NewLocalSecretStore(
|
|
map[string][]byte{"v1": bytes.Repeat([]byte{0x77}, 32)}, "v1")
|
|
if storeError != nil {
|
|
testInstance.Fatalf("der Schluesselspeicher liess sich nicht bauen: %v", storeError)
|
|
}
|
|
|
|
return builtStore
|
|
}
|
|
|
|
// verificationFixture ist ein Repository mit einem Backup.
|
|
type verificationFixture struct {
|
|
// RepositoryPath ist der Pfad des Repositorys.
|
|
RepositoryPath string
|
|
// Repository ist das geoeffnete Repository.
|
|
Repository *repository.LocalRepository
|
|
// SecretStore entschluesselt die Datenschluessel.
|
|
SecretStore crypto.SecretStore
|
|
// BackupID ist die Kennung des Backups.
|
|
BackupID string
|
|
// SourceContents sind die gesicherten Inhalte je Pfad.
|
|
SourceContents map[string][]byte
|
|
}
|
|
|
|
// buildFixture legt ein Repository mit einem verschluesselten Backup an.
|
|
func buildFixture(testInstance *testing.T) *verificationFixture {
|
|
testInstance.Helper()
|
|
|
|
repositoryPath := filepath.Join(testInstance.TempDir(), "repository")
|
|
|
|
createdRepository, createError := repository.Create(context.Background(), repositoryPath,
|
|
repository.CreateOptions{Name: "Pruefung", CreatedByVersion: "test"}, discardLogger())
|
|
if createError != nil {
|
|
testInstance.Fatalf("das Repository liess sich nicht anlegen: %v", createError)
|
|
}
|
|
|
|
testInstance.Cleanup(func() { _ = createdRepository.Close() })
|
|
|
|
secretStore := buildTestSecretStore(testInstance)
|
|
engine := backupengine.NewEngine(createdRepository, secretStore, discardLogger())
|
|
|
|
sourceContents := map[string][]byte{
|
|
"dokumente/bericht.txt": []byte(strings.Repeat("Ein Bericht. ", 400)),
|
|
"dokumente/notiz.txt": []byte("Kurze Notiz."),
|
|
"bilder/foto.bin": buildIncompressibleData(48 << 10),
|
|
}
|
|
|
|
backupSources := []backupengine.BackupSource{
|
|
{Path: "bilder", EntryType: "directory", Mode: "0755"},
|
|
{Path: "dokumente", EntryType: "directory", Mode: "0755"},
|
|
}
|
|
|
|
for entryPath, entryContent := range sourceContents {
|
|
backupSources = append(backupSources, backupengine.BackupSource{
|
|
Path: entryPath,
|
|
EntryType: "file",
|
|
Mode: "0644",
|
|
Reader: bytes.NewReader(entryContent),
|
|
})
|
|
}
|
|
|
|
if _, backupError := engine.Backup(context.Background(), backupengine.BackupOptions{
|
|
BackupID: "pruef-1",
|
|
BackupType: repository.BackupTypeFull,
|
|
Source: repository.SourceInformation{SourceType: "filesystem", SourceID: "/test"},
|
|
CompressionLevel: backupengine.CompressionOff,
|
|
EncryptionEnabled: true,
|
|
}, backupSources); backupError != nil {
|
|
testInstance.Fatalf("das Backup liess sich nicht anlegen: %v", backupError)
|
|
}
|
|
|
|
return &verificationFixture{
|
|
RepositoryPath: repositoryPath,
|
|
Repository: createdRepository,
|
|
SecretStore: secretStore,
|
|
BackupID: "pruef-1",
|
|
SourceContents: sourceContents,
|
|
}
|
|
}
|
|
|
|
// buildIncompressibleData erzeugt reproduzierbare, kaum komprimierbare Daten.
|
|
func buildIncompressibleData(byteCount int) []byte {
|
|
generatedData := make([]byte, byteCount)
|
|
|
|
var generatorState uint64 = 0x9E3779B97F4A7C15
|
|
for byteIndex := range generatedData {
|
|
generatorState ^= generatorState << 13
|
|
generatorState ^= generatorState >> 7
|
|
generatorState ^= generatorState << 17
|
|
generatedData[byteIndex] = byte(generatorState)
|
|
}
|
|
|
|
return generatedData
|
|
}
|
|
|
|
// chunkPathOf bildet den Ablagepfad eines Blocks.
|
|
func chunkPathOf(repositoryPath string, chunkIdentifier string) string {
|
|
return filepath.Join(repositoryPath, "chunks",
|
|
chunkIdentifier[0:2], chunkIdentifier[2:4], chunkIdentifier)
|
|
}
|
|
|
|
// anyChunkIdentifier liefert eine beliebige Blockkennung des Backups.
|
|
func anyChunkIdentifier(testInstance *testing.T, fixture *verificationFixture) string {
|
|
testInstance.Helper()
|
|
|
|
backupManifest, readError := fixture.Repository.ReadManifest(context.Background(), fixture.BackupID)
|
|
if readError != nil {
|
|
testInstance.Fatalf("das Manifest liess sich nicht lesen: %v", readError)
|
|
}
|
|
|
|
for chunkIdentifier := range backupManifest.UniqueChunkIdentifiers() {
|
|
return chunkIdentifier
|
|
}
|
|
|
|
testInstance.Fatal("das Backup enthaelt keine Bloecke")
|
|
|
|
return ""
|
|
}
|
|
|
|
// findFinding sucht einen Befund anhand seines Codes.
|
|
func findFinding(report *Report, findingCode string) *Finding {
|
|
for findingIndex := range report.Findings {
|
|
if report.Findings[findingIndex].Code == findingCode {
|
|
return &report.Findings[findingIndex]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestVerifyIntactBackup prueft den Normalfall.
|
|
func TestVerifyIntactBackup(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
report, verifyError := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeChunkIntegrity)
|
|
if verifyError != nil {
|
|
testInstance.Fatalf("die Pruefung schlug fehl: %v", verifyError)
|
|
}
|
|
|
|
if !report.IsClean() {
|
|
testInstance.Fatalf("ein unversehrtes Backup wurde beanstandet: %+v", report.Findings)
|
|
}
|
|
|
|
if report.ChunksChecked == 0 {
|
|
testInstance.Error("es wurden keine Bloecke geprueft; die Pruefung ist ohne Aussage")
|
|
}
|
|
|
|
if report.BytesRead == 0 {
|
|
testInstance.Error("es wurden keine Bytes gelesen; die Inhaltspruefung fand nicht statt")
|
|
}
|
|
}
|
|
|
|
// TestVerifyDetectsCorruptedChunk ist der wichtigste Test dieser Datei.
|
|
//
|
|
// Ein Datentraeger kann Daten still verfaelschen. Nur eine Pruefung, die den
|
|
// Inhalt liest und neu hasht, deckt das auf.
|
|
func TestVerifyDetectsCorruptedChunk(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
chunkIdentifier := anyChunkIdentifier(testInstance, fixture)
|
|
chunkPath := chunkPathOf(fixture.RepositoryPath, chunkIdentifier)
|
|
|
|
originalData, readError := os.ReadFile(chunkPath)
|
|
if readError != nil {
|
|
testInstance.Fatalf("der Block liess sich nicht lesen: %v", readError)
|
|
}
|
|
|
|
// Ein einziges Bit kippen — die leiseste denkbare Beschaedigung.
|
|
corruptedData := make([]byte, len(originalData))
|
|
copy(corruptedData, originalData)
|
|
corruptedData[len(corruptedData)/2] ^= 0x01
|
|
|
|
if writeError := os.WriteFile(chunkPath, corruptedData, 0o600); writeError != nil {
|
|
testInstance.Fatalf("der Block liess sich nicht veraendern: %v", writeError)
|
|
}
|
|
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
// Die Existenzpruefung merkt nichts — der Block ist ja da.
|
|
presenceReport, _ := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeChunkPresence)
|
|
|
|
if !presenceReport.IsClean() {
|
|
testInstance.Error("die Existenzpruefung meldete einen Fehler, obwohl der Block vorhanden ist")
|
|
}
|
|
|
|
// Erst die Inhaltspruefung deckt es auf.
|
|
integrityReport, _ := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeChunkIntegrity)
|
|
|
|
if !integrityReport.HasCorruption() {
|
|
testInstance.Fatal("ein gekipptes Bit wurde nicht erkannt")
|
|
}
|
|
|
|
corruptionFinding := findFinding(integrityReport, "CHUNKS_CORRUPTED")
|
|
if corruptionFinding == nil {
|
|
testInstance.Fatalf("die Beschaedigung wurde nicht benannt: %+v", integrityReport.Findings)
|
|
}
|
|
|
|
// Der Befund muss das betroffene Objekt nennen.
|
|
if corruptionFinding.Detail == "" {
|
|
testInstance.Error("der Befund nennt kein betroffenes Objekt")
|
|
}
|
|
|
|
if !strings.Contains(integrityReport.Summary(), "BESCHAEDIGT") {
|
|
testInstance.Errorf("die Zusammenfassung beschoenigt den Befund: %q", integrityReport.Summary())
|
|
}
|
|
}
|
|
|
|
// TestVerifyDetectsMissingChunk prueft den fehlenden Block.
|
|
func TestVerifyDetectsMissingChunk(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
chunkIdentifier := anyChunkIdentifier(testInstance, fixture)
|
|
|
|
if removeError := os.Remove(chunkPathOf(fixture.RepositoryPath, chunkIdentifier)); removeError != nil {
|
|
testInstance.Fatalf("der Block liess sich nicht entfernen: %v", removeError)
|
|
}
|
|
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
report, _ := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeChunkPresence)
|
|
|
|
if report.IsClean() {
|
|
testInstance.Fatal("ein fehlender Block wurde nicht erkannt")
|
|
}
|
|
|
|
// Fehlend ist nicht beschaedigt: Der Block koennte anderswo noch existieren.
|
|
if report.HasCorruption() {
|
|
testInstance.Error("ein fehlender Block wurde als Beschaedigung gemeldet")
|
|
}
|
|
|
|
if findFinding(report, "CHUNKS_MISSING") == nil {
|
|
testInstance.Errorf("der fehlende Block wurde nicht benannt: %+v", report.Findings)
|
|
}
|
|
}
|
|
|
|
// TestManifestVerificationIsHonestAboutItsLimits prueft die schwache Aussage.
|
|
//
|
|
// Die Manifestpruefung stellt fest, dass das Backup abgeschlossen wurde — nicht,
|
|
// ob die Daten noch da sind. Das muss in der Zusammenfassung stehen.
|
|
func TestManifestVerificationIsHonestAboutItsLimits(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
// Alle Bloecke entfernen: Das Manifest bleibt stimmig, die Daten sind weg.
|
|
backupManifest, _ := fixture.Repository.ReadManifest(context.Background(), fixture.BackupID)
|
|
|
|
for chunkIdentifier := range backupManifest.UniqueChunkIdentifiers() {
|
|
_ = os.Remove(chunkPathOf(fixture.RepositoryPath, chunkIdentifier))
|
|
}
|
|
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
manifestReport, _ := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeManifest)
|
|
|
|
// Die Manifestpruefung findet nichts — sie hat die Bloecke nie angesehen.
|
|
if !manifestReport.IsClean() {
|
|
testInstance.Error("die Manifestpruefung beanstandete etwas, das sie nicht geprueft hat")
|
|
}
|
|
|
|
// Sie sagt aber, dass sie nichts ueber die Daten weiss.
|
|
if !strings.Contains(manifestReport.Summary(), "nicht geprueft") {
|
|
testInstance.Fatalf("die Manifestpruefung verschweigt ihre Grenze: %q", manifestReport.Summary())
|
|
}
|
|
|
|
// Und die Blockpruefung deckt den Verlust sofort auf.
|
|
presenceReport, _ := verifier.VerifyBackup(context.Background(), fixture.BackupID, TypeChunkPresence)
|
|
|
|
if presenceReport.IsClean() {
|
|
testInstance.Error("die Blockpruefung uebersah ein vollstaendig geleertes Repository")
|
|
}
|
|
}
|
|
|
|
// TestVerifyChainFindsBrokenParent prueft die Kettenpruefung.
|
|
func TestVerifyChainFindsBrokenParent(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
engine := backupengine.NewEngine(fixture.Repository, fixture.SecretStore, discardLogger())
|
|
|
|
// Eine Zusatzsicherung auf das vorhandene Backup.
|
|
if _, backupError := engine.Backup(context.Background(), backupengine.BackupOptions{
|
|
BackupID: "pruef-2",
|
|
BackupType: repository.BackupTypeIncremental,
|
|
ParentBackupID: fixture.BackupID,
|
|
Source: repository.SourceInformation{SourceType: "filesystem", SourceID: "/test"},
|
|
CompressionLevel: backupengine.CompressionOff,
|
|
EncryptionEnabled: true,
|
|
}, []backupengine.BackupSource{
|
|
{Path: "neu.txt", EntryType: "file", Mode: "0644", Reader: bytes.NewReader([]byte("neu"))},
|
|
}); backupError != nil {
|
|
testInstance.Fatalf("die Zusatzsicherung liess sich nicht anlegen: %v", backupError)
|
|
}
|
|
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
intactReport, _ := verifier.VerifyChain(context.Background(), "pruef-2")
|
|
|
|
if !intactReport.IsClean() {
|
|
testInstance.Fatalf("eine vollstaendige Kette wurde beanstandet: %+v", intactReport.Findings)
|
|
}
|
|
|
|
if intactReport.ChainLength != 2 {
|
|
testInstance.Errorf("die Kette wurde mit %d Backups gezaehlt, erwartet waren 2", intactReport.ChainLength)
|
|
}
|
|
|
|
// Das Elternbackup verschwindet.
|
|
if removeError := os.Remove(filepath.Join(fixture.RepositoryPath, "manifests",
|
|
fixture.BackupID+".manifest.json")); removeError != nil {
|
|
testInstance.Fatalf("das Elternmanifest liess sich nicht entfernen: %v", removeError)
|
|
}
|
|
|
|
brokenReport, _ := verifier.VerifyChain(context.Background(), "pruef-2")
|
|
|
|
if brokenReport.IsClean() {
|
|
testInstance.Fatal("eine unterbrochene Kette wurde nicht erkannt")
|
|
}
|
|
|
|
if findFinding(brokenReport, "CHAIN_BROKEN") == nil {
|
|
testInstance.Errorf("die Unterbrechung wurde nicht benannt: %+v", brokenReport.Findings)
|
|
}
|
|
}
|
|
|
|
// TestRestoreTestSucceedsOnIntactBackup prueft den Nachweis.
|
|
func TestRestoreTestSucceedsOnIntactBackup(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
tester := NewRestoreTester(fixture.Repository, fixture.SecretStore, discardLogger())
|
|
|
|
workingDirectory := testInstance.TempDir()
|
|
|
|
report, testError := tester.RunTest(context.Background(), RestoreTestOptions{
|
|
BackupID: fixture.BackupID,
|
|
WorkingDirectory: workingDirectory,
|
|
})
|
|
if testError != nil {
|
|
testInstance.Fatalf("der Test schlug fehl: %v", testError)
|
|
}
|
|
|
|
if !report.Succeeded() {
|
|
testInstance.Fatalf("ein unversehrtes Backup bestand den Test nicht: %+v", report.Findings)
|
|
}
|
|
|
|
if report.FilesCompared != len(fixture.SourceContents) {
|
|
testInstance.Errorf("es wurden %d Dateien verglichen, gesichert waren %d",
|
|
report.FilesCompared, len(fixture.SourceContents))
|
|
}
|
|
|
|
if report.FilesMismatched != 0 {
|
|
testInstance.Errorf("%d Dateien wichen ab", report.FilesMismatched)
|
|
}
|
|
}
|
|
|
|
// TestRestoreTestCleansUpAfterItself prueft das Aufraeumen.
|
|
//
|
|
// Ein Test, der Platz belegt, wird bald abgeschaltet.
|
|
func TestRestoreTestCleansUpAfterItself(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
tester := NewRestoreTester(fixture.Repository, fixture.SecretStore, discardLogger())
|
|
|
|
workingDirectory := testInstance.TempDir()
|
|
|
|
if _, testError := tester.RunTest(context.Background(), RestoreTestOptions{
|
|
BackupID: fixture.BackupID,
|
|
WorkingDirectory: workingDirectory,
|
|
}); testError != nil {
|
|
testInstance.Fatalf("der Test schlug fehl: %v", testError)
|
|
}
|
|
|
|
remainingEntries, readError := os.ReadDir(workingDirectory)
|
|
if readError != nil {
|
|
testInstance.Fatalf("das Arbeitsverzeichnis liess sich nicht lesen: %v", readError)
|
|
}
|
|
|
|
if len(remainingEntries) != 0 {
|
|
testInstance.Fatalf("der Test hinterliess %d Objekte im Arbeitsverzeichnis", len(remainingEntries))
|
|
}
|
|
}
|
|
|
|
// TestRestoreTestDetectsCorruption ist der Nachweis, dass der Test etwas taugt.
|
|
//
|
|
// Er muss aufdecken, was die Blockpruefung erkennt — und mehr: Fehler in
|
|
// Entschluesselung, Dekompression und Zusammensetzung fallen nur hier auf.
|
|
func TestRestoreTestDetectsCorruption(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
chunkIdentifier := anyChunkIdentifier(testInstance, fixture)
|
|
chunkPath := chunkPathOf(fixture.RepositoryPath, chunkIdentifier)
|
|
|
|
originalData, _ := os.ReadFile(chunkPath)
|
|
|
|
corruptedData := make([]byte, len(originalData))
|
|
copy(corruptedData, originalData)
|
|
corruptedData[len(corruptedData)/2] ^= 0xFF
|
|
|
|
if writeError := os.WriteFile(chunkPath, corruptedData, 0o600); writeError != nil {
|
|
testInstance.Fatalf("der Block liess sich nicht veraendern: %v", writeError)
|
|
}
|
|
|
|
tester := NewRestoreTester(fixture.Repository, fixture.SecretStore, discardLogger())
|
|
|
|
report, testError := tester.RunTest(context.Background(), RestoreTestOptions{
|
|
BackupID: fixture.BackupID,
|
|
WorkingDirectory: testInstance.TempDir(),
|
|
})
|
|
if testError != nil {
|
|
testInstance.Fatalf("der Test brach ab, statt zu berichten: %v", testError)
|
|
}
|
|
|
|
if report.Succeeded() {
|
|
testInstance.Fatal("ein beschaedigtes Backup bestand den Wiederherstellungstest")
|
|
}
|
|
}
|
|
|
|
// TestPartialRestoreTestSaysSo prueft die Ehrlichkeit des Teiltests.
|
|
//
|
|
// Ein Teiltest belegt nicht die Wiederherstellbarkeit des gesamten Backups.
|
|
func TestPartialRestoreTestSaysSo(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
|
|
tester := NewRestoreTester(fixture.Repository, fixture.SecretStore, discardLogger())
|
|
|
|
report, testError := tester.RunTest(context.Background(), RestoreTestOptions{
|
|
BackupID: fixture.BackupID,
|
|
PathPrefix: "dokumente",
|
|
WorkingDirectory: testInstance.TempDir(),
|
|
})
|
|
if testError != nil {
|
|
testInstance.Fatalf("der Teiltest schlug fehl: %v", testError)
|
|
}
|
|
|
|
if !report.Succeeded() {
|
|
testInstance.Fatalf("der Teiltest schlug fehl: %+v", report.Findings)
|
|
}
|
|
|
|
if !report.WasPartialTest {
|
|
testInstance.Error("der Teiltest wurde nicht als solcher gefuehrt")
|
|
}
|
|
|
|
partialFinding := findFinding(&report.Report, "PARTIAL_TEST")
|
|
if partialFinding == nil {
|
|
testInstance.Fatalf("der Teiltest wurde nicht ausgewiesen: %+v", report.Findings)
|
|
}
|
|
|
|
if !strings.Contains(partialFinding.Message, "nicht die Wiederherstellbarkeit des gesamten") {
|
|
testInstance.Errorf("der Hinweis ist zu schwach: %q", partialFinding.Message)
|
|
}
|
|
|
|
// Nur die zwei Dateien unter dokumente/ wurden verglichen.
|
|
if report.FilesCompared != 2 {
|
|
testInstance.Errorf("es wurden %d Dateien verglichen, erwartet waren 2", report.FilesCompared)
|
|
}
|
|
}
|
|
|
|
// TestVerifyUnknownBackupReportsInsteadOfFailing prueft die Fehlerbehandlung.
|
|
func TestVerifyUnknownBackupReportsInsteadOfFailing(testInstance *testing.T) {
|
|
fixture := buildFixture(testInstance)
|
|
verifier := NewVerifier(fixture.Repository)
|
|
|
|
report, verifyError := verifier.VerifyBackup(context.Background(), "gibt-es-nicht", TypeChunkIntegrity)
|
|
|
|
// Ein unbekanntes Backup ist ein Befund, kein Programmfehler.
|
|
if verifyError != nil {
|
|
testInstance.Fatalf("die Pruefung brach ab, statt zu berichten: %v", verifyError)
|
|
}
|
|
|
|
if report.IsClean() {
|
|
testInstance.Fatal("ein unbekanntes Backup wurde als einwandfrei gemeldet")
|
|
}
|
|
|
|
if findFinding(report, "MANIFEST_UNREADABLE") == nil {
|
|
testInstance.Errorf("das fehlende Manifest wurde nicht benannt: %+v", report.Findings)
|
|
}
|
|
}
|