/* 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" "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" ) type snapshotMockes struct { policyMock *uploadermocks.Policy snapshotMock *uploadermocks.Snapshot uploderMock *uploadermocks.Uploader repoWriterMock *repomocks.RepositoryWriter } type mockArgs struct { methodName string returns []interface{} } func injectSnapshotFuncs() *snapshotMockes { s := &snapshotMockes{ policyMock: &uploadermocks.Policy{}, snapshotMock: &uploadermocks.Snapshot{}, uploderMock: &uploadermocks.Uploader{}, repoWriterMock: &repomocks.RepositoryWriter{}, } applyRetentionPolicyFunc = s.policyMock.ApplyRetentionPolicy loadSnapshotFunc = s.snapshotMock.LoadSnapshot saveSnapshotFunc = s.snapshotMock.SaveSnapshot return s } func MockFuncs(s *snapshotMockes, args []mockArgs) { s.snapshotMock.On("LoadSnapshot", mock.Anything, mock.Anything, mock.Anything).Return(args[0].returns...) s.snapshotMock.On("SaveSnapshot", mock.Anything, mock.Anything, mock.Anything).Return(args[1].returns...) s.policyMock.On("TreeForSource", mock.Anything, mock.Anything, mock.Anything).Return(args[2].returns...) s.policyMock.On("ApplyRetentionPolicy", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(args[3].returns...) s.policyMock.On("SetPolicy", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(args[4].returns...) s.uploderMock.On("Upload", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(args[5].returns...) s.repoWriterMock.On("Flush", mock.Anything).Return(args[6].returns...) } func TestSnapshotSource(t *testing.T) { ctx := context.TODO() sourceInfo := snapshot.SourceInfo{ UserName: "testUserName", Host: "testHost", Path: "/var", } rootDir, err := getLocalFSEntry(sourceInfo.Path) assert.NoError(t, err) log := logrus.New() manifest := &snapshot.Manifest{ ID: "test", RootEntry: &snapshot.DirEntry{}, } testCases := []struct { name string args []mockArgs notError bool }{ { name: "regular test", 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}}, }, notError: true, }, { name: "failed to load snapshot", args: []mockArgs{ {methodName: "LoadSnapshot", returns: []interface{}{manifest, errors.New("failed to load snapshot")}}, {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}}, }, notError: false, }, { name: "failed to save snapshot", args: []mockArgs{ {methodName: "LoadSnapshot", returns: []interface{}{manifest, nil}}, {methodName: "SaveSnapshot", returns: []interface{}{manifest.ID, errors.New("failed to save snapshot")}}, {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}}, }, notError: false, }, { name: "failed to apply policy", 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, errors.New("failed to save snapshot")}}, {methodName: "SetPolicy", returns: []interface{}{nil}}, {methodName: "Upload", returns: []interface{}{manifest, nil}}, {methodName: "Flush", returns: []interface{}{nil}}, }, notError: false, }, { name: "failed to upload snapshot", 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, errors.New("failed to upload snapshot")}}, {methodName: "Flush", returns: []interface{}{nil}}, }, notError: false, }, { name: "failed to flush repo", args: []mockArgs{ {methodName: "LoadSnapshot", returns: []interface{}{manifest, nil}}, {methodName: "SaveSnapshot", returns: []interface{}{manifest.ID, errors.New("failed to save snapshot")}}, {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{}{errors.New("failed to flush repo")}}, }, notError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := injectSnapshotFuncs() MockFuncs(s, tc.args) _, _, err = SnapshotSource(ctx, s.repoWriterMock, s.uploderMock, sourceInfo, rootDir, false, "/", nil, log, "TestSnapshotSource") if tc.notError { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } 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.SnapshotRequesterTag: "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.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value", "anotherCustomTag": "123", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, }, }, nil }, expectedSnapshots: []*snapshot.Manifest{ { Tags: map[string]string{ uploader.SnapshotRequesterTag: "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.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value1", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, }, { Tags: map[string]string{ uploader.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value2", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, }, }, nil }, expectedSnapshots: []*snapshot.Manifest{ { Tags: map[string]string{ uploader.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value1", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, }, }, expectedError: nil, }, // Snapshot with different requester { name: "Snapshot with different requester", listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { return []*snapshot.Manifest{ { Tags: map[string]string{ uploader.SnapshotRequesterTag: "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.SnapshotRequesterTag: "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.SnapshotRequesterTag: "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.SnapshotRequesterTag: "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.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value1", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, }, { Tags: map[string]string{ uploader.SnapshotRequesterTag: "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.SnapshotRequesterTag: "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.SnapshotRequesterTag: "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value3", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, StartTime: fs.UTCTimestampFromTime(time.Now().Add(-time.Hour)), }, }, expectedError: nil, }, // Snapshot with manifest SnapshotRequesterTag not found { name: "Snapshot with manifest SnapshotRequesterTag not found", listSnapshotsFunc: func(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) ([]*snapshot.Manifest, error) { return []*snapshot.Manifest{ { Tags: map[string]string{ "requester": "user1", uploader.SnapshotUploaderTag: "uploader1", "otherTag": "value", "snapshotRequestor": "user1", "snapshotUploader": "uploader1", }, IncompleteReason: "reason", }, }, nil }, expectedSnapshots: []*snapshot.Manifest{}, expectedError: nil, }, // Snapshot with manifest SnapshotRequesterTag 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.SnapshotRequesterTag: "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) }) } }