From 38d5003c6b551b4115f131a5adf11e1983c9f3b3 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 29 Jun 2023 11:17:40 +0800 Subject: [PATCH] add ut for pkg/repository Signed-off-by: Lyndon-Li --- pkg/repository/provider/unified_repo_test.go | 464 +++++++++++++++++ .../udmrepo/kopialib/backend/common_test.go | 198 +++++++ .../kopialib/backend/file_system_test.go | 80 +++ .../udmrepo/kopialib/backend/gcs_test.go | 47 +- .../udmrepo/kopialib/backend/mocks/Reader.go | 101 ++++ .../udmrepo/kopialib/backend/mocks/Writer.go | 114 ++++ .../udmrepo/kopialib/backend/s3_test.go | 83 ++- .../udmrepo/kopialib/lib_repo_test.go | 485 ++++++++++++++++++ 8 files changed, 1566 insertions(+), 6 deletions(-) create mode 100644 pkg/repository/udmrepo/kopialib/backend/common_test.go create mode 100644 pkg/repository/udmrepo/kopialib/backend/file_system_test.go create mode 100644 pkg/repository/udmrepo/kopialib/backend/mocks/Reader.go create mode 100644 pkg/repository/udmrepo/kopialib/backend/mocks/Writer.go diff --git a/pkg/repository/provider/unified_repo_test.go b/pkg/repository/provider/unified_repo_test.go index 80b2611df..8ca73127b 100644 --- a/pkg/repository/provider/unified_repo_test.go +++ b/pkg/repository/provider/unified_repo_test.go @@ -887,3 +887,467 @@ func TestForget(t *testing.T) { }) } } + +func TestInitRepo(t *testing.T) { + testCases := []struct { + name string + funcTable localFuncTable + getter *credmock.SecretStore + repoService *reposervicenmocks.BackupRepoService + retFuncInit interface{} + credStoreReturn string + credStoreError error + expectedErr string + }{ + { + name: "get repo option fail", + expectedErr: "error to get repo options: error to get repo password: invalid credentials interface", + }, + { + name: "repo init fail", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return errors.New("fake-error-1") + }, + expectedErr: "error to init backup repo: fake-error-1", + }, + { + name: "succeed", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + funcTable = tc.funcTable + + var secretStore velerocredentials.SecretStore + if tc.getter != nil { + tc.getter.On("Get", mock.Anything, mock.Anything).Return(tc.credStoreReturn, tc.credStoreError) + secretStore = tc.getter + } + + urp := unifiedRepoProvider{ + credentialGetter: velerocredentials.CredentialGetter{ + FromSecret: secretStore, + }, + repoService: tc.repoService, + log: velerotest.NewLogger(), + } + + if tc.repoService != nil { + tc.repoService.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(tc.retFuncInit) + } + + err := urp.InitRepo(context.Background(), RepoParam{ + BackupLocation: &velerov1api.BackupStorageLocation{}, + BackupRepo: &velerov1api.BackupRepository{}, + }) + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestConnectToRepo(t *testing.T) { + testCases := []struct { + name string + funcTable localFuncTable + getter *credmock.SecretStore + repoService *reposervicenmocks.BackupRepoService + retFuncInit interface{} + credStoreReturn string + credStoreError error + expectedErr string + }{ + { + name: "get repo option fail", + expectedErr: "error to get repo options: error to get repo password: invalid credentials interface", + }, + { + name: "repo init fail", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return errors.New("fake-error-1") + }, + expectedErr: "error to connect backup repo: fake-error-1", + }, + { + name: "succeed", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + funcTable = tc.funcTable + + var secretStore velerocredentials.SecretStore + if tc.getter != nil { + tc.getter.On("Get", mock.Anything, mock.Anything).Return(tc.credStoreReturn, tc.credStoreError) + secretStore = tc.getter + } + + urp := unifiedRepoProvider{ + credentialGetter: velerocredentials.CredentialGetter{ + FromSecret: secretStore, + }, + repoService: tc.repoService, + log: velerotest.NewLogger(), + } + + if tc.repoService != nil { + tc.repoService.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(tc.retFuncInit) + } + + err := urp.ConnectToRepo(context.Background(), RepoParam{ + BackupLocation: &velerov1api.BackupStorageLocation{}, + BackupRepo: &velerov1api.BackupRepository{}, + }) + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestBoostRepoConnect(t *testing.T) { + var backupRepo *reposervicenmocks.BackupRepo + + testCases := []struct { + name string + funcTable localFuncTable + getter *credmock.SecretStore + repoService *reposervicenmocks.BackupRepoService + backupRepo *reposervicenmocks.BackupRepo + retFuncInit interface{} + retFuncOpen []interface{} + credStoreReturn string + credStoreError error + expectedErr string + }{ + { + name: "get repo option fail", + expectedErr: "error to get repo options: error to get repo password: invalid credentials interface", + }, + { + name: "repo not opened and connect fail", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncOpen: []interface{}{ + func(context.Context, udmrepo.RepoOptions) udmrepo.BackupRepo { + return backupRepo + }, + + func(context.Context, udmrepo.RepoOptions) error { + return errors.New("fake-error-1") + }, + }, + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return errors.New("fake-error-2") + }, + expectedErr: "error to connect backup repo: fake-error-2", + }, + { + name: "repo not opened and connect succeed", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncOpen: []interface{}{ + func(context.Context, udmrepo.RepoOptions) udmrepo.BackupRepo { + return backupRepo + }, + + func(context.Context, udmrepo.RepoOptions) error { + return errors.New("fake-error-1") + }, + }, + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return nil + }, + }, + { + name: "repo is opened", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + backupRepo: new(reposervicenmocks.BackupRepo), + retFuncOpen: []interface{}{ + func(context.Context, udmrepo.RepoOptions) udmrepo.BackupRepo { + return backupRepo + }, + + func(context.Context, udmrepo.RepoOptions) error { + return nil + }, + }, + retFuncInit: func(context.Context, udmrepo.RepoOptions, bool) error { + return nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + funcTable = tc.funcTable + + var secretStore velerocredentials.SecretStore + if tc.getter != nil { + tc.getter.On("Get", mock.Anything, mock.Anything).Return(tc.credStoreReturn, tc.credStoreError) + secretStore = tc.getter + } + + urp := unifiedRepoProvider{ + credentialGetter: velerocredentials.CredentialGetter{ + FromSecret: secretStore, + }, + repoService: tc.repoService, + log: velerotest.NewLogger(), + } + + backupRepo = tc.backupRepo + + if tc.repoService != nil { + tc.repoService.On("Open", mock.Anything, mock.Anything).Return(tc.retFuncOpen[0], tc.retFuncOpen[1]) + tc.repoService.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(tc.retFuncInit) + } + + if tc.backupRepo != nil { + backupRepo.On("Close", mock.Anything).Return(nil) + } + + err := urp.BoostRepoConnect(context.Background(), RepoParam{ + BackupLocation: &velerov1api.BackupStorageLocation{}, + BackupRepo: &velerov1api.BackupRepository{}, + }) + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestPruneRepo(t *testing.T) { + testCases := []struct { + name string + funcTable localFuncTable + getter *credmock.SecretStore + repoService *reposervicenmocks.BackupRepoService + retFuncMaintain interface{} + credStoreReturn string + credStoreError error + expectedErr string + }{ + { + name: "get repo option fail", + expectedErr: "error to get repo options: error to get repo password: invalid credentials interface", + }, + { + name: "repo maintain fail", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncMaintain: func(context.Context, udmrepo.RepoOptions) error { + return errors.New("fake-error-1") + }, + expectedErr: "error to prune backup repo: fake-error-1", + }, + { + name: "succeed", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + retFuncMaintain: func(context.Context, udmrepo.RepoOptions) error { + return nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + funcTable = tc.funcTable + + var secretStore velerocredentials.SecretStore + if tc.getter != nil { + tc.getter.On("Get", mock.Anything, mock.Anything).Return(tc.credStoreReturn, tc.credStoreError) + secretStore = tc.getter + } + + urp := unifiedRepoProvider{ + credentialGetter: velerocredentials.CredentialGetter{ + FromSecret: secretStore, + }, + repoService: tc.repoService, + log: velerotest.NewLogger(), + } + + if tc.repoService != nil { + tc.repoService.On("Maintain", mock.Anything, mock.Anything).Return(tc.retFuncMaintain) + } + + err := urp.PruneRepo(context.Background(), RepoParam{ + BackupLocation: &velerov1api.BackupStorageLocation{}, + BackupRepo: &velerov1api.BackupRepository{}, + }) + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestGetStorageType(t *testing.T) { + testCases := []struct { + name string + backupLocation *velerov1api.BackupStorageLocation + expectedRet string + }{ + { + name: "wrong backend type", + backupLocation: &velerov1api.BackupStorageLocation{}, + }, + { + name: "aws provider", + backupLocation: &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "velero.io/aws", + }, + }, + expectedRet: "s3", + }, + { + name: "azure provider", + backupLocation: &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "velero.io/azure", + }, + }, + expectedRet: "azure", + }, + { + name: "gcp provider", + backupLocation: &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "velero.io/gcp", + }, + }, + expectedRet: "gcs", + }, + { + name: "fs provider", + backupLocation: &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "velero.io/fs", + }, + }, + expectedRet: "filesystem", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ret := getStorageType(tc.backupLocation) + assert.Equal(t, tc.expectedRet, ret) + }) + } +} diff --git a/pkg/repository/udmrepo/kopialib/backend/common_test.go b/pkg/repository/udmrepo/kopialib/backend/common_test.go new file mode 100644 index 000000000..daf6e8479 --- /dev/null +++ b/pkg/repository/udmrepo/kopialib/backend/common_test.go @@ -0,0 +1,198 @@ +/* +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 backend + +import ( + "context" + "testing" + "time" + + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/content" + "github.com/kopia/kopia/repo/encryption" + "github.com/kopia/kopia/repo/format" + "github.com/kopia/kopia/repo/hashing" + "github.com/kopia/kopia/repo/splitter" + "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" +) + +func TestSetupNewRepositoryOptions(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + expected repo.NewRepositoryOptions + }{ + { + name: "with hash algo", + flags: map[string]string{ + udmrepo.StoreOptionGenHashAlgo: "fake-hash", + }, + expected: repo.NewRepositoryOptions{ + BlockFormat: format.ContentFormat{ + Hash: "fake-hash", + Encryption: encryption.DefaultAlgorithm, + }, + ObjectFormat: format.ObjectFormat{ + Splitter: splitter.DefaultAlgorithm, + }, + }, + }, + { + name: "with encrypt algo", + flags: map[string]string{ + udmrepo.StoreOptionGenEncryptAlgo: "fake-encrypt", + }, + expected: repo.NewRepositoryOptions{ + BlockFormat: format.ContentFormat{ + Hash: hashing.DefaultAlgorithm, + Encryption: "fake-encrypt", + }, + ObjectFormat: format.ObjectFormat{ + Splitter: splitter.DefaultAlgorithm, + }, + }, + }, + { + name: "with splitter algo", + flags: map[string]string{ + udmrepo.StoreOptionGenSplitAlgo: "fake-splitter", + }, + expected: repo.NewRepositoryOptions{ + BlockFormat: format.ContentFormat{ + Hash: hashing.DefaultAlgorithm, + Encryption: encryption.DefaultAlgorithm, + }, + ObjectFormat: format.ObjectFormat{ + Splitter: "fake-splitter", + }, + }, + }, + { + name: "with retention algo", + flags: map[string]string{ + udmrepo.StoreOptionGenRetentionMode: "fake-retention-mode", + }, + expected: repo.NewRepositoryOptions{ + BlockFormat: format.ContentFormat{ + Hash: hashing.DefaultAlgorithm, + Encryption: encryption.DefaultAlgorithm, + }, + ObjectFormat: format.ObjectFormat{ + Splitter: splitter.DefaultAlgorithm, + }, + RetentionMode: "fake-retention-mode", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ret := SetupNewRepositoryOptions(context.Background(), tc.flags) + assert.Equal(t, tc.expected, ret) + }) + } +} + +func TestSetupConnectOptions(t *testing.T) { + defaultCacheOption := content.CachingOptions{ + MaxCacheSizeBytes: 2000 << 20, + MaxMetadataCacheSizeBytes: 2000 << 20, + MaxListCacheDuration: content.DurationSeconds(time.Duration(30) * time.Second), + } + + testCases := []struct { + name string + repoOptions udmrepo.RepoOptions + expected repo.ConnectOptions + }{ + { + name: "with domain", + repoOptions: udmrepo.RepoOptions{ + GeneralOptions: map[string]string{ + udmrepo.GenOptionOwnerDomain: "fake-domain", + }, + }, + expected: repo.ConnectOptions{ + CachingOptions: defaultCacheOption, + ClientOptions: repo.ClientOptions{ + Hostname: "fake-domain", + }, + }, + }, + { + name: "with username", + repoOptions: udmrepo.RepoOptions{ + GeneralOptions: map[string]string{ + udmrepo.GenOptionOwnerName: "fake-user", + }, + }, + expected: repo.ConnectOptions{ + CachingOptions: defaultCacheOption, + ClientOptions: repo.ClientOptions{ + Username: "fake-user", + }, + }, + }, + { + name: "with wrong readonly", + repoOptions: udmrepo.RepoOptions{ + GeneralOptions: map[string]string{ + udmrepo.StoreOptionGenReadOnly: "fake-bool", + }, + }, + expected: repo.ConnectOptions{ + CachingOptions: defaultCacheOption, + ClientOptions: repo.ClientOptions{}, + }, + }, + { + name: "with correct readonly", + repoOptions: udmrepo.RepoOptions{ + GeneralOptions: map[string]string{ + udmrepo.StoreOptionGenReadOnly: "true", + }, + }, + expected: repo.ConnectOptions{ + CachingOptions: defaultCacheOption, + ClientOptions: repo.ClientOptions{ + ReadOnly: true, + }, + }, + }, + { + name: "with description", + repoOptions: udmrepo.RepoOptions{ + Description: "fake-description", + }, + expected: repo.ConnectOptions{ + CachingOptions: defaultCacheOption, + ClientOptions: repo.ClientOptions{ + Description: "fake-description", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ret := SetupConnectOptions(context.Background(), tc.repoOptions) + assert.Equal(t, tc.expected, ret) + }) + } +} diff --git a/pkg/repository/udmrepo/kopialib/backend/file_system_test.go b/pkg/repository/udmrepo/kopialib/backend/file_system_test.go new file mode 100644 index 000000000..fe9b8e624 --- /dev/null +++ b/pkg/repository/udmrepo/kopialib/backend/file_system_test.go @@ -0,0 +1,80 @@ +/* +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 backend + +import ( + "context" + "testing" + + "github.com/kopia/kopia/repo/blob/filesystem" + "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" +) + +func TestFSSetup(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + expectedOptions filesystem.Options + expectedErr string + }{ + { + name: "must have fs path", + flags: map[string]string{}, + expectedErr: "key " + udmrepo.StoreOptionFsPath + " not found", + }, + { + name: "with fs path only", + flags: map[string]string{ + udmrepo.StoreOptionFsPath: "fake/path", + }, + expectedOptions: filesystem.Options{ + Path: "fake/path", + FileMode: 0o600, + DirectoryMode: 0o700, + }, + }, + { + name: "with prefix", + flags: map[string]string{ + udmrepo.StoreOptionFsPath: "fake/path", + udmrepo.StoreOptionPrefix: "fake-prefix", + }, + expectedOptions: filesystem.Options{ + Path: "fake/path/fake-prefix", + FileMode: 0o600, + DirectoryMode: 0o700, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fsFlags := FsBackend{} + + err := fsFlags.Setup(context.Background(), tc.flags) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedOptions, fsFlags.options) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} diff --git a/pkg/repository/udmrepo/kopialib/backend/gcs_test.go b/pkg/repository/udmrepo/kopialib/backend/gcs_test.go index 7abdcab3e..759e9baae 100644 --- a/pkg/repository/udmrepo/kopialib/backend/gcs_test.go +++ b/pkg/repository/udmrepo/kopialib/backend/gcs_test.go @@ -20,6 +20,7 @@ import ( "context" "testing" + "github.com/kopia/kopia/repo/blob/gcs" "github.com/stretchr/testify/assert" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" @@ -27,9 +28,10 @@ import ( func TestGcsSetup(t *testing.T) { testCases := []struct { - name string - flags map[string]string - expectedErr string + name string + flags map[string]string + expectedOptions gcs.Options + expectedErr string }{ { name: "must have bucket name", @@ -43,6 +45,44 @@ func TestGcsSetup(t *testing.T) { }, expectedErr: "key " + udmrepo.StoreOptionCredentialFile + " not found", }, + { + name: "with prefix", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionCredentialFile: "fake-credential", + udmrepo.StoreOptionPrefix: "fake-prefix", + }, + expectedOptions: gcs.Options{ + BucketName: "fake-bucket", + ServiceAccountCredentialsFile: "fake-credential", + Prefix: "fake-prefix", + }, + }, + { + name: "with wrong readonly", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionCredentialFile: "fake-credential", + udmrepo.StoreOptionGcsReadonly: "fake-bool", + }, + expectedOptions: gcs.Options{ + BucketName: "fake-bucket", + ServiceAccountCredentialsFile: "fake-credential", + }, + }, + { + name: "with correct readonly", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionCredentialFile: "fake-credential", + udmrepo.StoreOptionGcsReadonly: "true", + }, + expectedOptions: gcs.Options{ + BucketName: "fake-bucket", + ServiceAccountCredentialsFile: "fake-credential", + ReadOnly: true, + }, + }, } for _, tc := range testCases { @@ -53,6 +93,7 @@ func TestGcsSetup(t *testing.T) { if tc.expectedErr == "" { assert.NoError(t, err) + assert.Equal(t, tc.expectedOptions, gcsFlags.options) } else { assert.EqualError(t, err, tc.expectedErr) } diff --git a/pkg/repository/udmrepo/kopialib/backend/mocks/Reader.go b/pkg/repository/udmrepo/kopialib/backend/mocks/Reader.go new file mode 100644 index 000000000..8efe8ee66 --- /dev/null +++ b/pkg/repository/udmrepo/kopialib/backend/mocks/Reader.go @@ -0,0 +1,101 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Reader is an autogenerated mock type for the Reader type +type Reader struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *Reader) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Length provides a mock function with given fields: +func (_m *Reader) Length() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Read provides a mock function with given fields: p +func (_m *Reader) Read(p []byte) (int, error) { + ret := _m.Called(p) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Seek provides a mock function with given fields: offset, whence +func (_m *Reader) Seek(offset int64, whence int) (int64, error) { + ret := _m.Called(offset, whence) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(int64, int) (int64, error)); ok { + return rf(offset, whence) + } + if rf, ok := ret.Get(0).(func(int64, int) int64); ok { + r0 = rf(offset, whence) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(int64, int) error); ok { + r1 = rf(offset, whence) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewReader(t mockConstructorTestingTNewReader) *Reader { + mock := &Reader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/repository/udmrepo/kopialib/backend/mocks/Writer.go b/pkg/repository/udmrepo/kopialib/backend/mocks/Writer.go new file mode 100644 index 000000000..21f66334e --- /dev/null +++ b/pkg/repository/udmrepo/kopialib/backend/mocks/Writer.go @@ -0,0 +1,114 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + object "github.com/kopia/kopia/repo/object" + mock "github.com/stretchr/testify/mock" +) + +// Writer is an autogenerated mock type for the Writer type +type Writer struct { + mock.Mock +} + +// Checkpoint provides a mock function with given fields: +func (_m *Writer) Checkpoint() (object.ID, error) { + ret := _m.Called() + + var r0 object.ID + var r1 error + if rf, ok := ret.Get(0).(func() (object.ID, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() object.ID); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(object.ID) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Close provides a mock function with given fields: +func (_m *Writer) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Result provides a mock function with given fields: +func (_m *Writer) Result() (object.ID, error) { + ret := _m.Called() + + var r0 object.ID + var r1 error + if rf, ok := ret.Get(0).(func() (object.ID, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() object.ID); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(object.ID) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Write provides a mock function with given fields: p +func (_m *Writer) Write(p []byte) (int, error) { + ret := _m.Called(p) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewWriter interface { + mock.TestingT + Cleanup(func()) +} + +// NewWriter creates a new instance of Writer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewWriter(t mockConstructorTestingTNewWriter) *Writer { + mock := &Writer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/repository/udmrepo/kopialib/backend/s3_test.go b/pkg/repository/udmrepo/kopialib/backend/s3_test.go index c1f5e036b..43a761688 100644 --- a/pkg/repository/udmrepo/kopialib/backend/s3_test.go +++ b/pkg/repository/udmrepo/kopialib/backend/s3_test.go @@ -20,6 +20,7 @@ import ( "context" "testing" + "github.com/kopia/kopia/repo/blob/s3" "github.com/stretchr/testify/assert" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" @@ -27,15 +28,91 @@ import ( func TestS3Setup(t *testing.T) { testCases := []struct { - name string - flags map[string]string - expectedErr string + name string + flags map[string]string + expectedOptions s3.Options + expectedErr string }{ { name: "must have bucket name", flags: map[string]string{}, expectedErr: "key " + udmrepo.StoreOptionOssBucket + " not found", }, + { + name: "with bucket only", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + }, + }, + { + name: "with others", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionS3KeyID: "fake-ak", + udmrepo.StoreOptionS3SecretKey: "fake-sk", + udmrepo.StoreOptionS3Endpoint: "fake-endpoint", + udmrepo.StoreOptionOssRegion: "fake-region", + udmrepo.StoreOptionPrefix: "fake-prefix", + udmrepo.StoreOptionS3Token: "fake-token", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + AccessKeyID: "fake-ak", + SecretAccessKey: "fake-sk", + Endpoint: "fake-endpoint", + Region: "fake-region", + Prefix: "fake-prefix", + SessionToken: "fake-token", + }, + }, + { + name: "with wrong tls", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionS3DisableTLS: "fake-bool", + udmrepo.StoreOptionS3DisableTLSVerify: "fake-bool", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + }, + }, + { + name: "with correct tls", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionS3DisableTLS: "true", + udmrepo.StoreOptionS3DisableTLSVerify: "false", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + DoNotUseTLS: true, + DoNotVerifyTLS: false, + }, + }, + { + name: "with wrong ca", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionS3CustomCA: "fake-base-64", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + }, + }, + { + name: "with correct ca", + flags: map[string]string{ + udmrepo.StoreOptionOssBucket: "fake-bucket", + udmrepo.StoreOptionS3CustomCA: "ZmFrZS1jYQ==", + }, + expectedOptions: s3.Options{ + BucketName: "fake-bucket", + RootCA: []byte{'f', 'a', 'k', 'e', '-', 'c', 'a'}, + }, + }, } for _, tc := range testCases { diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 8c01827ae..e444c20d6 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -18,12 +18,14 @@ package kopialib import ( "context" + "math" "os" "testing" "time" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/manifest" + "github.com/kopia/kopia/repo/object" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -713,3 +715,486 @@ func TestFlush(t *testing.T) { }) } } + +func TestNewObjectWriter(t *testing.T) { + rawObjWriter := repomocks.NewWriter(t) + testCases := []struct { + name string + rawWriter *repomocks.DirectRepositoryWriter + rawWriterRet object.Writer + expectedRet udmrepo.ObjectWriter + }{ + { + name: "raw writer is nil", + }, + { + name: "new object writer fail", + rawWriter: repomocks.NewDirectRepositoryWriter(t), + }, + { + name: "succeed", + rawWriter: repomocks.NewDirectRepositoryWriter(t), + rawWriterRet: rawObjWriter, + expectedRet: &kopiaObjectWriter{rawWriter: rawObjWriter}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaRepository{} + + if tc.rawWriter != nil { + tc.rawWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(tc.rawWriterRet) + kr.rawWriter = tc.rawWriter + } + + ret := kr.NewObjectWriter(context.Background(), udmrepo.ObjectWriteOptions{}) + + assert.Equal(t, tc.expectedRet, ret) + }) + } +} + +func TestUpdateProgress(t *testing.T) { + testCases := []struct { + name string + progress int64 + uploaded int64 + throttle logThrottle + logMessage string + }{ + { + name: "should not output", + throttle: logThrottle{ + lastTime: math.MaxInt64, + }, + }, + { + name: "should output", + progress: 100, + uploaded: 200, + logMessage: "Repo uploaded 300 bytes.", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logMessage := "" + kr := &kopiaRepository{ + logger: velerotest.NewSingleLogger(&logMessage), + throttle: tc.throttle, + uploaded: tc.uploaded, + } + + kr.updateProgress(tc.progress) + + if len(tc.logMessage) > 0 { + assert.Contains(t, logMessage, tc.logMessage) + } else { + assert.Equal(t, "", logMessage) + } + }) + } +} + +func TestReaderRead(t *testing.T) { + testCases := []struct { + name string + rawObjReader *repomocks.Reader + rawReaderRetErr error + expectedErr string + }{ + { + name: "raw reader is nil", + expectedErr: "object reader is closed or not open", + }, + { + name: "raw read fail", + rawObjReader: repomocks.NewReader(t), + rawReaderRetErr: errors.New("fake-read-error"), + expectedErr: "fake-read-error", + }, + { + name: "succeed", + rawObjReader: repomocks.NewReader(t), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectReader{} + + if tc.rawObjReader != nil { + tc.rawObjReader.On("Read", mock.Anything).Return(0, tc.rawReaderRetErr) + kr.rawReader = tc.rawObjReader + } + + _, err := kr.Read(nil) + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestReaderSeek(t *testing.T) { + testCases := []struct { + name string + rawObjReader *repomocks.Reader + rawReaderRet int64 + rawReaderRetErr error + expectedRet int64 + expectedErr string + }{ + { + name: "raw reader is nil", + expectedErr: "object reader is closed or not open", + }, + { + name: "raw seek fail", + rawObjReader: repomocks.NewReader(t), + rawReaderRetErr: errors.New("fake-seek-error"), + expectedErr: "fake-seek-error", + }, + { + name: "succeed", + rawObjReader: repomocks.NewReader(t), + rawReaderRet: 100, + expectedRet: 100, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectReader{} + + if tc.rawObjReader != nil { + tc.rawObjReader.On("Seek", mock.Anything, mock.Anything).Return(tc.rawReaderRet, tc.rawReaderRetErr) + kr.rawReader = tc.rawObjReader + } + + ret, err := kr.Seek(0, 0) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedRet, ret) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestReaderClose(t *testing.T) { + testCases := []struct { + name string + rawObjReader *repomocks.Reader + rawReaderRetErr error + expectedErr string + }{ + { + name: "raw reader is nil", + }, + { + name: "raw close fail", + rawObjReader: repomocks.NewReader(t), + rawReaderRetErr: errors.New("fake-close-error"), + expectedErr: "fake-close-error", + }, + { + name: "succeed", + rawObjReader: repomocks.NewReader(t), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectReader{} + + if tc.rawObjReader != nil { + tc.rawObjReader.On("Close").Return(tc.rawReaderRetErr) + kr.rawReader = tc.rawObjReader + } + + err := kr.Close() + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestReaderLength(t *testing.T) { + testCases := []struct { + name string + rawObjReader *repomocks.Reader + rawReaderRet int64 + expectedRet int64 + }{ + { + name: "raw reader is nil", + expectedRet: -1, + }, + { + name: "raw length fail", + rawObjReader: repomocks.NewReader(t), + rawReaderRet: 0, + expectedRet: 0, + }, + { + name: "succeed", + rawObjReader: repomocks.NewReader(t), + rawReaderRet: 200, + expectedRet: 200, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectReader{} + + if tc.rawObjReader != nil { + tc.rawObjReader.On("Length").Return(tc.rawReaderRet) + kr.rawReader = tc.rawObjReader + } + + ret := kr.Length() + + assert.Equal(t, tc.expectedRet, ret) + }) + } +} + +func TestWriterWrite(t *testing.T) { + testCases := []struct { + name string + rawObjWriter *repomocks.Writer + rawWrtierRet int + rawWriterRetErr error + expectedRet int + expectedErr string + }{ + { + name: "raw writer is nil", + expectedErr: "object writer is closed or not open", + }, + { + name: "raw read fail", + rawObjWriter: repomocks.NewWriter(t), + rawWriterRetErr: errors.New("fake-write-error"), + expectedErr: "fake-write-error", + }, + { + name: "succeed", + rawObjWriter: repomocks.NewWriter(t), + rawWrtierRet: 200, + expectedRet: 200, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectWriter{} + + if tc.rawObjWriter != nil { + tc.rawObjWriter.On("Write", mock.Anything).Return(tc.rawWrtierRet, tc.rawWriterRetErr) + kr.rawWriter = tc.rawObjWriter + } + + ret, err := kr.Write(nil) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedRet, ret) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestWriterCheckpoint(t *testing.T) { + testCases := []struct { + name string + rawObjWriter *repomocks.Writer + rawWrtierRet object.ID + rawWriterRetErr error + expectedRet udmrepo.ID + expectedErr string + }{ + { + name: "raw writer is nil", + expectedErr: "object writer is closed or not open", + }, + { + name: "raw checkpoint fail", + rawObjWriter: repomocks.NewWriter(t), + rawWriterRetErr: errors.New("fake-checkpoint-error"), + expectedErr: "error to checkpoint object: fake-checkpoint-error", + }, + { + name: "succeed", + rawObjWriter: repomocks.NewWriter(t), + rawWrtierRet: object.ID{}, + expectedRet: udmrepo.ID(""), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectWriter{} + + if tc.rawObjWriter != nil { + tc.rawObjWriter.On("Checkpoint").Return(tc.rawWrtierRet, tc.rawWriterRetErr) + kr.rawWriter = tc.rawObjWriter + } + + ret, err := kr.Checkpoint() + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedRet, ret) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestWriterResult(t *testing.T) { + testCases := []struct { + name string + rawObjWriter *repomocks.Writer + rawWrtierRet object.ID + rawWriterRetErr error + expectedRet udmrepo.ID + expectedErr string + }{ + { + name: "raw writer is nil", + expectedErr: "object writer is closed or not open", + }, + { + name: "raw result fail", + rawObjWriter: repomocks.NewWriter(t), + rawWriterRetErr: errors.New("fake-result-error"), + expectedErr: "error to wait object: fake-result-error", + }, + { + name: "succeed", + rawObjWriter: repomocks.NewWriter(t), + rawWrtierRet: object.ID{}, + expectedRet: udmrepo.ID(""), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectWriter{} + + if tc.rawObjWriter != nil { + tc.rawObjWriter.On("Result").Return(tc.rawWrtierRet, tc.rawWriterRetErr) + kr.rawWriter = tc.rawObjWriter + } + + ret, err := kr.Result() + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedRet, ret) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestWriterClose(t *testing.T) { + testCases := []struct { + name string + rawObjWriter *repomocks.Writer + rawWriterRetErr error + expectedErr string + }{ + { + name: "raw writer is nil", + }, + { + name: "raw close fail", + rawObjWriter: repomocks.NewWriter(t), + rawWriterRetErr: errors.New("fake-close-error"), + expectedErr: "fake-close-error", + }, + { + name: "succeed", + rawObjWriter: repomocks.NewWriter(t), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaObjectWriter{} + + if tc.rawObjWriter != nil { + tc.rawObjWriter.On("Close").Return(tc.rawWriterRetErr) + kr.rawWriter = tc.rawObjWriter + } + + err := kr.Close() + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestMaintainProgress(t *testing.T) { + testCases := []struct { + name string + progress int64 + uploaded int64 + throttle logThrottle + logMessage string + }{ + { + name: "should not output", + throttle: logThrottle{ + lastTime: math.MaxInt64, + }, + }, + { + name: "should output", + progress: 100, + uploaded: 200, + logMessage: "Repo maintenance uploaded 300 bytes.", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logMessage := "" + km := &kopiaMaintenance{ + logger: velerotest.NewSingleLogger(&logMessage), + throttle: tc.throttle, + uploaded: tc.uploaded, + } + + km.maintainProgress(tc.progress) + + if len(tc.logMessage) > 0 { + assert.Contains(t, logMessage, tc.logMessage) + } else { + assert.Equal(t, "", logMessage) + } + }) + } +}