From caa5c6ea2f5d839862ec0b6f6590d2e22e029b18 Mon Sep 17 00:00:00 2001 From: Ming Date: Mon, 5 Jun 2023 13:07:26 +0000 Subject: [PATCH] Add UT for uploader Signed-off-by: Ming --- changelogs/unreleased/6374-qiuming-best | 1 + pkg/repository/backup_repo_op.go | 2 +- pkg/repository/ensurer.go | 2 +- pkg/repository/ensurer_test.go | 4 +- pkg/uploader/kopia/progress_test.go | 96 ++++ pkg/uploader/kopia/shim_test.go | 204 +++++++++ pkg/uploader/kopia/snapshot.go | 11 +- pkg/uploader/kopia/snapshot_test.go | 555 ++++++++++++++++++++++++ pkg/uploader/provider/kopia.go | 3 +- pkg/uploader/provider/kopia_test.go | 273 +++++++++++- pkg/uploader/provider/provider_test.go | 98 +++++ pkg/uploader/provider/restic.go | 25 +- pkg/uploader/provider/restic_test.go | 326 +++++++++++++- 13 files changed, 1542 insertions(+), 58 deletions(-) create mode 100644 changelogs/unreleased/6374-qiuming-best create mode 100644 pkg/uploader/kopia/progress_test.go create mode 100644 pkg/uploader/kopia/shim_test.go create mode 100644 pkg/uploader/provider/provider_test.go diff --git a/changelogs/unreleased/6374-qiuming-best b/changelogs/unreleased/6374-qiuming-best new file mode 100644 index 0000000000..88a803d65e --- /dev/null +++ b/changelogs/unreleased/6374-qiuming-best @@ -0,0 +1 @@ +Add unit test for pkg/uploader diff --git a/pkg/repository/backup_repo_op.go b/pkg/repository/backup_repo_op.go index e1c021fa8f..7025349a97 100644 --- a/pkg/repository/backup_repo_op.go +++ b/pkg/repository/backup_repo_op.go @@ -91,7 +91,7 @@ func GetBackupRepository(ctx context.Context, cli client.Client, namespace strin return repo, nil } -func newBackupRepository(namespace string, key BackupRepositoryKey) *velerov1api.BackupRepository { +func NewBackupRepository(namespace string, key BackupRepositoryKey) *velerov1api.BackupRepository { return &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, diff --git a/pkg/repository/ensurer.go b/pkg/repository/ensurer.go index 569730035e..4bdd05fba9 100644 --- a/pkg/repository/ensurer.go +++ b/pkg/repository/ensurer.go @@ -108,7 +108,7 @@ func (r *Ensurer) repoLock(key BackupRepositoryKey) *sync.Mutex { } func (r *Ensurer) createBackupRepositoryAndWait(ctx context.Context, namespace string, backupRepoKey BackupRepositoryKey) (*velerov1api.BackupRepository, error) { - toCreate := newBackupRepository(namespace, backupRepoKey) + toCreate := NewBackupRepository(namespace, backupRepoKey) if err := r.repoClient.Create(ctx, toCreate, &client.CreateOptions{}); err != nil { return nil, errors.Wrap(err, "unable to create backup repository resource") } diff --git a/pkg/repository/ensurer_test.go b/pkg/repository/ensurer_test.go index c9e71ea8c0..72dff8a3ae 100644 --- a/pkg/repository/ensurer_test.go +++ b/pkg/repository/ensurer_test.go @@ -30,7 +30,7 @@ import ( ) func TestEnsureRepo(t *testing.T) { - bkRepoObj := newBackupRepository(velerov1.DefaultNamespace, BackupRepositoryKey{ + bkRepoObj := NewBackupRepository(velerov1.DefaultNamespace, BackupRepositoryKey{ VolumeNamespace: "fake-ns", BackupLocation: "fake-bsl", RepositoryType: "fake-repo-type", @@ -121,7 +121,7 @@ func TestEnsureRepo(t *testing.T) { } func TestCreateBackupRepositoryAndWait(t *testing.T) { - bkRepoObj := newBackupRepository(velerov1.DefaultNamespace, BackupRepositoryKey{ + bkRepoObj := NewBackupRepository(velerov1.DefaultNamespace, BackupRepositoryKey{ VolumeNamespace: "fake-ns", BackupLocation: "fake-bsl", RepositoryType: "fake-repo-type", diff --git a/pkg/uploader/kopia/progress_test.go b/pkg/uploader/kopia/progress_test.go new file mode 100644 index 0000000000..7b349cbe14 --- /dev/null +++ b/pkg/uploader/kopia/progress_test.go @@ -0,0 +1,96 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kopia + +import ( + "testing" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/vmware-tanzu/velero/pkg/uploader" +) + +type fakeProgressUpdater struct{} + +func (f *fakeProgressUpdater) UpdateProgress(p *uploader.Progress) {} + +func TestThrottle_ShouldOutput(t *testing.T) { + testCases := []struct { + interval time.Duration + throttle int64 + expectedOutput bool + }{ + {interval: time.Second, expectedOutput: true}, + {interval: time.Second, throttle: time.Now().UnixNano() + int64(time.Nanosecond*10000), expectedOutput: false}, + } + p := new(Progress) + for _, tc := range testCases { + // Setup + p.InitThrottle(tc.interval) + p.outputThrottle.throttle = int64(tc.throttle) + // Perform the test + + output := p.outputThrottle.ShouldOutput() + + // Verify the result + if output != tc.expectedOutput { + t.Errorf("Expected ShouldOutput to return %v, but got %v", tc.expectedOutput, output) + } + } +} + +func TestProgress(t *testing.T) { + fileName := "test-filename" + var numBytes int64 = 1 + testCases := []struct { + interval time.Duration + throttle int64 + }{ + {interval: time.Second}, + {interval: time.Second, throttle: time.Now().UnixNano() + int64(time.Nanosecond*10000)}, + } + p := new(Progress) + p.Log = logrus.New() + p.Updater = &fakeProgressUpdater{} + for _, tc := range testCases { + // Setup + p.InitThrottle(tc.interval) + p.outputThrottle.throttle = int64(tc.throttle) + p.InitThrottle(time.Duration(time.Second)) + // All below calls put together for the implementation are empty or just very simple and just want to cover testing + // If wanting to write unit tests for some functions could remove it and with writing new function alone + p.UpdateProgress() + p.UploadedBytes(numBytes) + p.Error("test-path", nil, true) + p.Error("test-path", errors.New("processing error"), false) + p.UploadStarted() + p.EstimatedDataSize(1, numBytes) + p.CachedFile(fileName, numBytes) + p.HashedBytes(numBytes) + p.HashingFile(fileName) + p.ExcludedFile(fileName, numBytes) + p.ExcludedDir(fileName) + p.FinishedHashingFile(fileName, numBytes) + p.StartedDirectory(fileName) + p.FinishedDirectory(fileName) + p.UploadFinished() + p.ProgressBytes(numBytes, numBytes) + p.FinishedFile(fileName, nil) + } +} diff --git a/pkg/uploader/kopia/shim_test.go b/pkg/uploader/kopia/shim_test.go new file mode 100644 index 0000000000..50c99b9e18 --- /dev/null +++ b/pkg/uploader/kopia/shim_test.go @@ -0,0 +1,204 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kopia + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/content" + "github.com/kopia/kopia/repo/manifest" + "github.com/kopia/kopia/repo/object" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" +) + +func TestShimRepo(t *testing.T) { + ctx := context.Background() + backupRepo := &mocks.BackupRepo{} + backupRepo.On("Time").Return(time.Time{}) + shim := NewShimRepo(backupRepo) + // All below calls put together for the implementation are empty or just very simple, and just want to cover testing + // If wanting to write unit tests for some functions could remove it and with writing new function alone + shim.VerifyObject(ctx, object.ID{}) + shim.Time() + shim.ClientOptions() + shim.Refresh(ctx) + shim.ContentInfo(ctx, content.ID{}) + shim.PrefetchContents(ctx, []content.ID{}, "hint") + shim.PrefetchObjects(ctx, []object.ID{}, "hint") + shim.UpdateDescription("desc") + shim.NewWriter(ctx, repo.WriteSessionOptions{}) + shim.ReplaceManifests(ctx, map[string]string{}, nil) + shim.OnSuccessfulFlush(func(ctx context.Context, w repo.RepositoryWriter) error { return nil }) + + backupRepo.On("Close", mock.Anything).Return(nil) + NewShimRepo(backupRepo).Close(ctx) + + var id udmrepo.ID + backupRepo.On("PutManifest", mock.Anything, mock.Anything).Return(id, nil) + NewShimRepo(backupRepo).PutManifest(ctx, map[string]string{}, nil) + + var mf manifest.ID + backupRepo.On("DeleteManifest", mock.Anything, mock.Anything).Return(nil) + NewShimRepo(backupRepo).DeleteManifest(ctx, mf) + + backupRepo.On("Flush", mock.Anything).Return(nil) + NewShimRepo(backupRepo).Flush(ctx) + + var objID object.ID + backupRepo.On("ConcatenateObjects", mock.Anything, mock.Anything).Return(objID) + NewShimRepo(backupRepo).ConcatenateObjects(ctx, []object.ID{}) + + backupRepo.On("NewObjectWriter", mock.Anything, mock.Anything).Return(nil) + NewShimRepo(backupRepo).NewObjectWriter(ctx, object.WriterOptions{}) +} + +func TestOpenObject(t *testing.T) { + tests := []struct { + name string + backupRepo *mocks.BackupRepo + isOpenObjectError bool + isReaderNil bool + }{ + { + name: "Success", + backupRepo: func() *mocks.BackupRepo { + backupRepo := &mocks.BackupRepo{} + backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(&shimObjectReader{}, nil) + return backupRepo + }(), + }, + { + name: "Open object error", + backupRepo: func() *mocks.BackupRepo { + backupRepo := &mocks.BackupRepo{} + backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(&shimObjectReader{}, errors.New("Error open object")) + return backupRepo + }(), + isOpenObjectError: true, + }, + { + name: "Get nil reader", + backupRepo: func() *mocks.BackupRepo { + backupRepo := &mocks.BackupRepo{} + backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(nil, nil) + return backupRepo + }(), + isReaderNil: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + reader, err := NewShimRepo(tc.backupRepo).OpenObject(ctx, object.ID{}) + if tc.isOpenObjectError { + assert.Contains(t, err.Error(), "failed to open object") + } else if tc.isReaderNil { + assert.Nil(t, reader) + } else { + assert.NotNil(t, reader) + assert.Nil(t, err) + } + }) + } +} + +func TestFindManifests(t *testing.T) { + meta := []*udmrepo.ManifestEntryMetadata{} + tests := []struct { + name string + backupRepo *mocks.BackupRepo + isGetManifestError bool + }{ + { + name: "Success", + backupRepo: func() *mocks.BackupRepo { + backupRepo := &mocks.BackupRepo{} + backupRepo.On("FindManifests", mock.Anything, mock.Anything).Return(meta, nil) + return backupRepo + }(), + }, + { + name: "Failed to find manifest", + isGetManifestError: true, + backupRepo: func() *mocks.BackupRepo { + backupRepo := &mocks.BackupRepo{} + backupRepo.On("FindManifests", mock.Anything, mock.Anything).Return(meta, + errors.New("failed to find manifest")) + return backupRepo + }(), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + _, err := NewShimRepo(tc.backupRepo).FindManifests(ctx, map[string]string{}) + if tc.isGetManifestError { + assert.Contains(t, err.Error(), "failed") + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestShimObjReader(t *testing.T) { + reader := new(shimObjectReader) + objReader := &mocks.ObjectReader{} + reader.repoReader = objReader + // All below calls put together for the implementation are empty or just very simple, and just want to cover testing + // If wanting to write unit tests for some functions could remove it and with writing new function alone + objReader.On("Seek", mock.Anything, mock.Anything).Return(int64(0), nil) + reader.Seek(int64(0), 0) + + objReader.On("Read", mock.Anything).Return(0, nil) + reader.Read(nil) + + objReader.On("Close").Return(nil) + reader.Close() + + objReader.On("Length").Return(int64(0)) + reader.Length() +} + +func TestShimObjWriter(t *testing.T) { + writer := new(shimObjectWriter) + objWriter := &mocks.ObjectWriter{} + writer.repoWriter = objWriter + // All below calls put together for the implementation are empty or just very simple, and just want to cover testing + // If wanting to write unit tests for some functions could remove it and with writing new function alone + var id udmrepo.ID + objWriter.On("Checkpoint").Return(id, nil) + writer.Checkpoint() + + objWriter.On("Result").Return(id, nil) + writer.Result() + + objWriter.On("Write", mock.Anything).Return(0, nil) + writer.Write(nil) + + objWriter.On("Close").Return(nil) + writer.Close() +} diff --git a/pkg/uploader/kopia/snapshot.go b/pkg/uploader/kopia/snapshot.go index 96d2ddc1d2..d4165822ec 100644 --- a/pkg/uploader/kopia/snapshot.go +++ b/pkg/uploader/kopia/snapshot.go @@ -47,6 +47,9 @@ import ( var applyRetentionPolicyFunc = policy.ApplyRetentionPolicy var saveSnapshotFunc = snapshot.SaveSnapshot var loadSnapshotFunc = snapshot.LoadSnapshot +var listSnapshotsFunc = snapshot.ListSnapshots +var filesystemEntryFunc = snapshotfs.FilesystemEntryFromIDWithPath +var restoreEntryFunc = restore.Entry // SnapshotUploader which mainly used for UT test that could overwrite Upload interface type SnapshotUploader interface { @@ -84,7 +87,7 @@ func setupDefaultPolicy() *policy.Tree { } // Backup backup specific sourcePath and update progress -func Backup(ctx context.Context, fsUploader *snapshotfs.Uploader, repoWriter repo.RepositoryWriter, sourcePath string, +func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { if fsUploader == nil { return nil, false, errors.New("get empty kopia uploader") @@ -238,7 +241,7 @@ func reportSnapshotStatus(manifest *snapshot.Manifest, policyTree *policy.Tree) // findPreviousSnapshotManifest returns the list of previous snapshots for a given source, including // last complete snapshot following it. func findPreviousSnapshotManifest(ctx context.Context, rep repo.Repository, sourceInfo snapshot.SourceInfo, snapshotTags map[string]string, noLaterThan *fs.UTCTimestamp) ([]*snapshot.Manifest, error) { - man, err := snapshot.ListSnapshots(ctx, rep, sourceInfo) + man, err := listSnapshotsFunc(ctx, rep, sourceInfo) if err != nil { return nil, err } @@ -294,7 +297,7 @@ func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress, log.Infof("Restore from snapshot %s, description %s, created time %v, tags %v", snapshotID, snapshot.Description, snapshot.EndTime.ToTime(), snapshot.Tags) - rootEntry, err := snapshotfs.FilesystemEntryFromIDWithPath(kopiaCtx, rep, snapshotID, false) + rootEntry, err := filesystemEntryFunc(kopiaCtx, rep, snapshotID, false) if err != nil { return 0, 0, errors.Wrapf(err, "Unable to get filesystem entry for snapshot %v", snapshotID) } @@ -317,7 +320,7 @@ func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress, return 0, 0, errors.Wrap(err, "error to init output") } - stat, err := restore.Entry(kopiaCtx, rep, output, rootEntry, restore.Options{ + stat, err := restoreEntryFunc(kopiaCtx, rep, output, rootEntry, restore.Options{ Parallel: runtime.NumCPU(), RestoreDirEntryAtDepth: math.MaxInt32, Cancel: cancleCh, diff --git a/pkg/uploader/kopia/snapshot_test.go b/pkg/uploader/kopia/snapshot_test.go index ea927dd44c..7090cd4e61 100644 --- a/pkg/uploader/kopia/snapshot_test.go +++ b/pkg/uploader/kopia/snapshot_test.go @@ -18,15 +18,23 @@ package kopia import ( "context" + "strings" "testing" + "time" + "github.com/kopia/kopia/fs" + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" + "github.com/kopia/kopia/snapshot/restore" + "github.com/kopia/kopia/snapshot/snapshotfs" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" repomocks "github.com/vmware-tanzu/velero/pkg/repository/mocks" + "github.com/vmware-tanzu/velero/pkg/uploader" uploadermocks "github.com/vmware-tanzu/velero/pkg/uploader/mocks" ) @@ -179,5 +187,552 @@ func TestSnapshotSource(t *testing.T) { } }) } +} + +func TestReportSnapshotStatus(t *testing.T) { + testCases := []struct { + shouldError bool + expectedResult string + expectedSize int64 + directorySummary *fs.DirectorySummary + expectedErrors []string + }{ + { + shouldError: false, + expectedResult: "sample-manifest-id", + expectedSize: 1024, + directorySummary: &fs.DirectorySummary{ + TotalFileSize: 1024, + }, + }, + { + shouldError: true, + expectedResult: "", + expectedSize: 0, + directorySummary: &fs.DirectorySummary{ + FailedEntries: []*fs.EntryWithError{ + { + EntryPath: "/path/to/file.txt", + Error: "Unknown file error", + }, + }, + }, + expectedErrors: []string{"Error when processing /path/to/file.txt: Unknown file error"}, + }, + } + + for _, tc := range testCases { + manifest := &snapshot.Manifest{ + ID: manifest.ID("sample-manifest-id"), + Stats: snapshot.Stats{ + TotalFileSize: 1024, + }, + RootEntry: &snapshot.DirEntry{ + DirSummary: tc.directorySummary, + }, + } + + result, size, err := reportSnapshotStatus(manifest, setupDefaultPolicy()) + + switch { + case tc.shouldError && err == nil: + t.Errorf("expected error, but got nil") + case !tc.shouldError && err != nil: + t.Errorf("unexpected error: %v", err) + case tc.shouldError && err != nil: + expectedErr := strings.Join(tc.expectedErrors, "\n") + if err.Error() != expectedErr { + t.Errorf("unexpected error: got %v, want %v", err, expectedErr) + } + } + + if result != tc.expectedResult { + t.Errorf("unexpected result: got %v, want %v", result, tc.expectedResult) + } + + if size != tc.expectedSize { + t.Errorf("unexpected size: got %v, want %v", size, tc.expectedSize) + } + } +} + +func TestFindPreviousSnapshotManifest(t *testing.T) { + // Prepare test data + sourceInfo := snapshot.SourceInfo{ + UserName: "user1", + Host: "host1", + Path: "/path/to/dir1", + } + snapshotTags := map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + } + noLaterThan := fs.UTCTimestampFromTime(time.Now()) + + testCases := []struct { + name string + listSnapshotsFunc func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) + expectedSnapshots []*snapshot.Manifest + expectedError error + }{ + // No matching snapshots + { + name: "No matching snapshots", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{}, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + { + name: "Error getting manifest", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{}, errors.New("Error getting manifest") + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: errors.New("Error getting manifest"), + }, + // Only one matching snapshot + { + name: "One matching snapshot", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "anotherCustomTag": "123", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "anotherCustomTag": "123", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + }, + expectedError: nil, + }, + // Multiple matching snapshots + { + name: "Multiple matching snapshots", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value1", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value2", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value1", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + }, + expectedError: nil, + }, + // Snapshot with different requestor + { + name: "Snapshot with different requestor", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user2", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "snapshotRequestor": "user2", + "snapshotUploader": "uploader1", + }, + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + // Snapshot with different uploader + { + name: "Snapshot with different uploader", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader2", + "otherTag": "value", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader2", + }, + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + // Snapshot with a later start time + { + name: "Snapshot with a later start time", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + StartTime: fs.UTCTimestampFromTime(time.Now().Add(time.Hour)), + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + // Snapshot with incomplete reason + { + name: "Snapshot with incomplete reason", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + IncompleteReason: "reason", + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + // Multiple snapshots with some matching conditions + { + name: "Multiple snapshots with matching conditions", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value1", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + }, + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value2", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + StartTime: fs.UTCTimestampFromTime(time.Now().Add(-time.Hour)), + IncompleteReason: "reason", + }, + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value3", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + StartTime: fs.UTCTimestampFromTime(time.Now().Add(-time.Hour)), + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value3", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + StartTime: fs.UTCTimestampFromTime(time.Now().Add(-time.Hour)), + }, + }, + expectedError: nil, + }, + // Snapshot with manifest SnapshotRequestorTag not found + { + name: "Snapshot with manifest SnapshotRequestorTag not found", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + "requestor": "user1", + uploader.SnapshotUploaderTag: "uploader1", + "otherTag": "value", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + IncompleteReason: "reason", + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + // Snapshot with manifest SnapshotRequestorTag not found + { + name: "Snapshot with manifest SnapshotUploaderTag not found", + listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { + return []*snapshot.Manifest{ + { + Tags: map[string]string{ + uploader.SnapshotRequestorTag: "user1", + "uploader": "uploader1", + "otherTag": "value", + "snapshotRequestor": "user1", + "snapshotUploader": "uploader1", + }, + IncompleteReason: "reason", + }, + }, nil + }, + expectedSnapshots: []*snapshot.Manifest{}, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var repo repo.Repository + listSnapshotsFunc = tc.listSnapshotsFunc + snapshots, err := findPreviousSnapshotManifest(context.Background(), repo, sourceInfo, snapshotTags, &noLaterThan) + + // Check if the returned error matches the expected error + if tc.expectedError != nil { + assert.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + assert.Nil(t, err) + } + + // Check the number of returned snapshots + if len(snapshots) != len(tc.expectedSnapshots) { + t.Errorf("Expected %d snapshots, got %d", len(tc.expectedSnapshots), len(snapshots)) + } + }) + } +} + +func TestBackup(t *testing.T) { + type testCase struct { + name string + sourcePath string + forceFull bool + parentSnapshot string + tags map[string]string + isEmptyUploader bool + isSnapshotSourceError bool + expectedError error + expectedEmpty bool + } + manifest := &snapshot.Manifest{ + ID: "test", + RootEntry: &snapshot.DirEntry{}, + } + // Define test cases + testCases := []testCase{ + { + name: "Successful backup", + sourcePath: "/", + tags: map[string]string{}, + expectedError: nil, + }, + { + name: "Empty fsUploader", + isEmptyUploader: true, + sourcePath: "/", + tags: nil, + expectedError: errors.New("get empty kopia uploader"), + }, + { + name: "Unable to read directory", + sourcePath: "/invalid/path", + tags: nil, + expectedError: errors.New("Unable to read dir"), + }, + { + name: "Failed to get snapshot manifests", + isSnapshotSourceError: true, + sourcePath: "/", + tags: nil, + expectedError: errors.New("Failed to get manifests"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := injectSnapshotFuncs() + args := []mockArgs{ + {methodName: "LoadSnapshot", returns: []interface{}{manifest, nil}}, + {methodName: "SaveSnapshot", returns: []interface{}{manifest.ID, nil}}, + {methodName: "TreeForSource", returns: []interface{}{nil, nil}}, + {methodName: "ApplyRetentionPolicy", returns: []interface{}{nil, nil}}, + {methodName: "SetPolicy", returns: []interface{}{nil}}, + {methodName: "Upload", returns: []interface{}{manifest, nil}}, + {methodName: "Flush", returns: []interface{}{nil}}, + } + MockFuncs(s, args) + if tc.isSnapshotSourceError { + s.repoWriterMock.On("FindManifests", mock.Anything, mock.Anything).Return(nil, errors.New("Failed to get manifests")) + s.repoWriterMock.On("Flush", mock.Anything).Return(errors.New("Failed to get manifests")) + } else { + s.repoWriterMock.On("FindManifests", mock.Anything, mock.Anything).Return(nil, nil) + } + + var isSnapshotEmpty bool + var snapshotInfo *uploader.SnapshotInfo + var err error + if tc.isEmptyUploader { + snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), nil, s.repoWriterMock, tc.sourcePath, tc.forceFull, tc.parentSnapshot, tc.tags, &logrus.Logger{}) + } else { + snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), s.uploderMock, s.repoWriterMock, tc.sourcePath, tc.forceFull, tc.parentSnapshot, tc.tags, &logrus.Logger{}) + } + // Check if the returned error matches the expected error + if tc.expectedError != nil { + assert.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, tc.expectedEmpty, isSnapshotEmpty) + + if err == nil { + assert.NotNil(t, snapshotInfo) + } + }) + } +} + +func TestRestore(t *testing.T) { + type testCase struct { + name string + snapshotID string + invalidManifestType bool + filesystemEntryFunc func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) + restoreEntryFunc func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) + dest string + expectedBytes int64 + expectedCount int32 + expectedError error + } + + // Define test cases + testCases := []testCase{ + { + name: "manifest is not a snapshot", + invalidManifestType: true, + dest: "/path/to/destination", + expectedError: errors.New("Unable to load snapshot"), + }, + { + name: "Failed to get filesystem entry", + snapshotID: "snapshot-123", + expectedError: errors.New("Unable to get filesystem entry"), + }, + { + name: "Failed to restore with filesystem entry", + filesystemEntryFunc: func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) { + return snapshotfs.EntryFromDirEntry(rep, &snapshot.DirEntry{Type: snapshot.EntryTypeFile}), nil + }, + restoreEntryFunc: func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) { + return restore.Stats{}, errors.New("Unable to get filesystem entry") + }, + snapshotID: "snapshot-123", + expectedError: errors.New("Unable to get filesystem entry"), + }, + { + name: "Expect sucessful", + filesystemEntryFunc: func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) { + return snapshotfs.EntryFromDirEntry(rep, &snapshot.DirEntry{Type: snapshot.EntryTypeFile}), nil + }, + restoreEntryFunc: func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) { + return restore.Stats{}, nil + }, + snapshotID: "snapshot-123", + expectedError: nil, + }, + } + em := &manifest.EntryMetadata{ + ID: "test", + Labels: map[string]string{}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.invalidManifestType { + em.Labels[manifest.TypeLabelKey] = "" + } else { + em.Labels[manifest.TypeLabelKey] = snapshot.ManifestType + } + + if tc.filesystemEntryFunc != nil { + filesystemEntryFunc = tc.filesystemEntryFunc + } + + if tc.restoreEntryFunc != nil { + restoreEntryFunc = tc.restoreEntryFunc + } + + repoWriterMock := &repomocks.RepositoryWriter{} + repoWriterMock.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(em, nil) + repoWriterMock.On("OpenObject", mock.Anything, mock.Anything).Return(em, nil) + + progress := new(Progress) + bytesRestored, fileCount, err := Restore(context.Background(), repoWriterMock, progress, tc.snapshotID, tc.dest, logrus.New(), nil) + + // Check if the returned error matches the expected error + if tc.expectedError != nil { + assert.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + assert.Nil(t, err) + } + + // Check the number of bytes restored + assert.Equal(t, tc.expectedBytes, bytesRestored) + + // Check the number of files restored + assert.Equal(t, tc.expectedCount, fileCount) + }) + } } diff --git a/pkg/uploader/provider/kopia.go b/pkg/uploader/provider/kopia.go index f578419a62..299db414d5 100644 --- a/pkg/uploader/provider/kopia.go +++ b/pkg/uploader/provider/kopia.go @@ -39,6 +39,7 @@ import ( // BackupFunc mainly used to make testing more convenient var BackupFunc = kopia.Backup var RestoreFunc = kopia.Restore +var BackupRepoServiceCreateFunc = service.Create // kopiaProvider recorded info related with kopiaProvider type kopiaProvider struct { @@ -73,7 +74,7 @@ func NewKopiaUploaderProvider( return nil, errors.Wrapf(err, "error to get repo options") } - repoSvc := service.Create(log) + repoSvc := BackupRepoServiceCreateFunc(log) log.WithField("repoUID", repoUID).Info("Opening backup repo") kp.bkRepo, err = repoSvc.Open(ctx, *repoOpt) diff --git a/pkg/uploader/provider/kopia_test.go b/pkg/uploader/provider/kopia_test.go index 207a3ed036..d8acad7357 100644 --- a/pkg/uploader/provider/kopia_test.go +++ b/pkg/uploader/provider/kopia_test.go @@ -18,48 +18,76 @@ package provider import ( "context" + "sync" "testing" + "time" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/snapshot/snapshotfs" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/internal/credentials/mocks" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme" + "github.com/vmware-tanzu/velero/pkg/repository" + udmrepo "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/uploader/kopia" ) +type FakeBackupProgressUpdater struct { + PodVolumeBackup *velerov1api.PodVolumeBackup + Log logrus.FieldLogger + Ctx context.Context + Cli client.Client +} + +func (f *FakeBackupProgressUpdater) UpdateProgress(p *uploader.Progress) {} + +type FakeRestoreProgressUpdater struct { + PodVolumeRestore *velerov1api.PodVolumeRestore + Log logrus.FieldLogger + Ctx context.Context + Cli client.Client +} + +func (f *FakeRestoreProgressUpdater) UpdateProgress(p *uploader.Progress) {} + func TestRunBackup(t *testing.T) { var kp kopiaProvider kp.log = logrus.New() updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: kp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()} + testCases := []struct { name string - hookBackupFunc func(ctx context.Context, fsUploader *snapshotfs.Uploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) + hookBackupFunc func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) notError bool }{ { name: "success to backup", - hookBackupFunc: func(ctx context.Context, fsUploader *snapshotfs.Uploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return &uploader.SnapshotInfo{}, false, nil }, notError: true, }, { name: "get error to backup", - hookBackupFunc: func(ctx context.Context, fsUploader *snapshotfs.Uploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return &uploader.SnapshotInfo{}, false, errors.New("failed to backup") }, notError: false, }, { name: "got empty snapshot", - hookBackupFunc: func(ctx context.Context, fsUploader *snapshotfs.Uploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, forceFull bool, parentSnapshot string, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return nil, true, errors.New("snapshot is empty") }, notError: false, @@ -117,20 +145,233 @@ func TestRunRestore(t *testing.T) { } } -type FakeBackupProgressUpdater struct { - PodVolumeBackup *velerov1api.PodVolumeBackup - Log logrus.FieldLogger - Ctx context.Context - Cli client.Client +func TestCheckContext(t *testing.T) { + testCases := []struct { + name string + finishChan chan struct{} + restoreChan chan struct{} + uploader *snapshotfs.Uploader + expectCancel bool + expectBackup bool + expectRestore bool + }{ + { + name: "FinishChan", + finishChan: make(chan struct{}), + restoreChan: make(chan struct{}), + uploader: &snapshotfs.Uploader{}, + expectCancel: false, + expectBackup: false, + expectRestore: false, + }, + { + name: "nil uploader", + finishChan: make(chan struct{}), + restoreChan: make(chan struct{}), + uploader: nil, + expectCancel: true, + expectBackup: false, + expectRestore: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + + if tc.expectBackup { + go func() { + wg.Wait() + tc.restoreChan <- struct{}{} + }() + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + time.Sleep(100 * time.Millisecond) + cancel() + wg.Done() + }() + + kp := &kopiaProvider{log: logrus.New()} + kp.CheckContext(ctx, tc.finishChan, tc.restoreChan, tc.uploader) + + if tc.expectCancel && tc.uploader != nil { + t.Error("Expected the uploader to be cancelled") + } + + if tc.expectBackup && tc.uploader == nil && len(tc.restoreChan) > 0 { + t.Error("Expected the restore channel to be closed") + } + }) + } } -func (f *FakeBackupProgressUpdater) UpdateProgress(p *uploader.Progress) {} +func TestGetPassword(t *testing.T) { + testCases := []struct { + name string + empytSecret bool + credGetterFunc func(*mocks.SecretStore, *v1.SecretKeySelector) + expectError bool + expectedPass string + }{ + { + name: "valid credentials interface", + credGetterFunc: func(ss *mocks.SecretStore, selector *v1.SecretKeySelector) { + ss.On("Get", selector).Return("test", nil) + }, + expectError: false, + expectedPass: "test", + }, + { + name: "empty from secret", + empytSecret: true, + expectError: true, + expectedPass: "", + }, + { + name: "ErrorGettingPassword", + credGetterFunc: func(ss *mocks.SecretStore, selector *v1.SecretKeySelector) { + ss.On("Get", selector).Return("", errors.New("error getting password")) + }, + expectError: true, + expectedPass: "", + }, + } -type FakeRestoreProgressUpdater struct { - PodVolumeRestore *velerov1api.PodVolumeRestore - Log logrus.FieldLogger - Ctx context.Context - Cli client.Client + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Mock CredentialGetter + credGetter := &credentials.CredentialGetter{} + mockCredGetter := &mocks.SecretStore{} + if !tc.empytSecret { + credGetter.FromSecret = mockCredGetter + } + repoKeySelector := &v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: "velero-repo-credentials"}, Key: "repository-password"} + + if tc.credGetterFunc != nil { + tc.credGetterFunc(mockCredGetter, repoKeySelector) + } + + kp := &kopiaProvider{ + credGetter: credGetter, + } + + password, err := kp.GetPassword(nil) + if tc.expectError { + assert.Error(t, err, "Expected an error") + } else { + assert.NoError(t, err, "Expected no error") + } + + assert.Equal(t, tc.expectedPass, password, "Expected password to match") + }) + } } -func (f *FakeRestoreProgressUpdater) UpdateProgress(p *uploader.Progress) {} +func (m *MockCredentialGetter) GetCredentials() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} + +// MockRepoSvc is a mock implementation of the RepoService interface. +type MockRepoSvc struct { + mock.Mock +} + +func (m *MockRepoSvc) Open(ctx context.Context, opts udmrepo.RepoOptions) (udmrepo.BackupRepo, error) { + args := m.Called(ctx, opts) + return args.Get(0).(udmrepo.BackupRepo), args.Error(1) +} + +func TestNewKopiaUploaderProvider(t *testing.T) { + requestorType := "testRequestor" + ctx := context.Background() + backupRepo := repository.NewBackupRepository(velerov1api.DefaultNamespace, repository.BackupRepositoryKey{VolumeNamespace: "fake-volume-ns-02", BackupLocation: "fake-bsl-02", RepositoryType: "fake-repository-type-02"}) + mockLog := logrus.New() + + // Define test cases + testCases := []struct { + name string + mockCredGetter *mocks.SecretStore + mockBackupRepoService udmrepo.BackupRepoService + expectedError string + }{ + { + name: "Success", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + repoKeySelector := &v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: "velero-repo-credentials"}, Key: "repository-password"} + mockCredGetter.On("Get", repoKeySelector).Return("test", nil) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + repoOpt := udmrepo.RepoOptions{StorageType: "", RepoPassword: "test", ConfigFilePath: "/root/udmrepo/repo-.conf", GeneralOptions: map[string]string{}, StorageOptions: map[string]string{}, Description: "Initial kopia uploader provider"} + backupRepoService.On("Open", context.Background(), repoOpt).Return(backupRepo, nil) + return backupRepoService + }(), + expectedError: "", + }, + { + name: "Error to get repo options", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + repoKeySelector := &v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: "velero-repo-credentials"}, Key: "repository-password"} + mockCredGetter.On("Get", repoKeySelector).Return("test", errors.New("failed to get password")) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + repoOpt := udmrepo.RepoOptions{StorageType: "", RepoPassword: "test", ConfigFilePath: "/root/udmrepo/repo-.conf", GeneralOptions: map[string]string{}, StorageOptions: map[string]string{}, Description: "Initial kopia uploader provider"} + backupRepoService.On("Open", context.Background(), repoOpt).Return(backupRepo, nil) + return backupRepoService + }(), + expectedError: "error to get repo options", + }, + { + name: "Error open repository service", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + repoKeySelector := &v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: "velero-repo-credentials"}, Key: "repository-password"} + mockCredGetter.On("Get", repoKeySelector).Return("test", nil) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + repoOpt := udmrepo.RepoOptions{StorageType: "", RepoPassword: "test", ConfigFilePath: "/root/udmrepo/repo-.conf", GeneralOptions: map[string]string{}, StorageOptions: map[string]string{}, Description: "Initial kopia uploader provider"} + backupRepoService.On("Open", context.Background(), repoOpt).Return(backupRepo, errors.New("failed to init repository")) + return backupRepoService + }(), + expectedError: "Failed to find kopia repository", + }, + } + + // Iterate through test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + credGetter := &credentials.CredentialGetter{FromSecret: tc.mockCredGetter} + BackupRepoServiceCreateFunc = func(logger logrus.FieldLogger) udmrepo.BackupRepoService { + return tc.mockBackupRepoService + } + // Call the function being tested. + _, err := NewKopiaUploaderProvider(requestorType, ctx, credGetter, backupRepo, mockLog) + + // Assertions + if tc.expectedError != "" { + assert.Contains(t, err.Error(), tc.expectedError) + } else { + assert.Nil(t, err) + } + + // Verify that the expected methods were called on the mocks. + tc.mockCredGetter.AssertExpectations(t) + }) + } +} diff --git a/pkg/uploader/provider/provider_test.go b/pkg/uploader/provider/provider_test.go new file mode 100644 index 0000000000..7bc900e88a --- /dev/null +++ b/pkg/uploader/provider/provider_test.go @@ -0,0 +1,98 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/internal/credentials/mocks" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme" +) + +type NewUploaderProviderTestCase struct { + Description string + UploaderType string + RequestorType string + ExpectedError string + needFromFile bool +} + +func TestNewUploaderProvider(t *testing.T) { + // Mock objects or dependencies + ctx := context.Background() + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + repoIdentifier := "repoIdentifier" + bsl := &velerov1api.BackupStorageLocation{} + backupRepo := &velerov1api.BackupRepository{} + credGetter := &credentials.CredentialGetter{} + repoKeySelector := &v1.SecretKeySelector{} + log := logrus.New() + + testCases := []NewUploaderProviderTestCase{ + { + Description: "When requestorType is empty, it should return an error", + UploaderType: "kopia", + RequestorType: "", + ExpectedError: "requestor type is empty", + }, + { + Description: "When FileStore credential is uninitialized, it should return an error", + UploaderType: "kopia", + RequestorType: "requestor", + ExpectedError: "uninitialized FileStore credentail", + }, + { + Description: "When uploaderType is kopia, it should return a KopiaUploaderProvider", + UploaderType: "kopia", + RequestorType: "requestor", + needFromFile: true, + ExpectedError: "invalid credentials interface", + }, + { + Description: "When uploaderType is not kopia, it should return a ResticUploaderProvider", + UploaderType: "restic", + RequestorType: "requestor", + needFromFile: true, + ExpectedError: "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.needFromFile { + mockFileGetter := &mocks.FileStore{} + mockFileGetter.On("Path", &v1.SecretKeySelector{}).Return("", nil) + credGetter.FromFile = mockFileGetter + + } + _, err := NewUploaderProvider(ctx, client, testCase.UploaderType, testCase.RequestorType, repoIdentifier, bsl, backupRepo, credGetter, repoKeySelector, log) + if testCase.ExpectedError == "" { + assert.Nil(t, err) + } else { + assert.Contains(t, err.Error(), testCase.ExpectedError) + } + }) + } +} diff --git a/pkg/uploader/provider/restic.go b/pkg/uploader/provider/restic.go index 7b68d2e5b9..c1f5415d70 100644 --- a/pkg/uploader/provider/restic.go +++ b/pkg/uploader/provider/restic.go @@ -33,9 +33,14 @@ import ( "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) -// ResticBackupCMDFunc and ResticRestoreCMDFunc are mainly used to make testing more convenient -var ResticBackupCMDFunc = restic.BackupCommand -var ResticRestoreCMDFunc = restic.RestoreCommand +// resticBackupCMDFunc and resticRestoreCMDFunc are mainly used to make testing more convenient +var resticBackupCMDFunc = restic.BackupCommand +var resticBackupFunc = restic.RunBackup +var resticGetSnapshotFunc = restic.GetSnapshotCommand +var resticGetSnapshotIDFunc = restic.GetSnapshotID +var resticRestoreCMDFunc = restic.RestoreCommand +var resticTempCACertFileFunc = restic.TempCACertFile +var resticCmdEnvFunc = restic.CmdEnv type resticProvider struct { repoIdentifier string @@ -68,13 +73,13 @@ func NewResticUploaderProvider( // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic if bsl.Spec.ObjectStorage != nil && bsl.Spec.ObjectStorage.CACert != nil { - provider.caCertFile, err = restic.TempCACertFile(bsl.Spec.ObjectStorage.CACert, bsl.Name, filesystem.NewFileSystem()) + provider.caCertFile, err = resticTempCACertFileFunc(bsl.Spec.ObjectStorage.CACert, bsl.Name, filesystem.NewFileSystem()) if err != nil { return nil, errors.Wrap(err, "error create temp cert file") } } - provider.cmdEnv, err = restic.CmdEnv(bsl, credGetter.FromFile) + provider.cmdEnv, err = resticCmdEnvFunc(bsl, credGetter.FromFile) if err != nil { return nil, errors.Wrap(err, "error generating repository cmnd env") } @@ -125,7 +130,7 @@ func (rp *resticProvider) RunBackup( "parentSnapshot": parentSnapshot, }) - backupCmd := ResticBackupCMDFunc(rp.repoIdentifier, rp.credentialsFile, path, tags) + backupCmd := resticBackupCMDFunc(rp.repoIdentifier, rp.credentialsFile, path, tags) backupCmd.Env = rp.cmdEnv backupCmd.CACertFile = rp.caCertFile if len(rp.extraFlags) != 0 { @@ -136,7 +141,7 @@ func (rp *resticProvider) RunBackup( backupCmd.ExtraFlags = append(backupCmd.ExtraFlags, fmt.Sprintf("--parent=%s", parentSnapshot)) } - summary, stderrBuf, err := restic.RunBackup(backupCmd, log, updater) + summary, stderrBuf, err := resticBackupFunc(backupCmd, log, updater) if err != nil { if strings.Contains(stderrBuf, "snapshot is empty") { log.Debugf("Restic backup got empty dir with %s path", path) @@ -145,13 +150,13 @@ func (rp *resticProvider) RunBackup( return "", false, errors.WithStack(fmt.Errorf("error running restic backup command %s with error: %v stderr: %v", backupCmd.String(), err, stderrBuf)) } // GetSnapshotID - snapshotIDCmd := restic.GetSnapshotCommand(rp.repoIdentifier, rp.credentialsFile, tags) + snapshotIDCmd := resticGetSnapshotFunc(rp.repoIdentifier, rp.credentialsFile, tags) snapshotIDCmd.Env = rp.cmdEnv snapshotIDCmd.CACertFile = rp.caCertFile if len(rp.extraFlags) != 0 { snapshotIDCmd.ExtraFlags = append(snapshotIDCmd.ExtraFlags, rp.extraFlags...) } - snapshotID, err := restic.GetSnapshotID(snapshotIDCmd) + snapshotID, err := resticGetSnapshotIDFunc(snapshotIDCmd) if err != nil { return "", false, errors.WithStack(fmt.Errorf("error getting snapshot id with error: %v", err)) } @@ -174,7 +179,7 @@ func (rp *resticProvider) RunRestore( "volumePath": volumePath, }) - restoreCmd := ResticRestoreCMDFunc(rp.repoIdentifier, rp.credentialsFile, snapshotID, volumePath) + restoreCmd := resticRestoreCMDFunc(rp.repoIdentifier, rp.credentialsFile, snapshotID, volumePath) restoreCmd.Env = rp.cmdEnv restoreCmd.CACertFile = rp.caCertFile if len(rp.extraFlags) != 0 { diff --git a/pkg/uploader/provider/restic_test.go b/pkg/uploader/provider/restic_test.go index f2ba7a9708..fd7b6d40b6 100644 --- a/pkg/uploader/provider/restic_test.go +++ b/pkg/uploader/provider/restic_test.go @@ -18,89 +18,369 @@ package provider import ( "context" + "errors" + "io/ioutil" + "os" "strings" "testing" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/vmware-tanzu/velero/internal/credentials" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) func TestResticRunBackup(t *testing.T) { - var rp resticProvider - rp.log = logrus.New() - updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()} testCases := []struct { - name string - hookBackupFunc func(repoIdentifier string, passwordFile string, path string, tags map[string]string) *restic.Command - hookRunBackupFunc func(backupCmd *restic.Command, log logrus.FieldLogger, updater uploader.ProgressUpdater) (string, string, error) - errorHandleFunc func(err error) bool + name string + nilUpdater bool + parentSnapshot string + rp *resticProvider + hookBackupFunc func(string, string, string, map[string]string) *restic.Command + hookResticBackupFunc func(*restic.Command, logrus.FieldLogger, uploader.ProgressUpdater) (string, string, error) + hookResticGetSnapshotFunc func(string, string, map[string]string) *restic.Command + hookResticGetSnapshotIDFunc func(*restic.Command) (string, error) + errorHandleFunc func(err error) bool }{ { - name: "wrong restic execute command", + name: "nil uploader", + rp: &resticProvider{log: logrus.New()}, + nilUpdater: true, hookBackupFunc: func(repoIdentifier string, passwordFile string, path string, tags map[string]string) *restic.Command { return &restic.Command{Command: "date"} }, errorHandleFunc: func(err error) bool { - return strings.Contains(err.Error(), "executable file not found in") + return strings.Contains(err.Error(), "Need to initial backup progress updater first") }, }, { - name: "wrong parsing json summary content", + name: "wrong restic execute command", + rp: &resticProvider{log: logrus.New()}, hookBackupFunc: func(repoIdentifier string, passwordFile string, path string, tags map[string]string) *restic.Command { - return &restic.Command{Command: "version"} + return &restic.Command{Command: "date"} }, errorHandleFunc: func(err error) bool { - return strings.Contains(err.Error(), "executable file not found in") + return strings.Contains(err.Error(), "error running") + }, + }, { + name: "has parent snapshot", + rp: &resticProvider{log: logrus.New()}, + parentSnapshot: "parentSnapshot", + hookBackupFunc: func(repoIdentifier string, passwordFile string, path string, tags map[string]string) *restic.Command { + return &restic.Command{Command: "date"} + }, + hookResticBackupFunc: func(*restic.Command, logrus.FieldLogger, uploader.ProgressUpdater) (string, string, error) { + return "", "", nil + }, + + hookResticGetSnapshotIDFunc: func(*restic.Command) (string, error) { return "test-snapshot-id", nil }, + errorHandleFunc: func(err error) bool { + return err == nil + }, + }, + { + name: "has extra flags", + rp: &resticProvider{log: logrus.New(), extraFlags: []string{"testFlags"}}, + hookBackupFunc: func(string, string, string, map[string]string) *restic.Command { + return &restic.Command{Command: "date"} + }, + hookResticBackupFunc: func(*restic.Command, logrus.FieldLogger, uploader.ProgressUpdater) (string, string, error) { + return "", "", nil + }, + hookResticGetSnapshotIDFunc: func(*restic.Command) (string, error) { return "test-snapshot-id", nil }, + errorHandleFunc: func(err error) bool { + return err == nil + }, + }, + { + name: "failed to get snapshot id", + rp: &resticProvider{log: logrus.New(), extraFlags: []string{"testFlags"}}, + hookBackupFunc: func(string, string, string, map[string]string) *restic.Command { + return &restic.Command{Command: "date"} + }, + hookResticBackupFunc: func(*restic.Command, logrus.FieldLogger, uploader.ProgressUpdater) (string, string, error) { + return "", "", nil + }, + hookResticGetSnapshotIDFunc: func(*restic.Command) (string, error) { + return "test-snapshot-id", errors.New("failed to get snapshot id") + }, + errorHandleFunc: func(err error) bool { + return strings.Contains(err.Error(), "failed to get snapshot id") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - ResticBackupCMDFunc = tc.hookBackupFunc - _, _, err := rp.RunBackup(context.Background(), "var", nil, false, "", &updater) - rp.log.Infof("test name %v error %v", tc.name, err) + var err error + parentSnapshot := tc.parentSnapshot + if tc.hookBackupFunc != nil { + resticBackupCMDFunc = tc.hookBackupFunc + } + if tc.hookResticBackupFunc != nil { + resticBackupFunc = tc.hookResticBackupFunc + } + if tc.hookResticGetSnapshotFunc != nil { + resticGetSnapshotFunc = tc.hookResticGetSnapshotFunc + } + if tc.hookResticGetSnapshotIDFunc != nil { + resticGetSnapshotIDFunc = tc.hookResticGetSnapshotIDFunc + } + if !tc.nilUpdater { + updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()} + _, _, err = tc.rp.RunBackup(context.Background(), "var", nil, false, parentSnapshot, &updater) + } else { + _, _, err = tc.rp.RunBackup(context.Background(), "var", nil, false, parentSnapshot, nil) + } + + tc.rp.log.Infof("test name %v error %v", tc.name, err) require.Equal(t, true, tc.errorHandleFunc(err)) }) } } func TestResticRunRestore(t *testing.T) { - var rp resticProvider - rp.log = logrus.New() - updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()} - ResticRestoreCMDFunc = func(repoIdentifier, passwordFile, snapshotID, target string) *restic.Command { + resticRestoreCMDFunc = func(repoIdentifier, passwordFile, snapshotID, target string) *restic.Command { return &restic.Command{Args: []string{""}} } testCases := []struct { name string + rp *resticProvider + nilUpdater bool hookResticRestoreFunc func(repoIdentifier, passwordFile, snapshotID, target string) *restic.Command errorHandleFunc func(err error) bool }{ + { + name: "wrong restic execute command", + rp: &resticProvider{log: logrus.New()}, + nilUpdater: true, + errorHandleFunc: func(err error) bool { + return strings.Contains(err.Error(), "Need to initial backup progress updater first") + }, + }, + { + name: "has extral flags", + rp: &resticProvider{log: logrus.New(), extraFlags: []string{"test-extra-flags"}}, + hookResticRestoreFunc: func(repoIdentifier, passwordFile, snapshotID, target string) *restic.Command { + return &restic.Command{Args: []string{"date"}} + }, + errorHandleFunc: func(err error) bool { + return strings.Contains(err.Error(), "error running command") + }, + }, { name: "wrong restic execute command", + rp: &resticProvider{log: logrus.New()}, hookResticRestoreFunc: func(repoIdentifier, passwordFile, snapshotID, target string) *restic.Command { return &restic.Command{Args: []string{"date"}} }, errorHandleFunc: func(err error) bool { - return strings.Contains(err.Error(), "executable file not found ") + return strings.Contains(err.Error(), "error running command") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - ResticRestoreCMDFunc = tc.hookResticRestoreFunc - err := rp.RunRestore(context.Background(), "", "var", &updater) - rp.log.Infof("test name %v error %v", tc.name, err) + resticRestoreCMDFunc = tc.hookResticRestoreFunc + var err error + if !tc.nilUpdater { + updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()} + err = tc.rp.RunRestore(context.Background(), "", "var", &updater) + } else { + err = tc.rp.RunRestore(context.Background(), "", "var", nil) + } + + tc.rp.log.Infof("test name %v error %v", tc.name, err) require.Equal(t, true, tc.errorHandleFunc(err)) }) } } + +func TestClose(t *testing.T) { + t.Run("Delete existing credentials file", func(t *testing.T) { + // Create temporary files for the credentials and caCert + credentialsFile, err := ioutil.TempFile("", "credentialsFile") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(credentialsFile.Name()) + + caCertFile, err := ioutil.TempFile("", "caCertFile") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(caCertFile.Name()) + rp := &resticProvider{ + credentialsFile: credentialsFile.Name(), + caCertFile: caCertFile.Name(), + } + // Test deleting an existing credentials file + err = rp.Close(context.Background()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = os.Stat(rp.credentialsFile) + if !os.IsNotExist(err) { + t.Errorf("expected credentials file to be deleted, got error: %v", err) + } + }) + + t.Run("Delete existing caCert file", func(t *testing.T) { + // Create temporary files for the credentials and caCert + caCertFile, err := ioutil.TempFile("", "caCertFile") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(caCertFile.Name()) + rp := &resticProvider{ + credentialsFile: "", + caCertFile: "", + } + err = rp.Close(context.Background()) + // Test deleting an existing caCert file + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = os.Stat(rp.caCertFile) + if !os.IsNotExist(err) { + t.Errorf("expected caCert file to be deleted, got error: %v", err) + } + }) +} + +type MockCredentialGetter struct { + mock.Mock +} + +func (m *MockCredentialGetter) Path(selector *v1.SecretKeySelector) (string, error) { + args := m.Called(selector) + return args.Get(0).(string), args.Error(1) +} + +func TestNewResticUploaderProvider(t *testing.T) { + testCases := []struct { + name string + emptyBSL bool + mockCredFunc func(*MockCredentialGetter, *v1.SecretKeySelector) + resticCmdEnvFunc func(backupLocation *velerov1api.BackupStorageLocation, credentialFileStore credentials.FileStore) ([]string, error) + resticTempCACertFileFunc func(caCert []byte, bsl string, fs filesystem.Interface) (string, error) + checkFunc func(provider Provider, err error) + }{ + { + name: "No error in creating temp credentials file", + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("temp-credentials", nil) + }, + checkFunc: func(provider Provider, err error) { + assert.NoError(t, err) + assert.NotNil(t, provider) + }, + }, { + name: "Error in creating temp credentials file", + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("", errors.New("error creating temp credentials file")) + }, + checkFunc: func(provider Provider, err error) { + assert.Error(t, err) + assert.Nil(t, provider) + }, + }, { + name: "ObjectStorage with CACert present and creating CACert file failed", + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("temp-credentials", nil) + }, + resticTempCACertFileFunc: func(caCert []byte, bsl string, fs filesystem.Interface) (string, error) { + return "", errors.New("error writing CACert file") + }, + checkFunc: func(provider Provider, err error) { + assert.Error(t, err) + assert.Nil(t, provider) + }, + }, { + name: "Generating repository cmd failed", + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("temp-credentials", nil) + }, + resticTempCACertFileFunc: func(caCert []byte, bsl string, fs filesystem.Interface) (string, error) { + return "test-ca", nil + }, + resticCmdEnvFunc: func(backupLocation *velerov1api.BackupStorageLocation, credentialFileStore credentials.FileStore) ([]string, error) { + return nil, errors.New("error generating repository cmnd env") + }, + checkFunc: func(provider Provider, err error) { + assert.Error(t, err) + assert.Nil(t, provider) + }, + }, { + name: "New provider with not nil bsl", + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("temp-credentials", nil) + }, + resticTempCACertFileFunc: func(caCert []byte, bsl string, fs filesystem.Interface) (string, error) { + return "test-ca", nil + }, + resticCmdEnvFunc: func(backupLocation *velerov1api.BackupStorageLocation, credentialFileStore credentials.FileStore) ([]string, error) { + return nil, nil + }, + checkFunc: func(provider Provider, err error) { + assert.NoError(t, err) + assert.NotNil(t, provider) + }, + }, + { + name: "New provider with nil bsl", + emptyBSL: true, + mockCredFunc: func(credGetter *MockCredentialGetter, repoKeySelector *v1.SecretKeySelector) { + credGetter.On("Path", repoKeySelector).Return("temp-credentials", nil) + }, + resticTempCACertFileFunc: func(caCert []byte, bsl string, fs filesystem.Interface) (string, error) { + return "test-ca", nil + }, + resticCmdEnvFunc: func(backupLocation *velerov1api.BackupStorageLocation, credentialFileStore credentials.FileStore) ([]string, error) { + return nil, nil + }, + checkFunc: func(provider Provider, err error) { + assert.NoError(t, err) + assert.NotNil(t, provider) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + repoIdentifier := "my-repo" + bsl := &velerov1api.BackupStorageLocation{} + if !tc.emptyBSL { + bsl = builder.ForBackupStorageLocation("test-ns", "test-name").CACert([]byte("my-cert")).Result() + } + credGetter := &credentials.CredentialGetter{} + repoKeySelector := &v1.SecretKeySelector{} + log := logrus.New() + + // Mock CredentialGetter + mockCredGetter := &MockCredentialGetter{} + credGetter.FromFile = mockCredGetter + tc.mockCredFunc(mockCredGetter, repoKeySelector) + if tc.resticCmdEnvFunc != nil { + resticCmdEnvFunc = tc.resticCmdEnvFunc + } + if tc.resticTempCACertFileFunc != nil { + resticTempCACertFileFunc = tc.resticTempCACertFileFunc + } + tc.checkFunc(NewResticUploaderProvider(repoIdentifier, bsl, credGetter, repoKeySelector, log)) + }) + } +}