From 8c244214bff928eacdd82b0c0715e9f4a24c38c2 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 3 Apr 2022 14:48:39 +0200 Subject: [PATCH] Add tests for upgrade migration --- internal/migrations/upgrade_repo_v2.go | 42 ++++++-- internal/migrations/upgrade_repo_v2_test.go | 112 ++++++++++++++++++++ 2 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 internal/migrations/upgrade_repo_v2_test.go diff --git a/internal/migrations/upgrade_repo_v2.go b/internal/migrations/upgrade_repo_v2.go index cf5ae8425..ada77444e 100644 --- a/internal/migrations/upgrade_repo_v2.go +++ b/internal/migrations/upgrade_repo_v2.go @@ -15,6 +15,26 @@ func init() { register(&UpgradeRepoV2{}) } +type UpgradeRepoV2Error struct { + UploadNewConfigError error + ReuploadOldConfigError error + + BackupFilePath string +} + +func (err *UpgradeRepoV2Error) Error() string { + if err.ReuploadOldConfigError != nil { + return fmt.Sprintf("error uploading config (%v), re-uploading old config filed failed as well (%v), but there is a backup of the config file in %v", err.UploadNewConfigError, err.ReuploadOldConfigError, err.BackupFilePath) + } + + return fmt.Sprintf("error uploading config (%v), re-uploaded old config was successful, there is a backup of the config file in %v", err.UploadNewConfigError, err.BackupFilePath) +} + +func (err *UpgradeRepoV2Error) Unwrap() error { + // consider the original upload error as the primary cause + return err.UploadNewConfigError +} + type UpgradeRepoV2 struct{} func (*UpgradeRepoV2) Name() string { @@ -69,7 +89,8 @@ func (m *UpgradeRepoV2) Apply(ctx context.Context, repo restic.Repository) error return fmt.Errorf("load config file failed: %w", err) } - err = ioutil.WriteFile(filepath.Join(tempdir, "config.old"), rawConfigFile, 0600) + backupFileName := filepath.Join(tempdir, "config") + err = ioutil.WriteFile(backupFileName, rawConfigFile, 0600) if err != nil { return fmt.Errorf("write config file backup to %v failed: %w", tempdir, err) } @@ -77,14 +98,21 @@ func (m *UpgradeRepoV2) Apply(ctx context.Context, repo restic.Repository) error // run the upgrade err = m.upgrade(ctx, repo) if err != nil { - // try contingency methods, reupload the original file - _ = repo.Backend().Remove(ctx, h) - uploadError := repo.Backend().Save(ctx, h, restic.NewByteReader(rawConfigFile, nil)) - if uploadError != nil { - return fmt.Errorf("error uploading config (%w), re-uploading old config filed failed as well (%v) but there is a backup in %v", err, uploadError, tempdir) + + // build an error we can return to the caller + repoError := &UpgradeRepoV2Error{ + UploadNewConfigError: err, + BackupFilePath: backupFileName, } - return fmt.Errorf("error uploading config (%w), re-uploadid old config, there is a backup in %v", err, tempdir) + // try contingency methods, reupload the original file + _ = repo.Backend().Remove(ctx, h) + err = repo.Backend().Save(ctx, h, restic.NewByteReader(rawConfigFile, nil)) + if err != nil { + repoError.ReuploadOldConfigError = err + } + + return repoError } _ = os.Remove(backupFileName) diff --git a/internal/migrations/upgrade_repo_v2_test.go b/internal/migrations/upgrade_repo_v2_test.go new file mode 100644 index 000000000..0d86d265c --- /dev/null +++ b/internal/migrations/upgrade_repo_v2_test.go @@ -0,0 +1,112 @@ +package migrations + +import ( + "context" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/test" +) + +func TestUpgradeRepoV2(t *testing.T) { + repo, cleanup := repository.TestRepositoryWithVersion(t, 1) + defer cleanup() + + if repo.Config().Version != 1 { + t.Fatal("test repo has wrong version") + } + + m := &UpgradeRepoV2{} + + ok, err := m.Check(context.Background(), repo) + if err != nil { + t.Fatal(err) + } + + if !ok { + t.Fatal("migration check returned false") + } + + err = m.Apply(context.Background(), repo) + if err != nil { + t.Fatal(err) + } +} + +type failBackend struct { + restic.Backend + + mu sync.Mutex + ConfigFileSavesUntilError uint +} + +func (be *failBackend) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader) error { + if h.Type != restic.ConfigFile { + return be.Backend.Save(ctx, h, rd) + } + + be.mu.Lock() + if be.ConfigFileSavesUntilError == 0 { + be.mu.Unlock() + return errors.New("failure induced for testing") + } + + be.ConfigFileSavesUntilError-- + be.mu.Unlock() + + return be.Backend.Save(ctx, h, rd) +} + +func TestUpgradeRepoV2Failure(t *testing.T) { + be, cleanup := repository.TestBackend(t) + defer cleanup() + + // wrap backend so that it fails upgrading the config after the initial write + be = &failBackend{ + ConfigFileSavesUntilError: 1, + Backend: be, + } + + repo, cleanup := repository.TestRepositoryWithBackend(t, be, 1) + defer cleanup() + + if repo.Config().Version != 1 { + t.Fatal("test repo has wrong version") + } + + m := &UpgradeRepoV2{} + + ok, err := m.Check(context.Background(), repo) + if err != nil { + t.Fatal(err) + } + + if !ok { + t.Fatal("migration check returned false") + } + + err = m.Apply(context.Background(), repo) + if err == nil { + t.Fatal("expected error returned from Apply(), got nil") + } + + upgradeErr := err.(*UpgradeRepoV2Error) + if upgradeErr.UploadNewConfigError == nil { + t.Fatal("expected upload error, got nil") + } + + if upgradeErr.ReuploadOldConfigError == nil { + t.Fatal("expected reupload error, got nil") + } + + if upgradeErr.BackupFilePath == "" { + t.Fatal("no backup file path found") + } + test.OK(t, os.Remove(upgradeErr.BackupFilePath)) + test.OK(t, os.Remove(filepath.Dir(upgradeErr.BackupFilePath))) +}