From 3785f83fcb4d469e78056bfa84b601d63307bc21 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Fri, 1 Sep 2023 17:36:15 +0300 Subject: [PATCH] chore(updater): increase test coverage (#179) --- .codecov.yml | 2 + .gitignore | 1 + Makefile | 4 +- go.sum | 2 - internal/models/argo.go | 2 + pkg/updater/interfaces.go | 23 +++ pkg/updater/updater.go | 7 +- pkg/updater/updater_test.go | 281 ++++++++++++++++++++++++++++++++++++ 8 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 .codecov.yml create mode 100644 pkg/updater/interfaces.go diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..83ddafeb --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "pkg/updater/interfaces.go" diff --git a/.gitignore b/.gitignore index eddbf9b9..d570c2c6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ argo-watcher # dynamicly generated files cmd/argo-watcher/docs cmd/argo-watcher/mock +pkg/updater/mock # goreleaser dist/ diff --git a/Makefile b/Makefile index c743ff19..2cf8a4dc 100644 --- a/Makefile +++ b/Makefile @@ -58,12 +58,10 @@ docs: ## Generate swagger docs .PHONY: mocks mocks: @echo "===> Generating mocks" -# generate API mock @mockgen --source=cmd/argo-watcher/argo_api.go --destination=cmd/argo-watcher/mock/argo_api.go --package=mock -# generate State mock @mockgen --source=cmd/argo-watcher/state/state.go --destination=cmd/argo-watcher/mock/state.go --package=mock -# generate Metrics mock @mockgen --source=cmd/argo-watcher/metrics.go --destination=cmd/argo-watcher/mock/metrics.go --package=mock + @mockgen --source=pkg/updater/interfaces.go --destination=pkg/updater/mock/interfaces.go --package=mock .PHONY: bootstrap bootstrap: ## Bootstrap docker compose setup diff --git a/go.sum b/go.sum index e93efcf1..d04559e5 100644 --- a/go.sum +++ b/go.sum @@ -1958,8 +1958,6 @@ gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHD gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= -gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/internal/models/argo.go b/internal/models/argo.go index a4c5704a..d377c6f0 100644 --- a/internal/models/argo.go +++ b/internal/models/argo.go @@ -247,6 +247,8 @@ func (app *Application) UpdateGitImageTag(task *Task) error { RepoURL: app.Spec.Source.RepoURL, BranchName: app.Spec.Source.TargetRevision, Path: app.Spec.Source.Path, + + GitHandler: updater.GitClient{}, } if err := git.Clone(); err != nil { diff --git a/pkg/updater/interfaces.go b/pkg/updater/interfaces.go new file mode 100644 index 00000000..3283fc24 --- /dev/null +++ b/pkg/updater/interfaces.go @@ -0,0 +1,23 @@ +package updater + +import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/go-git/go-git/v5/storage" +) + +type GitHandler interface { + Clone(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*git.Repository, error) + NewPublicKeysFromFile(user, path, passphrase string) (*ssh.PublicKeys, error) +} + +type GitClient struct{} + +func (GitClient) Clone(s storage.Storer, worktree billy.Filesystem, o *git.CloneOptions) (*git.Repository, error) { + return git.Clone(s, worktree, o) +} + +func (GitClient) NewPublicKeysFromFile(user, path, passphrase string) (*ssh.PublicKeys, error) { + return ssh.NewPublicKeysFromFile(user, path, passphrase) +} diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go index 1f3c3091..1e2e65f0 100644 --- a/pkg/updater/updater.go +++ b/pkg/updater/updater.go @@ -42,6 +42,8 @@ type GitRepo struct { fs billy.Filesystem localRepo *git.Repository sshAuth *ssh.PublicKeys + + GitHandler GitHandler } func (repo *GitRepo) Clone() error { @@ -49,14 +51,15 @@ func (repo *GitRepo) Clone() error { repo.fs = memfs.New() - if repo.sshAuth, err = ssh.NewPublicKeysFromFile("git", sshKeyPath, sshKeyPass); err != nil { + if repo.sshAuth, err = repo.GitHandler.NewPublicKeysFromFile("git", sshKeyPath, sshKeyPass); err != nil { return err } - repo.localRepo, err = git.Clone(memory.NewStorage(), repo.fs, &git.CloneOptions{ + repo.localRepo, err = repo.GitHandler.Clone(memory.NewStorage(), repo.fs, &git.CloneOptions{ URL: repo.RepoURL, ReferenceName: plumbing.ReferenceName("refs/heads/" + repo.BranchName), SingleBranch: true, + Depth: 1, // This is needed to avoid fetching the entire history, which is not needed in this case Auth: repo.sshAuth, }) diff --git a/pkg/updater/updater_test.go b/pkg/updater/updater_test.go index 377efb1a..0c89be00 100644 --- a/pkg/updater/updater_test.go +++ b/pkg/updater/updater_test.go @@ -1,13 +1,230 @@ package updater import ( + "errors" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/go-git/go-git/v5/storage/memory" + "github.com/shini4i/argo-watcher/pkg/updater/mock" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gopkg.in/yaml.v2" + "strings" "testing" + "time" ) +func TestGitRepoClone(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGitHandler := mock.NewMockGitHandler(ctrl) + + tests := []struct { + name string + mockSSH func() + expected error + }{ + { + name: "successful clone", + mockSSH: func() { + mockGitHandler.EXPECT().NewPublicKeysFromFile("git", sshKeyPath, sshKeyPass).Return(&ssh.PublicKeys{}, nil) + mockGitHandler.EXPECT().Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: "mockRepoURL", + ReferenceName: "refs/heads/mockBranch", + SingleBranch: true, + Depth: 1, + Auth: &ssh.PublicKeys{}, + }).Return(&git.Repository{}, nil) + }, + expected: nil, + }, + { + name: "failed NewPublicKeysFromFile", + mockSSH: func() { + mockGitHandler.EXPECT().NewPublicKeysFromFile("git", sshKeyPath, sshKeyPass).Return(nil, errors.New("failed to fetch keys")) + }, + expected: errors.New("failed to fetch keys"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSSH() + + gitRepo := GitRepo{ + RepoURL: "mockRepoURL", + BranchName: "mockBranch", + GitHandler: mockGitHandler, + } + + err := gitRepo.Clone() + + if tt.expected == nil { + assert.NoError(t, err, "Expected no error") + } else { + assert.EqualError(t, err, tt.expected.Error(), "Error mismatch") + } + }) + } +} + +func TestGetFileContent(t *testing.T) { + // 1. Setup an in-memory file system using billy + fs := memfs.New() + content := "Hello, World!" + fileName := "test.txt" + + // 2. Create a test file in that filesystem + file, err := fs.Create(fileName) + assert.NoError(t, err) + _, err = file.Write([]byte(content)) + assert.NoError(t, err) + err = file.Close() + assert.NoError(t, err) + + // 3. Create a GitRepo instance using the in-memory filesystem + repo := &GitRepo{ + fs: fs, + } + + t.Run("Successfully read content", func(t *testing.T) { + readContent, err := repo.getFileContent(fileName) + assert.NoError(t, err) + assert.Equal(t, content, strings.TrimSpace(string(readContent))) + }) + + t.Run("Error on non-existent file", func(t *testing.T) { + _, err := repo.getFileContent("non-existent.txt") + assert.Error(t, err) + }) +} + +func TestMergeOverrideFileContent(t *testing.T) { + fs := memfs.New() + repo := &GitRepo{ + fs: fs, + } + + // Test when the override file doesn't exist + t.Run("no existing file", func(t *testing.T) { + overrideContent := &ArgoOverrideFile{ + Helm: struct { + Parameters []ArgoParameterOverride `yaml:"parameters"` + }{ + Parameters: []ArgoParameterOverride{ + { + Name: "param1", + Value: "value1", + }, + }, + }, + } + result, err := repo.mergeOverrideFileContent("nonexistent.yaml", overrideContent) + assert.NoError(t, err) + assert.Equal(t, overrideContent, result) + }) + + // Test when the override file does exist + t.Run("existing file", func(t *testing.T) { + // Creating a dummy existing file + existingContent := ArgoOverrideFile{ + Helm: struct { + Parameters []ArgoParameterOverride `yaml:"parameters"` + }{ + Parameters: []ArgoParameterOverride{ + { + Name: "param1", + Value: "oldValue1", + }, + { + Name: "param2", + Value: "value2", + }, + }, + }, + } + + fileName := "existing.yaml" + contentBytes, _ := yaml.Marshal(existingContent) + file, _ := fs.Create(fileName) + _, err := file.Write(contentBytes) + assert.NoError(t, err) + err = file.Close() + assert.NoError(t, err) + + // Merge with this content + overrideContent := &ArgoOverrideFile{ + Helm: struct { + Parameters []ArgoParameterOverride `yaml:"parameters"` + }{ + Parameters: []ArgoParameterOverride{ + { + Name: "param1", + Value: "newValue1", + }, + }, + }, + } + + expectedMergedContent := &ArgoOverrideFile{ + Helm: struct { + Parameters []ArgoParameterOverride `yaml:"parameters"` + }{ + Parameters: []ArgoParameterOverride{ + { + Name: "param1", + Value: "newValue1", // This assumes newValue1 overwrites oldValue1 + }, + { + Name: "param2", + Value: "value2", + }, + }, + }, + } + + result, err := repo.mergeOverrideFileContent(fileName, overrideContent) + assert.NoError(t, err) + assert.Equal(t, expectedMergedContent, result) + }) + + // Test when the existing override file has invalid YAML format + t.Run("invalid YAML file", func(t *testing.T) { + // Creating a dummy existing file with invalid YAML + invalidYAMLContent := `helm: + parameters: + - name: param1 + value: value1` // The indentation is intentionally wrong to create an invalid YAML + + fileName := "invalid.yaml" + file, _ := fs.Create(fileName) + _, err := file.Write([]byte(invalidYAMLContent)) + assert.NoError(t, err) + err = file.Close() + assert.NoError(t, err) + + overrideContent := &ArgoOverrideFile{ + Helm: struct { + Parameters []ArgoParameterOverride `yaml:"parameters"` + }{ + Parameters: []ArgoParameterOverride{ + { + Name: "param1", + Value: "newValue1", + }, + }, + }, + } + + _, err = repo.mergeOverrideFileContent(fileName, overrideContent) + assert.Error(t, err) + }) +} + func TestMergeParameters(t *testing.T) { tests := []struct { name string @@ -199,3 +416,67 @@ func TestGitRepo_Close(t *testing.T) { assert.Nil(t, repo.fs) assert.Nil(t, repo.localRepo) } + +func TestVersionChanged(t *testing.T) { + t.Run("Repo without changes", func(t *testing.T) { + fs := memfs.New() + storer := memory.NewStorage() + + repo, err := git.Init(storer, fs) + assert.NoError(t, err) + + w, err := repo.Worktree() + assert.NoError(t, err) + + changed, err := versionChanged(w) + assert.NoError(t, err) + assert.False(t, changed, "Expected no changes in a newly initialized repo") + }) + + t.Run("Repo with changes", func(t *testing.T) { + fs := memfs.New() + storer := memory.NewStorage() + + repo, err := git.Init(storer, fs) + assert.NoError(t, err) + + w, err := repo.Worktree() + assert.NoError(t, err) + + // Create and commit a file + file, err := fs.Create("test.txt") + assert.NoError(t, err) + _, err = file.Write([]byte("Initial content")) + assert.NoError(t, err) + err = file.Close() + assert.NoError(t, err) + + _, err = w.Add("test.txt") + assert.NoError(t, err) + + _, err = w.Commit("Initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "John Doe", + Email: "johndoe@example.com", + When: time.Now(), + }, + }) + assert.NoError(t, err) + + // Make changes to the file + file, err = fs.Create("test.txt") + assert.NoError(t, err) + _, err = file.Write([]byte("Updated content")) + assert.NoError(t, err) + err = file.Close() + assert.NoError(t, err) + + _, err = w.Add("test.txt") + assert.NoError(t, err) + + // Test versionChanged function + changed, err := versionChanged(w) + assert.NoError(t, err) + assert.True(t, changed, "Expected changes after modifying the file") + }) +}