mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 14:43:49 +08:00

* Spike: Extras * Attempt to wire it up * Hack * Fix issue with jobs * Wire more things up * Fix more wiring stuff * Remove webhook secret key from main registration * Move secret encryption also outside register * Add TODOs in code * Add more explanations * Move connectors to different package * Move pull request job into webhooks * Separate registration * Remove duplicate files * Fix missing function * Extract webhook repository logic out of the core github repository * Use status patcher in webhook connector * Fix change in go mod * Change hooks signature * Remove TODOs * Remove Webhook methos from go-git * Remove leftover * Fix mistake in OpenAPI spec * Fix some tests * Fix some issues * Fix linting
1643 lines
43 KiB
Go
1643 lines
43 KiB
Go
package gogit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-git/go-billy/v5"
|
|
"github.com/go-git/go-billy/v5/memfs"
|
|
"github.com/go-git/go-git/v5"
|
|
plumbing "github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/client"
|
|
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/server"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/go-git/go-git/v5/storage/memory"
|
|
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
|
)
|
|
|
|
type dummySecret struct{}
|
|
|
|
func (d *dummySecret) Decrypt(ctx context.Context, encrypted []byte) ([]byte, error) {
|
|
token, ok := os.LookupEnv("gitwraptoken")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing token in environment")
|
|
}
|
|
return []byte(token), nil
|
|
}
|
|
|
|
func (d *dummySecret) Encrypt(ctx context.Context, plain []byte) ([]byte, error) {
|
|
panic("not implemented")
|
|
}
|
|
|
|
// FIXME!! NOTE!!!!!
|
|
// This is really just a sketchpad while trying to get things working
|
|
// the test makes destructive changes to a real git repository :)
|
|
// this should be removed before committing to main (likely sooner)
|
|
// and replaced with integration tests that check the more specific results
|
|
func TestGoGitWrapper(t *testing.T) {
|
|
_, ok := os.LookupEnv("gitwraptoken")
|
|
if !ok {
|
|
t.Skipf("no token found in environment")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
wrap, err := Clone(ctx, "testdata/clone", &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "unit-tester",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/grafana/git-ui-sync-demo",
|
|
Branch: "ryan-test",
|
|
},
|
|
},
|
|
},
|
|
repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
CreateIfNotExists: true,
|
|
Progress: os.Stdout,
|
|
},
|
|
&dummySecret{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
tree, err := wrap.ReadTree(ctx, "")
|
|
require.NoError(t, err)
|
|
|
|
jj, err := json.MarshalIndent(tree, "", " ")
|
|
require.NoError(t, err)
|
|
|
|
fmt.Printf("TREE:%s\n", string(jj))
|
|
|
|
ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{
|
|
Name: "xxxxx",
|
|
Email: "rrr@yyyy.zzz",
|
|
When: time.Now(),
|
|
})
|
|
|
|
for i := 0; i < 10; i++ {
|
|
fname := fmt.Sprintf("deep/path/in/test_%d.txt", i)
|
|
fmt.Printf("Write:%s\n", fname)
|
|
err = wrap.Write(ctx, fname, "", []byte(fmt.Sprintf("body/%d %s", i, time.Now())), "the commit message")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
fmt.Printf("push...\n")
|
|
err = wrap.Push(ctx, repository.PushOptions{
|
|
Timeout: 10,
|
|
Progress: os.Stdout,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestReadTree(t *testing.T) {
|
|
dir := t.TempDir()
|
|
gitRepo, err := git.PlainInit(dir, false)
|
|
require.NoError(t, err, "failed to init a new git repository")
|
|
tree, err := gitRepo.Worktree()
|
|
require.NoError(t, err, "failed to get worktree")
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test",
|
|
Namespace: "default",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
Title: "test",
|
|
Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow},
|
|
Type: v0alpha1.GitHubRepositoryType,
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/grafana/__unit-test",
|
|
Path: "grafana/",
|
|
Branch: "main",
|
|
},
|
|
},
|
|
Status: v0alpha1.RepositoryStatus{},
|
|
},
|
|
decryptedPassword: "password",
|
|
|
|
repo: gitRepo,
|
|
tree: &worktree{
|
|
Worktree: tree,
|
|
},
|
|
dir: dir,
|
|
}
|
|
|
|
err = os.WriteFile(filepath.Join(dir, "test.txt"), []byte("test"), 0644)
|
|
require.NoError(t, err, "failed to write test file")
|
|
|
|
err = os.Mkdir(filepath.Join(dir, "grafana"), 0750)
|
|
require.NoError(t, err, "failed to mkdir grafana")
|
|
|
|
err = os.WriteFile(filepath.Join(dir, "grafana", "test2.txt"), []byte("test"), 0644)
|
|
require.NoError(t, err, "failed to write grafana/test2 file")
|
|
|
|
ctx := context.Background()
|
|
entries, err := repo.ReadTree(ctx, "HEAD")
|
|
require.NoError(t, err, "failed to read tree")
|
|
|
|
// Here is the meat of why this test exists: the ReadTree call should only read the config.Spec.GitHub.Path files.
|
|
// All prefixes are removed (i.e. a file is just its name, not ${Path}/${Name}).
|
|
// And it does not include the directory in the listing, as it pretends to be the root.
|
|
require.Len(t, entries, 1, "entries from ReadTree")
|
|
require.Equal(t, entries[0].Path, "test2.txt", "entry path")
|
|
}
|
|
|
|
func TestGoGitRepo_History(t *testing.T) {
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test",
|
|
Namespace: "default",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test History method
|
|
ctx := context.Background()
|
|
_, err := repo.History(ctx, "test.txt", "")
|
|
require.Error(t, err, "History should return an error as it's not implemented")
|
|
require.Contains(t, err.Error(), "history is not yet implemented")
|
|
}
|
|
|
|
func TestGoGitRepo_Validate(t *testing.T) {
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test",
|
|
Namespace: "default",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Validate method
|
|
errs := repo.Validate()
|
|
require.Empty(t, errs, "Validate should return no errors")
|
|
}
|
|
|
|
func TestGoGitRepo_Read(t *testing.T) {
|
|
// Setup test cases
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
ref string
|
|
setupMock func(fs billy.Filesystem)
|
|
expectError bool
|
|
errorType error
|
|
checkResult func(t *testing.T, info *repository.FileInfo)
|
|
}{
|
|
{
|
|
name: "successfully read file",
|
|
path: "test.txt",
|
|
ref: "",
|
|
setupMock: func(fs billy.Filesystem) {
|
|
// Create a test file
|
|
f, err := fs.Create("grafana/test.txt")
|
|
require.NoError(t, err, "failed to create test file")
|
|
_, err = f.Write([]byte("test content"))
|
|
require.NoError(t, err, "failed to write test content")
|
|
err = f.Close()
|
|
require.NoError(t, err, "failed to close test file")
|
|
},
|
|
expectError: false,
|
|
checkResult: func(t *testing.T, info *repository.FileInfo) {
|
|
require.Equal(t, "test.txt", info.Path)
|
|
require.Equal(t, "test content", string(info.Data))
|
|
require.NotNil(t, info.Modified)
|
|
},
|
|
},
|
|
{
|
|
name: "empty path",
|
|
path: "",
|
|
ref: "",
|
|
setupMock: func(fs billy.Filesystem) {},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("expected path"),
|
|
},
|
|
{
|
|
name: "ref not supported",
|
|
path: "test.txt",
|
|
ref: "main",
|
|
setupMock: func(fs billy.Filesystem) {},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("ref unsupported"),
|
|
},
|
|
{
|
|
name: "file not found",
|
|
path: "nonexistent.txt",
|
|
ref: "",
|
|
setupMock: func(fs billy.Filesystem) {
|
|
// Don't create the file
|
|
},
|
|
expectError: true,
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "read directory",
|
|
path: "testdir",
|
|
ref: "",
|
|
setupMock: func(fs billy.Filesystem) {
|
|
// Create a test directory
|
|
err := fs.MkdirAll("grafana/testdir", 0755)
|
|
require.NoError(t, err, "failed to create test directory")
|
|
},
|
|
expectError: false,
|
|
checkResult: func(t *testing.T, info *repository.FileInfo) {
|
|
require.Equal(t, "testdir", info.Path)
|
|
require.Nil(t, info.Data)
|
|
require.NotNil(t, info.Modified)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup filesystem and repo
|
|
fs := memfs.New()
|
|
tt.setupMock(fs)
|
|
|
|
// Create a worktree with the filesystem
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
tree: &worktree{
|
|
Worktree: &git.Worktree{
|
|
Filesystem: fs,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Read method
|
|
ctx := context.Background()
|
|
info, err := repo.Read(ctx, tt.path, tt.ref)
|
|
|
|
// Check results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
if errors.Is(tt.errorType, repository.ErrFileNotFound) {
|
|
require.ErrorIs(t, err, repository.ErrFileNotFound)
|
|
} else {
|
|
require.Contains(t, err.Error(), tt.errorType.Error())
|
|
}
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, info)
|
|
tt.checkResult(t, info)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoGitRepo_Delete(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
ref string
|
|
pushOnWrite bool
|
|
setupMock func(mockTree *MockWorktree)
|
|
expectError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "delete existing file",
|
|
path: "testfile.txt",
|
|
ref: "",
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "delete non-existent file",
|
|
path: "nonexistent.txt",
|
|
ref: "",
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
mockTree.On("Remove", "grafana/nonexistent.txt").Return(plumbing.Hash{}, fs.ErrNotExist)
|
|
},
|
|
expectError: true,
|
|
errorType: repository.ErrFileNotFound,
|
|
},
|
|
{
|
|
name: "delete with other error",
|
|
path: "testfile.txt",
|
|
ref: "",
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, fmt.Errorf("some other error"))
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("some other error"),
|
|
},
|
|
{
|
|
name: "empty path",
|
|
path: "",
|
|
ref: "",
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("expected path"),
|
|
},
|
|
{
|
|
name: "with ref",
|
|
path: "testfile.txt",
|
|
ref: "main",
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("ref unsupported"),
|
|
},
|
|
{
|
|
name: "delete with push on write enabled",
|
|
path: "testfile.txt",
|
|
ref: "",
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil)
|
|
mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool {
|
|
return opts.Author != nil &&
|
|
opts.Author.Name == "Test User" &&
|
|
opts.Author.Email == "test@example.com" &&
|
|
opts.Author.When.After(time.Now().Add(-time.Minute)) &&
|
|
opts.Author.When.Before(time.Now().Add(time.Minute))
|
|
})).Return(plumbing.Hash{}, nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "delete with empty commit",
|
|
path: "testfile.txt",
|
|
ref: "",
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil)
|
|
mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool {
|
|
return opts.Author != nil &&
|
|
opts.Author.Name == "Test User" &&
|
|
opts.Author.Email == "test@example.com" &&
|
|
opts.Author.When.After(time.Now().Add(-time.Minute)) &&
|
|
opts.Author.When.Before(time.Now().Add(time.Minute))
|
|
})).Return(plumbing.Hash{}, git.ErrEmptyCommit)
|
|
},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup filesystem and repo
|
|
|
|
mockTree := NewMockWorktree(t)
|
|
tt.setupMock(mockTree)
|
|
|
|
// Create a worktree with the filesystem
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: tt.pushOnWrite,
|
|
},
|
|
}
|
|
|
|
// Test Delete method
|
|
ctx := context.Background()
|
|
// Set author signature for the test
|
|
ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{
|
|
Name: "Test User",
|
|
Email: "test@example.com",
|
|
When: time.Now(),
|
|
})
|
|
|
|
err := repo.Delete(ctx, tt.path, tt.ref, "test delete")
|
|
|
|
// Check results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
if errors.Is(tt.errorType, repository.ErrFileNotFound) {
|
|
require.ErrorIs(t, err, repository.ErrFileNotFound)
|
|
} else {
|
|
require.Contains(t, err.Error(), tt.errorType.Error())
|
|
}
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
mockTree.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// FIXME: missing coverage for Update / Create because we use Write for both
|
|
// when I think it shouldn't be the case as it's inconsistent with the other repository implementations
|
|
func TestGoGitRepo_Write(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
ref string
|
|
data []byte
|
|
pushOnWrite bool
|
|
setupMock func(mockTree *MockWorktree)
|
|
expectError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "successful write",
|
|
path: "test.txt",
|
|
ref: "",
|
|
data: []byte("test content"),
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
fs := memfs.New()
|
|
mockTree.On("Filesystem").Return(fs)
|
|
mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil)
|
|
mockTree.On("Commit", "test write", mock.MatchedBy(func(opts *git.CommitOptions) bool {
|
|
return opts.Author != nil &&
|
|
opts.Author.Name == "Test User" &&
|
|
opts.Author.Email == "test@example.com" &&
|
|
opts.Author.When.After(time.Now().Add(-time.Minute)) &&
|
|
opts.Author.When.Before(time.Now().Add(time.Minute))
|
|
})).Return(plumbing.NewHash("def456"), nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "create folder only",
|
|
path: "testdir/",
|
|
ref: "",
|
|
data: []byte{},
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
fs := memfs.New()
|
|
mockTree.On("Filesystem").Return(fs)
|
|
// No Add or Commit calls expected for directory creation
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "successful write without commit",
|
|
path: "test.txt",
|
|
ref: "",
|
|
data: []byte("test content"),
|
|
pushOnWrite: false,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
fs := memfs.New()
|
|
mockTree.On("Filesystem").Return(fs)
|
|
mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "write with directory creation",
|
|
path: "dir/test.txt",
|
|
ref: "",
|
|
data: []byte("test content"),
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
fs := memfs.New()
|
|
mockTree.On("Filesystem").Return(fs)
|
|
mockTree.On("Add", "grafana/dir/test.txt").Return(plumbing.NewHash("abc123"), nil)
|
|
mockTree.On("Commit", "test write", mock.Anything).Return(plumbing.NewHash("def456"), nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "error on add",
|
|
path: "test.txt",
|
|
ref: "",
|
|
data: []byte("test content"),
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
fs := memfs.New()
|
|
mockTree.On("Filesystem").Return(fs)
|
|
mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash(""), fmt.Errorf("add error"))
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("add error"),
|
|
},
|
|
{
|
|
name: "error with ref",
|
|
path: "test.txt",
|
|
ref: "main",
|
|
data: []byte("test content"),
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
// No mock setup needed as it should fail before using the mock
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("ref unsupported"),
|
|
},
|
|
{
|
|
name: "empty path",
|
|
path: "",
|
|
ref: "",
|
|
data: []byte("test content"),
|
|
pushOnWrite: true,
|
|
setupMock: func(mockTree *MockWorktree) {
|
|
// No mock setup needed as it should fail before using the mock
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("expected path"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup filesystem and repo
|
|
mockTree := NewMockWorktree(t)
|
|
tt.setupMock(mockTree)
|
|
|
|
// Create a worktree with the filesystem
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: tt.pushOnWrite,
|
|
},
|
|
}
|
|
|
|
// Test Write method
|
|
ctx := context.Background()
|
|
// Set author signature for the test
|
|
ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{
|
|
Name: "Test User",
|
|
Email: "test@example.com",
|
|
When: time.Now(),
|
|
})
|
|
|
|
err := repo.Update(ctx, tt.path, tt.ref, tt.data, "test write")
|
|
|
|
// Check results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.Contains(t, err.Error(), tt.errorType.Error())
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
mockTree.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoGitRepo_Test(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
treeInitialized bool
|
|
expectedResult bool
|
|
}{
|
|
{
|
|
name: "tree is initialized",
|
|
treeInitialized: true,
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
name: "tree is not initialized",
|
|
treeInitialized: false,
|
|
expectedResult: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup mock tree
|
|
mockTree := NewMockWorktree(t)
|
|
|
|
// Create repo with or without initialized tree
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
tree: nil,
|
|
}
|
|
|
|
if tt.treeInitialized {
|
|
repo.tree = mockTree
|
|
}
|
|
|
|
// Test the Test method
|
|
ctx := context.Background()
|
|
result, err := repo.Test(ctx)
|
|
|
|
// Verify results
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.Equal(t, tt.expectedResult, result.Success)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoGitRepo_Config(t *testing.T) {
|
|
// Create a test repository configuration
|
|
testConfig := &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create a repository instance with the test configuration
|
|
repo := &GoGitRepo{
|
|
config: testConfig,
|
|
tree: NewMockWorktree(t),
|
|
}
|
|
|
|
// Call the Config method
|
|
result := repo.Config()
|
|
|
|
// Verify the result
|
|
require.NotNil(t, result)
|
|
require.Equal(t, testConfig, result)
|
|
require.Equal(t, "test-repo", result.Name)
|
|
require.Equal(t, "test-namespace", result.Namespace)
|
|
require.Equal(t, "grafana/", result.Spec.GitHub.Path)
|
|
}
|
|
|
|
func TestGoGitRepo_Remove(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(t *testing.T) (*GoGitRepo, string)
|
|
expectError bool
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "successful removal",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, string) {
|
|
// Create a temporary directory that will be removed
|
|
tempDir, err := os.MkdirTemp("", "test-repo-*")
|
|
require.NoError(t, err)
|
|
|
|
// Create a repository instance
|
|
repo := &GoGitRepo{
|
|
dir: tempDir,
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
},
|
|
}
|
|
|
|
return repo, tempDir
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "directory already removed",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, string) {
|
|
// Create a temporary directory
|
|
tempDir, err := os.MkdirTemp("", "test-repo-*")
|
|
require.NoError(t, err)
|
|
|
|
// Remove it immediately to simulate it being already gone
|
|
err = os.RemoveAll(tempDir)
|
|
require.NoError(t, err)
|
|
|
|
// Create a repository instance pointing to the removed directory
|
|
repo := &GoGitRepo{
|
|
dir: tempDir,
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
},
|
|
}
|
|
|
|
return repo, tempDir
|
|
},
|
|
expectError: false, // RemoveAll doesn't error if directory doesn't exist
|
|
},
|
|
{
|
|
name: "invalid directory path",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, string) {
|
|
// Create a repository instance with an invalid directory path
|
|
// that should cause an error when trying to remove
|
|
invalidPath := string([]byte{0})
|
|
|
|
repo := &GoGitRepo{
|
|
dir: invalidPath,
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test-repo",
|
|
Namespace: "test-namespace",
|
|
},
|
|
},
|
|
}
|
|
|
|
return repo, invalidPath
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "invalid argument",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup the test
|
|
repo, _ := tt.setupMock(t)
|
|
|
|
// Test the Remove method
|
|
ctx := context.Background()
|
|
err := repo.Remove(ctx)
|
|
|
|
// Verify results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.expectedErrMsg != "" {
|
|
require.Contains(t, err.Error(), tt.expectedErrMsg)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
// Verify the directory no longer exists
|
|
_, statErr := os.Stat(repo.dir)
|
|
require.True(t, os.IsNotExist(statErr), "Directory should not exist after removal")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoGitRepo_Push(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree)
|
|
pushOpts repository.PushOptions
|
|
expectError bool
|
|
errorType error
|
|
}{
|
|
{
|
|
name: "successful push",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool {
|
|
if o.Auth == nil {
|
|
return false
|
|
}
|
|
// Verify we're using basic auth with expected credentials
|
|
basicAuth, ok := o.Auth.(*githttp.BasicAuth)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return basicAuth.Username == "grafana" && basicAuth.Password == "test-token"
|
|
})).Return(nil)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push error",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(fmt.Errorf("network error"))
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("network error"),
|
|
},
|
|
{
|
|
name: "already up to date",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(git.NoErrAlreadyUpToDate)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with custom timeout",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{
|
|
Timeout: 5 * time.Minute,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with custom progress writer",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool {
|
|
return o.Progress != nil && o.Progress != io.Discard
|
|
})).Return(nil)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{
|
|
Progress: &bytes.Buffer{},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with BeforeFn success",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, nil
|
|
},
|
|
pushOpts: repository.PushOptions{
|
|
BeforeFn: func() error {
|
|
return nil
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with BeforeFn error",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
// No mock expectations since BeforeFn will fail before PushContext is called
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: NewMockRepository(t),
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: true,
|
|
},
|
|
}
|
|
|
|
return repo, repo.repo.(*MockRepository), nil
|
|
},
|
|
pushOpts: repository.PushOptions{
|
|
BeforeFn: func() error {
|
|
return fmt.Errorf("before function failed")
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("before function failed"),
|
|
},
|
|
{
|
|
name: "push with PushOnWrites=false commits changes",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
mockTree := NewMockWorktree(t)
|
|
mockTree.On("Commit", "exported from grafana", mock.MatchedBy(func(o *git.CommitOptions) bool {
|
|
return o.All == true
|
|
})).Return(plumbing.NewHash("abc123"), nil)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
tree: mockTree,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, mockTree
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with PushOnWrites=false and empty commit",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockRepo := NewMockRepository(t)
|
|
mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
mockTree := NewMockWorktree(t)
|
|
mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, git.ErrEmptyCommit)
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: mockRepo,
|
|
tree: mockTree,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
},
|
|
}
|
|
|
|
return repo, mockRepo, mockTree
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "push with PushOnWrites=false and commit error",
|
|
setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) {
|
|
mockTree := NewMockWorktree(t)
|
|
mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, fmt.Errorf("commit error"))
|
|
|
|
repo := &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
repo: NewMockRepository(t),
|
|
tree: mockTree,
|
|
decryptedPassword: "test-token",
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
},
|
|
}
|
|
|
|
return repo, repo.repo.(*MockRepository), mockTree
|
|
},
|
|
pushOpts: repository.PushOptions{},
|
|
expectError: true,
|
|
errorType: fmt.Errorf("commit error"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup the test
|
|
repo, mockRepo, mockTree := tt.setupMock(t)
|
|
|
|
// Test the Push method
|
|
ctx := context.Background()
|
|
err := repo.Push(ctx, tt.pushOpts)
|
|
|
|
// Verify results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.errorType != nil {
|
|
require.Contains(t, err.Error(), tt.errorType.Error())
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify mock expectations if mocks were created
|
|
if mockRepo != nil {
|
|
mockRepo.AssertExpectations(t)
|
|
}
|
|
if mockTree != nil {
|
|
mockTree.AssertExpectations(t)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoGitRepo_ReadTree(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupMock func(t *testing.T) *GoGitRepo
|
|
ref string
|
|
expectError bool
|
|
expectedErrMsg string
|
|
expectedFiles []repository.FileTreeEntry
|
|
}{
|
|
{
|
|
name: "successful read with files",
|
|
setupMock: func(t *testing.T) *GoGitRepo {
|
|
mockFS := memfs.New()
|
|
|
|
// Create test files in the mock filesystem
|
|
require.NoError(t, mockFS.MkdirAll("grafana/folder1", 0750))
|
|
file1, err := mockFS.Create("grafana/file1.txt")
|
|
require.NoError(t, err)
|
|
_, err = file1.Write([]byte("test content"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, file1.Close())
|
|
|
|
file2, err := mockFS.Create("grafana/folder1/file2.txt")
|
|
require.NoError(t, err)
|
|
_, err = file2.Write([]byte("nested file content"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, file2.Close())
|
|
|
|
mockTree := NewMockWorktree(t)
|
|
mockTree.On("Filesystem").Return(mockFS)
|
|
|
|
return &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "grafana/",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
}
|
|
},
|
|
ref: "main",
|
|
expectError: false,
|
|
expectedFiles: []repository.FileTreeEntry{
|
|
{Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"},
|
|
{Path: "folder1", Size: 0, Blob: false},
|
|
{Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"},
|
|
},
|
|
},
|
|
{
|
|
name: "filesystem error",
|
|
setupMock: func(t *testing.T) *GoGitRepo {
|
|
mockTree := NewMockWorktree(t)
|
|
mockFS := memfs.New()
|
|
|
|
// Create a filesystem that will return an error when accessed
|
|
mockTree.On("Filesystem").Return(mockFS)
|
|
|
|
return &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "non-existent-path/",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
}
|
|
},
|
|
ref: "main",
|
|
expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries
|
|
expectedFiles: []repository.FileTreeEntry{},
|
|
},
|
|
{
|
|
name: "successful read with empty path",
|
|
setupMock: func(t *testing.T) *GoGitRepo {
|
|
mockFS := memfs.New()
|
|
|
|
// Create test files in the mock filesystem
|
|
file1, err := mockFS.Create("file1.txt")
|
|
require.NoError(t, err)
|
|
_, err = file1.Write([]byte("test content"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, file1.Close())
|
|
|
|
require.NoError(t, mockFS.MkdirAll("folder1", 0750))
|
|
file2, err := mockFS.Create("folder1/file2.txt")
|
|
require.NoError(t, err)
|
|
_, err = file2.Write([]byte("nested file content"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, file2.Close())
|
|
|
|
// Create .git directory which should be ignored
|
|
require.NoError(t, mockFS.MkdirAll(".git", 0750))
|
|
gitFile, err := mockFS.Create(".git/config")
|
|
require.NoError(t, err)
|
|
_, err = gitFile.Write([]byte("git config"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, gitFile.Close())
|
|
|
|
mockTree := NewMockWorktree(t)
|
|
mockTree.On("Filesystem").Return(mockFS)
|
|
|
|
return &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
}
|
|
},
|
|
ref: "main",
|
|
expectError: false,
|
|
expectedFiles: []repository.FileTreeEntry{
|
|
{Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"},
|
|
{Path: "folder1", Size: 0, Blob: false},
|
|
{Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"},
|
|
},
|
|
},
|
|
{
|
|
name: "filesystem error",
|
|
setupMock: func(t *testing.T) *GoGitRepo {
|
|
mockTree := NewMockWorktree(t)
|
|
mockFS := memfs.New()
|
|
|
|
// Create a filesystem that will return an error when accessed
|
|
mockTree.On("Filesystem").Return(mockFS)
|
|
|
|
return &GoGitRepo{
|
|
config: &v0alpha1.Repository{
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
Path: "non-existent-path/",
|
|
},
|
|
},
|
|
},
|
|
tree: mockTree,
|
|
}
|
|
},
|
|
ref: "main",
|
|
expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries
|
|
expectedFiles: []repository.FileTreeEntry{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup the test
|
|
repo := tt.setupMock(t)
|
|
|
|
// Test the ReadTree method
|
|
ctx := context.Background()
|
|
entries, err := repo.ReadTree(ctx, tt.ref)
|
|
|
|
// Verify results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
if tt.expectedErrMsg != "" {
|
|
require.Contains(t, err.Error(), tt.expectedErrMsg)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
|
|
// Sort entries for consistent comparison
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Path < entries[j].Path
|
|
})
|
|
sort.Slice(tt.expectedFiles, func(i, j int) bool {
|
|
return tt.expectedFiles[i].Path < tt.expectedFiles[j].Path
|
|
})
|
|
|
|
require.Equal(t, len(tt.expectedFiles), len(entries), "Number of entries should match")
|
|
for i, expected := range tt.expectedFiles {
|
|
require.Equal(t, expected.Path, entries[i].Path, "Path should match")
|
|
require.Equal(t, expected.Size, entries[i].Size, "Size should match")
|
|
require.Equal(t, expected.Blob, entries[i].Blob, "Blob flag should match")
|
|
if expected.Blob {
|
|
require.Equal(t, expected.Hash, entries[i].Hash, "Hash should match")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify mock expectations
|
|
repo.tree.(*MockWorktree).AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClone(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
root string
|
|
config *v0alpha1.Repository
|
|
createRepo bool
|
|
opts repository.CloneOptions
|
|
setupMock func(secrets *secrets.MockService)
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "successful clone",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/test/repo",
|
|
Branch: "main",
|
|
},
|
|
},
|
|
},
|
|
createRepo: true,
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {
|
|
mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "successful clone with create if not exists",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/test/repo",
|
|
Branch: "non-existent-branch",
|
|
},
|
|
},
|
|
},
|
|
createRepo: true,
|
|
opts: repository.CloneOptions{
|
|
PushOnWrites: false,
|
|
CreateIfNotExists: true,
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {
|
|
mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil)
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "timeout cancellation",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/test/repo",
|
|
Branch: "main",
|
|
},
|
|
},
|
|
},
|
|
opts: repository.CloneOptions{
|
|
Timeout: 1 * time.Millisecond, // Very short timeout to trigger cancellation
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {
|
|
mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil)
|
|
// Simulate a slow operation that will be cancelled by timeout
|
|
time.Sleep(20 * time.Millisecond)
|
|
},
|
|
expectError: true,
|
|
errorMsg: "context deadline exceeded",
|
|
},
|
|
{
|
|
name: "empty root",
|
|
root: "",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {},
|
|
expectError: true,
|
|
errorMsg: "missing root config",
|
|
},
|
|
{
|
|
name: "missing namespace",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: "test-repo",
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {},
|
|
expectError: true,
|
|
errorMsg: "missing namespace",
|
|
},
|
|
{
|
|
name: "missing name",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {},
|
|
expectError: true,
|
|
errorMsg: "missing name",
|
|
},
|
|
{
|
|
name: "beforeFn error",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
},
|
|
opts: repository.CloneOptions{
|
|
BeforeFn: func() error {
|
|
return fmt.Errorf("beforeFn error")
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {},
|
|
expectError: true,
|
|
errorMsg: "beforeFn error",
|
|
},
|
|
{
|
|
name: "secret decryption error",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
EncryptedToken: []byte("test-token"),
|
|
},
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {
|
|
mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), fmt.Errorf("error decrypting token"))
|
|
},
|
|
expectError: true,
|
|
errorMsg: "error decrypting token",
|
|
},
|
|
{
|
|
name: "clone error",
|
|
root: "testdata/clone",
|
|
config: &v0alpha1.Repository{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "test-ns",
|
|
Name: "test-repo",
|
|
},
|
|
Spec: v0alpha1.RepositorySpec{
|
|
GitHub: &v0alpha1.GitHubRepositoryConfig{
|
|
URL: "https://github.com/test/repo",
|
|
Branch: "main",
|
|
},
|
|
},
|
|
},
|
|
setupMock: func(mockSecrets *secrets.MockService) {
|
|
mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil)
|
|
},
|
|
expectError: true,
|
|
errorMsg: "clone error",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup test environment
|
|
mockSecrets := secrets.NewMockService(t)
|
|
tt.setupMock(mockSecrets)
|
|
|
|
// Create a temporary directory for each test
|
|
if tt.root != "" {
|
|
tempDir := t.TempDir()
|
|
tt.root = tempDir
|
|
}
|
|
if tt.createRepo {
|
|
tt.config.Spec.GitHub.URL = createTestRepo(t)
|
|
}
|
|
|
|
// Execute the test
|
|
ctx := context.Background()
|
|
repo, err := Clone(ctx, tt.root, tt.config, tt.opts, mockSecrets)
|
|
|
|
// Verify results
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.errorMsg)
|
|
require.Nil(t, repo)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, repo)
|
|
|
|
// Verify the returned repository
|
|
gitRepo, ok := repo.(*GoGitRepo)
|
|
require.True(t, ok)
|
|
require.Equal(t, tt.config, gitRepo.config)
|
|
require.NotEmpty(t, gitRepo.dir)
|
|
require.NotNil(t, gitRepo.tree)
|
|
require.NotNil(t, gitRepo.repo)
|
|
|
|
// Clean up
|
|
err = repo.Remove(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
mockSecrets.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func createTestRepo(t *testing.T) string {
|
|
// Create memory filesystem
|
|
fs := memfs.New()
|
|
|
|
// Initialize new repo
|
|
repo, err := git.Init(memory.NewStorage(), fs)
|
|
require.NoError(t, err, "Failed to init test repo")
|
|
|
|
w, err := repo.Worktree()
|
|
require.NoError(t, err, "Failed to get worktree")
|
|
|
|
// Create a dummy file
|
|
f, err := fs.Create("README.md")
|
|
require.NoError(t, err, "Failed to create file")
|
|
_, err = f.Write([]byte("Hello, world!"))
|
|
require.NoError(t, err, "Failed to write content")
|
|
err = f.Close()
|
|
require.NoError(t, err, "Failed to close file")
|
|
|
|
// Add and commit the file
|
|
_, err = w.Add("README.md")
|
|
require.NoError(t, err, "Failed to add file")
|
|
|
|
// Create initial commit
|
|
_, err = w.Commit("initial commit", &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: "Test User",
|
|
Email: "test@example.com",
|
|
When: time.Now(),
|
|
},
|
|
})
|
|
require.NoError(t, err, "Failed to commit")
|
|
// Create a branch
|
|
headRef, err := repo.Head()
|
|
require.NoError(t, err, "Failed to get HEAD reference")
|
|
|
|
// Create a new branch reference pointing to the current HEAD commit
|
|
branchRef := plumbing.NewBranchReferenceName("main")
|
|
ref := plumbing.NewHashReference(branchRef, headRef.Hash())
|
|
|
|
// Save the reference to create the branch
|
|
err = repo.Storer.SetReference(ref)
|
|
require.NoError(t, err, "Failed to create branch")
|
|
|
|
// Checkout the new branch
|
|
err = w.Checkout(&git.CheckoutOptions{
|
|
Branch: branchRef,
|
|
})
|
|
require.NoError(t, err, "Failed to checkout branch")
|
|
|
|
// Create a map of repositories for the server
|
|
repos := make(map[string]*git.Repository)
|
|
repos["test-repo.git"] = repo
|
|
|
|
// Create and install the server
|
|
loader := server.MapLoader{
|
|
"file://test-repo.git": repo.Storer,
|
|
}
|
|
srv := server.NewServer(loader)
|
|
client.InstallProtocol("file", srv)
|
|
|
|
return "file://test-repo.git"
|
|
}
|