package repository import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" ) // --------------------------------------------------------------------------- // Integritätslauf // --------------------------------------------------------------------------- func TestScanReportsHealthyRepository(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Eins"), []byte("Zwei")}) writeTestBackup(testInstance, testRepository, "backup-2", [][]byte{[]byte("Drei")}) scanReport, scanError := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}) if scanError != nil { testInstance.Fatalf("der Integritätslauf schlug fehl: %v", scanError) } if !scanReport.IsHealthy() { testInstance.Fatalf("das Repository soll einwandfrei sein: %s", scanReport.Summary()) } if scanReport.BackupsChecked != 2 || scanReport.BackupsHealthy != 2 { testInstance.Errorf("es sollen 2 Backups geprüft und einwandfrei sein, waren %d/%d", scanReport.BackupsHealthy, scanReport.BackupsChecked) } } func TestScanDetectsCorruptedChunk(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Wird beschädigt")}) // Ein Chunk wird auf dem Datenträger verfälscht. chunkIdentifier := manifest.Entries[0].Chunks[0].Identifier chunkFilePath, _ := testRepository.chunkPath(chunkIdentifier) _ = os.Chmod(chunkFilePath, 0o600) _ = os.WriteFile(chunkFilePath, []byte("voellig andere daten"), 0o600) scanReport, _ := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}) if scanReport.IsHealthy() { testInstance.Fatal("ein beschädigtes Repository darf nicht als einwandfrei gelten") } if scanReport.CorruptedChunks != 1 { testInstance.Errorf("es soll 1 beschädigter Chunk gemeldet werden, waren %d", scanReport.CorruptedChunks) } // Das betroffene Backup muss namentlich benannt werden. if len(scanReport.AffectedBackupIDs) != 1 || scanReport.AffectedBackupIDs[0] != "backup-1" { testInstance.Errorf("das betroffene Backup fehlt im Bericht: %v", scanReport.AffectedBackupIDs) } // Die Zusammenfassung darf das Problem nicht beschönigen. if !strings.Contains(scanReport.Summary(), "NICHT vollständig") { testInstance.Errorf("die Zusammenfassung ist zu harmlos: %s", scanReport.Summary()) } } func TestScanDetectsMissingChunk(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Wird geloescht")}) chunkIdentifier := manifest.Entries[0].Chunks[0].Identifier chunkFilePath, _ := testRepository.chunkPath(chunkIdentifier) if removeError := os.Remove(chunkFilePath); removeError != nil { testInstance.Fatalf("der Chunk konnte nicht entfernt werden: %v", removeError) } scanReport, _ := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}) if scanReport.MissingChunks != 1 { testInstance.Errorf("es soll 1 fehlender Chunk gemeldet werden, waren %d", scanReport.MissingChunks) } if scanReport.IsHealthy() { testInstance.Fatal("ein Backup mit fehlendem Chunk darf nicht als einwandfrei gelten") } } func TestScanWithoutContentVerificationOnlyChecksPresence(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Inhalt")}) // Der Inhalt wird verfälscht, die Datei bleibt aber vorhanden. chunkIdentifier := manifest.Entries[0].Chunks[0].Identifier chunkFilePath, _ := testRepository.chunkPath(chunkIdentifier) _ = os.Chmod(chunkFilePath, 0o600) _ = os.WriteFile(chunkFilePath, []byte("andere daten"), 0o600) // Der schnelle Lauf prüft nur die Anwesenheit und findet die Beschädigung // erwartungsgemäß nicht. quickReport, _ := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: false}) if quickReport.CorruptedChunks != 0 { testInstance.Error("der schnelle Lauf soll keine Inhaltsprüfung vornehmen") } // Der vollständige Lauf findet sie. fullReport, _ := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}) if fullReport.CorruptedChunks != 1 { testInstance.Errorf("der vollständige Lauf soll die Beschädigung finden, waren %d", fullReport.CorruptedChunks) } } func TestScanFindsOrphanedChunks(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Benutzt")}) // Ein Chunk ohne zugehöriges Backup, wie er nach einem Abbruch zurückbleibt. _, _, _ = testRepository.writeChunk(context.Background(), []byte("Von niemandem benutzt")) scanReport, _ := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}) if scanReport.OrphanedChunks != 1 { testInstance.Errorf("es soll 1 verwaister Chunk gemeldet werden, waren %d", scanReport.OrphanedChunks) } // Ein verwaister Chunk ist kein Datenverlust, das Repository bleibt gesund. if !scanReport.IsHealthy() { testInstance.Error("verwaiste Chunks dürfen das Repository nicht als beschädigt gelten lassen") } } func TestScanReportIsPersistedInRepository(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Eins")}) if _, scanError := testRepository.Scan(context.Background(), ScanOptions{VerifyChunkContents: true}); scanError != nil { testInstance.Fatalf("der Integritätslauf schlug fehl: %v", scanError) } // Der Bericht gehört ins Repository, damit nach einem Wiederaufbau // erkennbar bleibt, wann zuletzt geprüft wurde. verificationDirectory := filepath.Join(testRepository.RootPath(), directoryVerification) directoryEntries, readError := os.ReadDir(verificationDirectory) if readError != nil { testInstance.Fatalf("das Prüfverzeichnis konnte nicht gelesen werden: %v", readError) } if len(directoryEntries) == 0 { testInstance.Fatal("es wurde kein Prüfbericht abgelegt") } } // --------------------------------------------------------------------------- // Bereinigung // --------------------------------------------------------------------------- func TestPruneRemovesOnlyOrphanedChunks(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Benutzt")}) usedIdentifier := manifest.Entries[0].Chunks[0].Identifier orphanedIdentifier, _, _ := testRepository.writeChunk(context.Background(), []byte("Verwaist")) removedCount, _, pruneError := testRepository.PruneOrphanedChunks(context.Background(), false) if pruneError != nil { testInstance.Fatalf("die Bereinigung schlug fehl: %v", pruneError) } if removedCount != 1 { testInstance.Errorf("es soll 1 Chunk entfernt werden, waren %d", removedCount) } // Der benutzte Chunk muss erhalten bleiben. if _, readError := testRepository.ReadChunk(context.Background(), usedIdentifier); readError != nil { testInstance.Fatalf("ein benutzter Chunk wurde entfernt: %v", readError) } // Der verwaiste ist weg. if _, readError := testRepository.ReadChunk(context.Background(), orphanedIdentifier); !errors.Is(readError, ErrChunkNotFound) { testInstance.Error("der verwaiste Chunk wurde nicht entfernt") } } func TestPruneDryRunChangesNothing(testInstance *testing.T) { testRepository := newTestRepository(testInstance) orphanedIdentifier, _, _ := testRepository.writeChunk(context.Background(), []byte("Verwaist")) removedCount, _, _ := testRepository.PruneOrphanedChunks(context.Background(), true) if removedCount != 1 { testInstance.Errorf("die Simulation soll 1 Chunk melden, waren %d", removedCount) } // In der Simulation darf nichts verschwinden. if _, readError := testRepository.ReadChunk(context.Background(), orphanedIdentifier); readError != nil { testInstance.Fatalf("die Simulation hat Daten entfernt: %v", readError) } } func TestPruneRefusesWhenManifestIsUnreadable(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Daten")}) // Ein unlesbares Manifest bedeutet: die Menge der benötigten Chunks ist unbekannt. manifestFilePath := testRepository.manifestPath("backup-1") _ = os.Chmod(manifestFilePath, 0o600) _ = os.WriteFile(manifestFilePath, []byte("kaputt"), 0o600) // Eine Bereinigung auf unvollständiger Grundlage könnte Daten vernichten. if _, _, pruneError := testRepository.PruneOrphanedChunks(context.Background(), false); pruneError == nil { testInstance.Fatal("die Bereinigung muss bei einem unlesbaren Manifest abbrechen") } } // --------------------------------------------------------------------------- // Sperre und Neustart // --------------------------------------------------------------------------- func TestSecondWriterIsRejectedWhileRepositoryIsLocked(testInstance *testing.T) { testRepository := newTestRepository(testInstance) // Ein zweiter Schreiber würde Katalog und Manifeste durcheinanderbringen. _, openError := Open(context.Background(), testRepository.RootPath(), OpenOptions{}, newTestLogger()) if !errors.Is(openError, ErrRepositoryLocked) { testInstance.Fatalf("ein zweiter Schreiber muss abgelehnt werden, war: %v", openError) } } func TestReadOnlyAccessIsPossibleWhileLocked(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Daten")}) // Eine Wiederherstellung muss auch während eines laufenden Backups möglich sein. readerRepository, openError := Open(context.Background(), testRepository.RootPath(), OpenOptions{ReadOnly: true}, newTestLogger()) if openError != nil { testInstance.Fatalf("der lesende Zugriff wurde abgelehnt: %v", openError) } defer func() { _ = readerRepository.Close() }() if _, readError := readerRepository.ReadManifest(context.Background(), "backup-1"); readError != nil { testInstance.Fatalf("das Manifest konnte lesend nicht geöffnet werden: %v", readError) } } func TestReadOnlyRepositoryRefusesToWrite(testInstance *testing.T) { testRepository := newTestRepository(testInstance) repositoryPath := testRepository.RootPath() _ = testRepository.Close() readerRepository, _ := Open(context.Background(), repositoryPath, OpenOptions{ReadOnly: true}, newTestLogger()) defer func() { _ = readerRepository.Close() }() // Ohne Sperre darf nicht geschrieben werden. if _, beginError := readerRepository.BeginBackup(context.Background(), "neu"); beginError == nil { testInstance.Fatal("ein nur lesend geöffnetes Repository darf nicht beschrieben werden") } } func TestRepositorySurvivesReopen(testInstance *testing.T) { testRepository := newTestRepository(testInstance) repositoryPath := testRepository.RootPath() writeTestBackup(testInstance, testRepository, "vor-neustart", [][]byte{[]byte("Bestandsdaten")}) // Ein Neustart des Dienstes: schließen und erneut öffnen. if closeError := testRepository.Close(); closeError != nil { testInstance.Fatalf("das Repository konnte nicht geschlossen werden: %v", closeError) } reopenedRepository, openError := Open(context.Background(), repositoryPath, OpenOptions{}, newTestLogger()) if openError != nil { testInstance.Fatalf("das Repository konnte nach dem Neustart nicht geöffnet werden: %v", openError) } defer func() { _ = reopenedRepository.Close() }() if _, readError := reopenedRepository.ReadManifest(context.Background(), "vor-neustart"); readError != nil { testInstance.Fatalf("das Backup überstand den Neustart nicht: %v", readError) } // Nach dem Neustart muss weitergeschrieben werden können. writeTestBackup(testInstance, reopenedRepository, "nach-neustart", [][]byte{[]byte("Neue Daten")}) catalogEntries, _ := reopenedRepository.ListBackups(context.Background()) if len(catalogEntries) != 2 { testInstance.Fatalf("es sollen 2 Backups vorliegen, waren %d", len(catalogEntries)) } } func TestBreakLockAllowsRecoveryAfterCrash(testInstance *testing.T) { testRepository := newTestRepository(testInstance) repositoryPath := testRepository.RootPath() // Ein Absturz hinterlässt die Sperrdatei, ohne dass ein Prozess läuft. // Der Prozess wird hier nicht sauber geschlossen, die Sperre bleibt liegen. testRepository.lockHandle = nil if _, openError := Open(context.Background(), repositoryPath, OpenOptions{}, newTestLogger()); !errors.Is(openError, ErrRepositoryLocked) { testInstance.Fatalf("die zurückgebliebene Sperre soll den Zugriff verhindern, war: %v", openError) } // Erst der ausdrückliche Eingriff gibt das Repository frei. if breakError := BreakLock(repositoryPath); breakError != nil { testInstance.Fatalf("die Sperre konnte nicht gelöst werden: %v", breakError) } recoveredRepository, openError := Open(context.Background(), repositoryPath, OpenOptions{}, newTestLogger()) if openError != nil { testInstance.Fatalf("das Repository konnte nach dem Lösen der Sperre nicht geöffnet werden: %v", openError) } _ = recoveredRepository.Close() } // --------------------------------------------------------------------------- // Unterbrochener Schreibvorgang // --------------------------------------------------------------------------- func TestInterruptedSessionLeavesNoVisibleBackup(testInstance *testing.T) { testRepository := newTestRepository(testInstance) repositoryPath := testRepository.RootPath() // Eine Session wird geöffnet und Daten geschrieben, aber nie abgeschlossen. backupWriter, _ := testRepository.BeginBackup(context.Background(), "abgestuerzt") _, _, _ = backupWriter.WriteChunk(context.Background(), []byte("Daten vor dem Absturz")) // Der Absturz: kein Commit, kein Abort, das Journal bleibt liegen. testRepository.lockHandle = nil _ = BreakLock(repositoryPath) recoveredRepository, openError := Open(context.Background(), repositoryPath, OpenOptions{}, newTestLogger()) if openError != nil { testInstance.Fatalf("das Repository konnte nicht geöffnet werden: %v", openError) } defer func() { _ = recoveredRepository.Close() }() // Ein unvollständiges Backup darf nicht sichtbar sein. if _, readError := recoveredRepository.ReadManifest(context.Background(), "abgestuerzt"); !errors.Is(readError, ErrBackupNotFound) { testInstance.Fatalf("ein abgestürzter Lauf darf kein Backup hinterlassen, war: %v", readError) } // Der Abbruch muss aber auffindbar sein und darf nicht still verschwinden. staleSessions, staleError := recoveredRepository.FindStaleSessions(context.Background()) if staleError != nil { testInstance.Fatalf("die offenen Sessions konnten nicht ermittelt werden: %v", staleError) } if len(staleSessions) != 1 { testInstance.Fatalf("es soll 1 zurückgebliebene Session gefunden werden, waren %d", len(staleSessions)) } if staleSessions[0].BackupID != "abgestuerzt" || staleSessions[0].HasManifest { testInstance.Errorf("die zurückgebliebene Session wurde falsch beschrieben: %+v", staleSessions[0]) } // Nach dem Aufräumen ist sie verschwunden. if cleanupError := recoveredRepository.CleanupStaleSession(context.Background(), staleSessions[0].SessionID); cleanupError != nil { testInstance.Fatalf("die Session konnte nicht aufgeräumt werden: %v", cleanupError) } remainingSessions, _ := recoveredRepository.FindStaleSessions(context.Background()) if len(remainingSessions) != 0 { testInstance.Errorf("nach dem Aufräumen soll keine Session zurückbleiben, waren %d", len(remainingSessions)) } } func TestChunksOfInterruptedSessionAreReused(testInstance *testing.T) { testRepository := newTestRepository(testInstance) sharedContent := []byte("Dieser Block wurde vor dem Absturz geschrieben.") // Ein abgebrochener Lauf hinterlässt seine Chunks. firstWriter, _ := testRepository.BeginBackup(context.Background(), "erster-versuch") _, _, _ = firstWriter.WriteChunk(context.Background(), sharedContent) _ = firstWriter.Abort(context.Background()) // Der zweite Anlauf findet sie vor und muss sie nicht erneut übertragen. secondWriter, _ := testRepository.BeginBackup(context.Background(), "zweiter-versuch") _, wasNew, _ := secondWriter.WriteChunk(context.Background(), sharedContent) if wasNew { testInstance.Error("bereits übertragene Blöcke sollen nach einem Abbruch wiederverwendet werden") } _ = secondWriter.Abort(context.Background()) } // --------------------------------------------------------------------------- // Aufbewahrungsschutz // --------------------------------------------------------------------------- func TestHardenedRepositoryProtectsBackupFromDeletion(testInstance *testing.T) { hardenedRepository, createError := Create(context.Background(), testInstance.TempDir(), CreateOptions{ Name: "Gehärtetes Repository", Kind: KindHardenedLinux, CreatedByVersion: "test", }, newTestLogger()) if createError != nil { testInstance.Fatalf("das Repository konnte nicht angelegt werden: %v", createError) } defer func() { _ = hardenedRepository.Close() }() // Der Löschschutz greift bis in das Aufräumen des Tests hinein: Ohne diese // Freigabe scheitert t.TempDir am geschützten Manifest. testInstance.Cleanup(func() { releaseAllImmutableFlagsForTest(testInstance, hardenedRepository.RootPath()) }) // Ein gehärtetes Repository ist immer unveränderlich. if !hardenedRepository.Descriptor().Immutable { testInstance.Fatal("ein gehärtetes Repository muss unveränderlich sein") } writeTestBackup(testInstance, hardenedRepository, "geschuetzt", [][]byte{[]byte("Wichtige Daten")}) manifest, _ := hardenedRepository.ReadManifest(context.Background(), "geschuetzt") if manifest.ImmutableUntil == nil { testInstance.Fatal("dem Backup fehlt die Aufbewahrungsfrist") } // Das Löschen muss scheitern, solange die Frist läuft. deleteError := hardenedRepository.DeleteBackup(context.Background(), "geschuetzt") if !errors.Is(deleteError, ErrRetentionLocked) { testInstance.Fatalf("ein geschütztes Backup darf nicht gelöscht werden, war: %v", deleteError) } // Das Backup ist danach unverändert vorhanden. if _, readError := hardenedRepository.ReadManifest(context.Background(), "geschuetzt"); readError != nil { testInstance.Fatalf("das geschützte Backup wurde beschädigt: %v", readError) } } func TestDeleteRemovesBackupAfterRetentionExpired(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "abgelaufen", [][]byte{[]byte("Alte Daten")}) // Ein gewöhnliches Repository setzt keine Frist; das Löschen ist erlaubt. if deleteError := testRepository.DeleteBackup(context.Background(), "abgelaufen"); deleteError != nil { testInstance.Fatalf("das Backup konnte nicht gelöscht werden: %v", deleteError) } if _, readError := testRepository.ReadManifest(context.Background(), "abgelaufen"); !errors.Is(readError, ErrBackupNotFound) { testInstance.Fatalf("das Backup ist noch vorhanden, war: %v", readError) } // Der Katalog folgt der Löschung. catalogEntries, _ := testRepository.ListBackups(context.Background()) if len(catalogEntries) != 0 { testInstance.Errorf("der Katalog soll leer sein, enthielt %d Einträge", len(catalogEntries)) } } func TestRetentionLockExpires(testInstance *testing.T) { testRepository := newTestRepository(testInstance) backupWriter, _ := testRepository.BeginBackup(context.Background(), "kurze-frist") _, _, _ = backupWriter.WriteChunk(context.Background(), []byte("Daten")) // Eine bereits abgelaufene Frist. expiredRetention := time.Now().Add(-time.Hour) _ = backupWriter.Commit(context.Background(), &Manifest{ Source: SourceInformation{SourceType: "filesystem"}, ImmutableUntil: &expiredRetention, }) // Nach Ablauf der Frist darf gelöscht werden. if deleteError := testRepository.DeleteBackup(context.Background(), "kurze-frist"); deleteError != nil { testInstance.Fatalf("nach Ablauf der Frist muss das Löschen möglich sein: %v", deleteError) } } // --------------------------------------------------------------------------- // Zustand // --------------------------------------------------------------------------- func TestHealthReportsUsableRepository(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Daten")}) healthReport, healthError := testRepository.Health(context.Background()) if healthError != nil { testInstance.Fatalf("der Zustand konnte nicht ermittelt werden: %v", healthError) } // Der Status haengt vom tatsaechlichen Fuellstand des Testrechners ab. // Entscheidend ist hier, dass das Repository erreichbar und beschreibbar // ist; die Schwellenbewertung wird gesondert geprueft. if healthReport.Status == HealthStatusCritical && healthReport.CapacityBytes == 0 { testInstance.Errorf("das Repository soll erreichbar sein, war %q: %s", healthReport.Status, healthReport.Message) } if healthReport.BackupCount != 1 { testInstance.Errorf("es soll 1 Backup gezählt werden, waren %d", healthReport.BackupCount) } // Ohne Kapazitätsangabe wäre die Kapazitätswarnung wirkungslos. if healthReport.CapacityBytes <= 0 { testInstance.Error("die Kapazität konnte nicht ermittelt werden") } } func TestHealthDetectsUnreachableRepository(testInstance *testing.T) { testRepository := newTestRepository(testInstance) // Der Descriptor verschwindet, etwa weil der Datenträger nicht mehr eingebunden ist. if removeError := os.Remove(testRepository.descriptorPath()); removeError != nil { testInstance.Fatalf("der Descriptor konnte nicht entfernt werden: %v", removeError) } healthReport, _ := testRepository.Health(context.Background()) if healthReport.Status != HealthStatusCritical { testInstance.Fatalf("ein unerreichbares Repository muss als kritisch gelten, war %q", healthReport.Status) } // Der Anwender braucht eine Handlungsempfehlung, keinen Fehlercode. if healthReport.RecommendedAction == "" { testInstance.Error("es fehlt eine Handlungsempfehlung") } } // --------------------------------------------------------------------------- // Backup-Ketten // --------------------------------------------------------------------------- func TestValidateChainDetectsBrokenChain(testInstance *testing.T) { testRepository := newTestRepository(testInstance) // Eine Kette aus Vollsicherung und zwei Zusatzsicherungen. writeChainBackup(testInstance, testRepository, "voll", "kette-1", "", BackupTypeFull) writeChainBackup(testInstance, testRepository, "inkrement-1", "kette-1", "voll", BackupTypeIncremental) writeChainBackup(testInstance, testRepository, "inkrement-2", "kette-1", "inkrement-1", BackupTypeIncremental) catalog, _ := testRepository.Catalog(context.Background()) validationResult := catalog.ValidateChain("kette-1") if !validationResult.IsValid { testInstance.Fatalf("die vollständige Kette soll gültig sein: %s", validationResult.Message) } // Das mittlere Glied fällt aus. if deleteError := testRepository.DeleteBackup(context.Background(), "inkrement-1"); deleteError != nil { testInstance.Fatalf("das Backup konnte nicht gelöscht werden: %v", deleteError) } brokenCatalog, _ := testRepository.Catalog(context.Background()) brokenResult := brokenCatalog.ValidateChain("kette-1") // Eine unterbrochene Kette darf niemals als in Ordnung erscheinen. if brokenResult.IsValid { testInstance.Fatal("die unterbrochene Kette wurde als gültig gemeldet") } if len(brokenResult.AffectedBackupIDs) == 0 { testInstance.Error("die betroffenen Wiederherstellungspunkte wurden nicht benannt") } } // writeChainBackup schreibt ein Backup als Teil einer Kette. func writeChainBackup(testInstance *testing.T, testRepository *LocalRepository, backupID string, chainID string, parentID string, backupType BackupType) { testInstance.Helper() backupWriter, beginError := testRepository.BeginBackup(context.Background(), backupID) if beginError != nil { testInstance.Fatalf("die Schreibsession konnte nicht geöffnet werden: %v", beginError) } chunkReference, _, _ := backupWriter.WriteChunk(context.Background(), []byte("Daten von "+backupID)) commitError := backupWriter.Commit(context.Background(), &Manifest{ ChainID: chainID, ParentBackupID: parentID, BackupType: backupType, Source: SourceInformation{SourceType: "filesystem", SourceID: "quelle-1"}, Entries: []ManifestEntry{{ Path: "/daten.bin", EntryType: "file", Chunks: []ChunkReference{chunkReference}, }}, }) if commitError != nil { testInstance.Fatalf("das Backup konnte nicht abgeschlossen werden: %v", commitError) } } func TestEvaluateCapacityThresholds(testInstance *testing.T) { capacityCases := []struct { usedPercentage float64 expectedStatus HealthStatus }{ {0, HealthStatusHealthy}, {50, HealthStatusHealthy}, {79.9, HealthStatusHealthy}, {80, HealthStatusWarning}, {85, HealthStatusWarning}, {89.9, HealthStatusWarning}, {90, HealthStatusCritical}, {99.9, HealthStatusCritical}, } for _, capacityCase := range capacityCases { actualStatus, statusMessage, recommendedAction := evaluateCapacity(capacityCase.usedPercentage) if actualStatus != capacityCase.expectedStatus { testInstance.Errorf("bei %.1f %% Belegung soll der Status %q sein, war %q", capacityCase.usedPercentage, capacityCase.expectedStatus, actualStatus) } // Ein Problem ohne Handlungsempfehlung laesst den Anwender ratlos zurueck. if actualStatus != HealthStatusHealthy && recommendedAction == "" { testInstance.Errorf("bei %.1f %% Belegung fehlt die Handlungsempfehlung", capacityCase.usedPercentage) } if statusMessage == "" { testInstance.Errorf("bei %.1f %% Belegung fehlt die Meldung", capacityCase.usedPercentage) } } }