Add UT for uploader (#6374)

Signed-off-by: Ming <mqiu@vmware.com>
This commit is contained in:
qiuming
2023-06-13 16:58:47 +08:00
committed by GitHub
parent 8cd55d1826
commit 78025a09b6
10 changed files with 1527 additions and 56 deletions

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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, realSource string,
func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource 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")
@@ -254,7 +257,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, log logrus.FieldLogger) ([]*snapshot.Manifest, error) {
man, err := snapshot.ListSnapshots(ctx, rep, sourceInfo)
man, err := listSnapshotsFunc(ctx, rep, sourceInfo)
if err != nil {
return nil, err
}
@@ -312,7 +315,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)
}
@@ -335,7 +338,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,

View File

@@ -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,545 @@ 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, logrus.New())
// 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"),
},
}
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 successful",
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)
})
}
}

View File

@@ -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)

View File

@@ -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, realSource 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, realSource 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, realSource 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, realSource 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, realSource 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, realSource 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, realSource 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, realSource 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,227 @@ 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{}
mockCredGetter.On("Get", mock.Anything).Return("test", nil)
return mockCredGetter
}(),
mockBackupRepoService: func() udmrepo.BackupRepoService {
backupRepoService := &udmrepomocks.BackupRepoService{}
var backupRepo udmrepo.BackupRepo
backupRepoService.On("Open", context.Background(), mock.Anything).Return(backupRepo, nil)
return backupRepoService
}(),
expectedError: "",
},
{
name: "Error to get repo options",
mockCredGetter: func() *mocks.SecretStore {
mockCredGetter := &mocks.SecretStore{}
mockCredGetter.On("Get", mock.Anything).Return("test", errors.New("failed to get password"))
return mockCredGetter
}(),
mockBackupRepoService: func() udmrepo.BackupRepoService {
backupRepoService := &udmrepomocks.BackupRepoService{}
var backupRepo udmrepo.BackupRepo
backupRepoService.On("Open", context.Background(), mock.Anything).Return(backupRepo, nil)
return backupRepoService
}(),
expectedError: "error to get repo options",
},
{
name: "Error open repository service",
mockCredGetter: func() *mocks.SecretStore {
mockCredGetter := &mocks.SecretStore{}
mockCredGetter.On("Get", mock.Anything).Return("test", nil)
return mockCredGetter
}(),
mockBackupRepoService: func() udmrepo.BackupRepoService {
backupRepoService := &udmrepomocks.BackupRepoService{}
var backupRepo udmrepo.BackupRepo
backupRepoService.On("Open", context.Background(), mock.Anything).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)
})
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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")
}
@@ -134,7 +139,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 {
@@ -145,7 +150,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)
@@ -154,13 +159,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))
}
@@ -183,7 +188,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 {

View File

@@ -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", "", map[string]string{}, false, parentSnapshot, &updater)
} else {
_, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, 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",
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(), "executable file not found ")
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(), "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))
})
}
}