diff --git a/cla-backend-go/gitlab_api/client.go b/cla-backend-go/gitlab_api/client.go index 9566aefeb..07e3f58b2 100644 --- a/cla-backend-go/gitlab_api/client.go +++ b/cla-backend-go/gitlab_api/client.go @@ -19,6 +19,27 @@ import ( goGitLab "github.com/xanzy/go-gitlab" ) +type GitLabClient interface { + GetMergeRequestCommits(projectID int, mergeID int, opts *goGitLab.GetMergeRequestCommitsOptions) ([]*goGitLab.Commit, error) + ListUsers(opts *goGitLab.ListUsersOptions) ([]*goGitLab.User, error) + SetCommitStatus(projectID int, commitSHA string, opts *goGitLab.SetCommitStatusOptions) error + GetProject(gitLabProjectID int, opts *goGitLab.GetProjectOptions) (*goGitLab.Project, error) + // EditProject(projectID int,) + ListGroupProjects(groupID int, opts *goGitLab.ListGroupProjectsOptions) ([]*goGitLab.Project, *goGitLab.Response, error) + ListGroupMembers(gid interface{}, opt *goGitLab.ListGroupMembersOptions) ([]*goGitLab.GroupMember, *goGitLab.Response, error) + CurrentUser() (*goGitLab.User, *goGitLab.Response, error) + ListUserProjects(user interface{}, opt *goGitLab.ListProjectsOptions) ([]*goGitLab.Project, *goGitLab.Response, error) + DeleteProjectHook(projectID, webhookID int) (*goGitLab.Response, error) + ListProjectHooks(projectID int, opts *goGitLab.ListProjectHooksOptions) ([]*goGitLab.ProjectHook, *goGitLab.Response, error) + AddProjectHook(projectID int, opts *goGitLab.AddProjectHookOptions) (*goGitLab.ProjectHook, *goGitLab.Response, error) + EditProjectHook(projectID, existingID int, opts *goGitLab.EditProjectHookOptions) (*goGitLab.ProjectHook, *goGitLab.Response, error) +} + +// Client is the gitlab client +type GitLabClientWrapper struct { + gitlabClient *goGitLab.Client +} + // OauthSuccessResponse is success response from Gitlab type OauthSuccessResponse struct { AccessToken string `json:"access_token"` @@ -29,7 +50,7 @@ type OauthSuccessResponse struct { } // NewGitlabOauthClient creates a new gitlab client from the given oauth info, authInfo is encrypted -func NewGitlabOauthClient(authInfo string, gitLabApp *App) (*goGitLab.Client, error) { +func NewGitlabOauthClient(authInfo string, gitLabApp *App) (GitLabClient, error) { if authInfo == "" { return nil, errors.New("unable to decrypt auth info - authentication info input is nil") } @@ -47,7 +68,14 @@ func NewGitlabOauthClient(authInfo string, gitLabApp *App) (*goGitLab.Client, er } log.Infof("creating oauth client with access token : %s", oauthResp.AccessToken) - return goGitLab.NewOAuthClient(oauthResp.AccessToken) + client, err := goGitLab.NewOAuthClient(oauthResp.AccessToken) + if err != nil { + return nil, err + } + + return &GitLabClientWrapper{ + gitlabClient: client, + }, nil } // NewGitlabOauthClientFromAccessToken creates a new gitlab client from the given access token @@ -154,3 +182,70 @@ func decrypt(key, cipherText []byte) ([]byte, error) { return cipherText, nil } + +func (c *GitLabClientWrapper) ListGroupMembers(gid interface{}, opt *goGitLab.ListGroupMembersOptions) ([]*goGitLab.GroupMember, *goGitLab.Response, error) { + return c.gitlabClient.Groups.ListGroupMembers(gid, opt) +} + +func (c *GitLabClientWrapper) GetProject(gitLabProjectID int, opts *goGitLab.GetProjectOptions) (*goGitLab.Project, error) { + project, _, err := c.gitlabClient.Projects.GetProject(gitLabProjectID, opts) + if err != nil { + return nil, err + } + return project, err +} + +func (c *GitLabClientWrapper) CurrentUser() (*goGitLab.User, *goGitLab.Response, error) { + return c.gitlabClient.Users.CurrentUser() +} + +// GetMergeRequestCommits returns the commits for the given merge request +func (c *GitLabClientWrapper) GetMergeRequestCommits(projectID int, mergeID int, opts *goGitLab.GetMergeRequestCommitsOptions) ([]*goGitLab.Commit, error) { + commits, _, err := c.gitlabClient.MergeRequests.GetMergeRequestCommits(projectID, mergeID, opts) + if err != nil { + return nil, err + } + return commits, nil +} + +// ListUsers returns the list of users +func (c *GitLabClientWrapper) ListUsers(opts *goGitLab.ListUsersOptions) ([]*goGitLab.User, error) { + users, _, err := c.gitlabClient.Users.ListUsers(opts) + if err != nil { + return nil, err + } + return users, nil +} + +// SetCommitStatus sets the commit status +func (c *GitLabClientWrapper) SetCommitStatus(projectID int, commitSHA string, opts *goGitLab.SetCommitStatusOptions) error { + _, _, err := c.gitlabClient.Commits.SetCommitStatus(projectID, commitSHA, opts) + if err != nil { + return err + } + return nil +} + +func (c *GitLabClientWrapper) ListGroupProjects(groupID int, opts *goGitLab.ListGroupProjectsOptions) ([]*goGitLab.Project, *goGitLab.Response, error) { + return c.gitlabClient.Groups.ListGroupProjects(groupID, opts) +} + +func (c *GitLabClientWrapper) ListUserProjects(user interface{}, opt *goGitLab.ListProjectsOptions) ([]*goGitLab.Project, *goGitLab.Response, error) { + return c.gitlabClient.Projects.ListUserProjects(user, opt) +} + +func (c *GitLabClientWrapper) DeleteProjectHook(projectID, webhookID int) (*goGitLab.Response, error) { + return c.gitlabClient.Projects.DeleteProjectHook(projectID, webhookID) +} + +func (c *GitLabClientWrapper) ListProjectHooks(projectID int, opts *goGitLab.ListProjectHooksOptions) ([]*goGitLab.ProjectHook, *goGitLab.Response, error) { + return c.gitlabClient.Projects.ListProjectHooks(projectID, opts) +} + +func (c *GitLabClientWrapper) AddProjectHook(projectID int, opts *goGitLab.AddProjectHookOptions) (*goGitLab.ProjectHook, *goGitLab.Response, error) { + return c.gitlabClient.Projects.AddProjectHook(projectID, opts) +} + +func (c *GitLabClientWrapper) EditProjectHook(projectID, existingID int, opts *goGitLab.EditProjectHookOptions) (*goGitLab.ProjectHook, *goGitLab.Response, error) { + return c.gitlabClient.Projects.EditProjectHook(projectID, existingID, opts) +} diff --git a/cla-backend-go/gitlab_api/client_groups.go b/cla-backend-go/gitlab_api/client_groups.go index edbb249bf..a39604123 100644 --- a/cla-backend-go/gitlab_api/client_groups.go +++ b/cla-backend-go/gitlab_api/client_groups.go @@ -147,7 +147,7 @@ func GetGroupByFullPath(ctx context.Context, client *goGitLab.Client, fullPath s } // GetGroupProjectListByGroupID returns a list of GitLab projects under the specified Organization -func GetGroupProjectListByGroupID(ctx context.Context, client *goGitLab.Client, groupID int) ([]*goGitLab.Project, error) { +func GetGroupProjectListByGroupID(ctx context.Context, client GitLabClient, groupID int) ([]*goGitLab.Project, error) { f := logrus.Fields{ "functionName": "gitlab_api.client_groups.GetGroupProjectListByGroupID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -168,7 +168,7 @@ func GetGroupProjectListByGroupID(ctx context.Context, client *goGitLab.Client, var projectList []*goGitLab.Project for { // https://docs.gitlab.com/ee/api/groups.html#list-a-groups-projects - projects, resp, listProjectsErr := client.Groups.ListGroupProjects(groupID, opts) + projects, resp, listProjectsErr := client.ListGroupProjects(groupID, opts) if listProjectsErr != nil { msg := fmt.Sprintf("unable to list projects, error: %+v", listProjectsErr) log.WithFields(f).WithError(listProjectsErr).Warn(msg) @@ -193,7 +193,7 @@ func GetGroupProjectListByGroupID(ctx context.Context, client *goGitLab.Client, } // ListGroupMembers lists the members of a given groupID -func ListGroupMembers(ctx context.Context, client *goGitLab.Client, groupID int) ([]*goGitLab.GroupMember, error) { +func ListGroupMembers(ctx context.Context, client GitLabClient, groupID int) ([]*goGitLab.GroupMember, error) { f := logrus.Fields{ "functionName": "gitlab_api.client_groups.GetGroupMembers", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -202,7 +202,7 @@ func ListGroupMembers(ctx context.Context, client *goGitLab.Client, groupID int) log.WithFields(f).Debugf("fetching gitlab members for groupID: %d", groupID) opts := &goGitLab.ListGroupMembersOptions{} - members, _, err := client.Groups.ListGroupMembers(groupID, opts) + members, _, err := client.ListGroupMembers(groupID, opts) if err != nil { log.WithFields(f).Debugf("unable to fetch members for gitlab GroupID : %d", groupID) return nil, err @@ -212,7 +212,7 @@ func ListGroupMembers(ctx context.Context, client *goGitLab.Client, groupID int) // ListUserProjectGroups fetches the unique groups of a gitlab users groups, // note: it doesn't list the projects/groups the user is member of ..., it's very limited -func ListUserProjectGroups(ctx context.Context, client *goGitLab.Client, userID int) ([]*UserGroup, error) { +func ListUserProjectGroups(ctx context.Context, client GitLabClient, userID int) ([]*UserGroup, error) { f := logrus.Fields{ "functionName": "gitlab_api.client_groups.ListUserProjectGroups", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -226,7 +226,7 @@ func ListUserProjectGroups(ctx context.Context, client *goGitLab.Client, userID userGroupsMap := map[string]*UserGroup{} for { log.WithFields(f).Debugf("fetching projects for user id : %d with options : %v", userID, listOptions.ListOptions) - projects, resp, err := client.Projects.ListUserProjects(userID, listOptions) + projects, resp, err := client.ListUserProjects(userID, listOptions) if err != nil { msg := fmt.Sprintf("listing user : %d projects failed : %v", userID, err) log.WithFields(f).Warn(msg) diff --git a/cla-backend-go/gitlab_api/client_projects.go b/cla-backend-go/gitlab_api/client_projects.go index ec8fcd4a6..7dd8932a6 100644 --- a/cla-backend-go/gitlab_api/client_projects.go +++ b/cla-backend-go/gitlab_api/client_projects.go @@ -85,7 +85,7 @@ func getProjectListWithOptions(ctx context.Context, client *goGitLab.Client, opt } // GetProjectByID returns the GitLab project for the specified ID -func GetProjectByID(ctx context.Context, client *goGitLab.Client, gitLabProjectID int) (*goGitLab.Project, error) { +func GetProjectByID(ctx context.Context, client GitLabClient, gitLabProjectID int) (*goGitLab.Project, error) { f := logrus.Fields{ "functionName": "gitlab.client.GetProjectByID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -93,17 +93,12 @@ func GetProjectByID(ctx context.Context, client *goGitLab.Client, gitLabProjectI } // Query GitLab for repos - fetch the list of repositories available to the GitLab App - project, resp, getProjectErr := client.Projects.GetProject(gitLabProjectID, &goGitLab.GetProjectOptions{}) + project, getProjectErr := client.GetProject(gitLabProjectID, &goGitLab.GetProjectOptions{}) if getProjectErr != nil { msg := fmt.Sprintf("unable to get project by ID: %d, error: %+v", gitLabProjectID, getProjectErr) log.WithFields(f).WithError(getProjectErr).Warn(msg) return nil, errors.New(msg) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - msg := fmt.Sprintf("unable to get project by ID: %d, status code: %d", gitLabProjectID, resp.StatusCode) - log.WithFields(f).WithError(getProjectErr).Warn(msg) - return nil, errors.New(msg) - } if project == nil { msg := fmt.Sprintf("unable to get project by ID: %d, project is empty", gitLabProjectID) log.WithFields(f).WithError(getProjectErr).Warn(msg) @@ -116,14 +111,14 @@ func GetProjectByID(ctx context.Context, client *goGitLab.Client, gitLabProjectI // EnableMergePipelineProtection enables the pipeline protection on given project, by default it's // turned off and when a new MR is raised users can merge requests bypassing the pipelines. With this // setting gitlab disables the Merge button if any of the pipelines are failing -func EnableMergePipelineProtection(ctx context.Context, gitlabClient *goGitLab.Client, projectID int) error { +func EnableMergePipelineProtection(ctx context.Context, gitlabClient GitLabClient, projectID int) error { f := logrus.Fields{ "functionName": "gitlab.client.EnableMergePipelineProtection", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gitLabProjectID": projectID, } - project, _, err := gitlabClient.Projects.GetProject(projectID, &goGitLab.GetProjectOptions{}) + project, err := gitlabClient.GetProject(projectID, &goGitLab.GetProjectOptions{}) if err != nil { return fmt.Errorf("fetching project failed : %v", err) } @@ -135,7 +130,7 @@ func EnableMergePipelineProtection(ctx context.Context, gitlabClient *goGitLab.C project.OnlyAllowMergeIfPipelineSucceeds = true log.WithFields(f).Debugf("Enabling Merge Pipeline protection") - _, _, err = gitlabClient.Projects.EditProject(projectID, &goGitLab.EditProjectOptions{ + _, _, err = gitlabClient.EditProject(projectID, &goGitLab.EditProjectOptions{ OnlyAllowMergeIfPipelineSucceeds: goGitLab.Bool(true), }) diff --git a/cla-backend-go/gitlab_api/client_users.go b/cla-backend-go/gitlab_api/client_users.go index 3a3d31df7..76382938d 100644 --- a/cla-backend-go/gitlab_api/client_users.go +++ b/cla-backend-go/gitlab_api/client_users.go @@ -15,14 +15,14 @@ import ( ) // GetUserByName gets a gitlab user object by the given name -func GetUserByName(ctx context.Context, client *goGitLab.Client, name string) (*goGitLab.User, error) { +func GetUserByName(ctx context.Context, client GitLabClient, name string) (*goGitLab.User, error) { f := logrus.Fields{ "functionName": "gitlab_api.client_users.GetUserByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "name": name, } - users, resp, err := client.Users.ListUsers(&goGitLab.ListUsersOptions{ + users, err := client.ListUsers(&goGitLab.ListUsersOptions{ ListOptions: goGitLab.ListOptions{ Page: 0, PerPage: 10, @@ -35,11 +35,6 @@ func GetUserByName(ctx context.Context, client *goGitLab.Client, name string) (* log.WithFields(f).WithError(err).Warn(msg) return nil, errors.New(msg) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - msg := fmt.Sprintf("unable to get user using query: %s, status code: %d", name, resp.StatusCode) - log.WithFields(f).Warn(msg) - return nil, errors.New(msg) - } if len(users) == 0 { return nil, nil diff --git a/cla-backend-go/gitlab_api/mocks/mock_client.go b/cla-backend-go/gitlab_api/mocks/mock_client.go new file mode 100644 index 000000000..63ca32fcf --- /dev/null +++ b/cla-backend-go/gitlab_api/mocks/mock_client.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go + +// Package mock_gitlab is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + gitlab "github.com/xanzy/go-gitlab" +) + +// MockGitLabClient is a mock of GitLabClient interface. +type MockGitLabClient struct { + ctrl *gomock.Controller + recorder *MockGitLabClientMockRecorder +} + +// MockGitLabClientMockRecorder is the mock recorder for MockGitLabClient. +type MockGitLabClientMockRecorder struct { + mock *MockGitLabClient +} + +// NewMockGitLabClient creates a new mock instance. +func NewMockGitLabClient(ctrl *gomock.Controller) *MockGitLabClient { + mock := &MockGitLabClient{ctrl: ctrl} + mock.recorder = &MockGitLabClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGitLabClient) EXPECT() *MockGitLabClientMockRecorder { + return m.recorder +} + +// GetMergeRequestCommits mocks base method. +func (m *MockGitLabClient) GetMergeRequestCommits(projectID, mergeID int, opts *gitlab.GetMergeRequestCommitsOptions) ([]*gitlab.Commit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMergeRequestCommits", projectID, mergeID, opts) + ret0, _ := ret[0].([]*gitlab.Commit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMergeRequestCommits indicates an expected call of GetMergeRequestCommits. +func (mr *MockGitLabClientMockRecorder) GetMergeRequestCommits(projectID, mergeID, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMergeRequestCommits", reflect.TypeOf((*MockGitLabClient)(nil).GetMergeRequestCommits), projectID, mergeID, opts) +} + +// ListUsers mocks base method. +func (m *MockGitLabClient) ListUsers(opts *gitlab.ListUsersOptions) ([]*gitlab.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListUsers", opts) + ret0, _ := ret[0].([]*gitlab.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListUsers indicates an expected call of ListUsers. +func (mr *MockGitLabClientMockRecorder) ListUsers(opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockGitLabClient)(nil).ListUsers), opts) +} + +// SetCommitStatus mocks base method. +func (m *MockGitLabClient) SetCommitStatus(projectID int, commitSHA string, opts *gitlab.SetCommitStatusOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetCommitStatus", projectID, commitSHA, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetCommitStatus indicates an expected call of SetCommitStatus. +func (mr *MockGitLabClientMockRecorder) SetCommitStatus(projectID, commitSHA, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCommitStatus", reflect.TypeOf((*MockGitLabClient)(nil).SetCommitStatus), projectID, commitSHA, opts) +} diff --git a/cla-backend-go/gitlab_api/mr.go b/cla-backend-go/gitlab_api/mr.go index a0db1d259..523df6d01 100644 --- a/cla-backend-go/gitlab_api/mr.go +++ b/cla-backend-go/gitlab_api/mr.go @@ -14,6 +14,16 @@ import ( "github.com/xanzy/go-gitlab" ) +type UserCommitSummary struct { + AuthorID int + AuthorUsername string + CommitSha string + AuthorName string + AuthorEmail string + Authorized bool + Affiliated bool +} + // FetchMrInfo is responsible for fetching the MR info for given project func FetchMrInfo(client *gitlab.Client, projectID int, mergeID int) (*gitlab.MergeRequest, error) { m, _, err := client.MergeRequests.GetMergeRequest(projectID, mergeID, &gitlab.GetMergeRequestsOptions{}) @@ -24,7 +34,7 @@ func FetchMrInfo(client *gitlab.Client, projectID int, mergeID int) (*gitlab.Mer return m, nil } -func GetLatestCommit(client *gitlab.Client, projectID int, mergeID int) (*gitlab.Commit, error) { +func GetLatestCommit(client GitLabClient, projectID int, mergeID int) (*gitlab.Commit, error) { f := logrus.Fields{ "functionName": "gitlab_api.GetLatestCommit", "projectID": projectID, @@ -32,7 +42,7 @@ func GetLatestCommit(client *gitlab.Client, projectID int, mergeID int) (*gitlab } log.WithFields(f).Debug("fetching latest commit...") - commits, _, err := client.MergeRequests.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) + commits, err := client.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) if err != nil { return nil, fmt.Errorf("fetching merge request commits : %d for project : %v failed : %v", mergeID, projectID, err) } @@ -45,28 +55,26 @@ func GetLatestCommit(client *gitlab.Client, projectID int, mergeID int) (*gitlab } // FetchMrParticipants is responsible to get unique mr participants -func FetchMrParticipants(client *gitlab.Client, projectID int, mergeID int) ([]*gitlab.User, error) { +func FetchMrParticipants(client GitLabClient, projectID int, mergeID int) ([]*UserCommitSummary, error) { f := logrus.Fields{ "functionName": "gitlab_api.FetchMrParticipants", "projectID": projectID, "mergeID": mergeID, } + + results := make([]*UserCommitSummary, 0) + log.WithFields(f).Debug("fetching mr participants...") - commits, response, err := client.MergeRequests.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) + commits, err := client.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) if err != nil { return nil, fmt.Errorf("fetching gitlab participants for project : %d and merge id : %d, failed : %v", projectID, mergeID, err) } - if response.StatusCode != 200 { - return nil, fmt.Errorf("fetching gitlab participants for project : %d and merge id : %d, failed with status code : %d", projectID, mergeID, response.StatusCode) - } if len(commits) == 0 { log.WithFields(f).Debugf("no commits found for project : %d and merge id : %d", projectID, mergeID) - return nil, nil + return results, nil } - var results []*gitlab.User - for _, commit := range commits { log.WithFields(f).Debugf("commit information: %v", commit) // The author is the person who originally wrote the code. The committer, on the other hand, is assumed to be @@ -82,6 +90,8 @@ func FetchMrParticipants(client *gitlab.Client, projectID int, mergeID int) ([]* return nil, getUserErr } + user.CommitSha = commit.ShortID + results = append(results, user) } @@ -89,7 +99,7 @@ func FetchMrParticipants(client *gitlab.Client, projectID int, mergeID int) ([]* } // SetCommitStatus is responsible for setting the MR status for commit sha -func SetCommitStatus(client *gitlab.Client, projectID int, commitSha string, state gitlab.BuildStateValue, message string, targetURL string) error { +func SetCommitStatus(client GitLabClient, projectID int, commitSha string, state gitlab.BuildStateValue, message string, targetURL string) error { f := logrus.Fields{ "functionName": "gitlab_api.SetCommitStatus", "projectID": projectID, @@ -110,7 +120,7 @@ func SetCommitStatus(client *gitlab.Client, projectID int, commitSha string, sta options.TargetURL = gitlab.String(targetURL) } - _, _, err := client.Commits.SetCommitStatus(projectID, commitSha, options) + err := client.SetCommitStatus(projectID, commitSha, options) if err != nil { return fmt.Errorf("setting commit status for the sha : %s and project id : %d failed : %v", commitSha, projectID, err) } @@ -161,23 +171,24 @@ func SetMrComment(client *gitlab.Client, projectID int, mergeID int, message str } // getUser is responsible for fetching the user info for given user email -func getUser(client *gitlab.Client, email, name *string) (*gitlab.User, error) { +func getUser(client GitLabClient, email, name *string) (*UserCommitSummary, error) { f := logrus.Fields{ "functionName": "gitlab_api.getUser", "email": *email, "name": *name, } - user := &gitlab.User{ - Email: *email, - Name: *name, + user := &UserCommitSummary{ + AuthorEmail: *email, + AuthorName: *name, } - users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{ + users, err := client.ListUsers(&gitlab.ListUsersOptions{ Active: utils.Bool(true), Blocked: utils.Bool(false), Search: email, }) + if err != nil { log.WithFields(f).Warnf("unable to find user for email : %s, error : %v", utils.StringValue(email), err) return nil, err @@ -193,8 +204,8 @@ func getUser(client *gitlab.Client, email, name *string) (*gitlab.User, error) { for _, found := range users { if strings.EqualFold(found.Name, *name) { log.WithFields(f).Debugf("found matching user : %+v - updating GitLab username and ID", found) - user.Username = found.Username - user.ID = found.ID + user.AuthorID = found.ID + user.AuthorUsername = found.Username break } } diff --git a/cla-backend-go/gitlab_api/mr_test.go b/cla-backend-go/gitlab_api/mr_test.go new file mode 100644 index 000000000..1514da308 --- /dev/null +++ b/cla-backend-go/gitlab_api/mr_test.go @@ -0,0 +1,123 @@ +package gitlab + +import ( + "fmt" + "testing" + + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/communitybridge/easycla/cla-backend-go/gitlab_api/mocks" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + goGitLab "github.com/xanzy/go-gitlab" +) + +func TestFetchMrParticipants(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGitLabClient := mocks.NewMockGitLabClient(ctrl) + + projectID := 123 + mergeID := 456 + + commits := []*goGitLab.Commit{ + { + ID: "commit1", + AuthorEmail: "author1@example.com", + AuthorName: "Author 1", + ShortID: "shortID1", + }, + { + ID: "commit2", + AuthorEmail: "author2@example.com", + AuthorName: "Author 2", + ShortID: "shortID2", + }, + } + + users := []*goGitLab.User{ + { + ID: 1, + Username: "author_username1", + Email: "author1@example.com", + Name: "Author 1", + }, + { + ID: 2, + Username: "author_username2", + Email: "author2@example.com", + Name: "Author 2", + }, + } + + testCases := []struct { + name string + mockSetup func() + expectedResults []*gitlab_api.UserCommitSummary + expectedError error + }{ + { + name: "Successful fetch with participants", + mockSetup: func() { + mockGitLabClient.EXPECT().GetMergeRequestCommits(projectID, mergeID, &goGitLab.GetMergeRequestCommitsOptions{}).Return(commits, nil) + for index, commit := range commits { + mockGitLabClient.EXPECT().ListUsers(&goGitLab.ListUsersOptions{Active: utils.Bool(true), Blocked: utils.Bool(false), Search: &commit.AuthorEmail}).Return([]*goGitLab.User{users[index]}, nil) + } + }, + expectedResults: []*gitlab_api.UserCommitSummary{ + { + CommitSha: "shortID1", + AuthorName: "Author 1", + AuthorEmail: "author1@example.com", + Authorized: false, + Affiliated: false, + AuthorID: 1, + AuthorUsername: "author_username1", + }, + { + CommitSha: "shortID2", + AuthorName: "Author 2", + AuthorEmail: "author2@example.com", + Authorized: false, + Affiliated: false, + AuthorID: 2, + AuthorUsername: "author_username2", + }, + }, + expectedError: nil, + }, + { + name: "No commits found", + mockSetup: func() { + mockGitLabClient.EXPECT().GetMergeRequestCommits(projectID, mergeID, &goGitLab.GetMergeRequestCommitsOptions{}).Return([]*goGitLab.Commit{}, nil) + }, + expectedResults: []*gitlab_api.UserCommitSummary{}, + expectedError: nil, + }, + { + name: "Error fetching commits", + mockSetup: func() { + mergeRequestError := fmt.Errorf("error fetching merge request commits") + mockGitLabClient.EXPECT().GetMergeRequestCommits(projectID, mergeID, &goGitLab.GetMergeRequestCommitsOptions{}).Return(nil, mergeRequestError) + }, + expectedResults: nil, + expectedError: fmt.Errorf("fetching gitlab participants for project : %d and merge id : %d, failed : %v", projectID, mergeID, fmt.Errorf("error fetching merge request commits")), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.mockSetup() + + results, err := gitlab_api.FetchMrParticipants(mockGitLabClient, projectID, mergeID) + + if tc.expectedError != nil { + assert.EqualError(t, err, tc.expectedError.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.expectedResults, results) + }) + } +} diff --git a/cla-backend-go/gitlab_api/webhook.go b/cla-backend-go/gitlab_api/webhook.go index 35cc958d2..50144f026 100644 --- a/cla-backend-go/gitlab_api/webhook.go +++ b/cla-backend-go/gitlab_api/webhook.go @@ -6,19 +6,20 @@ package gitlab import ( "fmt" + gitlab "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" "github.com/xanzy/go-gitlab" ) // SetWebHook is responsible for adding the webhook for given projectID, if webhook is there already // tries to set the attributes if anything is missing, should be idempotent operation -func SetWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int, token string) error { +func SetWebHook(gitLabClient GitLabClient, hookURL string, projectID int, token string) error { existingWebHook, err := findExistingWebHook(gitLabClient, hookURL, projectID) if err != nil { return err } if existingWebHook == nil { - _, _, err = gitLabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + _, _, err = gitLabClient.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ URL: gitlab.String(hookURL), MergeRequestsEvents: gitlab.Bool(true), PushEvents: gitlab.Bool(true), @@ -33,7 +34,7 @@ func SetWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int, toke } if !existingWebHook.EnableSSLVerification || !existingWebHook.MergeRequestsEvents || !existingWebHook.PushEvents { - _, _, err = gitLabClient.Projects.EditProjectHook(projectID, existingWebHook.ID, &gitlab.EditProjectHookOptions{ + _, _, err = gitLabClient.EditProjectHook(projectID, existingWebHook.ID, &gitlab.EditProjectHookOptions{ URL: gitlab.String(hookURL), MergeRequestsEvents: gitlab.Bool(true), PushEvents: gitlab.Bool(true), @@ -50,7 +51,7 @@ func SetWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int, toke } // RemoveWebHook removes existing webhook from the given project -func RemoveWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int) error { +func RemoveWebHook(gitLabClient GitLabClient, hookURL string, projectID int) error { existingWebHook, err := findExistingWebHook(gitLabClient, hookURL, projectID) if err != nil { return err @@ -60,13 +61,13 @@ func RemoveWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int) e return nil } - _, err = gitLabClient.Projects.DeleteProjectHook(projectID, existingWebHook.ID) + _, err = gitLabClient.DeleteProjectHook(projectID, existingWebHook.ID) return err } -func findExistingWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int) (*gitlab.ProjectHook, error) { - hooks, _, err := gitLabClient.Projects.ListProjectHooks(projectID, &gitlab.ListProjectHooksOptions{}) +func findExistingWebHook(gitLabClient GitLabClient, hookURL string, projectID int) (*gitlab.ProjectHook, error) { + hooks, _, err := gitLabClient.ListProjectHooks(projectID, &gitlab.ListProjectHooksOptions{}) if err != nil { return nil, fmt.Errorf("fetching hooks for project : %d, failed : %v", projectID, err) } diff --git a/cla-backend-go/v2/gitlab-activity/service_test.go b/cla-backend-go/v2/gitlab-activity/service_test.go index 761840bf1..12533987b 100644 --- a/cla-backend-go/v2/gitlab-activity/service_test.go +++ b/cla-backend-go/v2/gitlab-activity/service_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/xanzy/go-gitlab" @@ -201,3 +202,111 @@ func TestPrepareMrCommentContent(t *testing.T) { } } + +func TestService_ProcessMergeActivity(t *testing.T) { + type testCase struct { + name string + secretToken string + input *ProcessMergeActivityInput + expectedError error + expectedCommitSha string + expectedMissing []*gitlab_api.UserCommitSummary + expectedSigned []*gitlab_api.UserCommitSummary + expectedCommitMsg string + expectedCommitURL string + expectedCommitStat gitlab.CommitStatusValue + } + + cases := []testCase{ + { + name: "Valid Merge Activity", + secretToken: "secret", + input: &ProcessMergeActivityInput{ + ProjectName: "project", + ProjectPath: "path", + ProjectNamespace: "namespace", + ProjectID: "projectID", + MergeID: "mergeID", + RepositoryPath: "repositoryPath", + LastCommitSha: "lastCommitSha", + }, + expectedError: nil, + expectedCommitSha: "lastCommitSha", + expectedMissing: []*gitlab_api.UserCommitSummary{}, + expectedSigned: []*gitlab_api.UserCommitSummary{}, + expectedCommitMsg: "EasyCLA check passed. You are authorized to contribute.", + expectedCommitURL: "", + expectedCommitStat: gitlab.Success, + }, + { + name: "Invalid Merge Activity", + secretToken: "secret", + input: &ProcessMergeActivityInput{ + ProjectName: "project", + ProjectPath: "path", + ProjectNamespace: "namespace", + ProjectID: "projectID", + MergeID: "mergeID", + RepositoryPath: "repositoryPath", + LastCommitSha: "lastCommitSha", + }, + expectedError: fmt.Errorf("fetching internal gitlab org for following path : repositoryPath failed : error"), + expectedCommitSha: "", + expectedMissing: nil, + expectedSigned: nil, + expectedCommitMsg: "", + expectedCommitURL: "", + expectedCommitStat: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + service := &service{ + gitlabOrgService: mock_gitlab.NewMockOrgService(ctrl), + gitLabApp: mock_gitlab.NewMockApp(ctrl), + } + + ctx := context.Background() + + // Mock getGitlabOrganizationFromProjectPath + service.gitlabOrgService.EXPECT().GetGitlabOrganizationFromProjectPath(ctx, tc.input.ProjectPath, tc.input.ProjectNamespace).Return(&gitlab_api.GitlabOrganization{}, tc.expectedError) + + // Mock RefreshGitLabOrganizationAuth + service.gitlabOrgService.EXPECT().RefreshGitLabOrganizationAuth(ctx, gomock.Any()).Return(&gitlab_api.OauthResponse{}, tc.expectedError) + + // Mock NewGitlabOauthClient + service.gitLabApp.EXPECT().NewGitlabOauthClient(gomock.Any(), gomock.Any()).Return(nil, tc.expectedError) + + // Mock GetLatestCommit + mockGitlabClient := mock_gitlab.NewMockClient(ctrl) + mockGitlabClient.EXPECT().GetLatestCommit(gomock.Any(), tc.input.ProjectID, tc.input.MergeID).Return(&gitlab_api.Commit{}, tc.expectedError) + service.gitLabApp.EXPECT().GetGitlabClient().Return(mockGitlabClient, tc.expectedError) + + // Mock FetchMrInfo + mockGitlabClient.EXPECT().FetchMrInfo(gomock.Any(), tc.input.ProjectID, tc.input.MergeID).Return(nil, tc.expectedError) + + // Mock getGitlabRepoByName + service.getGitlabRepoByName = func(ctx context.Context, repositoryPath string) (*gitlab_api.GitlabRepository, error) { + return &gitlab_api.GitlabRepository{}, tc.expectedError + } + + // Mock hasUserSigned + service.hasUserSigned = func(ctx context.Context, claGroupID string, user *gitlab_api.UserCommitSummary) (bool, error) { + return false, tc.expectedError + } + + // Mock SetCommitStatus + mockGitlabClient.EXPECT().SetCommitStatus(gomock.Any(), tc.input.ProjectID, tc.expectedCommitSha, tc.expectedCommitStat, tc.expectedCommitMsg, tc.expectedCommitURL).Return(tc.expectedError) + + // Mock SetMrComment + mockGitlabClient.EXPECT().SetMrComment(gomock.Any(), tc.input.ProjectID, tc.input.MergeID, gomock.Any()).Return(tc.expectedError) + + err := service.ProcessMergeActivity(ctx, tc.secretToken, tc.input) + assert.Equal(t, tc.expectedError, err) + }) + } +} diff --git a/cla-backend-go/v2/gitlab_organizations/service.go b/cla-backend-go/v2/gitlab_organizations/service.go index 160274217..35d21b9c8 100644 --- a/cla-backend-go/v2/gitlab_organizations/service.go +++ b/cla-backend-go/v2/gitlab_organizations/service.go @@ -6,9 +6,7 @@ package gitlab_organizations import ( "context" "encoding/json" - "errors" "fmt" - "io" "net/http" "net/url" "sort" @@ -19,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/company" + gitlab "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" "github.com/communitybridge/easycla/cla-backend-go/signatures" "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" @@ -588,7 +587,7 @@ func (s *Service) toGitLabProjectOrganizationList(ctx context.Context, dbModels } else { rorg.Repositories = s.updateRepositoryStatus(glClient, toGitLabProjectResponse(repoList)) - user, _, userErr := glClient.Users.CurrentUser() + user, _, userErr := glClient.CurrentUser() if userErr != nil { log.WithFields(f).Warnf("using gitlab client for gitlab org: %s failed : %v", org.OrganizationID, userErr) rorg.ConnectionStatus = utils.ConnectionFailure @@ -981,7 +980,7 @@ func toGitLabProjectResponse(gitLabListRepos *v2Models.GitlabRepositoriesList) [ } // updateRepositoryStatus is a helper function to set/add the repository connection status -func (s *Service) updateRepositoryStatus(glClient *goGitLab.Client, gitLabProjectRepos []*v2Models.GitlabProjectRepository) []*v2Models.GitlabProjectRepository { +func (s *Service) updateRepositoryStatus(glClient gitlabApi.GitLabClient, gitLabProjectRepos []*v2Models.GitlabProjectRepository) []*v2Models.GitlabProjectRepository { f := logrus.Fields{ "functionName": "v2.gitlab_organizations.service.updateRepositoryStatus", } @@ -1001,8 +1000,8 @@ func (s *Service) updateRepositoryStatus(glClient *goGitLab.Client, gitLabProjec opts := &goGitLab.GetProjectOptions{} for _, repo := range gitLabProjectRepos { // Create a go routine to this concurrently - go func(glClient *goGitLab.Client, repo *v2Models.GitlabProjectRepository) { - projectModel, resp, lookupErr := glClient.Projects.GetProject(int(repo.RepositoryGitlabID), opts) // OK to convert int64 to int as it is the ID and probably should be typed as a int anyway + go func(glClient gitlab.GitLabClient, repo *v2Models.GitlabProjectRepository) { + projectModel, lookupErr := glClient.GetProject(int(repo.RepositoryGitlabID), opts) // OK to convert int64 to int as it is the ID and probably should be typed as a int anyway if lookupErr != nil { log.WithFields(f).WithError(lookupErr).Warnf("problem loading GitLab project by external ID: %d, error: %v", repo.RepositoryGitlabID, lookupErr) repo.ConnectionStatus = utils.ConnectionFailure @@ -1013,26 +1012,26 @@ func (s *Service) updateRepositoryStatus(glClient *goGitLab.Client, gitLabProjec } return } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - responseBody, readErr := io.ReadAll(resp.Body) - if readErr != nil { - log.WithFields(f).WithError(lookupErr).Warnf("problem loading GitLab project by external ID: %d, error: %v", repo.RepositoryGitlabID, readErr) - responseChannel <- responseChannelModel{ - RepositoryExternalID: repo.RepositoryGitlabID, - ConnectionStatus: utils.ConnectionFailure, - Error: readErr, - } - return - } - msg := fmt.Sprintf("problem loading GitLab project by external ID: %d, response code: %d, body: %s", repo.RepositoryGitlabID, resp.StatusCode, responseBody) - log.WithFields(f).Warnf(msg) - responseChannel <- responseChannelModel{ - RepositoryExternalID: repo.RepositoryGitlabID, - ConnectionStatus: utils.ConnectionFailure, - Error: errors.New(msg), - } - return - } + // if resp.StatusCode < 200 || resp.StatusCode > 299 { + // responseBody, readErr := io.ReadAll(resp.Body) + // if readErr != nil { + // log.WithFields(f).WithError(lookupErr).Warnf("problem loading GitLab project by external ID: %d, error: %v", repo.RepositoryGitlabID, readErr) + // responseChannel <- responseChannelModel{ + // RepositoryExternalID: repo.RepositoryGitlabID, + // ConnectionStatus: utils.ConnectionFailure, + // Error: readErr, + // } + // return + // } + // msg := fmt.Sprintf("problem loading GitLab project by external ID: %d, response code: %d, body: %s", repo.RepositoryGitlabID, resp.StatusCode, responseBody) + // log.WithFields(f).Warnf(msg) + // responseChannel <- responseChannelModel{ + // RepositoryExternalID: repo.RepositoryGitlabID, + // ConnectionStatus: utils.ConnectionFailure, + // Error: errors.New(msg), + // } + // return + // } log.WithFields(f).Debugf("connected to project/repo: %s", projectModel.PathWithNamespace) responseChannel <- responseChannelModel{