package github import ( "context" "errors" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" field "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/grafana/apps/provisioning/pkg/repository/git" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) func TestNewGitHub(t *testing.T) { tests := []struct { name string config *provisioning.Repository token string expectedError string expectedOwner string expectedRepo string }{ { name: "successful creation", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, token: "token123", expectedError: "", expectedOwner: "grafana", expectedRepo: "grafana", }, { name: "invalid URL format", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", }, }, }, token: "token123", expectedError: "parse owner and repo", }, { name: "URL with .git extension", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana.git", Branch: "main", }, }, }, token: "token123", expectedError: "", expectedOwner: "grafana", expectedRepo: "grafana", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { factory := ProvideFactory() factory.Client = http.DefaultClient gitRepo := git.NewMockGitRepository(t) // Call the function under test repo, err := NewRepository( context.Background(), tt.config, gitRepo, factory, common.RawSecureValue(tt.token), ) // Check results if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) assert.Nil(t, repo) } else { require.NoError(t, err) require.NotNil(t, repo) assert.Equal(t, tt.expectedOwner, repo.Owner()) assert.Equal(t, tt.expectedRepo, repo.Repo()) concreteRepo, ok := repo.(*githubRepository) require.True(t, ok) assert.Equal(t, gitRepo, concreteRepo.GitRepository) } }) } } func TestParseOwnerRepoGithub(t *testing.T) { tests := []struct { name string url string expectedOwner string expectedRepo string expectedError string }{ { name: "valid GitHub URL", url: "https://github.com/grafana/grafana", expectedOwner: "grafana", expectedRepo: "grafana", }, { name: "valid GitHub URL with .git", url: "https://github.com/grafana/grafana.git", expectedOwner: "grafana", expectedRepo: "grafana", }, { name: "invalid URL format", url: "invalid-url", expectedError: "parse", }, { name: "missing repo name", url: "https://github.com/grafana", expectedError: "unable to parse repo+owner from url", }, { name: "URL with special characters", url: "https://github.com/user%", expectedError: "parse", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { owner, repo, err := ParseOwnerRepoGithub(tt.url) if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) } else { require.NoError(t, err) assert.Equal(t, tt.expectedOwner, owner) assert.Equal(t, tt.expectedRepo, repo) } }) } } func TestGitHubRepositoryValidate(t *testing.T) { tests := []struct { name string config *provisioning.Repository mockSetup func(m *git.MockGitRepository) expectedErrors int errorFields []string }{ { name: "valid configuration", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", Path: "dashboards", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", Path: "dashboards", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }) m.On("Validate").Return(field.ErrorList{}) }, expectedErrors: 0, }, { name: "missing GitHub config", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: nil, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: nil, }, }) }, expectedErrors: 1, errorFields: []string{"spec.github"}, }, { name: "missing URL", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }) }, expectedErrors: 1, errorFields: []string{"spec.github.url"}, }, { name: "invalid URL format", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }) }, expectedErrors: 1, errorFields: []string{"spec.github.url"}, }, { name: "non-GitHub URL", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://gitlab.com/grafana/grafana", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Config").Return(&provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://gitlab.com/grafana/grafana", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }) }, expectedErrors: 1, errorFields: []string{"spec.github.url"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) if tt.mockSetup != nil { tt.mockSetup(mockGitRepo) } repo := &githubRepository{ config: tt.config, GitRepository: mockGitRepo, } errors := repo.Validate() assert.Equal(t, tt.expectedErrors, len(errors), "Expected %d errors, got %d, errors: %v", tt.expectedErrors, len(errors), errors) if tt.expectedErrors > 0 { errorFields := make([]string, 0, len(errors)) for _, err := range errors { errorFields = append(errorFields, err.Field) } for _, expectedField := range tt.errorFields { assert.Contains(t, errorFields, expectedField, "Expected error for field %s", expectedField) } } mockGitRepo.AssertExpectations(t) }) } } func TestGitHubRepositoryTest(t *testing.T) { tests := []struct { name string config *provisioning.Repository mockSetup func(m *git.MockGitRepository) expectedResult *provisioning.TestResults expectedError error }{ { name: "successful test", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(m *git.MockGitRepository) { m.On("Test", mock.Anything).Return(&provisioning.TestResults{ Code: http.StatusOK, Success: true, }, nil) }, expectedResult: &provisioning.TestResults{ Code: http.StatusOK, Success: true, }, }, { name: "invalid URL", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, }, mockSetup: func(_ *git.MockGitRepository) { // No mock calls expected as validation fails first }, expectedResult: &provisioning.TestResults{ Code: http.StatusBadRequest, Success: false, Errors: []provisioning.ErrorDetails{{ Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.url", Detail: "parse \"invalid-url\": invalid URI for request", }}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) if tt.mockSetup != nil { tt.mockSetup(mockGitRepo) } repo := &githubRepository{ config: tt.config, GitRepository: mockGitRepo, owner: "grafana", repo: "grafana", } result, err := repo.Test(context.Background()) if tt.expectedError != nil { assert.Error(t, err) assert.Equal(t, tt.expectedError.Error(), err.Error()) } else { assert.NoError(t, err) } if tt.expectedResult != nil { assert.Equal(t, tt.expectedResult.Code, result.Code) assert.Equal(t, tt.expectedResult.Success, result.Success) if len(tt.expectedResult.Errors) > 0 { assert.Equal(t, len(tt.expectedResult.Errors), len(result.Errors)) for i, expectedError := range tt.expectedResult.Errors { assert.Equal(t, expectedError.Type, result.Errors[i].Type) assert.Equal(t, expectedError.Field, result.Errors[i].Field) assert.Contains(t, result.Errors[i].Detail, "parse") } } } mockGitRepo.AssertExpectations(t) }) } } func TestGitHubRepositoryHistory(t *testing.T) { tests := []struct { name string config *provisioning.Repository path string ref string mockSetup func(m *MockClient) expectedResult []provisioning.HistoryItem expectedError error }{ { name: "successful history retrieval", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "dashboard.json", ref: "main", mockSetup: func(m *MockClient) { commits := []Commit{ { Ref: "abc123", Message: "Update dashboard", Author: &CommitAuthor{ Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, Committer: &CommitAuthor{ Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), }, } m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). Return(commits, nil) }, expectedResult: []provisioning.HistoryItem{ { Ref: "abc123", Message: "Update dashboard", Authors: []provisioning.Author{ { Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, }, }, { name: "file not found", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "nonexistent.json", ref: "main", mockSetup: func(m *MockClient) { m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/nonexistent.json", "main"). Return(nil, ErrResourceNotFound) }, expectedError: repository.ErrFileNotFound, }, { name: "use default branch when ref is empty", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "dashboard.json", ref: "", mockSetup: func(m *MockClient) { commits := []Commit{ { Ref: "abc123", Message: "Update dashboard", Author: &CommitAuthor{ Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), }, } m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). Return(commits, nil) }, expectedResult: []provisioning.HistoryItem{ { Ref: "abc123", Message: "Update dashboard", Authors: []provisioning.Author{ { Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, }, }, { name: "committer different from author", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "dashboard.json", ref: "main", mockSetup: func(m *MockClient) { commits := []Commit{ { Ref: "abc123", Message: "Update dashboard", Author: &CommitAuthor{ Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, Committer: &CommitAuthor{ Name: "Jane Smith", Username: "janesmith", AvatarURL: "https://example.com/avatar2.png", }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), }, } m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). Return(commits, nil) }, expectedResult: []provisioning.HistoryItem{ { Ref: "abc123", Message: "Update dashboard", Authors: []provisioning.Author{ { Name: "John Doe", Username: "johndoe", AvatarURL: "https://example.com/avatar1.png", }, { Name: "Jane Smith", Username: "janesmith", AvatarURL: "https://example.com/avatar2.png", }, }, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, }, }, { name: "commit with no author", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "dashboard.json", ref: "main", mockSetup: func(m *MockClient) { commits := []Commit{ { Ref: "abc123", Message: "Update dashboard", Author: nil, Committer: nil, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), }, } m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). Return(commits, nil) }, expectedResult: []provisioning.HistoryItem{ { Ref: "abc123", Message: "Update dashboard", Authors: []provisioning.Author{}, CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, }, }, { name: "other API error", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", Path: "dashboards", }, }, }, path: "dashboard.json", ref: "main", mockSetup: func(m *MockClient) { m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). Return(nil, errors.New("API error")) }, expectedError: errors.New("get commits: API error"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := NewMockClient(t) if tt.mockSetup != nil { tt.mockSetup(mockClient) } repo := &githubRepository{ config: tt.config, gh: mockClient, owner: "grafana", repo: "grafana", } history, err := repo.History(context.Background(), tt.path, tt.ref) if tt.expectedError != nil { require.Error(t, err) var statusErr *apierrors.StatusError if errors.As(tt.expectedError, &statusErr) { var actualStatusErr *apierrors.StatusError require.True(t, errors.As(err, &actualStatusErr)) require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) } else { require.Equal(t, tt.expectedError.Error(), err.Error()) } } else { require.NoError(t, err) require.Equal(t, tt.expectedResult, history) } mockClient.AssertExpectations(t) }) } } func TestGitHubRepositoryResourceURLs(t *testing.T) { tests := []struct { name string file *repository.FileInfo config *provisioning.Repository expectedURLs *provisioning.RepositoryURLs expectedError error }{ { name: "file with ref", file: &repository.FileInfo{ Path: "dashboards/test.json", Ref: "feature-branch", }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: &provisioning.RepositoryURLs{ RepositoryURL: "https://github.com/grafana/grafana", SourceURL: "https://github.com/grafana/grafana/blob/feature-branch/dashboards/test.json", CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", }, }, { name: "file without ref uses default branch", file: &repository.FileInfo{ Path: "dashboards/test.json", Ref: "", }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: &provisioning.RepositoryURLs{ RepositoryURL: "https://github.com/grafana/grafana", SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", }, }, { name: "empty path returns nil", file: &repository.FileInfo{ Path: "", Ref: "feature-branch", }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: nil, }, { name: "nil github config returns nil", file: &repository.FileInfo{ Path: "dashboards/test.json", Ref: "feature-branch", }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: nil, }, }, expectedURLs: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &githubRepository{ config: tt.config, owner: "grafana", repo: "grafana", } urls, err := repo.ResourceURLs(context.Background(), tt.file) if tt.expectedError != nil { require.Error(t, err) require.Equal(t, tt.expectedError.Error(), err.Error()) } else { require.NoError(t, err) require.Equal(t, tt.expectedURLs, urls) } }) } } func TestGitHubRepositoryRefURLs(t *testing.T) { tests := []struct { name string ref string config *provisioning.Repository expectedURLs *provisioning.RepositoryURLs expectedError error }{ { name: "ref different from branch", ref: "feature-branch", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: &provisioning.RepositoryURLs{ SourceURL: "https://github.com/grafana/grafana/tree/feature-branch", CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", }, }, { name: "ref same as branch", ref: "main", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: &provisioning.RepositoryURLs{ SourceURL: "https://github.com/grafana/grafana/tree/main", }, }, { name: "empty ref returns nil", ref: "", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, }, expectedURLs: nil, }, { name: "nil github config returns nil", ref: "feature-branch", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: nil, }, }, expectedURLs: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &githubRepository{ config: tt.config, owner: "grafana", repo: "grafana", } urls, err := repo.RefURLs(context.Background(), tt.ref) if tt.expectedError != nil { require.Error(t, err) require.Equal(t, tt.expectedError.Error(), err.Error()) } else { require.NoError(t, err) require.Equal(t, tt.expectedURLs, urls) } }) } } // Test simple delegation functions func TestGitHubRepositoryDelegation(t *testing.T) { ctx := context.Background() config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, Secure: provisioning.SecureValues{ Token: common.InlineSecureValue{ Name: "with-name", }, }, } t.Run("Config delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) mockGitRepo.On("Config").Return(config) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result := repo.Config() assert.Equal(t, config, result) mockGitRepo.AssertExpectations(t) }) t.Run("Read delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) expectedFileInfo := &repository.FileInfo{ Path: "test.yaml", Data: []byte("test data"), Ref: "main", Hash: "abc123", } mockGitRepo.On("Read", ctx, "test.yaml", "main").Return(expectedFileInfo, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.Read(ctx, "test.yaml", "main") require.NoError(t, err) assert.Equal(t, expectedFileInfo, result) mockGitRepo.AssertExpectations(t) }) t.Run("ReadTree delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) expectedEntries := []repository.FileTreeEntry{ {Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true}, } mockGitRepo.On("ReadTree", ctx, "main").Return(expectedEntries, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.ReadTree(ctx, "main") require.NoError(t, err) assert.Equal(t, expectedEntries, result) mockGitRepo.AssertExpectations(t) }) t.Run("Create delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) data := []byte("test content") mockGitRepo.On("Create", ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") require.NoError(t, err) mockGitRepo.AssertExpectations(t) }) t.Run("Update delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) data := []byte("updated content") mockGitRepo.On("Update", ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") require.NoError(t, err) mockGitRepo.AssertExpectations(t) }) t.Run("Write delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) data := []byte("file content") mockGitRepo.On("Write", ctx, "file.yaml", "main", data, "Write file").Return(nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } err := repo.Write(ctx, "file.yaml", "main", data, "Write file") require.NoError(t, err) mockGitRepo.AssertExpectations(t) }) t.Run("Delete delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) mockGitRepo.On("Delete", ctx, "file.yaml", "main", "Delete file").Return(nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } err := repo.Delete(ctx, "file.yaml", "main", "Delete file") require.NoError(t, err) mockGitRepo.AssertExpectations(t) }) t.Run("LatestRef delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) expectedRef := "abc123def456" mockGitRepo.On("LatestRef", ctx).Return(expectedRef, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.LatestRef(ctx) require.NoError(t, err) assert.Equal(t, expectedRef, result) mockGitRepo.AssertExpectations(t) }) t.Run("ListRefs delegates to git repo but adds ref URL", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) // The git repo returns refs without RefURL gitRepoRefs := []provisioning.RefItem{ {Name: "main", Hash: "abc123def456"}, {Name: "feature", Hash: "def456ghi789"}, } mockGitRepo.On("ListRefs", ctx).Return(gitRepoRefs, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.ListRefs(ctx) require.NoError(t, err) // The returned refs should have RefURL set expectedRefs := []provisioning.RefItem{ { Name: "main", Hash: "abc123def456", RefURL: "https://github.com/grafana/grafana/tree/main", }, { Name: "feature", Hash: "def456ghi789", RefURL: "https://github.com/grafana/grafana/tree/feature", }, } assert.Equal(t, expectedRefs, result) mockGitRepo.AssertExpectations(t) }) t.Run("CompareFiles delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) expectedChanges := []repository.VersionedFileChange{ { Action: repository.FileActionCreated, Path: "new-file.yaml", Ref: "feature-branch", }, } mockGitRepo.On("CompareFiles", ctx, "main", "feature-branch").Return(expectedChanges, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.CompareFiles(ctx, "main", "feature-branch") require.NoError(t, err) assert.Equal(t, expectedChanges, result) mockGitRepo.AssertExpectations(t) }) t.Run("Stage delegates to git repo", func(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) mockStagedRepo := repository.NewMockStagedRepository(t) opts := repository.StageOptions{ Mode: repository.StageModeCommitOnEach, Timeout: 10 * time.Second, } mockGitRepo.On("Stage", ctx, opts).Return(mockStagedRepo, nil) repo := &githubRepository{ config: config, GitRepository: mockGitRepo, } result, err := repo.Stage(ctx, opts) require.NoError(t, err) assert.Equal(t, mockStagedRepo, result) mockGitRepo.AssertExpectations(t) }) } // Test GitHub-specific accessor methods func TestGitHubRepositoryAccessors(t *testing.T) { config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/grafana/grafana", Branch: "main", }, }, } t.Run("Owner returns correct owner", func(t *testing.T) { repo := &githubRepository{ config: config, owner: "grafana", repo: "grafana", } result := repo.Owner() assert.Equal(t, "grafana", result) }) t.Run("Repo returns correct repo", func(t *testing.T) { repo := &githubRepository{ config: config, owner: "grafana", repo: "grafana", } result := repo.Repo() assert.Equal(t, "grafana", result) }) t.Run("Client returns correct client", func(t *testing.T) { mockClient := NewMockClient(t) repo := &githubRepository{ config: config, gh: mockClient, owner: "grafana", repo: "grafana", } result := repo.Client() assert.Equal(t, mockClient, result) }) } func TestGithubRepository_Move(t *testing.T) { tests := []struct { name string oldPath string newPath string ref string comment string setupMock func(*git.MockGitRepository) expectedErr error }{ { name: "successful move delegates to git repository", oldPath: "old.yaml", newPath: "new.yaml", ref: "main", comment: "move file", setupMock: func(mockGitRepo *git.MockGitRepository) { mockGitRepo.EXPECT().Move(context.Background(), "old.yaml", "new.yaml", "main", "move file").Return(nil) }, expectedErr: nil, }, { name: "move error from git repository", oldPath: "old.yaml", newPath: "new.yaml", ref: "main", comment: "move file", setupMock: func(mockGitRepo *git.MockGitRepository) { mockGitRepo.EXPECT().Move(context.Background(), "old.yaml", "new.yaml", "main", "move file").Return(errors.New("git move failed")) }, expectedErr: errors.New("git move failed"), }, { name: "successful directory move", oldPath: "old/", newPath: "new/", ref: "main", comment: "move directory", setupMock: func(mockGitRepo *git.MockGitRepository) { mockGitRepo.EXPECT().Move(context.Background(), "old/", "new/", "main", "move directory").Return(nil) }, expectedErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock git repository mockGitRepo := git.NewMockGitRepository(t) // Setup mock expectations tt.setupMock(mockGitRepo) // Create GitHub repository config := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", }, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/example/repo", }, }, } githubRepo := &githubRepository{ config: config, GitRepository: mockGitRepo, owner: "example", repo: "repo", } // Execute move operation err := githubRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment) // Verify results if tt.expectedErr != nil { require.Error(t, err) assert.Equal(t, tt.expectedErr.Error(), err.Error()) } else { require.NoError(t, err) } }) } }