Files
atlantis/server/events/vcs/azuredevops_client_test.go
Michel Z. Santello df0d58d29b fix: vcs-status-name hardcoded in PullIsMergeable function (#2312)
* feat: add userConfig.VCSStatusName to applyCommandRunner context

* fix: use vcsstatusname from context

* chore: normalize tests

* chore: update interface mock
2022-06-16 09:26:20 -07:00

625 lines
17 KiB
Go

package vcs_test
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/mcdafydd/go-azuredevops/azuredevops"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/events/vcs/fixtures"
. "github.com/runatlantis/atlantis/testing"
)
func TestAzureDevopsClient_MergePull(t *testing.T) {
cases := []struct {
description string
response string
code int
expErr string
}{
{
"success",
adMergeSuccess,
200,
"",
},
{
"405",
`{"message":"405 Method Not Allowed"}`,
405,
"405 {message: 405 Method Not Allowed}",
},
{
"406",
`{"message":"406 Branch cannot be merged"}`,
406,
"406 {message: 406 Branch cannot be merged}",
},
}
// Set default pull request completion options
mcm := azuredevops.NoFastForward.String()
twi := new(bool)
*twi = true
completionOptions := azuredevops.GitPullRequestCompletionOptions{
BypassPolicy: new(bool),
BypassReason: azuredevops.String(""),
DeleteSourceBranch: new(bool),
MergeCommitMessage: azuredevops.String("commit message"),
MergeStrategy: &mcm,
SquashMerge: new(bool),
TransitionWorkItems: twi,
TriggeredByAutoComplete: new(bool),
}
id := azuredevops.IdentityRef{}
pull := azuredevops.GitPullRequest{
PullRequestID: azuredevops.Int(22),
}
userIDResponse := `{
"members": [
{
"id": "6416203b-98bb-4910-8f8a-b12aa19a399f"
}
],
"continuationToken": null,
"totalCount": 0,
"items": [
{
"id": "6416203b-98bb-4910-8f8a-b12aa19a399f"
}
]
}`
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
// The first request should hit this URL.
case "/owner/project/_apis/git/repositories/repo/pullrequests/22?api-version=5.1-preview.1":
w.WriteHeader(c.code)
w.Write([]byte(c.response)) // nolint: errcheck
case "/owner/_apis/userentitlements?$filter=name+eq+'user'&$api-version=6.0-preview.3":
w.WriteHeader(c.code)
w.Write([]byte(userIDResponse)) // nolint: errcheck
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
client.Client.VsaexBaseURL = *testServerURL
Ok(t, err)
defer disableSSLVerification()()
merge, _, err := client.Client.PullRequests.Merge(context.Background(),
"owner",
"project",
"repo",
pull.GetPullRequestID(),
&pull,
completionOptions,
id,
)
if err != nil {
fmt.Printf("Merge failed: %+v\n", err)
return
}
fmt.Printf("Successfully merged pull request: %+v\n", merge)
err = client.MergePull(models.PullRequest{
Num: 22,
BaseRepo: models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
},
}, 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 TestAzureDevopsClient_UpdateStatus(t *testing.T) {
cases := []struct {
status models.CommitStatus
expState string
supportsIterations bool
}{
{
models.PendingCommitStatus,
"pending",
true,
},
{
models.SuccessCommitStatus,
"succeeded",
true,
},
{
models.FailedCommitStatus,
"failed",
true,
},
{
models.PendingCommitStatus,
"pending",
false,
},
{
models.SuccessCommitStatus,
"succeeded",
false,
},
{
models.FailedCommitStatus,
"failed",
false,
},
}
iterResponse := `{"count": 2, "value": [{"id": 1, "sourceRefCommit": { "commitId": "oldsha"}}, {"id": 2, "sourceRefCommit": { "commitId": "sha"}}]}`
prResponse := `{"supportsIterations": %t}`
partResponse := `{"context":{"genre":"Atlantis Bot","name":"src"},"description":"description","state":"%s","targetUrl":"https://google.com"`
for _, c := range cases {
prResponse := fmt.Sprintf(prResponse, c.supportsIterations)
t.Run(c.expState, func(t *testing.T) {
gotRequest := false
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/owner/project/_apis/git/repositories/repo/pullrequests/22/statuses?api-version=5.1-preview.1":
gotRequest = true
defer r.Body.Close() // nolint: errcheck
body, err := io.ReadAll(r.Body)
Ok(t, err)
exp := fmt.Sprintf(partResponse, c.expState)
if c.supportsIterations == true {
exp = fmt.Sprintf("%s%s}\n", exp, `,"iterationId":2`)
} else {
exp = fmt.Sprintf("%s}\n", exp)
}
Equals(t, exp, string(body))
w.Write([]byte(exp)) // nolint: errcheck
case "/owner/project/_apis/git/repositories/repo/pullrequests/22/iterations?api-version=5.1":
w.Write([]byte(iterResponse)) // nolint: errcheck
case "/owner/project/_apis/git/pullrequests/22?api-version=5.1-preview.1":
w.Write([]byte(prResponse)) // nolint: errcheck
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
Ok(t, err)
defer disableSSLVerification()()
repo := models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
}
err = client.UpdateStatus(repo, models.PullRequest{
Num: 22,
BaseRepo: repo,
HeadCommit: "sha",
}, c.status, "src", "description", "https://google.com")
Ok(t, err)
Assert(t, gotRequest, "expected to get the request")
})
}
}
// GetModifiedFiles should make multiple requests if more than one page
// and concat results.
func TestAzureDevopsClient_GetModifiedFiles(t *testing.T) {
itemRespTemplate := `{
"changes": [
{
"item": {
"gitObjectType": "blob",
"path": "%s",
"url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit"
},
"changeType": "add"
},
{
"item": {
"gitObjectType": "blob",
"path": "%s",
"url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit"
},
"changeType": "add"
}
]}`
resp := fmt.Sprintf(itemRespTemplate, "/file1.txt", "/file1.txt", "/file2.txt", "/file2.txt")
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
// The first request should hit this URL.
case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
w.Write([]byte(fixtures.ADPullJSON)) // nolint: errcheck
// The second should hit this URL.
case "/owner/project/_apis/git/repositories/repo/diffs/commits?api-version=5.1&baseVersion=new_feature&targetVersion=npaulk/my_work":
// We write a header that means there's an additional page.
w.Write([]byte(resp)) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
Ok(t, err)
defer disableSSLVerification()()
files, err := client.GetModifiedFiles(models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.AzureDevops,
Hostname: "dev.azure.com",
},
}, models.PullRequest{
Num: 1,
})
Ok(t, err)
Equals(t, []string{"file1.txt", "file2.txt"}, files)
}
func TestAzureDevopsClient_PullIsMergeable(t *testing.T) {
type Policy struct {
genre string
name string
status string
}
cases := []struct {
testName string
mergeStatus string
policy Policy
expMergeable bool
}{
{
"merge conflicts",
azuredevops.MergeConflicts.String(),
Policy{
"Not Atlantis",
"foo",
"approved",
},
false,
},
{
"rejected policy status",
azuredevops.MergeSucceeded.String(),
Policy{
"Not Atlantis",
"foo",
"rejected",
},
false,
},
{
"merge succeeded",
azuredevops.MergeSucceeded.String(),
Policy{
"Not Atlantis",
"foo",
"approved",
},
true,
},
{
"pending policy status",
azuredevops.MergeSucceeded.String(),
Policy{
"Not Atlantis",
"foo",
"pending",
},
false,
},
{
"atlantis apply status rejected",
azuredevops.MergeSucceeded.String(),
Policy{
"Atlantis Bot/atlantis",
"apply",
"rejected",
},
true,
},
}
jsonPullRequestBytes, err := os.ReadFile("fixtures/azuredevops-pr.json")
Ok(t, err)
jsonPolicyEvaluationBytes, err := os.ReadFile("fixtures/azuredevops-policyevaluations.json")
Ok(t, err)
pullRequestBody := string(jsonPullRequestBytes)
policyEvaluationsBody := string(jsonPolicyEvaluationBytes)
for _, c := range cases {
t.Run(c.testName, func(t *testing.T) {
pullRequestResponse := strings.Replace(pullRequestBody, `"mergeStatus": "notSet"`, fmt.Sprintf(`"mergeStatus": "%s"`, c.mergeStatus), 1)
policyEvaluationsResponse := strings.Replace(policyEvaluationsBody, `"status": "approved"`, fmt.Sprintf(`"status": "%s"`, c.policy.status), 1)
policyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `"statusGenre": "Atlantis Bot/atlantis"`, fmt.Sprintf(`"statusGenre": "%s"`, c.policy.genre), 1)
policyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `"statusName": "plan"`, fmt.Sprintf(`"statusName": "%s"`, c.policy.name), 1)
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
w.Write([]byte(pullRequestResponse)) // nolint: errcheck
return
case "/owner/project/_apis/policy/evaluations?api-version=5.1-preview&artifactId=vstfs%3A%2F%2F%2FCodeReview%2FCodeReviewId%2F33333333-3333-3333-333333333333%2F1":
w.Write([]byte(policyEvaluationsResponse)) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
Ok(t, err)
defer disableSSLVerification()()
actMergeable, err := client.PullIsMergeable(models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.AzureDevops,
Hostname: "dev.azure.com",
},
}, models.PullRequest{
Num: 1,
}, "atlantis-test")
Ok(t, err)
Equals(t, c.expMergeable, actMergeable)
})
}
}
func TestAzureDevopsClient_PullIsApproved(t *testing.T) {
cases := []struct {
testName string
reviewerUniqueName string
reviewerVote int
expApproved bool
}{
{
"approved",
"atlantis.reviewer@example.com",
azuredevops.VoteApproved,
true,
},
{
"approved with suggestions",
"atlantis.reviewer@example.com",
azuredevops.VoteApprovedWithSuggestions,
true,
},
{
"no vote",
"atlantis.reviewer@example.com",
azuredevops.VoteNone,
false,
},
{
"vote waiting for author",
"atlantis.reviewer@example.com",
azuredevops.VoteWaitingForAuthor,
false,
},
{
"vote rejected",
"atlantis.reviewer@example.com",
azuredevops.VoteRejected,
false,
},
{
"approved only by author",
"atlantis.author@example.com",
azuredevops.VoteApproved,
false,
},
}
jsBytes, err := os.ReadFile("fixtures/azuredevops-pr.json")
Ok(t, err)
json := string(jsBytes)
for _, c := range cases {
t.Run(c.testName, func(t *testing.T) {
response := strings.Replace(json, `"vote": 0,`, fmt.Sprintf(`"vote": %d,`, c.reviewerVote), 1)
response = strings.Replace(response, "atlantis.reviewer@example.com", c.reviewerUniqueName, 1)
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
w.Write([]byte(response)) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
Ok(t, err)
defer disableSSLVerification()()
approvalStatus, err := client.PullIsApproved(models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.AzureDevops,
Hostname: "dev.azure.com",
},
}, models.PullRequest{
Num: 1,
})
Ok(t, err)
Equals(t, c.expApproved, approvalStatus.IsApproved)
})
}
}
func TestAzureDevopsClient_GetPullRequest(t *testing.T) {
// Use a real Azure DevOps json response and edit the mergeable_state field.
jsBytes, err := os.ReadFile("fixtures/azuredevops-pr.json")
Ok(t, err)
response := string(jsBytes)
t.Run("get pull request", func(t *testing.T) {
testServer := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
w.Write([]byte(response)) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
Ok(t, err)
defer disableSSLVerification()()
_, err = client.GetPullRequest(models.Repo{
FullName: "owner/project/repo",
Owner: "owner",
Name: "repo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.AzureDevops,
Hostname: "dev.azure.com",
},
}, 1)
Ok(t, err)
})
}
func TestAzureDevopsClient_MarkdownPullLink(t *testing.T) {
client, err := vcs.NewAzureDevopsClient("hostname", "user", "token")
Ok(t, err)
pull := models.PullRequest{Num: 1}
s, _ := client.MarkdownPullLink(pull)
exp := "!1"
Equals(t, exp, s)
}
var adMergeSuccess = `{
"status": "completed",
"mergeStatus": "succeeded",
"autoCompleteSetBy": {
"id": "54d125f7-69f7-4191-904f-c5b96b6261c8",
"displayName": "Jamal Hartnett",
"uniqueName": "fabrikamfiber4@hotmail.com",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/54d125f7-69f7-4191-904f-c5b96b6261c8",
"imageUrl": "https://dev.azure.com/fabrikam/DefaultCollection/_api/_common/identityImage?id=54d125f7-69f7-4191-904f-c5b96b6261c8"
},
"pullRequestId": 22,
"completionOptions": {
"bypassPolicy":false,
"bypassReason":"",
"deleteSourceBranch":false,
"mergeCommitMessage":"TEST MERGE COMMIT MESSAGE",
"mergeStrategy":"noFastForward",
"squashMerge":false,
"transitionWorkItems":true,
"triggeredByAutoComplete":false
}
}
`
func TestAzureDevopsClient_GitStatusContextFromSrc(t *testing.T) {
cases := []struct {
src string
expGenre string
expName string
}{
{
"atlantis/plan",
"Atlantis Bot/atlantis",
"plan",
},
{
"atlantis/foo/bar/biz/baz",
"Atlantis Bot/atlantis/foo/bar/biz",
"baz",
},
{
"foo",
"Atlantis Bot",
"foo",
},
{
"",
"Atlantis Bot",
"",
},
}
for _, c := range cases {
result := vcs.GitStatusContextFromSrc(c.src)
Equals(t, &c.expName, result.Name)
Equals(t, &c.expGenre, result.Genre)
}
}