package vcs import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path" "strings" "testing" "time" "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" gitlab "gitlab.com/gitlab-org/api/client-go" . "github.com/runatlantis/atlantis/testing" ) var projectID = 4580910 const gitlabPipelineSuccessMrID = 488598 const updateStatusDescription = "description" const updateStatusTargetUrl = "https://google.com" const updateStatusSrc = "src" const updateStatusHeadBranch = "test" /* UpdateStatus request JSON body object */ type UpdateStatusJsonBody struct { State string `json:"state"` Context string `json:"context"` TargetUrl string `json:"target_url"` Description string `json:"description"` PipelineId int `json:"pipeline_id"` Ref string `json:"ref"` } /* GetCommit response last_pipeline JSON object */ type GetCommitResponseLastPipeline struct { ID int `json:"id"` } /* GetCommit response JSON object */ type GetCommitResponse struct { LastPipeline GetCommitResponseLastPipeline `json:"last_pipeline"` } /* Empty struct for JSON marshalling */ type EmptyStruct struct{} // Test that the base url gets set properly. func TestNewGitlabClient_BaseURL(t *testing.T) { gitlabClientUnderTest = true defer func() { gitlabClientUnderTest = false }() cases := []struct { Hostname string ExpBaseURL string }{ { "gitlab.com", "https://gitlab.com/api/v4/", }, { "custom.domain", "https://custom.domain/api/v4/", }, { "http://custom.domain", "http://custom.domain/api/v4/", }, { "http://custom.domain:8080", "http://custom.domain:8080/api/v4/", }, { "https://custom.domain", "https://custom.domain/api/v4/", }, { "https://custom.domain/", "https://custom.domain/api/v4/", }, { "https://custom.domain/basepath/", "https://custom.domain/basepath/api/v4/", }, } for _, c := range cases { t.Run(c.Hostname, func(t *testing.T) { log := logging.NewNoopLogger(t) client, err := NewGitlabClient(c.Hostname, "token", []string{}, log) Ok(t, err) Equals(t, c.ExpBaseURL, client.Client.BaseURL().String()) }) } } // This function gets called even if GitlabClient is nil // so we need to test that. func TestGitlabClient_SupportsCommonMarkNil(t *testing.T) { var gl *GitlabClient Equals(t, false, gl.SupportsCommonMark()) } func TestGitlabClient_SupportsCommonMark(t *testing.T) { cases := []struct { version string exp bool }{ { "11.0", false, }, { "11.1", true, }, { "11.2", true, }, { "12.0", true, }, } for _, c := range cases { t.Run(c.version, func(t *testing.T) { vers, err := version.NewVersion(c.version) Ok(t, err) gl := GitlabClient{ Version: vers, } Equals(t, c.exp, gl.SupportsCommonMark()) }) } } func TestGitlabClient_GetModifiedFiles(t *testing.T) { logger := logging.NewNoopLogger(t) cases := []struct { attempts int }{ {1}, {2}, {3}, } changesPending, err := os.ReadFile("testdata/gitlab-changes-pending.json") Ok(t, err) changesAvailable, err := os.ReadFile("testdata/gitlab-changes-available.json") Ok(t, err) for _, c := range cases { t.Run(fmt.Sprintf("Gitlab returns MR changes after %d attempts", c.attempts), func(t *testing.T) { numAttempts := 0 testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/lkysow%2Fatlantis-example/merge_requests/8312/changes?page=1&per_page=100": w.WriteHeader(200) numAttempts++ if numAttempts < c.attempts { w.Write(changesPending) // nolint: errcheck t.Logf("returning changesPending for attempt %d", numAttempts) return } t.Logf("returning changesAvailable for attempt %d", numAttempts) w.Write(changesAvailable) // nolint: errcheck default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, PollingInterval: time.Second * 0, PollingTimeout: time.Second * 10, } filenames, err := client.GetModifiedFiles( logger, models.Repo{ FullName: "lkysow/atlantis-example", Owner: "lkysow", Name: "atlantis-example", }, models.PullRequest{ Num: 8312, BaseRepo: models.Repo{ FullName: "lkysow/atlantis-example", Owner: "lkysow", Name: "atlantis-example", }, }) Ok(t, err) Equals(t, []string{"somefile.yaml"}, filenames) }) } } func TestGitlabClient_MergePull(t *testing.T) { logger := logging.NewNoopLogger(t) mergeSuccess, err := os.ReadFile("testdata/github-pull-request.json") Ok(t, err) pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json") Ok(t, err) projectSuccess, err := os.ReadFile("testdata/gitlab-project-success.json") Ok(t, err) cases := []struct { description string glResponse []byte code int expErr string }{ { "success", mergeSuccess, 200, "", }, { "405", []byte(`{"message":"405 Method Not Allowed"}`), 405, "405 {message: 405 Method Not Allowed}", }, { "406", []byte(`{"message":"406 Branch cannot be merged"}`), 406, "406 {message: 406 Branch cannot be merged}", }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { // The first request should hit this URL. case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1/merge": w.WriteHeader(c.code) w.Write(c.glResponse) // nolint: errcheck case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1": w.WriteHeader(http.StatusOK) w.Write(pipelineSuccess) // nolint: errcheck case "/api/v4/projects/4580910": w.WriteHeader(http.StatusOK) w.Write(projectSuccess) // nolint: errcheck case "/api/v4/": // Rate limiter requests. w.WriteHeader(http.StatusOK) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } err = client.MergePull( logger, models.PullRequest{ Num: 1, BaseRepo: models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", }, }, models.PullRequestOptions{ DeleteSourceBranchOnMerge: false, }) if c.expErr == "" { Ok(t, err) } else { ErrContains(t, c.expErr, err) ErrContains(t, "unable to merge merge request, it may not be in a mergeable state", err) } }) } } func TestGitlabClient_UpdateStatus(t *testing.T) { logger := logging.NewNoopLogger(t) cases := []struct { status models.CommitStatus expState string }{ { models.PendingCommitStatus, "running", }, { models.SuccessCommitStatus, "success", }, { models.FailedCommitStatus, "failed", }, } for _, c := range cases { t.Run(c.expState, func(t *testing.T) { gotRequest := false testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha": gotRequest = true var updateStatusJsonBody UpdateStatusJsonBody err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody) Ok(t, err) Equals(t, c.expState, updateStatusJsonBody.State) Equals(t, updateStatusSrc, updateStatusJsonBody.Context) Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl) Equals(t, updateStatusDescription, updateStatusJsonBody.Description) Equals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId) defer r.Body.Close() // nolint: errcheck setStatusJsonResponse, err := json.Marshal(EmptyStruct{}) Ok(t, err) _, err = w.Write(setStatusJsonResponse) Ok(t, err) case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha": w.WriteHeader(http.StatusOK) getCommitResponse := GetCommitResponse{ LastPipeline: GetCommitResponseLastPipeline{ ID: gitlabPipelineSuccessMrID, }, } getCommitJsonResponse, err := json.Marshal(getCommitResponse) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) case "/api/v4/": // Rate limiter requests. w.WriteHeader(http.StatusOK) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } repo := models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", } err = client.UpdateStatus( logger, repo, models.PullRequest{ Num: 1, BaseRepo: repo, HeadCommit: "sha", HeadBranch: updateStatusHeadBranch, }, c.status, updateStatusSrc, updateStatusDescription, updateStatusTargetUrl, ) Ok(t, err) Assert(t, gotRequest, "expected to get the request") }) } } func TestGitlabClient_UpdateStatusGetCommitRetryable(t *testing.T) { logger := logging.NewNoopLogger(t) cases := []struct { title string status models.CommitStatus commitsWithNoLastPipeline int expNumberOfRequests int expRefOrPipelineId string }{ // Ensure that GetCommit with last pipeline id sets the pipeline id. { title: "GetCommit with a pipeline id", status: models.PendingCommitStatus, commitsWithNoLastPipeline: 0, expNumberOfRequests: 1, expRefOrPipelineId: "PipelineId", }, // Ensure that 1 x GetCommit with no pipelines sets the pipeline id. { title: "1 x GetCommit with no last pipeline id", status: models.PendingCommitStatus, commitsWithNoLastPipeline: 1, expNumberOfRequests: 2, expRefOrPipelineId: "PipelineId", }, // Ensure that 2 x GetCommit with no last pipeline id sets the ref. { title: "2 x GetCommit with no last pipeline id", status: models.PendingCommitStatus, commitsWithNoLastPipeline: 2, expNumberOfRequests: 2, expRefOrPipelineId: "Ref", }, } for _, c := range cases { t.Run(c.title, func(t *testing.T) { handledNumberOfRequests := 0 testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha": var updateStatusJsonBody UpdateStatusJsonBody err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody) Ok(t, err) Equals(t, "running", updateStatusJsonBody.State) Equals(t, updateStatusSrc, updateStatusJsonBody.Context) Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl) Equals(t, updateStatusDescription, updateStatusJsonBody.Description) if c.expRefOrPipelineId == "Ref" { Equals(t, updateStatusHeadBranch, updateStatusJsonBody.Ref) } else { Equals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId) } defer r.Body.Close() getCommitJsonResponse, err := json.Marshal(EmptyStruct{}) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha": handledNumberOfRequests++ noCommitLastPipeline := handledNumberOfRequests <= c.commitsWithNoLastPipeline w.WriteHeader(http.StatusOK) if noCommitLastPipeline { getCommitJsonResponse, err := json.Marshal(EmptyStruct{}) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) } else { getCommitResponse := GetCommitResponse{ LastPipeline: GetCommitResponseLastPipeline{ ID: gitlabPipelineSuccessMrID, }, } getCommitJsonResponse, err := json.Marshal(getCommitResponse) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) } case "/api/v4/": // Rate limiter requests. w.WriteHeader(http.StatusOK) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, PollingInterval: 10 * time.Millisecond, } repo := models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", } err = client.UpdateStatus( logger, repo, models.PullRequest{ Num: 1, BaseRepo: repo, HeadCommit: "sha", HeadBranch: updateStatusHeadBranch, }, c.status, updateStatusSrc, updateStatusDescription, updateStatusTargetUrl, ) Ok(t, err) Assert(t, c.expNumberOfRequests == handledNumberOfRequests, fmt.Sprintf("expected %d number of requests, but processed %d", c.expNumberOfRequests, handledNumberOfRequests)) }) } } func TestGitlabClient_UpdateStatusSetCommitStatusConflictRetryable(t *testing.T) { logger := logging.NewNoopLogger(t) cases := []struct { status models.CommitStatus numberOfConflicts int expNumberOfRequests int expState string expError bool }{ // Ensure that 0 x 409 Conflict succeeds { status: models.PendingCommitStatus, numberOfConflicts: 0, expNumberOfRequests: 1, expState: "running", }, // Ensure that 5 x 409 Conflict still succeeds { status: models.PendingCommitStatus, numberOfConflicts: 5, expNumberOfRequests: 6, expState: "running", }, // Ensure that 10 x 409 Conflict still fail due to running out of retries { status: models.FailedCommitStatus, numberOfConflicts: 100, // anything larger than 10 is fine expNumberOfRequests: 10, expState: "failed", expError: true, }, } for _, c := range cases { t.Run(c.expState, func(t *testing.T) { handledNumberOfRequests := 0 testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha": handledNumberOfRequests++ shouldSendConflict := handledNumberOfRequests <= c.numberOfConflicts var updateStatusJsonBody UpdateStatusJsonBody err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody) Ok(t, err) Equals(t, c.expState, updateStatusJsonBody.State) Equals(t, updateStatusSrc, updateStatusJsonBody.Context) Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl) Equals(t, updateStatusDescription, updateStatusJsonBody.Description) defer r.Body.Close() // nolint: errcheck if shouldSendConflict { w.WriteHeader(http.StatusConflict) } getCommitJsonResponse, err := json.Marshal(EmptyStruct{}) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha": w.WriteHeader(http.StatusOK) getCommitResponse := GetCommitResponse{ LastPipeline: GetCommitResponseLastPipeline{ ID: gitlabPipelineSuccessMrID, }, } getCommitJsonResponse, err := json.Marshal(getCommitResponse) Ok(t, err) _, err = w.Write(getCommitJsonResponse) Ok(t, err) case "/api/v4/": // Rate limiter requests. w.WriteHeader(http.StatusOK) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, PollingInterval: 10 * time.Millisecond, } repo := models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", } err = client.UpdateStatus( logger, repo, models.PullRequest{ Num: 1, BaseRepo: repo, HeadCommit: "sha", HeadBranch: "test", }, c.status, updateStatusSrc, updateStatusDescription, updateStatusTargetUrl, ) if c.expError { ErrContains(t, "failed to update commit status for 'runatlantis/atlantis' @ 'sha' to 'src' after 10 attempts", err) ErrContains(t, "409", err) } else { Ok(t, err) } Assert(t, c.expNumberOfRequests == handledNumberOfRequests, fmt.Sprintf("expected %d number of requests, but processed %d", c.expNumberOfRequests, handledNumberOfRequests)) }) } } func TestGitlabClient_PullIsMergeable(t *testing.T) { logger := logging.NewNoopLogger(t) gitlabClientUnderTest = true gitlabVersionOver15_6 := "15.8.3-ee" gitlabVersion15_6 := "15.6.0-ee" gitlabVersionUnder15_6 := "15.3.2-ce" gitlabServerVersions := []string{gitlabVersionOver15_6, gitlabVersion15_6, gitlabVersionUnder15_6} vcsStatusName := "atlantis-test" defaultMr := 1 noHeadPipelineMR := 2 ciMustPassSuccessMR := 3 ciMustPassFailureMR := 4 needRebaseMR := 5 pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json") Ok(t, err) projectSuccess, err := os.ReadFile("testdata/gitlab-project-success.json") Ok(t, err) detailedMergeStatusCiMustPass, err := os.ReadFile("testdata/gitlab-detailed-merge-status-ci-must-pass.json") Ok(t, err) detailedMergeStatusNeedRebase, err := os.ReadFile("testdata/gitlab-detailed-merge-status-need-rebase.json") Ok(t, err) headPipelineNotAvailable, err := os.ReadFile("testdata/gitlab-head-pipeline-not-available.json") Ok(t, err) cases := []struct { statusName string status models.CommitStatus gitlabVersion []string mrID int expState bool }{ { fmt.Sprintf("%s/apply: resource/default", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, defaultMr, true, }, { fmt.Sprintf("%s/apply", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, defaultMr, true, }, { fmt.Sprintf("%s/plan: resource/default", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, defaultMr, false, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.PendingCommitStatus, gitlabServerVersions, defaultMr, false, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.SuccessCommitStatus, gitlabServerVersions, defaultMr, true, }, { fmt.Sprintf("%s/apply", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, ciMustPassSuccessMR, true, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, ciMustPassFailureMR, false, }, { fmt.Sprintf("%s/apply", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, needRebaseMR, true, }, { fmt.Sprintf("%s/apply: resource/default", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, noHeadPipelineMR, true, }, { fmt.Sprintf("%s/apply", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, noHeadPipelineMR, true, }, { fmt.Sprintf("%s/plan: resource/default", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, noHeadPipelineMR, false, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.PendingCommitStatus, gitlabServerVersions, noHeadPipelineMR, false, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.FailedCommitStatus, gitlabServerVersions, noHeadPipelineMR, false, }, { fmt.Sprintf("%s/plan", vcsStatusName), models.SuccessCommitStatus, gitlabServerVersions, noHeadPipelineMR, true, }, } for _, serverVersion := range gitlabServerVersions { for _, c := range cases { t.Run(c.statusName, func(t *testing.T) { testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/": // Rate limiter requests. w.WriteHeader(http.StatusOK) case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", defaultMr): w.WriteHeader(http.StatusOK) w.Write(pipelineSuccess) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", noHeadPipelineMR): w.WriteHeader(http.StatusOK) w.Write(headPipelineNotAvailable) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", ciMustPassSuccessMR): w.WriteHeader(http.StatusOK) w.Write(detailedMergeStatusCiMustPass) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", ciMustPassFailureMR): w.WriteHeader(http.StatusOK) w.Write(detailedMergeStatusCiMustPass) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", needRebaseMR): w.WriteHeader(http.StatusOK) w.Write(detailedMergeStatusNeedRebase) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/%v", projectID): w.WriteHeader(http.StatusOK) w.Write(projectSuccess) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/%v/repository/commits/67cb91d3f6198189f433c045154a885784ba6977/statuses", projectID): w.WriteHeader(http.StatusOK) response := fmt.Sprintf(`[{"id":133702594,"sha":"67cb91d3f6198189f433c045154a885784ba6977","ref":"patch-1","status":"%s","name":"%s","target_url":null,"description":"ApplySuccess","created_at":"2018-12-12T18:31:57.957Z","started_at":null,"finished_at":"2018-12-12T18:31:58.480Z","allow_failure":false,"coverage":null,"author":{"id":1755902,"username":"lkysow","name":"LukeKysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon","web_url":"https://gitlab.com/lkysow"}}]`, c.status, c.statusName) w.Write([]byte(response)) // nolint: errcheck case "/api/v4/version": w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") type version struct { Version string } v := version{Version: serverVersion} err := json.NewEncoder(w).Encode(v) Ok(t, err) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } repo := models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", VCSHost: models.VCSHost{ Type: models.Gitlab, Hostname: "gitlab.com", }, } mergeable, err := client.PullIsMergeable( logger, repo, models.PullRequest{ Num: c.mrID, BaseRepo: repo, HeadCommit: "67cb91d3f6198189f433c045154a885784ba6977", }, vcsStatusName, []string{}) Ok(t, err) Equals(t, c.expState, mergeable) }) } } } func TestGitlabClient_MarkdownPullLink(t *testing.T) { logger := logging.NewNoopLogger(t) gitlabClientUnderTest = true defer func() { gitlabClientUnderTest = false }() client, err := NewGitlabClient("gitlab.com", "token", []string{}, logger) Ok(t, err) pull := models.PullRequest{Num: 1} s, _ := client.MarkdownPullLink(pull) exp := "!1" Equals(t, exp, s) } func TestGitlabClient_HideOldComments(t *testing.T) { logger := logging.NewNoopLogger(t) type notePutCallDetails struct { noteID string comment []string } type jsonBody struct { Body string } authorID := 1 authorUserName := "pipin" authorEmail := "admin@example.com" pullNum := 123 userCommentIDs := [1]string{"1"} planCommentIDs := [2]string{"3", "5"} systemCommentIDs := [1]string{"4"} summaryCommentIDs := [1]string{"2"} planComments := [3]string{"Ran Plan for 2 projects:", "Ran Plan for dir: `stack1` workspace: `default`", "Ran Plan for 2 projects:"} summaryHeader := fmt.Sprintf("
Superseded Atlantis %s", command.Plan.TitleString()) summaryFooter := "
" lineFeed := "\\n" issueResp := "[" + fmt.Sprintf(`{"id":%s,"body":"User comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`, userCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," + fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`, summaryCommentIDs[0], summaryHeader+lineFeed+planComments[2]+lineFeed+summaryFooter, authorID, authorUserName, authorEmail, pullNum) + "," + fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`, planCommentIDs[0], planComments[0], authorID, authorUserName, authorEmail, pullNum) + "," + fmt.Sprintf(`{"id":%s,"body":"System comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": true,"project_id": %d}`, systemCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," + fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`, planCommentIDs[1], planComments[1], authorID, authorUserName, authorEmail, pullNum) + "]" repo := models.Repo{ FullName: "runatlantis/atlantis", Owner: "runatlantis", Name: "atlantis", VCSHost: models.VCSHost{ Type: models.Gitlab, Hostname: "gitlab.com", }, } cases := []struct { dir string processedComments int processedCommentIds []string processedPlanComment []string }{ { "", 2, []string{planCommentIDs[0], planCommentIDs[1]}, []string{planComments[0], planComments[1]}, }, { "stack1", 1, []string{planCommentIDs[1]}, []string{planComments[1]}, }, { "stack2", 0, []string{}, []string{}, }, } for _, c := range cases { t.Run(c.dir, func(t *testing.T) { gitlabClientUnderTest = true defer func() { gitlabClientUnderTest = false }() gotNotePutCalls := make([]notePutCallDetails, 0, 1) testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": switch r.RequestURI { case "/api/v4/user": w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") response := fmt.Sprintf(`{"id": %d,"username": "%s", "email": "%s"}`, authorID, authorUserName, authorEmail) w.Write([]byte(response)) // nolint: errcheck case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes?order_by=created_at&sort=asc", pullNum): w.WriteHeader(http.StatusOK) response := issueResp w.Write([]byte(response)) // nolint: errcheck default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } case "PUT": switch { case strings.HasPrefix(r.RequestURI, fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes/", pullNum)): w.WriteHeader(http.StatusOK) var body jsonBody json.NewDecoder(r.Body).Decode(&body) // nolint: errcheck notePutCallDetail := notePutCallDetails{ noteID: path.Base(r.RequestURI), comment: strings.Split(body.Body, "\n"), } gotNotePutCalls = append(gotNotePutCalls, notePutCallDetail) response := "{}" w.Write([]byte(response)) // nolint: errcheck default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } default: t.Errorf("got unexpected method at %q", r.Method) http.Error(w, "not found", http.StatusNotFound) } }), ) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } err = client.HidePrevCommandComments(logger, repo, pullNum, command.Plan.TitleString(), c.dir) Ok(t, err) // Check the correct number of plan comments have been processed Equals(t, c.processedComments, len(gotNotePutCalls)) // Check the correct comments have been processed for i := 0; i < c.processedComments; i++ { Equals(t, c.processedCommentIds[i], gotNotePutCalls[i].noteID) Equals(t, summaryHeader, gotNotePutCalls[i].comment[0]) Equals(t, c.processedPlanComment[i], gotNotePutCalls[i].comment[1]) Equals(t, summaryFooter, gotNotePutCalls[i].comment[2]) } }) } } func TestGitlabClient_GetPullLabels(t *testing.T) { logger := logging.NewNoopLogger(t) mergeSuccessWithLabel, err := os.ReadFile("testdata/gitlab-merge-success-with-label.json") Ok(t, err) testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1": w.WriteHeader(http.StatusOK) w.Write(mergeSuccessWithLabel) // nolint: errcheck default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } labels, err := client.GetPullLabels( logger, models.Repo{ FullName: "runatlantis/atlantis", }, models.PullRequest{ Num: 1, }, ) Ok(t, err) Equals(t, []string{"work in progress"}, labels) } func TestGitlabClient_GetPullLabels_EmptyResponse(t *testing.T) { logger := logging.NewNoopLogger(t) pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json") Ok(t, err) testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1": w.WriteHeader(http.StatusOK) w.Write(pipelineSuccess) // nolint: errcheck default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } labels, err := client.GetPullLabels( logger, models.Repo{ FullName: "runatlantis/atlantis", }, models.PullRequest{ Num: 1, }) Ok(t, err) Equals(t, 0, len(labels)) } // GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to. func TestGitlabClient_GetTeamNamesForUser(t *testing.T) { logger := logging.NewNoopLogger(t) groupMembershipSuccess, err := os.ReadFile("testdata/gitlab-group-membership-success.json") Ok(t, err) userSuccess, err := os.ReadFile("testdata/gitlab-user-success.json") Ok(t, err) userEmpty, err := os.ReadFile("testdata/gitlab-user-none.json") Ok(t, err) multipleUsers, err := os.ReadFile("testdata/gitlab-user-multiple.json") Ok(t, err) configuredGroups := []string{"someorg/group1", "someorg/group2", "someorg/group3", "someorg/group4"} cases := []struct { userName string expErr string expTeams []string }{ { userName: "testuser", expTeams: []string{"someorg/group1", "someorg/group2"}, }, { userName: "none", expErr: "GET /users returned no user", }, { userName: "multiuser", expErr: "GET /users returned more than 1 user", }, } for _, c := range cases { t.Run(c.userName, func(t *testing.T) { testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/users?username=testuser": w.WriteHeader(http.StatusOK) w.Write(userSuccess) // nolint: errcheck case "/api/v4/users?username=none": w.WriteHeader(http.StatusOK) w.Write(userEmpty) // nolint: errcheck case "/api/v4/users?username=multiuser": w.WriteHeader(http.StatusOK) w.Write(multipleUsers) // nolint: errcheck case "/api/v4/groups/someorg%2Fgroup1/members/123", "/api/v4/groups/someorg%2Fgroup2/members/123": w.WriteHeader(http.StatusOK) w.Write(groupMembershipSuccess) // nolint: errcheck case "/api/v4/groups/someorg%2Fgroup3/members/123": http.Error(w, "forbidden", http.StatusForbidden) case "/api/v4/groups/someorg%2Fgroup4/members/123": http.Error(w, "not found", http.StatusNotFound) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, ConfiguredGroups: configuredGroups, } teams, err := client.GetTeamNamesForUser( logger, models.Repo{ Owner: "someorg", }, models.User{ Username: c.userName, }) if c.expErr == "" { Ok(t, err) Equals(t, c.expTeams, teams) } else { ErrContains(t, c.expErr, err) } }) } } func TestGithubClient_DiscardReviews(t *testing.T) { logger := logging.NewNoopLogger(t) cases := []struct { description string repoFullName string pullReqeustId int wantErr bool }{ { "success", "runatlantis/atlantis", 42, false, }, { "error", "runatlantis/atlantis", 32, true, }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/42/reset_approvals": w.WriteHeader(http.StatusOK) case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/32/reset_approvals": http.Error(w, "No bot token", http.StatusUnauthorized) default: t.Errorf("got unexpected request at %q", r.RequestURI) http.Error(w, "not found", http.StatusNotFound) } })) internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) Ok(t, err) client := &GitlabClient{ Client: internalClient, Version: nil, } repo := models.Repo{ FullName: c.repoFullName, } pr := models.PullRequest{ Num: c.pullReqeustId, } if err := client.DiscardReviews(logger, repo, pr); (err != nil) != c.wantErr { t.Errorf("DiscardReviews() error = %v", err) } }) } }