mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-01 23:48:58 +00:00
* feat: add userConfig.VCSStatusName to applyCommandRunner context * fix: use vcsstatusname from context * chore: normalize tests * chore: update interface mock
625 lines
17 KiB
Go
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)
|
|
}
|
|
}
|