* Move repository package to apps
* Move operators to grafana/grafana
* Go mod tidy
* Own package by git sync team for now
* Merged
* Do not use settings in local extra
* Remove dependency on webhook extra
* Hack to work around issue with secure contracts
* Sync Go modules
* Revert "Move operators to grafana/grafana"
This reverts commit 9f19b30a2e.
4259 lines
117 KiB
Go
4259 lines
117 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
|
|
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
|
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
|
"github.com/grafana/nanogit"
|
|
"github.com/grafana/nanogit/mocks"
|
|
"github.com/grafana/nanogit/protocol"
|
|
"github.com/grafana/nanogit/protocol/hash"
|
|
)
|
|
|
|
func TestGitRepository_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *provisioning.Repository
|
|
gitConfig RepositoryConfig
|
|
want field.ErrorList // number of expected validation errors
|
|
}{
|
|
{
|
|
name: "valid config",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: "configs",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "missing URL",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Required(field.NewPath("spec", "test_type", "url"), "a git url is required"),
|
|
},
|
|
},
|
|
{
|
|
name: "invalid URL scheme",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "http://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "url"), "http://git.example.com/repo.git", "invalid git URL format"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing host",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https:///repo.git", // URL with missing host
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "url"), "https:///repo.git", "invalid git URL format"),
|
|
},
|
|
},
|
|
{
|
|
name: "unparseable url",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "://not a valid url",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "url"), "://not a valid url", "invalid git URL format"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing branch",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "", // Empty branch
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Required(field.NewPath("spec", "test_type", "branch"), "a git branch is required"),
|
|
},
|
|
},
|
|
{
|
|
name: "invalid branch name",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "invalid/branch*name", // Invalid branch name
|
|
Token: "token123",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "branch"), "invalid/branch*name", "invalid branch name"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing token for R/W repository",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "", // Empty token
|
|
},
|
|
want: field.ErrorList{
|
|
field.Required(field.NewPath("secure", "token"), "a git access token is required"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing token for read-only repository",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
Workflows: nil, // read-only
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "", // Empty token
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "unsafe path",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: "../unsafe/path",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "path"), "../unsafe/path", "path contains traversal attempt (./ or ../)"),
|
|
},
|
|
},
|
|
{
|
|
name: "absolute path",
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: "/absolute/path",
|
|
},
|
|
want: field.ErrorList{
|
|
field.Invalid(field.NewPath("spec", "test_type", "path"), "/absolute/path", "path must be relative"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: tt.config,
|
|
gitConfig: tt.gitConfig,
|
|
}
|
|
|
|
errors := gitRepo.Validate()
|
|
require.Equal(t, tt.want, errors)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidGitURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid HTTPS URL",
|
|
url: "https://git.example.com/owner/repo.git",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "invalid HTTP URL",
|
|
url: "http://git.example.com/owner/repo.git",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "missing scheme",
|
|
url: "git.example.com/owner/repo.git",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "empty path",
|
|
url: "https://git.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no path",
|
|
url: "https://git.example.com",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "missing host",
|
|
url: "https:///repo.git",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "unparseable url",
|
|
url: "://bad-url",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "invalid URL",
|
|
url: "not-a-url",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isValidGitURL(tt.url)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewGit(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
config := &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitConfig := RepositoryConfig{
|
|
URL: "https://git.example.com/owner/repo.git",
|
|
Branch: "main",
|
|
Token: "test-token",
|
|
Path: "configs",
|
|
}
|
|
|
|
// This should succeed in creating the client but won't be able to connect
|
|
// We just test that the basic structure is created correctly
|
|
gitRepo, err := NewRepository(ctx, config, gitConfig)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gitRepo)
|
|
require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL())
|
|
require.Equal(t, "main", gitRepo.Branch())
|
|
require.Equal(t, config, gitRepo.Config())
|
|
}
|
|
|
|
func TestCreateSignature(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
t.Run("should use default signature when no context signature", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "Grafana", author.Name)
|
|
require.Equal(t, "noreply@grafana.com", author.Email)
|
|
require.False(t, author.Time.IsZero())
|
|
|
|
require.Equal(t, "Grafana", committer.Name)
|
|
require.Equal(t, "noreply@grafana.com", committer.Email)
|
|
require.False(t, committer.Time.IsZero())
|
|
})
|
|
|
|
t.Run("should use context signature when available", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "John Doe", author.Name)
|
|
require.Equal(t, "john@example.com", author.Email)
|
|
require.Equal(t, time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), author.Time)
|
|
|
|
require.Equal(t, "John Doe", committer.Name)
|
|
require.Equal(t, "john@example.com", committer.Email)
|
|
require.Equal(t, time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), committer.Time)
|
|
})
|
|
|
|
t.Run("should fallback to default when context signature has empty name", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "",
|
|
Email: "john@example.com",
|
|
When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "Grafana", author.Name)
|
|
require.Equal(t, "noreply@grafana.com", author.Email)
|
|
require.False(t, author.Time.IsZero())
|
|
|
|
require.Equal(t, "Grafana", committer.Name)
|
|
require.Equal(t, "noreply@grafana.com", committer.Email)
|
|
require.False(t, committer.Time.IsZero())
|
|
})
|
|
|
|
t.Run("should use current time when signature time is zero", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
When: time.Time{}, // Zero time
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
before := time.Now()
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
after := time.Now()
|
|
|
|
require.Equal(t, "John Doe", author.Name)
|
|
require.Equal(t, "john@example.com", author.Email)
|
|
require.True(t, author.Time.After(before.Add(-time.Second)))
|
|
require.True(t, author.Time.Before(after.Add(time.Second)))
|
|
|
|
require.Equal(t, "John Doe", committer.Name)
|
|
require.Equal(t, "john@example.com", committer.Email)
|
|
require.True(t, committer.Time.After(before.Add(-time.Second)))
|
|
require.True(t, committer.Time.Before(after.Add(time.Second)))
|
|
})
|
|
}
|
|
|
|
func TestEnsureBranchExists(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
t.Run("should reject invalid branch name", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
_, err := gitRepo.ensureBranchExists(ctx, "feature//branch")
|
|
|
|
require.Error(t, err)
|
|
var statusErr *apierrors.StatusError
|
|
require.True(t, errors.As(err, &statusErr))
|
|
require.Equal(t, int32(400), statusErr.Status().Code)
|
|
require.Equal(t, "invalid branch name", statusErr.Status().Message)
|
|
})
|
|
|
|
t.Run("should validate branch names for validation errors only", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
branchName string
|
|
shouldError bool
|
|
}{
|
|
{"invalid double slash", "feature//branch", true},
|
|
{"invalid double dot", "feature..branch", true},
|
|
{"invalid ending with dot", "feature.", true},
|
|
{"invalid starting with slash", "/feature", true},
|
|
{"invalid ending with slash", "feature/", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
_, err := gitRepo.ensureBranchExists(ctx, tc.branchName)
|
|
|
|
if tc.shouldError {
|
|
require.Error(t, err)
|
|
var statusErr *apierrors.StatusError
|
|
require.True(t, errors.As(err, &statusErr))
|
|
require.Equal(t, int32(400), statusErr.Status().Code)
|
|
require.Equal(t, "invalid branch name", statusErr.Status().Message)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHistory(t *testing.T) {
|
|
t.Run("should return not implemented", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
history, err := (&gitRepository{}).History(ctx, "", "")
|
|
|
|
require.Error(t, err)
|
|
var statusErr *apierrors.StatusError
|
|
require.True(t, errors.As(err, &statusErr))
|
|
require.Equal(t, int32(http.StatusNotImplemented), statusErr.Status().Code)
|
|
require.Equal(t, metav1.StatusReasonMethodNotAllowed, statusErr.Status().Reason)
|
|
require.Equal(t, "history is not supported for pure git repositories", statusErr.Status().Message)
|
|
require.Nil(t, history)
|
|
})
|
|
}
|
|
|
|
func TestGitRepository_Test(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
wantResults *provisioning.TestResults
|
|
wantError error
|
|
}{
|
|
{
|
|
name: "success - all checks pass",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(true, nil)
|
|
mockClient.RepoExistsReturns(true, nil)
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: true,
|
|
Errors: nil,
|
|
Code: http.StatusOK,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - not authorized (error)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(false, errors.New("auth error"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("secure", "token").String(),
|
|
Detail: "failed check if authorized: auth error",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - not authorized (false result)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(false, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("secure", "token").String(),
|
|
Detail: "not authorized",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - repository not found (error)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(true, nil)
|
|
mockClient.RepoExistsReturns(false, errors.New("repo error"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("spec", "test_type", "url").String(),
|
|
Detail: "failed check if repository exists: repo error",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - repository not found (false result)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(true, nil)
|
|
mockClient.RepoExistsReturns(false, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("spec", "test_type", "url").String(),
|
|
Detail: "repository not found",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - branch not found (error)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(true, nil)
|
|
mockClient.RepoExistsReturns(true, nil)
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("branch not found"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "nonexistent",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("spec", "test_type", "branch").String(),
|
|
Detail: "failed to check if branch exists: branch not found",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "failure - branch not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.IsAuthorizedReturns(true, nil)
|
|
mockClient.RepoExistsReturns(true, nil)
|
|
mockClient.GetRefReturns(nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "nonexistent",
|
|
},
|
|
wantResults: &provisioning.TestResults{
|
|
Success: false,
|
|
Errors: []provisioning.ErrorDetails{
|
|
{
|
|
Type: metav1.CauseTypeFieldValueInvalid,
|
|
Field: field.NewPath("spec", "test_type", "branch").String(),
|
|
Detail: "branch not found",
|
|
},
|
|
},
|
|
Code: http.StatusBadRequest,
|
|
},
|
|
wantError: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
}
|
|
|
|
results, err := gitRepo.Test(context.Background())
|
|
require.NoError(t, err, "Test method should not return an error")
|
|
|
|
require.Equal(t, tt.wantResults, results, "Test results mismatch")
|
|
require.Equal(t, tt.wantError, err, "Test error mismatch")
|
|
|
|
// Verify the mock calls
|
|
require.Equal(t, 1, mockClient.IsAuthorizedCallCount(), "IsAuthorized should be called exactly once")
|
|
|
|
if mockClient.RepoExistsCallCount() > 0 {
|
|
require.Equal(t, 1, mockClient.RepoExistsCallCount(), "RepoExists should be called at most once")
|
|
}
|
|
|
|
if mockClient.GetRefCallCount() > 0 {
|
|
require.Equal(t, 1, mockClient.GetRefCallCount(), "GetRef should be called at most once")
|
|
_, ref := mockClient.GetRefArgsForCall(0)
|
|
require.Equal(t, "refs/heads/"+tt.gitConfig.Branch, ref, "GetRef should be called with correct branch reference")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Read(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
filePath string
|
|
ref string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - read file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{
|
|
Content: []byte("file content"),
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
filePath: "test.yaml",
|
|
ref: "main",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "can a read directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"),
|
|
}, nil)
|
|
mockClient.GetTreeByPathReturns(&nanogit.Tree{
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
filePath: "subdir/",
|
|
ref: "main",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - file not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
filePath: "missing.yaml",
|
|
ref: "main",
|
|
wantError: true,
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "failure - ref not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
filePath: "test.yaml",
|
|
ref: "nonexistent",
|
|
wantError: true,
|
|
errorType: repository.ErrRefNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
}
|
|
|
|
fileInfo, err := gitRepo.Read(context.Background(), tt.filePath, tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, fileInfo)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fileInfo)
|
|
require.Equal(t, tt.filePath, fileInfo.Path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_ReadTree(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
ref string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - read tree",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetFlatTreeReturns(&nanogit.FlatTree{
|
|
Entries: []nanogit.FlatTreeEntry{
|
|
{
|
|
Path: "configs/test.yaml",
|
|
Hash: hash.Hash{},
|
|
Type: protocol.ObjectTypeBlob,
|
|
},
|
|
},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
ref: "main",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - ref not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
ref: "nonexistent",
|
|
wantError: true,
|
|
errorType: repository.ErrRefNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
entries, err := gitRepo.ReadTree(context.Background(), tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, entries)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entries)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Create(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
path string
|
|
ref string
|
|
data []byte
|
|
comment string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - create file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "test.yaml",
|
|
ref: "main",
|
|
data: []byte("test content"),
|
|
comment: "Add test file",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - file already exists",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "existing.yaml",
|
|
ref: "main",
|
|
data: []byte("test content"),
|
|
comment: "Add existing file",
|
|
wantError: true,
|
|
errorType: repository.ErrFileAlreadyExists,
|
|
},
|
|
{
|
|
name: "success - create directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "newdir/",
|
|
ref: "main",
|
|
data: nil,
|
|
comment: "Add directory",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Create(context.Background(), tt.path, tt.ref, tt.data, tt.comment)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Update(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
path string
|
|
ref string
|
|
data []byte
|
|
comment string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - update file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.UpdateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "test.yaml",
|
|
ref: "main",
|
|
data: []byte("updated content"),
|
|
comment: "Update test file",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - file not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.UpdateBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "missing.yaml",
|
|
ref: "main",
|
|
data: []byte("content"),
|
|
comment: "Update missing file",
|
|
wantError: true,
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "failure - cannot update directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught early
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "directory/",
|
|
ref: "main",
|
|
data: []byte("content"),
|
|
comment: "Update directory",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Update(context.Background(), tt.path, tt.ref, tt.data, tt.comment)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Delete(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient, *mocks.FakeStagedWriter)
|
|
assertions func(*testing.T, *mocks.FakeClient, *mocks.FakeStagedWriter)
|
|
gitConfig RepositoryConfig
|
|
path string
|
|
ref string
|
|
comment string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - delete file",
|
|
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
|
|
mockWriter.DeleteBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "test.yaml",
|
|
ref: "main",
|
|
comment: "Delete test file",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "success - delete directory",
|
|
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter.DeleteTreeReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
assertions: func(t *testing.T, fakeClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
|
|
require.Equal(t, 1, mockWriter.DeleteTreeCallCount(), "DeleteTree should be called once")
|
|
_, p := mockWriter.DeleteTreeArgsForCall(0)
|
|
require.Equal(t, "configs/testdir", p, "DeleteTree should be called with correct path")
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "testdir/",
|
|
ref: "main",
|
|
comment: "Delete test directory",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - file not found",
|
|
setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter.DeleteBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "missing.yaml",
|
|
ref: "main",
|
|
comment: "Delete missing file",
|
|
wantError: true,
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
tt.setupMock(mockClient, mockWriter)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Delete(context.Background(), tt.path, tt.ref, tt.comment)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if tt.assertions != nil {
|
|
tt.assertions(t, mockClient, mockWriter)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_ListRefs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
wantError bool
|
|
wantRefs []provisioning.RefItem
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "success - list refs",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.ListRefsReturns([]nanogit.Ref{
|
|
{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"),
|
|
},
|
|
{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.MustFromHex("1234567890abcdef1234567890abcdef12345678"),
|
|
},
|
|
{
|
|
Name: "refs/tags/v1.0.0",
|
|
Hash: hash.MustFromHex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
|
|
},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantError: false,
|
|
wantRefs: []provisioning.RefItem{
|
|
{
|
|
Name: "main",
|
|
Hash: "abcdef1234567890abcdef1234567890abcdef12",
|
|
},
|
|
{
|
|
Name: "feature",
|
|
Hash: "1234567890abcdef1234567890abcdef12345678",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "failure - list refs error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.ListRefsReturns(nil, errors.New("list refs failed"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantError: true,
|
|
errorType: errors.New("list refs failed"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
refs, err := gitRepo.ListRefs(context.Background())
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.Contains(t, err.Error(), tt.errorType.Error())
|
|
}
|
|
require.Nil(t, refs)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, refs)
|
|
require.Equal(t, len(tt.wantRefs), len(refs))
|
|
for i, wantRef := range tt.wantRefs {
|
|
require.Equal(t, wantRef.Name, refs[i].Name)
|
|
require.Equal(t, wantRef.Hash, refs[i].Hash)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_LatestRef(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
wantError bool
|
|
wantRef string
|
|
}{
|
|
{
|
|
name: "success - get latest ref",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, // Non-empty hash
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
wantError: false,
|
|
wantRef: "", // Hash would be converted to string - we just check it's not empty
|
|
},
|
|
{
|
|
name: "failure - branch not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("branch not found"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "nonexistent",
|
|
},
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
ref, err := gitRepo.LatestRef(context.Background())
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Empty(t, ref)
|
|
} else {
|
|
require.NoError(t, err)
|
|
// For success case with non-empty hash, ref should not be empty
|
|
if tt.name == "success - get latest ref" {
|
|
require.NotEmpty(t, ref)
|
|
}
|
|
}
|
|
|
|
// Verify GetRef was called with correct branch
|
|
require.Equal(t, 1, mockClient.GetRefCallCount())
|
|
_, branchRef := mockClient.GetRefArgsForCall(0)
|
|
require.Equal(t, "refs/heads/"+tt.gitConfig.Branch, branchRef)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_History(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
history, err := gitRepo.History(context.Background(), "test.yaml", "main")
|
|
|
|
require.Error(t, err)
|
|
require.Nil(t, history)
|
|
|
|
var statusErr *apierrors.StatusError
|
|
require.True(t, errors.As(err, &statusErr))
|
|
require.Equal(t, int32(http.StatusNotImplemented), statusErr.Status().Code)
|
|
require.Equal(t, metav1.StatusReasonMethodNotAllowed, statusErr.Status().Reason)
|
|
require.Equal(t, "history is not supported for pure git repositories", statusErr.Status().Message)
|
|
}
|
|
|
|
func TestGitRepository_Write(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
path string
|
|
ref string
|
|
data []byte
|
|
message string
|
|
fileExists bool
|
|
wantError bool
|
|
expectCreate bool
|
|
expectUpdate bool
|
|
}{
|
|
{
|
|
name: "success - create new file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call for Read check - file not found
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{}, nanogit.ErrObjectNotFound)
|
|
|
|
// Second call for Create
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "newfile.yaml",
|
|
ref: "main",
|
|
data: []byte("content"),
|
|
message: "Add new file",
|
|
fileExists: false,
|
|
wantError: false,
|
|
expectCreate: true,
|
|
expectUpdate: false,
|
|
},
|
|
{
|
|
name: "success - update existing file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call for Read check - file exists
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{},
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{
|
|
Content: []byte("old content"),
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
|
|
// Second call for Update
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{},
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.UpdateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
path: "existing.yaml",
|
|
ref: "main",
|
|
data: []byte("updated content"),
|
|
message: "Update existing file",
|
|
fileExists: true,
|
|
wantError: false,
|
|
expectCreate: false,
|
|
expectUpdate: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Write(context.Background(), tt.path, tt.ref, tt.data, tt.message)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify Read was called first to check if file exists
|
|
require.GreaterOrEqual(t, mockClient.GetRefCallCount(), 1)
|
|
|
|
if tt.expectCreate || tt.expectUpdate {
|
|
// Verify NewStagedWriter was called
|
|
require.Equal(t, 1, mockClient.NewStagedWriterCallCount())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_CompareFiles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
base string
|
|
ref string
|
|
wantError bool
|
|
wantChanges int
|
|
}{
|
|
{
|
|
name: "success - compare commits with changes",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Return refs for base and ref
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
|
|
// Return comparison results
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "configs/new-file.yaml",
|
|
Status: protocol.FileStatusAdded,
|
|
},
|
|
{
|
|
Path: "configs/modified-file.yaml",
|
|
Status: protocol.FileStatusModified,
|
|
},
|
|
{
|
|
Path: "configs/deleted-file.yaml",
|
|
Status: protocol.FileStatusDeleted,
|
|
},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: false,
|
|
wantChanges: 3,
|
|
},
|
|
{
|
|
name: "success - no changes",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
base: "main",
|
|
ref: "main",
|
|
wantError: false,
|
|
wantChanges: 0,
|
|
},
|
|
{
|
|
name: "failure - ref not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
base: "main",
|
|
ref: "nonexistent",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "failure - empty ref",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught early
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
base: "main",
|
|
ref: "",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
changes, err := gitRepo.CompareFiles(context.Background(), tt.base, tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, changes)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, changes)
|
|
require.Len(t, changes, tt.wantChanges)
|
|
|
|
// Verify the changes have correct actions
|
|
if tt.wantChanges > 0 {
|
|
for _, change := range changes {
|
|
require.NotEmpty(t, change.Path)
|
|
require.NotEmpty(t, change.Action)
|
|
require.Equal(t, tt.ref, change.Ref)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_ensureBranchExists(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
branchName string
|
|
wantError bool
|
|
expectCalls int // Expected number of GetRef calls
|
|
}{
|
|
{
|
|
name: "success - branch already exists",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
branchName: "feature",
|
|
wantError: false,
|
|
expectCalls: 1,
|
|
},
|
|
{
|
|
name: "success - create new branch",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call - branch doesn't exist
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Second call - get source branch
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
// CreateRef succeeds
|
|
mockClient.CreateRefReturns(nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
branchName: "new-feature",
|
|
wantError: false,
|
|
expectCalls: 2,
|
|
},
|
|
{
|
|
name: "failure - invalid branch name",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught early
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
branchName: "invalid//branch",
|
|
wantError: true,
|
|
expectCalls: 0,
|
|
},
|
|
{
|
|
name: "failure - source branch not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call - branch doesn't exist
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Second call - source branch also doesn't exist
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "nonexistent",
|
|
},
|
|
branchName: "new-feature",
|
|
wantError: true,
|
|
expectCalls: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
branchRef, err := gitRepo.ensureBranchExists(context.Background(), tt.branchName)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Equal(t, nanogit.Ref{}, branchRef)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, nanogit.Ref{}, branchRef)
|
|
require.Equal(t, "refs/heads/"+tt.branchName, branchRef.Name)
|
|
}
|
|
|
|
// Verify expected number of GetRef calls
|
|
require.Equal(t, tt.expectCalls, mockClient.GetRefCallCount())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_createSignature(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
t.Run("should use default signature when no context signature", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "Grafana", author.Name)
|
|
require.Equal(t, "noreply@grafana.com", author.Email)
|
|
require.False(t, author.Time.IsZero())
|
|
|
|
require.Equal(t, "Grafana", committer.Name)
|
|
require.Equal(t, "noreply@grafana.com", committer.Email)
|
|
require.False(t, committer.Time.IsZero())
|
|
})
|
|
|
|
t.Run("should use context signature when available", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "John Doe", author.Name)
|
|
require.Equal(t, "john@example.com", author.Email)
|
|
require.Equal(t, time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), author.Time)
|
|
|
|
require.Equal(t, "John Doe", committer.Name)
|
|
require.Equal(t, "john@example.com", committer.Email)
|
|
require.Equal(t, time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), committer.Time)
|
|
})
|
|
|
|
t.Run("should fallback to default when context signature has empty name", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "",
|
|
Email: "john@example.com",
|
|
When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
|
|
require.Equal(t, "Grafana", author.Name)
|
|
require.Equal(t, "noreply@grafana.com", author.Email)
|
|
require.False(t, author.Time.IsZero())
|
|
|
|
require.Equal(t, "Grafana", committer.Name)
|
|
require.Equal(t, "noreply@grafana.com", committer.Email)
|
|
require.False(t, committer.Time.IsZero())
|
|
})
|
|
|
|
t.Run("should use current time when signature time is zero", func(t *testing.T) {
|
|
sig := repository.CommitSignature{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
When: time.Time{}, // Zero time
|
|
}
|
|
ctx := repository.WithAuthorSignature(context.Background(), sig)
|
|
|
|
before := time.Now()
|
|
author, committer := gitRepo.createSignature(ctx)
|
|
after := time.Now()
|
|
|
|
require.Equal(t, "John Doe", author.Name)
|
|
require.Equal(t, "john@example.com", author.Email)
|
|
require.True(t, author.Time.After(before.Add(-time.Second)))
|
|
require.True(t, author.Time.Before(after.Add(time.Second)))
|
|
|
|
require.Equal(t, "John Doe", committer.Name)
|
|
require.Equal(t, "john@example.com", committer.Email)
|
|
require.True(t, committer.Time.After(before.Add(-time.Second)))
|
|
require.True(t, committer.Time.Before(after.Add(time.Second)))
|
|
})
|
|
}
|
|
|
|
func TestNewGitRepository(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gitConfig RepositoryConfig
|
|
wantError bool
|
|
expectURL string
|
|
}{
|
|
{
|
|
name: "success - with token",
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/owner/repo.git",
|
|
Branch: "main",
|
|
Token: "plain-token",
|
|
Path: "configs",
|
|
},
|
|
wantError: false,
|
|
expectURL: "https://git.example.com/owner/repo.git",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
config := &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitRepo, err := NewRepository(ctx, config, tt.gitConfig)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, gitRepo)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gitRepo)
|
|
require.Equal(t, tt.expectURL, gitRepo.URL())
|
|
require.Equal(t, tt.gitConfig.Branch, gitRepo.Branch())
|
|
require.Equal(t, config, gitRepo.Config())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Getters(t *testing.T) {
|
|
config := &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitConfig := RepositoryConfig{
|
|
URL: "https://git.example.com/owner/repo.git",
|
|
Branch: "feature-branch",
|
|
Token: "test-token",
|
|
Path: "configs",
|
|
}
|
|
|
|
gitRepo := &gitRepository{
|
|
config: config,
|
|
gitConfig: gitConfig,
|
|
}
|
|
|
|
t.Run("URL returns correct URL", func(t *testing.T) {
|
|
require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL())
|
|
})
|
|
|
|
t.Run("Branch returns correct branch", func(t *testing.T) {
|
|
require.Equal(t, "feature-branch", gitRepo.Branch())
|
|
})
|
|
|
|
t.Run("Config returns correct config", func(t *testing.T) {
|
|
require.Equal(t, config, gitRepo.Config())
|
|
})
|
|
}
|
|
|
|
func TestGitRepository_resolveRefToHash(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
gitConfig RepositoryConfig
|
|
ref string
|
|
wantError bool
|
|
expectRef string
|
|
}{
|
|
{
|
|
name: "success - empty ref uses default branch",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
ref: "",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "success - valid hex hash",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed for valid hash
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
ref: "abcdef1234567890abcdef1234567890abcdef12",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "success - branch reference",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
ref: "refs/heads/feature",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "failure - ref not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
ref: "nonexistent-ref",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "failure - client error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("client error"))
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
ref: "some-ref",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: tt.gitConfig,
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
refHash, err := gitRepo.resolveRefToHash(context.Background(), tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Equal(t, hash.Zero, refHash)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, refHash)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_commit(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeStagedWriter)
|
|
comment string
|
|
wantError bool
|
|
authorName string
|
|
authorEmail string
|
|
}{
|
|
{
|
|
name: "success - commit with default signature",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
},
|
|
comment: "Test commit",
|
|
wantError: false,
|
|
authorName: "Grafana",
|
|
authorEmail: "noreply@grafana.com",
|
|
},
|
|
{
|
|
name: "success - commit with context signature",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
},
|
|
comment: "Test commit with author",
|
|
wantError: false,
|
|
authorName: "John Doe",
|
|
authorEmail: "john@example.com",
|
|
},
|
|
{
|
|
name: "failure - commit error",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
|
|
},
|
|
comment: "Failed commit",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
tt.setupMock(mockWriter)
|
|
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if tt.authorName != "Grafana" {
|
|
// Add context signature for custom author
|
|
sig := repository.CommitSignature{
|
|
Name: tt.authorName,
|
|
Email: tt.authorEmail,
|
|
When: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
ctx = repository.WithAuthorSignature(ctx, sig)
|
|
}
|
|
|
|
err := gitRepo.commit(ctx, mockWriter, tt.comment)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify commit was called once
|
|
require.Equal(t, 1, mockWriter.CommitCallCount())
|
|
|
|
// Verify commit parameters
|
|
if !tt.wantError {
|
|
ctxParam, commentParam, authorParam, committerParam := mockWriter.CommitArgsForCall(0)
|
|
require.NotNil(t, ctxParam)
|
|
require.Equal(t, tt.comment, commentParam)
|
|
require.Equal(t, tt.authorName, authorParam.Name)
|
|
require.Equal(t, tt.authorEmail, authorParam.Email)
|
|
require.Equal(t, tt.authorName, committerParam.Name)
|
|
require.Equal(t, tt.authorEmail, committerParam.Email)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_commitAndPush(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeStagedWriter)
|
|
comment string
|
|
wantError bool
|
|
expectCommit bool
|
|
expectPush bool
|
|
}{
|
|
{
|
|
name: "success - commit and push",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
comment: "Test commit and push",
|
|
wantError: false,
|
|
expectCommit: true,
|
|
expectPush: true,
|
|
},
|
|
{
|
|
name: "failure - commit fails",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
|
|
},
|
|
comment: "Failed commit",
|
|
wantError: true,
|
|
expectCommit: true,
|
|
expectPush: false,
|
|
},
|
|
{
|
|
name: "failure - push fails",
|
|
setupMock: func(mockWriter *mocks.FakeStagedWriter) {
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(errors.New("push failed"))
|
|
},
|
|
comment: "Push failure",
|
|
wantError: true,
|
|
expectCommit: true,
|
|
expectPush: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
tt.setupMock(mockWriter)
|
|
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
err := gitRepo.commitAndPush(context.Background(), mockWriter, tt.comment)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if tt.expectCommit {
|
|
require.Equal(t, 1, mockWriter.CommitCallCount())
|
|
}
|
|
|
|
if tt.expectPush {
|
|
require.Equal(t, 1, mockWriter.PushCallCount())
|
|
} else {
|
|
require.Equal(t, 0, mockWriter.PushCallCount())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_logger(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
t.Run("creates new logger context", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
newCtx, logger := gitRepo.logger(ctx, "feature-branch")
|
|
|
|
require.NotNil(t, newCtx)
|
|
require.NotNil(t, logger)
|
|
require.NotEqual(t, ctx, newCtx)
|
|
})
|
|
|
|
t.Run("uses default branch when ref is empty", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
newCtx, logger := gitRepo.logger(ctx, "")
|
|
|
|
require.NotNil(t, newCtx)
|
|
require.NotNil(t, logger)
|
|
})
|
|
|
|
t.Run("returns existing logger when already present", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// First call creates the logger context
|
|
ctx1, logger1 := gitRepo.logger(ctx, "branch1")
|
|
|
|
// Second call should return the existing logger context
|
|
ctx2, logger2 := gitRepo.logger(ctx1, "branch2")
|
|
|
|
// When logger context already exists, it should return the same context
|
|
require.Equal(t, ctx1, ctx2)
|
|
|
|
// The logger should be the same instance from the existing context
|
|
require.NotNil(t, logger1)
|
|
require.NotNil(t, logger2)
|
|
|
|
// Both loggers should be functionally equivalent since they come from the same context
|
|
// We verify this by checking that they produce the same output
|
|
require.IsType(t, logger1, logger2)
|
|
})
|
|
}
|
|
|
|
func TestGitRepository_Stage(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
t.Run("calls NewStagedGitRepository", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
opts := repository.StageOptions{
|
|
Mode: repository.StageModeCommitAndPushOnEach,
|
|
}
|
|
|
|
// Since NewStagedGitRepository is not mocked and may panic, we expect this to fail
|
|
// We're testing that the method exists and forwards correctly
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Expected - NewStagedGitRepository isn't fully implemented for this test scenario
|
|
t.Logf("NewStagedGitRepository panicked as expected: %v", r)
|
|
}
|
|
}()
|
|
|
|
staged, err := gitRepo.Stage(ctx, opts)
|
|
|
|
// This will likely error/panic since we don't have a real implementation
|
|
// but we're testing that the method exists and forwards to NewStagedGitRepository
|
|
_ = staged
|
|
_ = err
|
|
})
|
|
}
|
|
|
|
func TestGitRepository_EdgeCases(t *testing.T) {
|
|
t.Run("create with data for directory should fail", func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{Branch: "main", Path: "configs"},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{Type: provisioning.GitHubRepositoryType},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Create(context.Background(), "newdir/", "main", []byte("data"), "comment")
|
|
|
|
// This should fail because we're providing data for a directory
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "data cannot be provided for a directory")
|
|
})
|
|
|
|
t.Run("update directory path should fail early", func(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
gitConfig: RepositoryConfig{Branch: "main", Path: "configs"},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{Type: provisioning.GitHubRepositoryType},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Update(context.Background(), "directory/", "main", []byte("data"), "comment")
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "cannot update a directory")
|
|
})
|
|
|
|
t.Run("write with read error should fail", func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{Tree: hash.Hash{}}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{}, errors.New("some read error"))
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{Branch: "main", Path: "configs"},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{Type: provisioning.GitHubRepositoryType},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Write(context.Background(), "test.yaml", "main", []byte("data"), "message")
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "check if file exists before writing")
|
|
})
|
|
}
|
|
|
|
func TestGitRepository_ValidateBranchNames(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
branchName string
|
|
expectValid bool
|
|
}{
|
|
{"valid simple branch", "main", true},
|
|
{"valid feature branch", "feature/new-feature", true},
|
|
{"valid branch with numbers", "release-v1.2.3", true},
|
|
{"invalid double slash", "feature//branch", false},
|
|
{"invalid double dot", "feature..branch", false},
|
|
{"invalid ending with dot", "feature.", false},
|
|
{"invalid starting with slash", "/feature", false},
|
|
{"invalid ending with slash", "feature/", false},
|
|
{"invalid with space", "feature branch", false},
|
|
{"invalid with tilde", "feature~1", false},
|
|
{"invalid with caret", "feature^1", false},
|
|
{"invalid with colon", "feature:branch", false},
|
|
{"invalid with question mark", "feature?", false},
|
|
{"invalid with asterisk", "feature*", false},
|
|
{"invalid with square brackets", "feature[1]", false},
|
|
{"invalid with backslash", "feature\\branch", false},
|
|
{"invalid empty string", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: tt.branchName,
|
|
Token: "token123",
|
|
},
|
|
}
|
|
|
|
errors := gitRepo.Validate()
|
|
|
|
if tt.expectValid {
|
|
// Should not have a branch validation error for invalid branch name
|
|
for _, err := range errors {
|
|
if err.Field == "spec.test_type.branch" && err.Type == field.ErrorTypeInvalid {
|
|
require.NotContains(t, err.Detail, "invalid branch name")
|
|
}
|
|
}
|
|
} else {
|
|
// Should have a branch validation error (either required or invalid)
|
|
found := false
|
|
for _, err := range errors {
|
|
if err.Field == "spec.test_type.branch" &&
|
|
(err.Type == field.ErrorTypeInvalid || err.Type == field.ErrorTypeRequired) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.True(t, found, "Expected branch validation error for: %s", tt.branchName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
ref string
|
|
wantError bool
|
|
wantHash string
|
|
}{
|
|
{
|
|
name: "valid short hash",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No setup needed for valid hash
|
|
},
|
|
ref: "abc123",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "invalid hex string treated as ref",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/invalid-hex",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
},
|
|
ref: "invalid-hex-zzz",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "refs/heads/ prefix handling",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
},
|
|
ref: "refs/heads/feature",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "refs/tags/ handling",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/tags/v1.0.0",
|
|
Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
|
|
}, nil)
|
|
},
|
|
ref: "refs/tags/v1.0.0",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
refHash, err := gitRepo.resolveRefToHash(context.Background(), tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, refHash)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, refHash)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_PathValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{"valid relative path", "configs/dir", false, ""},
|
|
{"path traversal with ../", "../configs", true, "path contains traversal attempt (./ or ../)"},
|
|
{"path traversal with ./", "./configs", true, "path contains traversal attempt (./ or ../)"},
|
|
{"absolute path", "/absolute/path", true, "path must be relative"},
|
|
{"empty path", "", false, ""},
|
|
{"path with multiple traversals", "../../etc/passwd", true, "path contains traversal attempt (./ or ../)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: "test_type",
|
|
},
|
|
},
|
|
gitConfig: RepositoryConfig{
|
|
URL: "https://git.example.com/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: tt.path,
|
|
},
|
|
}
|
|
|
|
errors := gitRepo.Validate()
|
|
|
|
if tt.expectError {
|
|
found := false
|
|
for _, err := range errors {
|
|
if err.Field == "spec.test_type.path" && err.Type == field.ErrorTypeInvalid {
|
|
require.Contains(t, err.Detail, tt.errorMsg)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.True(t, found, "Expected path validation error for: %s", tt.path)
|
|
} else {
|
|
// Should not have a path validation error
|
|
for _, err := range errors {
|
|
if err.Field == "spec.test_type.path" {
|
|
t.Errorf("Unexpected path validation error for %s: %s", tt.path, err.Detail)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
base string
|
|
ref string
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "both base and ref empty",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed
|
|
},
|
|
base: "",
|
|
ref: "",
|
|
wantError: true,
|
|
errorMsg: "base and ref cannot be empty",
|
|
},
|
|
{
|
|
name: "compare commits error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns(nil, errors.New("compare error"))
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: true,
|
|
errorMsg: "compare commits",
|
|
},
|
|
{
|
|
name: "file status type changed",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "configs/changed-type.yaml",
|
|
Status: protocol.FileStatusTypeChanged,
|
|
},
|
|
}, nil)
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "files outside configured path",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "other/file.yaml", // Outside configs/
|
|
Status: protocol.FileStatusAdded,
|
|
},
|
|
{
|
|
Path: "configs/included.yaml", // Inside configs/
|
|
Status: protocol.FileStatusAdded,
|
|
},
|
|
}, nil)
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "unknown file status",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "configs/unknown.yaml",
|
|
Status: protocol.FileStatus("unknown"),
|
|
},
|
|
}, nil)
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
changes, err := gitRepo.CompareFiles(context.Background(), tt.base, tt.ref)
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, changes)
|
|
if tt.errorMsg != "" {
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, changes)
|
|
|
|
// Verify that files outside configured path are filtered out
|
|
if tt.name == "files outside configured path" {
|
|
require.Len(t, changes, 1)
|
|
require.Equal(t, "included.yaml", changes[0].Path)
|
|
}
|
|
|
|
// Verify type changed files are handled as updates
|
|
if tt.name == "file status type changed" {
|
|
require.Len(t, changes, 1)
|
|
require.Equal(t, repository.FileActionUpdated, changes[0].Action)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_ReadTree_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "get flat tree error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetFlatTreeReturns(nil, errors.New("flat tree error"))
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "tree entries outside path",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.GetFlatTreeReturns(&nanogit.FlatTree{
|
|
Entries: []nanogit.FlatTreeEntry{
|
|
{
|
|
Path: "other/file.yaml", // Outside configs/
|
|
Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
Type: protocol.ObjectTypeBlob,
|
|
},
|
|
{
|
|
Path: "configs/included.yaml", // Inside configs/
|
|
Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
|
|
Type: protocol.ObjectTypeBlob,
|
|
},
|
|
{
|
|
Path: "configs/dir", // Directory without trailing slash
|
|
Hash: hash.Hash{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29},
|
|
Type: protocol.ObjectTypeTree,
|
|
},
|
|
},
|
|
}, nil)
|
|
},
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
entries, err := gitRepo.ReadTree(context.Background(), "main")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, entries)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entries)
|
|
|
|
if tt.name == "tree entries outside path" {
|
|
// Should only include entries inside the configured path
|
|
require.Len(t, entries, 2)
|
|
|
|
// Find the blob entry
|
|
var blobEntry *repository.FileTreeEntry
|
|
var dirEntry *repository.FileTreeEntry
|
|
for i := range entries {
|
|
if entries[i].Blob {
|
|
blobEntry = &entries[i]
|
|
} else {
|
|
dirEntry = &entries[i]
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, blobEntry)
|
|
require.Equal(t, "included.yaml", blobEntry.Path)
|
|
require.True(t, blobEntry.Blob)
|
|
|
|
require.NotNil(t, dirEntry)
|
|
require.Equal(t, "dir/", dirEntry.Path)
|
|
require.False(t, dirEntry.Blob)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_NewGitRepository_ClientError(t *testing.T) {
|
|
// This test would require mocking nanogit.NewHTTPClient which is difficult
|
|
// We test the path where the client creation would fail by using invalid URL
|
|
ctx := context.Background()
|
|
|
|
config := &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitConfig := RepositoryConfig{
|
|
URL: "://invalid-url", // This should cause nanogit.NewHTTPClient to fail
|
|
Branch: "main",
|
|
Token: "test-token",
|
|
Path: "configs",
|
|
}
|
|
|
|
gitRepo, err := NewRepository(ctx, config, gitConfig)
|
|
|
|
// We expect this to fail during client creation
|
|
require.Error(t, err)
|
|
require.Nil(t, gitRepo)
|
|
require.Contains(t, err.Error(), "create nanogit client")
|
|
}
|
|
|
|
func TestGitRepository_ensureBranchExists_ErrorConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "GetRef error other than not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("network error"))
|
|
},
|
|
wantError: true,
|
|
errorMsg: "check branch exists",
|
|
},
|
|
{
|
|
name: "CreateRef error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call - branch doesn't exist
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Second call - get source branch
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
// CreateRef fails
|
|
mockClient.CreateRefReturns(errors.New("create ref failed"))
|
|
},
|
|
wantError: true,
|
|
errorMsg: "create branch",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err := gitRepo.ensureBranchExists(context.Background(), "new-branch")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Read_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
filePath string
|
|
wantError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "get tree by path error (not ErrObjectNotFound)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetTreeByPathReturns(nil, errors.New("tree error"))
|
|
},
|
|
filePath: "directory/",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "get commit error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(nil, errors.New("commit error"))
|
|
},
|
|
filePath: "file.yaml",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "get blob by path error (not ErrObjectNotFound)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Hash: hash.MustFromHex("0102030405060708092a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(nil, errors.New("blob error"))
|
|
},
|
|
filePath: "file.yaml",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
fileInfo, err := gitRepo.Read(context.Background(), tt.filePath, "main")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Nil(t, fileInfo)
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fileInfo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Create_ErrorConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "NewStagedWriter error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error"))
|
|
},
|
|
wantError: true,
|
|
errorMsg: "create staged writer",
|
|
},
|
|
{
|
|
name: "CreateBlob error (not ErrObjectAlreadyExists)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob error"))
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
wantError: true,
|
|
errorMsg: "create blob",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Create(context.Background(), "test.yaml", "main", []byte("content"), "comment")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Update_ErrorConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "NewStagedWriter error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
|
|
}, nil)
|
|
mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error"))
|
|
},
|
|
wantError: true,
|
|
errorMsg: "create staged writer",
|
|
},
|
|
{
|
|
name: "UpdateBlob error (not ErrObjectNotFound)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob error"))
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
wantError: true,
|
|
errorMsg: "update blob",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Update(context.Background(), "test.yaml", "main", []byte("content"), "comment")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Delete_ErrorConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
path string
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "NewStagedWriter error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error"))
|
|
},
|
|
path: "test.yaml",
|
|
wantError: true,
|
|
errorMsg: "create staged writer",
|
|
},
|
|
{
|
|
name: "DeleteBlob error (not ErrObjectNotFound)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob error"))
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
path: "test.yaml",
|
|
wantError: true,
|
|
errorMsg: "delete blob",
|
|
},
|
|
{
|
|
name: "DeleteTree error (not ErrObjectNotFound)",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.DeleteTreeReturns(hash.Hash{}, errors.New("delete tree error"))
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
path: "testdir/",
|
|
wantError: true,
|
|
errorMsg: "delete tree",
|
|
},
|
|
{
|
|
name: "DeleteTree ErrObjectNotFound",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.DeleteTreeReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
},
|
|
path: "missing-dir/",
|
|
wantError: true,
|
|
errorMsg: "file not found", // Should return repository.ErrFileNotFound
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Delete(context.Background(), tt.path, "main", "comment")
|
|
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
// Only setup for ref resolution
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "configs/new-file.yaml",
|
|
Status: protocol.FileStatusAdded,
|
|
},
|
|
}, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
changes, err := gitRepo.CompareFiles(context.Background(), "", "feature")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, changes)
|
|
require.Len(t, changes, 1)
|
|
require.Equal(t, "new-file.yaml", changes[0].Path)
|
|
|
|
// Verify CompareCommits was called with empty base hash and feature hash
|
|
require.Equal(t, 1, mockClient.CompareCommitsCallCount())
|
|
_, baseHash, refHash := mockClient.CompareCommitsArgsForCall(0)
|
|
require.Equal(t, hash.Zero, baseHash) // Empty base should be zero hash
|
|
require.Equal(t, hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), refHash)
|
|
}
|
|
|
|
func TestGitRepository_EmptyRefHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
}{
|
|
{"Create with empty ref", "Create"},
|
|
{"Update with empty ref", "Update"},
|
|
{"Delete with empty ref", "Delete"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.UpdateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.DeleteBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
var err error
|
|
switch tt.method {
|
|
case "Create":
|
|
err = gitRepo.Create(context.Background(), "test.yaml", "", []byte("content"), "comment")
|
|
case "Update":
|
|
err = gitRepo.Update(context.Background(), "test.yaml", "", []byte("content"), "comment")
|
|
case "Delete":
|
|
err = gitRepo.Delete(context.Background(), "test.yaml", "", "comment")
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_CompareFiles_ResolveErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(*mocks.FakeClient)
|
|
base string
|
|
ref string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "resolve base ref error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("base ref error"))
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: "resolve base ref",
|
|
},
|
|
{
|
|
name: "resolve ref error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call succeeds for base
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
// Second call fails for ref
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, errors.New("ref error"))
|
|
},
|
|
base: "main",
|
|
ref: "feature",
|
|
wantError: "resolve ref",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
changes, err := gitRepo.CompareFiles(context.Background(), tt.base, tt.ref)
|
|
|
|
require.Error(t, err)
|
|
require.Nil(t, changes)
|
|
require.Contains(t, err.Error(), tt.wantError)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Read_EmptyRef(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{
|
|
Content: []byte("file content"),
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
fileInfo, err := gitRepo.Read(context.Background(), "test.yaml", "")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fileInfo)
|
|
require.Equal(t, "test.yaml", fileInfo.Path)
|
|
require.Equal(t, []byte("file content"), fileInfo.Data)
|
|
}
|
|
|
|
func TestGitRepository_ReadTree_EmptyRef(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetFlatTreeReturns(&nanogit.FlatTree{
|
|
Entries: []nanogit.FlatTreeEntry{
|
|
{
|
|
Path: "configs/test.yaml",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
Type: protocol.ObjectTypeBlob,
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
entries, err := gitRepo.ReadTree(context.Background(), "")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entries)
|
|
require.Len(t, entries, 1)
|
|
require.Equal(t, "test.yaml", entries[0].Path)
|
|
}
|
|
|
|
func TestGitRepository_Update_EnsureBranchExistsError(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("branch error"))
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Update(context.Background(), "test.yaml", "feature", []byte("content"), "comment")
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "branch error")
|
|
}
|
|
|
|
func TestGitRepository_Create_EnsureBranchExistsError(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("branch error"))
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Create(context.Background(), "test.yaml", "feature", []byte("content"), "comment")
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "branch error")
|
|
}
|
|
|
|
func TestGitRepository_Delete_EnsureBranchExistsError(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{}, errors.New("branch error"))
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := gitRepo.Delete(context.Background(), "test.yaml", "feature", "comment")
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "branch error")
|
|
}
|
|
|
|
func TestGitRepository_Update_DirectoryCheck(t *testing.T) {
|
|
gitRepo := &gitRepository{
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test the internal update function directly with directory path
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
err := gitRepo.update(context.Background(), "directory/", []byte("content"), mockWriter)
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "cannot update a directory")
|
|
}
|
|
|
|
func TestGitRepository_Write_DefaultRef(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
// First call for Read check - file not found
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{}, nanogit.ErrObjectNotFound)
|
|
|
|
// Second call for Create
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockWriter.CreateBlobReturns(hash.Hash{}, nil)
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Write with empty ref to trigger default branch usage
|
|
err := gitRepo.Write(context.Background(), "test.yaml", "", []byte("content"), "message")
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestGitRepository_Read_RefInFileInfo(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetBlobByPathReturns(&nanogit.Blob{
|
|
Content: []byte("file content"),
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Read with specific ref to ensure ref is preserved in FileInfo
|
|
fileInfo, err := gitRepo.Read(context.Background(), "test.yaml", "feature")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fileInfo)
|
|
require.Equal(t, "test.yaml", fileInfo.Path)
|
|
require.Equal(t, "feature", fileInfo.Ref) // Should preserve original ref, not hash
|
|
require.Equal(t, []byte("file content"), fileInfo.Data)
|
|
}
|
|
|
|
func TestGitRepository_Read_GetTreeByPath_NotFound(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetCommitReturns(&nanogit.Commit{
|
|
Tree: hash.Hash{30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49},
|
|
}, nil)
|
|
mockClient.GetTreeByPathReturns(nil, nanogit.ErrObjectNotFound)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test reading a directory that doesn't exist
|
|
fileInfo, err := gitRepo.Read(context.Background(), "missing-dir/", "main")
|
|
|
|
require.Error(t, err)
|
|
require.Nil(t, fileInfo)
|
|
require.ErrorIs(t, err, repository.ErrFileNotFound)
|
|
}
|
|
|
|
func TestGitRepository_ReadTree_GetFlatTree_NotFound(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetFlatTreeReturns(nil, nanogit.ErrObjectNotFound)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test reading tree when the commit doesn't exist
|
|
entries, err := gitRepo.ReadTree(context.Background(), "main")
|
|
|
|
require.Error(t, err)
|
|
require.Nil(t, entries)
|
|
require.ErrorIs(t, err, repository.ErrRefNotFound)
|
|
}
|
|
|
|
func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status protocol.FileStatus
|
|
}{
|
|
{"FileStatusAdded outside path", protocol.FileStatusAdded},
|
|
{"FileStatusModified outside path", protocol.FileStatusModified},
|
|
{"FileStatusDeleted outside path", protocol.FileStatusDeleted},
|
|
{"FileStatusTypeChanged outside path", protocol.FileStatusTypeChanged},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"),
|
|
}, nil)
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/feature",
|
|
Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"),
|
|
}, nil)
|
|
mockClient.CompareCommitsReturns([]nanogit.CommitFile{
|
|
{
|
|
Path: "other/file.yaml", // File outside "configs/" path
|
|
Status: tt.status,
|
|
},
|
|
{
|
|
Path: "configs/included.yaml", // File inside configured path
|
|
Status: tt.status,
|
|
},
|
|
}, nil)
|
|
|
|
gitRepo := &gitRepository{
|
|
client: mockClient,
|
|
gitConfig: RepositoryConfig{
|
|
Branch: "main",
|
|
Path: "configs",
|
|
},
|
|
config: &provisioning.Repository{
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitHubRepositoryType,
|
|
},
|
|
},
|
|
}
|
|
|
|
changes, err := gitRepo.CompareFiles(context.Background(), "main", "feature")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, changes)
|
|
// Should only include the file inside the configured path
|
|
require.Len(t, changes, 1)
|
|
require.Equal(t, "included.yaml", changes[0].Path)
|
|
require.Equal(t, "feature", changes[0].Ref)
|
|
|
|
// Verify the action based on status
|
|
switch tt.status {
|
|
case protocol.FileStatusAdded:
|
|
require.Equal(t, repository.FileActionCreated, changes[0].Action)
|
|
case protocol.FileStatusModified:
|
|
require.Equal(t, repository.FileActionUpdated, changes[0].Action)
|
|
case protocol.FileStatusDeleted:
|
|
require.Equal(t, repository.FileActionDeleted, changes[0].Action)
|
|
require.Equal(t, "main", changes[0].PreviousRef)
|
|
require.Equal(t, "included.yaml", changes[0].PreviousPath)
|
|
case protocol.FileStatusTypeChanged:
|
|
require.Equal(t, repository.FileActionUpdated, changes[0].Action)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Move(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldPath string
|
|
newPath string
|
|
ref string
|
|
comment string
|
|
setupMock func(*mocks.FakeClient)
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "successful move",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("1234567890abcdef1234567890abcdef12345678")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob - returns the hash of the moved blob
|
|
movedHash, _ := hash.FromHex("abcdef1234567890abcdef1234567890abcdef12")
|
|
mockWriter.MoveBlobReturns(movedHash, nil)
|
|
|
|
// Mock commit and push
|
|
commitHash, _ := hash.FromHex("fedcba0987654321fedcba0987654321fedcba09")
|
|
mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
},
|
|
{
|
|
name: "move with empty ref uses default branch",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "",
|
|
comment: "move file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior for default branch
|
|
refHash, _ := hash.FromHex("aaaa1111bbbb2222cccc3333dddd4444eeee5555")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob
|
|
moveHash, _ := hash.FromHex("bbbb2222cccc3333dddd4444eeee5555ffff6666")
|
|
mockWriter.MoveBlobReturns(moveHash, nil)
|
|
|
|
// Mock commit and push
|
|
commitHash, _ := hash.FromHex("cccc3333dddd4444eeee5555ffff6666aaaa1111")
|
|
mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
},
|
|
{
|
|
name: "successful directory move",
|
|
oldPath: "old/",
|
|
newPath: "new/",
|
|
ref: "main",
|
|
comment: "move directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("dddd4444eeee5555ffff6666aaaa1111bbbb2222")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveTree (for directories, we trim trailing slashes)
|
|
treeHash, _ := hash.FromHex("eeee5555ffff6666aaaa1111bbbb2222cccc3333")
|
|
mockWriter.MoveTreeReturns(treeHash, nil)
|
|
|
|
// Mock commit and push
|
|
commitHash, _ := hash.FromHex("ffff6666aaaa1111bbbb2222cccc3333dddd4444")
|
|
mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil)
|
|
mockWriter.PushReturns(nil)
|
|
},
|
|
},
|
|
{
|
|
name: "move file to directory type should fail",
|
|
oldPath: "file.yaml",
|
|
newPath: "directory/",
|
|
ref: "main",
|
|
comment: "move file to directory",
|
|
expectedError: "cannot move between file and directory types",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mocks needed as this should fail early during validation
|
|
},
|
|
},
|
|
{
|
|
name: "move non-existent file",
|
|
oldPath: "nonexistent.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move missing file",
|
|
expectedError: "file not found",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("aaaa0000bbbb1111cccc2222dddd3333eeee4444")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob to return not found error
|
|
mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
},
|
|
{
|
|
name: "move to existing file should fail",
|
|
oldPath: "old.yaml",
|
|
newPath: "existing.yaml",
|
|
ref: "main",
|
|
comment: "move to existing",
|
|
expectedError: "file already exists",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("ffff0000eeee1111dddd2222cccc3333bbbb4444")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob to return already exists error
|
|
mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists)
|
|
},
|
|
},
|
|
{
|
|
name: "branch creation fails",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "nonexistent-branch",
|
|
comment: "move on nonexistent branch",
|
|
expectedError: "get source branch ref",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock branch not found
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Mock getting source branch for creation - also fails
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
},
|
|
{
|
|
name: "staged writer creation fails",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move file",
|
|
expectedError: "create staged writer",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("1111aaaa2222bbbb3333cccc4444dddd5555eeee")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter failure
|
|
mockClient.NewStagedWriterReturns(nil, errors.New("writer creation failed"))
|
|
},
|
|
},
|
|
{
|
|
name: "commit fails",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move file",
|
|
expectedError: "commit changes",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("2222bbbb3333cccc4444dddd5555eeee6666ffff")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob success
|
|
moveHash, _ := hash.FromHex("3333cccc4444dddd5555eeee6666ffff7777aaaa")
|
|
mockWriter.MoveBlobReturns(moveHash, nil)
|
|
|
|
// Mock commit failure
|
|
mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed"))
|
|
},
|
|
},
|
|
{
|
|
name: "push fails",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move file",
|
|
expectedError: "push changes",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// Mock ensureBranchExists behavior
|
|
refHash, _ := hash.FromHex("4444dddd5555eeee6666ffff7777aaaa8888bbbb")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
// Mock NewStagedWriter
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
|
|
// Mock MoveBlob success
|
|
moveHash, _ := hash.FromHex("5555eeee6666ffff7777aaaa8888bbbb9999cccc")
|
|
mockWriter.MoveBlobReturns(moveHash, nil)
|
|
|
|
// Mock commit success
|
|
commitHash, _ := hash.FromHex("6666ffff7777aaaa8888bbbb9999cccc0000dddd")
|
|
mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil)
|
|
|
|
// Mock push failure
|
|
mockWriter.PushReturns(errors.New("push failed"))
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup mock
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
// Create repository config
|
|
config := &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitConfig := RepositoryConfig{
|
|
URL: "https://github.com/example/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: "configs",
|
|
}
|
|
|
|
gitRepo := &gitRepository{
|
|
config: config,
|
|
gitConfig: gitConfig,
|
|
client: mockClient,
|
|
}
|
|
|
|
// Execute move operation
|
|
err := gitRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment)
|
|
|
|
// Verify results
|
|
if tt.expectedError != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.expectedError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitRepository_Move_ErrorConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldPath string
|
|
newPath string
|
|
ref string
|
|
comment string
|
|
setupMock func(*mocks.FakeClient)
|
|
expectedError string
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "MoveBlob - ErrObjectNotFound",
|
|
oldPath: "missing.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move missing file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("1234567890abcdef1234567890abcdef12345678")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
expectedError: "file not found",
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "MoveBlob - ErrObjectAlreadyExists",
|
|
oldPath: "old.yaml",
|
|
newPath: "existing.yaml",
|
|
ref: "main",
|
|
comment: "move to existing file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("abcdef1234567890abcdef1234567890abcdef12")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists)
|
|
},
|
|
expectedError: "file already exists",
|
|
errorType: repository.ErrFileAlreadyExists,
|
|
},
|
|
{
|
|
name: "MoveBlob - generic error",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "main",
|
|
comment: "move file with generic error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("fedcba0987654321fedcba0987654321fedcba09")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveBlobReturns(hash.Hash{}, errors.New("network error"))
|
|
},
|
|
expectedError: "move blob: network error",
|
|
},
|
|
{
|
|
name: "MoveTree - ErrObjectNotFound",
|
|
oldPath: "missing-dir/",
|
|
newPath: "new-dir/",
|
|
ref: "main",
|
|
comment: "move missing directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("1111222233334444555566667777888899990000")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveTreeReturns(hash.Hash{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
expectedError: "file not found",
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "MoveTree - ErrObjectAlreadyExists",
|
|
oldPath: "old-dir/",
|
|
newPath: "existing-dir/",
|
|
ref: "main",
|
|
comment: "move to existing directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("aaaa1111bbbb2222cccc3333dddd4444eeee5555")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveTreeReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists)
|
|
},
|
|
expectedError: "file already exists",
|
|
errorType: repository.ErrFileAlreadyExists,
|
|
},
|
|
{
|
|
name: "MoveTree - generic error",
|
|
oldPath: "old-dir/",
|
|
newPath: "new-dir/",
|
|
ref: "main",
|
|
comment: "move directory with generic error",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
refHash, _ := hash.FromHex("ffff6666eeee5555dddd4444cccc3333bbbb2222")
|
|
mockClient.GetRefReturns(nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
|
|
mockWriter := &mocks.FakeStagedWriter{}
|
|
mockClient.NewStagedWriterReturns(mockWriter, nil)
|
|
mockWriter.MoveTreeReturns(hash.Hash{}, errors.New("permission denied"))
|
|
},
|
|
expectedError: "move tree: permission denied",
|
|
},
|
|
{
|
|
name: "invalid branch name",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "invalid//branch",
|
|
comment: "move with invalid branch",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught during branch validation
|
|
},
|
|
expectedError: "invalid branch name",
|
|
},
|
|
{
|
|
name: "ensure branch exists fails - source branch not found",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "new-branch",
|
|
comment: "move to new branch when source doesn't exist",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call - new branch doesn't exist
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Second call - source branch also doesn't exist
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
},
|
|
expectedError: "get source branch ref",
|
|
},
|
|
{
|
|
name: "ensure branch exists fails - branch creation error",
|
|
oldPath: "old.yaml",
|
|
newPath: "new.yaml",
|
|
ref: "new-branch",
|
|
comment: "move to new branch with creation failure",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// First call - new branch doesn't exist
|
|
mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound)
|
|
// Second call - get source branch succeeds
|
|
refHash, _ := hash.FromHex("7777aaaa8888bbbb9999cccc0000dddd1111eeee")
|
|
mockClient.GetRefReturnsOnCall(1, nanogit.Ref{
|
|
Name: "refs/heads/main",
|
|
Hash: refHash,
|
|
}, nil)
|
|
// CreateRef fails
|
|
mockClient.CreateRefReturns(errors.New("create ref failed"))
|
|
},
|
|
expectedError: "create branch",
|
|
},
|
|
{
|
|
name: "directory to file move type mismatch",
|
|
oldPath: "directory/",
|
|
newPath: "file.yaml",
|
|
ref: "main",
|
|
comment: "move directory to file",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught during validation
|
|
},
|
|
expectedError: "cannot move between file and directory types",
|
|
},
|
|
{
|
|
name: "file to directory move type mismatch",
|
|
oldPath: "file.yaml",
|
|
newPath: "directory/",
|
|
ref: "main",
|
|
comment: "move file to directory",
|
|
setupMock: func(mockClient *mocks.FakeClient) {
|
|
// No mock setup needed as error is caught during validation
|
|
},
|
|
expectedError: "cannot move between file and directory types",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockClient := &mocks.FakeClient{}
|
|
tt.setupMock(mockClient)
|
|
|
|
config := &provisioning.Repository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-repo",
|
|
},
|
|
Spec: provisioning.RepositorySpec{
|
|
Type: provisioning.GitRepositoryType,
|
|
},
|
|
}
|
|
|
|
gitConfig := RepositoryConfig{
|
|
URL: "https://github.com/example/repo.git",
|
|
Branch: "main",
|
|
Token: "token123",
|
|
Path: "configs",
|
|
}
|
|
|
|
gitRepo := &gitRepository{
|
|
config: config,
|
|
gitConfig: gitConfig,
|
|
client: mockClient,
|
|
}
|
|
|
|
err := gitRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment)
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.expectedError)
|
|
|
|
if tt.errorType != nil {
|
|
require.ErrorIs(t, err, tt.errorType)
|
|
}
|
|
})
|
|
}
|
|
}
|