344 lines
12 KiB
Go
344 lines
12 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
)
|
|
|
|
type mockRepoManager struct {
|
|
downloadErr error
|
|
commitID string
|
|
commitIDErr error
|
|
refs []string
|
|
refsErr error
|
|
files []string
|
|
filesErr error
|
|
|
|
downloadCalled int
|
|
commitIDCalled int
|
|
listRefsCalled int
|
|
listFilesCalled int
|
|
lastCloneOptions *git.CloneOptions
|
|
}
|
|
|
|
func (m *mockRepoManager) Download(_ context.Context, _ string, opts *git.CloneOptions) error {
|
|
m.downloadCalled++
|
|
m.lastCloneOptions = opts
|
|
return m.downloadErr
|
|
}
|
|
|
|
func (m *mockRepoManager) LatestCommitID(_ context.Context, _, _ string, _ *git.ListOptions) (string, error) {
|
|
m.commitIDCalled++
|
|
return m.commitID, m.commitIDErr
|
|
}
|
|
|
|
func (m *mockRepoManager) ListRefs(_ context.Context, _ string, _ *git.ListOptions) ([]string, error) {
|
|
m.listRefsCalled++
|
|
return m.refs, m.refsErr
|
|
}
|
|
|
|
func (m *mockRepoManager) ListFiles(_ context.Context, _ bool, _ *git.CloneOptions) ([]string, error) {
|
|
m.listFilesCalled++
|
|
return m.files, m.filesErr
|
|
}
|
|
|
|
func newTestService(ctx context.Context, cacheSize int, gitMgr, azureMgr RepoManager) *Service {
|
|
s := newService(ctx, cacheSize, 0)
|
|
s.git = gitMgr
|
|
s.azure = azureMgr
|
|
return s
|
|
}
|
|
|
|
func TestCloneRepository(t *testing.T) {
|
|
t.Parallel()
|
|
downloadErr := errors.New("clone failed")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
url string
|
|
referenceName string
|
|
gitManagerDownloadCalled int
|
|
azureManagerDownloadCalled int
|
|
managerErr bool
|
|
expectedError error
|
|
expectedReferenceName string
|
|
}{
|
|
{
|
|
name: "non-azure URL routes to git manager",
|
|
url: "https://github.com/example/repo.git",
|
|
gitManagerDownloadCalled: 1,
|
|
azureManagerDownloadCalled: 0,
|
|
},
|
|
{
|
|
name: "azure URL routes to azure manager",
|
|
url: "https://dev.azure.com/org/project/_git/repo",
|
|
gitManagerDownloadCalled: 0,
|
|
azureManagerDownloadCalled: 1,
|
|
},
|
|
{
|
|
name: "error from manager propagated",
|
|
url: "https://github.com/example/repo.git",
|
|
managerErr: true,
|
|
gitManagerDownloadCalled: 1,
|
|
expectedError: downloadErr,
|
|
},
|
|
{
|
|
name: "ReferenceName is passed to clone options",
|
|
url: "https://github.com/example/repo.git",
|
|
referenceName: "refs/heads/feature-branch",
|
|
gitManagerDownloadCalled: 1,
|
|
expectedReferenceName: "refs/heads/feature-branch",
|
|
},
|
|
{
|
|
name: "empty ReferenceName leaves clone options unset",
|
|
url: "https://github.com/example/repo.git",
|
|
referenceName: "",
|
|
gitManagerDownloadCalled: 1,
|
|
expectedReferenceName: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{}
|
|
azureMgr := &mockRepoManager{}
|
|
if tc.managerErr {
|
|
gitMgr.downloadErr = downloadErr
|
|
azureMgr.downloadErr = downloadErr
|
|
}
|
|
s := newTestService(t.Context(), 4, gitMgr, azureMgr)
|
|
|
|
err := s.CloneRepository(t.Context(), "/tmp", tc.url, tc.referenceName, "", "", false)
|
|
require.Equal(t, tc.expectedError, err)
|
|
require.Equal(t, tc.gitManagerDownloadCalled, gitMgr.downloadCalled)
|
|
require.Equal(t, tc.azureManagerDownloadCalled, azureMgr.downloadCalled)
|
|
|
|
activeMgr := gitMgr
|
|
if tc.azureManagerDownloadCalled > 0 {
|
|
activeMgr = azureMgr
|
|
}
|
|
if activeMgr.lastCloneOptions != nil {
|
|
require.Equal(t, plumbing.ReferenceName(tc.expectedReferenceName), activeMgr.lastCloneOptions.ReferenceName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLatestCommitID(t *testing.T) {
|
|
t.Parallel()
|
|
commitLookupErr := errors.New("commit lookup failed")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
url string
|
|
gitCommitID string
|
|
azureCommitID string
|
|
commitIDErr error
|
|
expectedID string
|
|
expectedError error
|
|
gitCalled int
|
|
azureCalled int
|
|
}{
|
|
{
|
|
name: "non-azure URL routes to git manager",
|
|
url: "https://github.com/example/repo.git",
|
|
gitCommitID: "abc123",
|
|
expectedID: "abc123",
|
|
gitCalled: 1,
|
|
},
|
|
{
|
|
name: "azure URL routes to azure manager",
|
|
url: "https://dev.azure.com/org/project/_git/repo",
|
|
azureCommitID: "def456",
|
|
expectedID: "def456",
|
|
azureCalled: 1,
|
|
},
|
|
{
|
|
name: "error propagated",
|
|
url: "https://github.com/example/repo.git",
|
|
commitIDErr: commitLookupErr,
|
|
expectedError: commitLookupErr,
|
|
gitCalled: 1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{commitID: tc.gitCommitID, commitIDErr: tc.commitIDErr}
|
|
azureMgr := &mockRepoManager{commitID: tc.azureCommitID}
|
|
s := newTestService(t.Context(), 4, gitMgr, azureMgr)
|
|
|
|
id, err := s.LatestCommitID(t.Context(), tc.url, "", "", "", false)
|
|
require.Equal(t, tc.expectedError, err)
|
|
require.Equal(t, tc.expectedID, id)
|
|
require.Equal(t, tc.gitCalled, gitMgr.commitIDCalled)
|
|
require.Equal(t, tc.azureCalled, azureMgr.commitIDCalled)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListRefs(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("cache hit on second call", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{refs: []string{"refs/heads/main", "refs/heads/develop"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
refs1, err := s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
refs2, err := s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, gitMgr.listRefsCalled, "expected manager to be called once")
|
|
require.Equal(t, refs1, refs2)
|
|
})
|
|
|
|
t.Run("hard refresh clears cache and calls manager again", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{refs: []string{"refs/heads/main"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
_, err := s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.NoError(t, err)
|
|
_, err = s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, gitMgr.listRefsCalled, "expected manager to be called twice with hard refresh")
|
|
})
|
|
|
|
t.Run("error propagated and not cached", func(t *testing.T) {
|
|
wantErr := errors.New("refs failed")
|
|
gitMgr := &mockRepoManager{refsErr: wantErr}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
_, err := s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.Equal(t, wantErr, err)
|
|
|
|
_, err = s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", true, false)
|
|
require.Equal(t, wantErr, err)
|
|
require.Equal(t, 2, gitMgr.listRefsCalled, "expected manager to be called twice after error")
|
|
})
|
|
|
|
t.Run("azure URL routes to azure manager", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{}
|
|
azureMgr := &mockRepoManager{refs: []string{"refs/heads/main"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, azureMgr)
|
|
|
|
_, err := s.ListRefs(t.Context(), "https://dev.azure.com/org/project/_git/repo", "", "", false, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, azureMgr.listRefsCalled, "expected azure.ListRefs to be called once")
|
|
require.Equal(t, 0, gitMgr.listRefsCalled, "expected git.ListRefs to not be called")
|
|
})
|
|
|
|
t.Run("cache disabled: manager always called", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{refs: []string{"refs/heads/main"}}
|
|
s := newTestService(t.Context(), 0, gitMgr, &mockRepoManager{})
|
|
|
|
_, err := s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.NoError(t, err)
|
|
_, err = s.ListRefs(t.Context(), "https://github.com/example/repo.git", "", "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, gitMgr.listRefsCalled, "expected manager to be called twice with cache disabled")
|
|
})
|
|
}
|
|
|
|
func TestListFiles(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("cache hit on second call", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{files: []string{"docker-compose.yml", "README.md"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
files1, err := s.ListFiles(t.Context(), "https://github.com/example/repo.git", "refs/heads/main", "", "", false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
files2, err := s.ListFiles(t.Context(), "https://github.com/example/repo.git", "refs/heads/main", "", "", false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, gitMgr.listFilesCalled, "expected manager to be called once")
|
|
require.Equal(t, files1, files2)
|
|
})
|
|
|
|
t.Run("hard refresh clears file cache", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{files: []string{"docker-compose.yml"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
_, err := s.ListFiles(t.Context(), "https://github.com/example/repo.git", "refs/heads/main", "", "", false, false, nil, false)
|
|
require.NoError(t, err)
|
|
_, err = s.ListFiles(t.Context(), "https://github.com/example/repo.git", "refs/heads/main", "", "", false, true, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, gitMgr.listFilesCalled, "expected manager to be called twice with hard refresh")
|
|
})
|
|
|
|
t.Run("azure URL routes to azure manager", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{}
|
|
azureMgr := &mockRepoManager{files: []string{"docker-compose.yml"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, azureMgr)
|
|
|
|
_, err := s.ListFiles(t.Context(), "https://dev.azure.com/org/project/_git/repo", "", "", "", false, false, nil, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, azureMgr.listFilesCalled, "expected azure.ListFiles to be called once")
|
|
require.Equal(t, 0, gitMgr.listFilesCalled, "expected git.ListFiles to not be called")
|
|
})
|
|
|
|
t.Run("extension filter applied", func(t *testing.T) {
|
|
gitMgr := &mockRepoManager{files: []string{"docker-compose.yml", "README.md", "stack.yml", "config.json"}}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
files, err := s.ListFiles(t.Context(), "https://github.com/example/repo.git", "", "", "", false, false, []string{".yml"}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"docker-compose.yml", "stack.yml"}, files)
|
|
})
|
|
|
|
t.Run("error is returned and not cached", func(t *testing.T) {
|
|
wantErr := errors.New("list files failed")
|
|
gitMgr := &mockRepoManager{filesErr: wantErr}
|
|
s := newTestService(t.Context(), 4, gitMgr, &mockRepoManager{})
|
|
|
|
files, err := s.ListFiles(t.Context(), "https://github.com/example/repo.git", "refs/heads/main", "", "", false, false, nil, false)
|
|
require.ErrorIs(t, err, wantErr)
|
|
require.Nil(t, files)
|
|
})
|
|
}
|
|
|
|
func TestFilterFiles(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
files []string
|
|
exts []string
|
|
expectedFiles []string
|
|
}{
|
|
{
|
|
name: "empty ext list returns all files",
|
|
files: []string{"a.yml", "b.json", "c.txt"},
|
|
exts: nil,
|
|
expectedFiles: []string{"a.yml", "b.json", "c.txt"},
|
|
},
|
|
{
|
|
name: "non-matching exts returns empty",
|
|
files: []string{"a.yml", "b.json"},
|
|
exts: []string{".txt"},
|
|
expectedFiles: nil,
|
|
},
|
|
{
|
|
name: "partial match returns only matching files",
|
|
files: []string{"a.yml", "b.json", "c.yml"},
|
|
exts: []string{".yml"},
|
|
expectedFiles: []string{"a.yml", "c.yml"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
require.Equal(t, tc.expectedFiles, filterFiles(tc.files, tc.exts))
|
|
})
|
|
}
|
|
}
|