package repository import ( "bytes" "context" "errors" "os" "testing" "github.com/syncova/syncova/packages/backupformat" ) func TestExportAndImportRoundTrip(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) originalManifest := writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{ []byte("Erster Block"), []byte("Zweiter Block"), []byte("Dritter Block"), }) // Export in einen portablen Container. var containerBuffer bytes.Buffer exportedBytes, exportError := sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") if exportError != nil { testInstance.Fatalf("der Export schlug fehl: %v", exportError) } if exportedBytes == 0 { testInstance.Fatal("der Container ist leer") } // Import in ein völlig anderes Repository - so wie eine Übertragung an // einen zweiten Standort abliefe. targetRepository := newTestRepository(testInstance) importResult, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(containerBuffer.Bytes())) if importError != nil { testInstance.Fatalf("der Import schlug fehl: %v", importError) } if importResult.BackupID != "backup-1" { testInstance.Errorf("die Backup-Kennung weicht ab: %q", importResult.BackupID) } if importResult.ChunksImported != 3 { testInstance.Errorf("es sollen 3 Chunks übernommen werden, waren %d", importResult.ChunksImported) } // Das eingelesene Manifest muss dem ursprünglichen entsprechen. importedManifest, readError := targetRepository.ReadManifest(context.Background(), "backup-1") if readError != nil { testInstance.Fatalf("das eingelesene Manifest ist nicht lesbar: %v", readError) } if importedManifest.ContentHash != originalManifest.ContentHash { testInstance.Error("die Prüfsumme des Manifests hat die Übertragung nicht überstanden") } // Die Daten selbst müssen bitgenau angekommen sein. for _, chunkReference := range originalManifest.Entries[0].Chunks { originalData, _ := sourceRepository.ReadChunk(context.Background(), chunkReference.Identifier) importedData, chunkError := targetRepository.ReadChunk(context.Background(), chunkReference.Identifier) if chunkError != nil { testInstance.Fatalf("ein Chunk fehlt nach dem Import: %v", chunkError) } if !bytes.Equal(originalData, importedData) { testInstance.Fatal("der Inhalt eines Chunks weicht nach der Übertragung ab") } } // Das Backup muss im Zielkatalog auftauchen. catalogEntries, _ := targetRepository.ListBackups(context.Background()) if len(catalogEntries) != 1 { testInstance.Fatalf("das Ziel-Repository soll 1 Backup führen, waren %d", len(catalogEntries)) } } func TestImportDeduplicatesAgainstExistingChunks(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) sharedContent := []byte("Dieser Block existiert bereits im Ziel.") writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{sharedContent, []byte("Nur hier")}) var containerBuffer bytes.Buffer _, _ = sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") // Das Ziel kennt einen der Blöcke bereits. targetRepository := newTestRepository(testInstance) _, _, _ = targetRepository.writeChunk(context.Background(), sharedContent) importResult, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(containerBuffer.Bytes())) if importError != nil { testInstance.Fatalf("der Import schlug fehl: %v", importError) } // Ein bereits vorhandener Block wird nicht erneut geschrieben. if importResult.ChunksAlreadyPresent != 1 { testInstance.Errorf("es soll 1 Block bereits vorhanden sein, waren %d", importResult.ChunksAlreadyPresent) } if importResult.ChunksImported != 1 { testInstance.Errorf("es soll 1 Block übernommen werden, waren %d", importResult.ChunksImported) } } func TestImportRejectsTruncatedContainer(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{[]byte("Daten")}) var containerBuffer bytes.Buffer _, _ = sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") // Eine abgebrochene Übertragung liefert einen unvollständigen Container. containerBytes := containerBuffer.Bytes() truncatedContainer := containerBytes[:len(containerBytes)-30] targetRepository := newTestRepository(testInstance) _, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(truncatedContainer)) if importError == nil { testInstance.Fatal("ein abgeschnittener Container darf nicht eingelesen werden") } // Es darf kein sichtbares Backup entstanden sein. if _, readError := targetRepository.ReadManifest(context.Background(), "backup-1"); !errors.Is(readError, ErrBackupNotFound) { testInstance.Fatalf("ein unvollständiger Import darf kein Backup hinterlassen, war: %v", readError) } } func TestImportRejectsCorruptedContainer(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{[]byte("Originaldaten")}) var containerBuffer bytes.Buffer _, _ = sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") // Ein Übertragungsfehler verfälscht ein Byte im Datenbereich. tamperedContainer := bytes.Clone(containerBuffer.Bytes()) dataPosition := bytes.Index(tamperedContainer, []byte("Originaldaten")) if dataPosition < 0 { testInstance.Fatal("der Datenbereich wurde nicht gefunden") } tamperedContainer[dataPosition] = 'X' targetRepository := newTestRepository(testInstance) _, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(tamperedContainer)) if importError == nil { testInstance.Fatal("ein verfälschter Container darf nicht eingelesen werden") } if _, readError := targetRepository.ReadManifest(context.Background(), "backup-1"); !errors.Is(readError, ErrBackupNotFound) { testInstance.Fatal("ein fehlgeschlagener Import darf kein Backup hinterlassen") } } func TestExportRefusesWhenChunkIsMissing(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Daten")}) // Ein Chunk verschwindet vom Datenträger. chunkFilePath, _ := testRepository.chunkPath(manifest.Entries[0].Chunks[0].Identifier) _ = os.Remove(chunkFilePath) // Ein Container ohne alle Blöcke wäre unbrauchbar und darf nicht entstehen. var containerBuffer bytes.Buffer _, exportError := testRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") if !errors.Is(exportError, ErrChunkNotFound) { testInstance.Fatalf("der Export muss bei fehlendem Chunk abbrechen, war: %v", exportError) } } func TestExportRefusesWhenChunkIsCorrupted(testInstance *testing.T) { testRepository := newTestRepository(testInstance) manifest := writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Originaldaten")}) // Ein Block auf dem Datenträger ist beschädigt. chunkFilePath, _ := testRepository.chunkPath(manifest.Entries[0].Chunks[0].Identifier) _ = os.Chmod(chunkFilePath, 0o600) _ = os.WriteFile(chunkFilePath, []byte("beschaedigt!!"), 0o600) var containerBuffer bytes.Buffer _, exportError := testRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") // Ein beschädigter Block darf nicht in einen Container gelangen: der // Empfänger hielte ihn sonst für gültige Daten. if !errors.Is(exportError, ErrChunkCorrupted) { testInstance.Fatalf("der Export muss bei beschädigtem Chunk abbrechen, war: %v", exportError) } } func TestImportRejectsDuplicateBackupIdentifier(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{[]byte("Daten")}) var containerBuffer bytes.Buffer _, _ = sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") targetRepository := newTestRepository(testInstance) // Der erste Import gelingt. if _, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(containerBuffer.Bytes())); importError != nil { testInstance.Fatalf("der erste Import schlug fehl: %v", importError) } // Der zweite würde ein bestehendes Backup überschreiben. if _, importError := targetRepository.ImportBackup(context.Background(), bytes.NewReader(containerBuffer.Bytes())); importError == nil { testInstance.Fatal("ein bereits vorhandenes Backup darf nicht überschrieben werden") } } func TestImportRefusesOnReadOnlyRepository(testInstance *testing.T) { sourceRepository := newTestRepository(testInstance) writeTestBackup(testInstance, sourceRepository, "backup-1", [][]byte{[]byte("Daten")}) var containerBuffer bytes.Buffer _, _ = sourceRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") targetRepository := newTestRepository(testInstance) targetPath := targetRepository.RootPath() _ = targetRepository.Close() readOnlyRepository, _ := Open(context.Background(), targetPath, OpenOptions{ReadOnly: true}, newTestLogger()) defer func() { _ = readOnlyRepository.Close() }() if _, importError := readOnlyRepository.ImportBackup(context.Background(), bytes.NewReader(containerBuffer.Bytes())); importError == nil { testInstance.Fatal("ein nur lesend geöffnetes Repository darf nichts aufnehmen") } } func TestExportedContainerCarriesChainInformation(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeChainBackup(testInstance, testRepository, "voll", "kette-1", "", BackupTypeFull) writeChainBackup(testInstance, testRepository, "inkrement", "kette-1", "voll", BackupTypeIncremental) var containerBuffer bytes.Buffer _, _ = testRepository.ExportBackup(context.Background(), "inkrement", &containerBuffer, "test") containerReader, readerError := backupformat.NewReader(bytes.NewReader(containerBuffer.Bytes())) if readerError != nil { testInstance.Fatalf("der Container konnte nicht gelesen werden: %v", readerError) } containerHeader := containerReader.Header() // Ohne Kettenangaben liesse sich eine Kette aus einzelnen Containern nicht // wieder zusammensetzen. if containerHeader.ChainID != "kette-1" { testInstance.Errorf("die Kettenkennung weicht ab: %q", containerHeader.ChainID) } if containerHeader.ParentBackupID != "voll" { testInstance.Errorf("das Elternbackup weicht ab: %q", containerHeader.ParentBackupID) } } func TestExportedContainerNamesSourceRepository(testInstance *testing.T) { testRepository := newTestRepository(testInstance) writeTestBackup(testInstance, testRepository, "backup-1", [][]byte{[]byte("Daten")}) var containerBuffer bytes.Buffer _, _ = testRepository.ExportBackup(context.Background(), "backup-1", &containerBuffer, "test") containerReader, _ := backupformat.NewReader(bytes.NewReader(containerBuffer.Bytes())) // Die Herkunft muss nachvollziehbar bleiben. if containerReader.Header().RepositoryID != testRepository.Descriptor().RepositoryID { testInstance.Error("der Container benennt sein Ursprungs-Repository nicht") } }