Files
atlantis/server/controllers/events/events_controller_test.go
renovate[bot] b3b3f5ab43 fix(deps): update module github.com/google/go-github/v68 to v71 in go.mod (main) (#5522)
Signed-off-by: RB <7775707+nitrocode@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: RB <7775707+nitrocode@users.noreply.github.com>
2025-04-11 01:52:03 -05:00

1031 lines
41 KiB
Go

// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package events_test
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/drmaxgit/go-azuredevops/azuredevops"
"github.com/google/go-github/v71/github"
. "github.com/petergtz/pegomock/v4"
events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
"github.com/runatlantis/atlantis/server/controllers/events/mocks"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
emocks "github.com/runatlantis/atlantis/server/events/mocks"
"github.com/runatlantis/atlantis/server/events/models"
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
gitlab "gitlab.com/gitlab-org/api/client-go"
)
const githubHeader = "X-Github-Event"
const giteaHeader = "X-Gitea-Event"
const gitlabHeader = "X-Gitlab-Event"
const azuredevopsHeader = "Request-Id"
var user = []byte("user")
var secret = []byte("secret")
func TestPost_NotGithubOrGitlab(t *testing.T) {
t.Log("when the request is not for gitlab or github a 400 is returned")
e, _, _, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Ignoring request")
}
func TestPost_UnsupportedVCSGithub(t *testing.T) {
t.Log("when the request is for an unsupported vcs a 400 is returned")
e, _, _, _, _, _, _, _, _ := setup(t)
e.SupportedVCSHosts = nil
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "value")
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Ignoring request since not configured to support GitHub")
}
func TestPost_UnsupportedVCSGitea(t *testing.T) {
t.Log("when the request is for an unsupported vcs a 400 is returned")
e, _, _, _, _, _, _, _, _ := setup(t)
e.SupportedVCSHosts = nil
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(giteaHeader, "value")
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Ignoring request since not configured to support Gitea")
}
func TestPost_UnsupportedVCSGitlab(t *testing.T) {
t.Log("when the request is for an unsupported vcs a 400 is returned")
e, _, _, _, _, _, _, _, _ := setup(t)
e.SupportedVCSHosts = nil
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Ignoring request since not configured to support GitLab")
}
func TestPost_InvalidGithubSecret(t *testing.T) {
t.Log("when the github payload can't be validated a 400 is returned")
e, v, _, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "value")
When(v.Validate(req, secret)).ThenReturn(nil, errors.New("err"))
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "err")
}
func TestPost_InvalidGiteaSecret(t *testing.T) {
t.Log("when the gitea payload can't be validated a 400 is returned")
e, v, _, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(giteaHeader, "value")
When(v.Validate(req, secret)).ThenReturn(nil, errors.New("err"))
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "request did not pass validation")
}
func TestPost_InvalidGitlabSecret(t *testing.T) {
t.Log("when the gitlab payload can't be validated a 400 is returned")
e, _, gl, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(nil, errors.New("err"))
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "err")
}
func TestPost_UnsupportedGithubEvent(t *testing.T) {
t.Log("when the event type is an unsupported github event we ignore it")
e, v, _, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "value")
When(v.Validate(req, nil)).ThenReturn([]byte(`{"not an event": ""}`), nil)
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring unsupported event")
}
func TestPost_UnsupportedGiteaEvent(t *testing.T) {
t.Log("when the event type is an unsupported gitea event we ignore it")
e, v, _, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(giteaHeader, "value")
e.GiteaWebhookSecret = nil
When(v.Validate(req, nil)).ThenReturn([]byte(`{"not an event": ""}`), nil)
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring unsupported Gitea event")
}
func TestPost_UnsupportedGitlabEvent(t *testing.T) {
t.Log("when the event type is an unsupported gitlab event we ignore it")
e, _, gl, _, _, _, _, _, _ := setup(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn([]byte(`{"not an event": ""}`), nil)
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring unsupported event")
}
// Test that if the comment comes from a commit rather than a merge request,
// we give an error and ignore it.
func TestPost_GitlabCommentOnCommit(t *testing.T) {
e, _, gl, _, _, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
w := httptest.NewRecorder()
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.CommitCommentEvent{}, nil)
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring comment on commit event")
}
func TestPost_GithubCommentNotCreated(t *testing.T) {
t.Log("when the event is a github comment but it's not a created event we ignore it")
e, v, _, _, _, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
// comment action is deleted, not created
event := `{"action": "deleted"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring comment event since action was not created")
}
func TestPost_GithubInvalidComment(t *testing.T) {
t.Log("when the event is a github comment without all expected data we return a 400")
e, v, _, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
event := `{"action": "created"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
When(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(models.Repo{}, models.User{}, 1, errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Failed parsing event")
}
func TestPost_GitlabCommentInvalidCommand(t *testing.T) {
t.Log("when the event is a gitlab comment with an invalid command we ignore it")
e, _, gl, _, _, _, _, _, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)
When(cp.Parse("", models.Gitlab)).ThenReturn(events.CommentParseResult{Ignore: true})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring non-command comment: \"\"")
}
func TestPost_GithubCommentInvalidCommand(t *testing.T) {
t.Log("when the event is a github comment with an invalid command we ignore it")
e, v, _, _, p, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
event := `{"action": "created"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
When(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(models.Repo{}, models.User{}, 1, nil)
When(cp.Parse("", models.Github)).ThenReturn(events.CommentParseResult{Ignore: true})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring non-command comment: \"\"")
vcsClient.VerifyWasCalled(Never()).ReactToComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(1), Eq(int64(1)), Eq("eyes"))
}
func TestPost_GitlabCommentNotAllowlisted(t *testing.T) {
t.Log("when the event is a gitlab comment from a repo that isn't allowlisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logger,
Scope: scope,
CommentParser: &events.CommentParser{ExecutableName: "atlantis"},
GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Gitlab},
RepoAllowlistChecker: &events.RepoAllowlistChecker{},
VCSClient: vcsClient,
}
requestJSON, err := os.ReadFile(filepath.Join("testdata", "gitlabMergeCommentEvent_notAllowlisted.json"))
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(requestJSON))
req.Header.Set(gitlabHeader, "Note Hook")
w := httptest.NewRecorder()
e.Post(w, req)
resp := w.Result()
defer resp.Body.Close()
Equals(t, http.StatusForbidden, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
exp := "Repo not allowlisted"
Assert(t, strings.Contains(string(body), exp), "exp %q to be contained in %q", exp, string(body))
expRepo, _ := models.NewRepo(models.Gitlab, "gitlabhq/gitlab-test", "https://example.com/gitlabhq/gitlab-test.git", "", "")
vcsClient.VerifyWasCalledOnce().CreateComment(
Any[logging.SimpleLogging](), Eq(expRepo), Eq(1), Eq("```\nError: This repo is not allowlisted for Atlantis.\n```"), Eq(""))
}
func TestPost_GitlabCommentNotAllowlistedWithSilenceErrors(t *testing.T) {
t.Log("when the event is a gitlab comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logger,
Scope: scope,
CommentParser: &events.CommentParser{ExecutableName: "atlantis"},
GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Gitlab},
RepoAllowlistChecker: &events.RepoAllowlistChecker{},
VCSClient: vcsClient,
SilenceAllowlistErrors: true,
}
requestJSON, err := os.ReadFile(filepath.Join("testdata", "gitlabMergeCommentEvent_notAllowlisted.json"))
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(requestJSON))
req.Header.Set(gitlabHeader, "Note Hook")
w := httptest.NewRecorder()
e.Post(w, req)
resp := w.Result()
defer resp.Body.Close()
Equals(t, http.StatusForbidden, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
exp := "Repo not allowlisted"
Assert(t, strings.Contains(string(body), exp), "exp %q to be contained in %q", exp, string(body))
vcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())
}
func TestPost_GithubCommentNotAllowlisted(t *testing.T) {
t.Log("when the event is a github comment from a repo that isn't allowlisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logger,
Scope: scope,
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
CommentParser: &events.CommentParser{ExecutableName: "atlantis"},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Github},
RepoAllowlistChecker: &events.RepoAllowlistChecker{},
VCSClient: vcsClient,
}
requestJSON, err := os.ReadFile(filepath.Join("testdata", "githubIssueCommentEvent_notAllowlisted.json"))
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(githubHeader, "issue_comment")
w := httptest.NewRecorder()
e.Post(w, req)
resp := w.Result()
defer resp.Body.Close()
Equals(t, http.StatusForbidden, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
exp := "Repo not allowlisted"
Assert(t, strings.Contains(string(body), exp), "exp %q to be contained in %q", exp, string(body))
expRepo, _ := models.NewRepo(models.Github, "baxterthehacker/public-repo", "https://github.com/baxterthehacker/public-repo.git", "", "")
vcsClient.VerifyWasCalledOnce().CreateComment(
Any[logging.SimpleLogging](), Eq(expRepo), Eq(2), Eq("```\nError: This repo is not allowlisted for Atlantis.\n```"), Eq(""))
}
func TestPost_GithubCommentNotAllowlistedWithSilenceErrors(t *testing.T) {
t.Log("when the event is a github comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logger,
Scope: scope,
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
CommentParser: &events.CommentParser{ExecutableName: "atlantis"},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Github},
RepoAllowlistChecker: &events.RepoAllowlistChecker{},
VCSClient: vcsClient,
SilenceAllowlistErrors: true,
}
requestJSON, err := os.ReadFile(filepath.Join("testdata", "githubIssueCommentEvent_notAllowlisted.json"))
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(githubHeader, "issue_comment")
w := httptest.NewRecorder()
e.Post(w, req)
resp := w.Result()
defer resp.Body.Close()
Equals(t, http.StatusForbidden, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
exp := "Repo not allowlisted"
Assert(t, strings.Contains(string(body), exp), "exp %q to be contained in %q", exp, string(body))
vcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())
}
func TestPost_GitlabCommentResponse(t *testing.T) {
// When the event is a gitlab comment that warrants a comment response we comment back.
e, _, gl, _, _, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)
When(cp.Parse("", models.Gitlab)).ThenReturn(events.CommentParseResult{CommentResponse: "a comment"})
w := httptest.NewRecorder()
e.Post(w, req)
vcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(0), Eq("a comment"), Eq(""))
ResponseContains(t, w, http.StatusOK, "Commenting back on pull request")
}
func TestPost_GithubCommentResponse(t *testing.T) {
t.Log("when the event is a github comment that warrants a comment response we comment back")
e, v, _, _, p, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
event := `{"action": "created"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
baseRepo := models.Repo{}
user := models.User{}
When(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)
When(cp.Parse("", models.Github)).ThenReturn(events.CommentParseResult{CommentResponse: "a comment"})
w := httptest.NewRecorder()
e.Post(w, req)
vcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(baseRepo), Eq(1), Eq("a comment"), Eq(""))
ResponseContains(t, w, http.StatusOK, "Commenting back on pull request")
}
func TestPost_GitlabCommentSuccess(t *testing.T) {
t.Log("when the event is a gitlab comment with a valid command we call the command handler")
e, _, gl, _, _, cr, _, _, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
cmd := events.CommentCommand{}
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)
When(cp.Parse(Any[string](), Eq(models.Gitlab))).ThenReturn(events.CommentParseResult{Command: &cmd})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
cr.VerifyWasCalledOnce().RunCommentCommand(models.Repo{}, &models.Repo{}, nil, models.User{}, 0, &cmd)
}
func TestPost_GithubCommentSuccess(t *testing.T) {
t.Log("when the event is a github comment with a valid command we call the command handler")
e, v, _, _, p, cr, _, _, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
event := `{"action": "created"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
baseRepo := models.Repo{}
user := models.User{}
cmd := events.CommentCommand{}
When(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)
When(cp.Parse("", models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
cr.VerifyWasCalledOnce().RunCommentCommand(baseRepo, nil, nil, user, 1, &cmd)
}
func TestPost_GithubCommentReaction(t *testing.T) {
t.Log("when the event is a github comment with a valid command we call the ReactToComment handler")
e, v, _, _, p, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
testComment := "atlantis plan"
event := fmt.Sprintf(`{"action": "created", "comment": {"body": "%v", "id": 1}}`, testComment)
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
baseRepo := models.Repo{}
user := models.User{}
cmd := events.CommentCommand{Name: command.Plan}
When(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)
When(cp.Parse(testComment, models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
vcsClient.VerifyWasCalledOnce().ReactToComment(Any[logging.SimpleLogging](), Eq(baseRepo), Eq(1), Eq(int64(1)), Eq("eyes"))
}
func TestPost_GilabCommentReaction(t *testing.T) {
t.Log("when the event is a gitlab comment with a valid command we call the ReactToComment handler")
e, _, gl, _, _, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
cmd := events.CommentCommand{}
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)
When(cp.Parse(Any[string](), Eq(models.Gitlab))).ThenReturn(events.CommentParseResult{Command: &cmd})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
vcsClient.VerifyWasCalledOnce().ReactToComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(0), Eq(int64(0)), Eq("eyes"))
}
func TestPost_GithubPullRequestInvalid(t *testing.T) {
t.Log("when the event is a github pull request with invalid data we return a 400")
e, v, _, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "pull_request")
event := `{"action": "closed"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
When(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(models.PullRequest{}, models.OpenedPullEvent, models.Repo{}, models.Repo{}, models.User{}, errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Error parsing pull data: err")
}
func TestPost_GitlabMergeRequestInvalid(t *testing.T) {
t.Log("when the event is a gitlab merge request with invalid data we return a 400")
e, _, gl, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusBadRequest, "Error parsing webhook: err")
}
func TestPost_GithubPullRequestNotAllowlisted(t *testing.T) {
t.Log("when the event is a github pull request to a non-allowlisted repo we return a 400")
e, v, _, _, _, _, _, _, _ := setup(t)
var err error
e.RepoAllowlistChecker, err = events.NewRepoAllowlistChecker("github.com/nevermatch")
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "pull_request")
event := `{"action": "closed"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusForbidden, "Pull request event from non-allowlisted repo")
}
func TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) {
t.Log("when the event is a gitlab merge request to a non-allowlisted repo we return a 400")
e, _, gl, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
var err error
e.RepoAllowlistChecker, err = events.NewRepoAllowlistChecker("github.com/nevermatch")
Ok(t, err)
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusForbidden, "Pull request event from non-allowlisted repo")
}
func TestPost_GithubPullRequestUnsupportedAction(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
e, v, _, _, _, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "pull_request")
event := `{"action": "unsupported"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
w := httptest.NewRecorder()
e.Parser = &events.EventParser{}
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring non-actionable pull request event")
}
func TestPost_GitlabMergeRequestUnsupportedAction(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
t.Log("when the event is a gitlab merge request to a non-allowlisted repo we return a 400")
e, _, gl, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
var event gitlab.MergeEvent
event.ObjectAttributes.Action = "unsupported"
When(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, repo, repo, models.User{}, nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring non-actionable pull request event")
}
func TestPost_AzureDevopsPullRequestIgnoreEvent(t *testing.T) {
t.Log("when the event is an azure devops pull request update that should not trigger workflow we ignore it")
e, _, _, ado, _, _, _, _, _ := setup(t)
event := `{
"subscriptionId": "11111111-1111-1111-1111-111111111111",
"notificationId": 1,
"id": "22222222-2222-2222-2222-222222222222",
"eventType": "git.pullrequest.updated",
"publisherId": "tfs",
"message": {
"text": "Dev %s pull request 1 (Name in repo)"
},
"resource": {}}`
cases := []struct {
message string
}{
{
"has changed the reviewer list on",
},
{
"has approved",
},
{
"has approved and left suggestions on",
},
{
"is waiting for the author on",
},
{
"rejected",
},
{
"voted on",
},
}
for _, c := range cases {
t.Run(c.message, func(t *testing.T) {
payload := fmt.Sprintf(event, c.message)
req, _ := http.NewRequest("GET", "", strings.NewReader(payload))
req.Header.Set(azuredevopsHeader, "reqID")
When(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)
w := httptest.NewRecorder()
e.Parser = &events.EventParser{}
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "pull request updated event is not a supported type")
})
}
}
func TestPost_AzureDevopsPullRequestDeletedCommentIgnoreEvent(t *testing.T) {
t.Log("when the event is an azure devops pull request deleted comment event we ignore it")
e, _, _, ado, _, _, _, _, _ := setup(t)
payload := `{
"subscriptionId": "11111111-1111-1111-1111-111111111111",
"notificationId": 1,
"id": "22222222-2222-2222-2222-222222222222",
"eventType": "ms.vss-code.git-pullrequest-comment-event",
"publisherId": "tfs",
"message": {
"text": "Dev has deleted a pull request comment"
},
"resource": {
"comment": {
"id": 1,
"isDeleted": true,
"commentType": "text"
}
}
}`
t.Run("Dev has deleted a pull request comment", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", strings.NewReader(payload))
req.Header.Set(azuredevopsHeader, "reqID")
When(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)
w := httptest.NewRecorder()
e.Parser = &events.EventParser{}
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring comment event since it is linked to deleting a pull request comment")
})
}
func TestPost_AzureDevopsPullRequestCommentWebhookTestIgnoreEvent(t *testing.T) {
t.Log("when the event is an azure devops webhook test we ignore it")
e, _, _, ado, _, _, _, _, _ := setup(t)
event := `{
"subscriptionId": "11111111-1111-1111-1111-111111111111",
"notificationId": 1,
"id": "22222222-2222-2222-2222-222222222222",
"eventType": "%s",
"publisherId": "tfs",
"message": {
"text": "%s"
},
"resource": {
"pullRequest": {
"repository":{
"url": "https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079"
}
},
"comment": {
"content": "This is my comment."
}
}}`
cases := []struct {
eventType string
message string
}{
{
"ms.vss-code.git-pullrequest-comment-event",
"Jamal Hartnett has edited a pull request comment",
},
}
for _, c := range cases {
t.Run(c.message, func(t *testing.T) {
payload := fmt.Sprintf(event, c.eventType, c.message)
req, _ := http.NewRequest("GET", "", strings.NewReader(payload))
req.Header.Set(azuredevopsHeader, "reqID")
When(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)
w := httptest.NewRecorder()
e.Parser = &events.EventParser{}
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring Azure DevOps Test Event with Repo URL")
})
}
}
func TestPost_AzureDevopsPullRequestWebhookTestIgnoreEvent(t *testing.T) {
t.Log("when the event is an azure devops webhook tests we ignore it")
e, _, _, ado, _, _, _, _, _ := setup(t)
event := `{
"subscriptionId": "11111111-1111-1111-1111-111111111111",
"notificationId": 1,
"id": "22222222-2222-2222-2222-222222222222",
"eventType": "%s",
"publisherId": "tfs",
"message": {
"text": "%s"
},
"resource": {
"repository":{
"url": "https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079"
}
}}`
cases := []struct {
eventType string
message string
}{
{
"git.pullrequest.created",
"Jamal Hartnett created a new pull request",
},
{
"git.pullrequest.updated",
"Jamal Hartnett marked the pull request as completed",
},
}
for _, c := range cases {
t.Run(c.message, func(t *testing.T) {
payload := fmt.Sprintf(event, c.eventType, c.message)
req, _ := http.NewRequest("GET", "", strings.NewReader(payload))
req.Header.Set(azuredevopsHeader, "reqID")
When(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)
w := httptest.NewRecorder()
e.Parser = &events.EventParser{}
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Ignoring Azure DevOps Test Event with Repo URL")
})
}
}
func TestPost_AzureDevopsPullRequestCommentPassingIgnores(t *testing.T) {
t.Log("when the event should not be ignored it should pass through all ignore statements without error")
e, _, _, ado, _, _, _, _, cp := setup(t)
testComment := "atlantis plan"
repo := models.Repo{}
cmd := events.CommentCommand{Name: command.Plan}
When(e.Parser.ParseAzureDevopsRepo(Any[*azuredevops.GitRepository]())).ThenReturn(repo, nil)
When(cp.Parse(testComment, models.AzureDevops)).ThenReturn(events.CommentParseResult{Command: &cmd})
payload := fmt.Sprintf(`{
"subscriptionId": "11111111-1111-1111-1111-111111111111",
"notificationId": 1,
"id": "22222222-2222-2222-2222-222222222222",
"eventType": "ms.vss-code.git-pullrequest-comment-event",
"publisherId": "tfs",
"message": {
"text": "Testing to see if comment passes ignore conditions"
},
"resource": {
"comment": {
"id": 1,
"commentType": "text",
"content": "%v"
},
"pullRequest": {
"pullRequestId": 1,
"repository": {}
}
}
}`, testComment)
t.Run("Testing to see if comment passes ignore conditions", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", strings.NewReader(payload))
req.Header.Set(azuredevopsHeader, "reqID")
When(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
})
}
func TestPost_GithubPullRequestClosedErrCleaningPull(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
t.Log("when the event is a closed pull request and we have an error calling CleanUpPull we return a 503")
RegisterMockTestingT(t)
e, v, _, _, p, _, c, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "pull_request")
event := `{"action": "closed"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
repo := models.Repo{}
pull := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pull, models.OpenedPullEvent, repo, repo, models.User{}, nil)
When(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pull)).ThenReturn(errors.New("cleanup err"))
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusInternalServerError, "Error cleaning pull request: cleanup err")
}
func TestPost_GitlabMergeRequestClosedErrCleaningPull(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
t.Log("when the event is a closed gitlab merge request and an error occurs calling CleanUpPull we return a 500")
e, _, gl, _, p, _, c, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
var event gitlab.MergeEvent
event.ObjectAttributes.Action = "close"
When(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)
When(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pullRequest)).ThenReturn(errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusInternalServerError, "Error cleaning pull request: err")
}
func TestPost_GithubClosedPullRequestSuccess(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
t.Log("when the event is a pull request and everything works we return a 200")
e, v, _, _, p, _, c, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "pull_request")
event := `{"action": "closed"}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
repo := models.Repo{}
pull := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pull, models.OpenedPullEvent, repo, repo, models.User{}, nil)
When(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pull)).ThenReturn(nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Pull request cleaned successfully")
}
func TestPost_GitlabMergeRequestSuccess(t *testing.T) {
t.Skip("relies too much on mocks, should use real event parser")
t.Log("when the event is a gitlab merge request and the cleanup works we return a 200")
e, _, gl, _, p, _, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Pull request cleaned successfully")
}
// Test Bitbucket server pull closed events.
func TestPost_BBServerPullClosed(t *testing.T) {
cases := []struct {
header string
}{
{
"pr:deleted",
},
{
"pr:merged",
},
{
"pr:declined",
},
}
for _, c := range cases {
t.Run(c.header, func(t *testing.T) {
RegisterMockTestingT(t)
pullCleaner := emocks.NewMockPullCleaner()
allowlist, err := events.NewRepoAllowlistChecker("*")
Ok(t, err)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
ec := &events_controllers.VCSEventsController{
PullCleaner: pullCleaner,
Parser: &events.EventParser{
BitbucketUser: "bb-user",
BitbucketToken: "bb-token",
BitbucketServerURL: "https://bbserver.com",
},
RepoAllowlistChecker: allowlist,
SupportedVCSHosts: []models.VCSHostType{models.BitbucketServer},
VCSClient: nil,
Logger: logger,
Scope: scope,
}
// Build HTTP request.
requestBytes, err := os.ReadFile(filepath.Join("testdata", "bb-server-pull-deleted-event.json"))
// Replace the eventKey field with our event type.
requestJSON := strings.Replace(string(requestBytes), `"eventKey":"pr:deleted",`, fmt.Sprintf(`"eventKey":"%s",`, c.header), -1)
Ok(t, err)
req, err := http.NewRequest("POST", "/events", bytes.NewBuffer([]byte(requestJSON)))
Ok(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Event-Key", c.header)
req.Header.Set("X-Request-ID", "request-id")
// Send the request.
w := httptest.NewRecorder()
ec.Post(w, req)
// Make our assertions.
ResponseContains(t, w, 200, "Pull request cleaned successfully")
expRepo := models.Repo{
FullName: "project/repository",
Owner: "project",
Name: "repository",
CloneURL: "https://bb-user:bb-token@bbserver.com/scm/proj/repository.git",
SanitizedCloneURL: "https://bb-user:<redacted>@bbserver.com/scm/proj/repository.git",
VCSHost: models.VCSHost{
Hostname: "bbserver.com",
Type: models.BitbucketServer,
},
}
pullCleaner.VerifyWasCalledOnce().CleanUpPull(
logger,
expRepo, models.PullRequest{
Num: 10,
HeadCommit: "2d9fb6b9a46eafb1dcef7b008d1a429d45ca742c",
URL: "https://bbserver.com/projects/PROJ/repos/repository/pull-requests/10",
HeadBranch: "decline-me",
BaseBranch: "main",
Author: "admin",
State: models.OpenPullState,
BaseRepo: expRepo,
})
})
}
}
func TestPost_PullOpenedOrUpdated(t *testing.T) {
cases := []struct {
Description string
HostType models.VCSHostType
Action string
}{
{
"github opened",
models.Github,
"opened",
},
{
"gitlab opened",
models.Gitlab,
"open",
},
{
"github synchronized",
models.Github,
"synchronize",
},
{
"gitlab update",
models.Gitlab,
"update",
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
e, v, gl, _, p, cr, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
var pullRequest models.PullRequest
var repo models.Repo
switch c.HostType {
case models.Gitlab:
req.Header.Set(gitlabHeader, "value")
var event gitlab.MergeEvent
event.ObjectAttributes.Action = c.Action
When(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)
repo = models.Repo{}
pullRequest = models.PullRequest{State: models.ClosedPullState}
When(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)
case models.Github:
req.Header.Set(githubHeader, "pull_request")
event := fmt.Sprintf(`{"action": "%s"}`, c.Action)
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
repo = models.Repo{}
pullRequest = models.PullRequest{State: models.ClosedPullState}
When(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)
}
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")
cr.VerifyWasCalledOnce().RunAutoplanCommand(models.Repo{}, models.Repo{}, models.PullRequest{State: models.ClosedPullState}, models.User{})
})
}
}
func setup(t *testing.T) (events_controllers.VCSEventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *mocks.MockAzureDevopsRequestValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) {
RegisterMockTestingT(t)
v := mocks.NewMockGithubRequestValidator()
gl := mocks.NewMockGitlabRequestParserValidator()
ado := mocks.NewMockAzureDevopsRequestValidator()
p := emocks.NewMockEventParsing()
cp := emocks.NewMockCommentParsing()
cr := emocks.NewMockCommandRunner()
c := emocks.NewMockPullCleaner()
vcsmock := vcsmocks.NewMockClient()
repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*")
Ok(t, err)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
ExecutableName: "atlantis",
EmojiReaction: "eyes",
TestingMode: true,
Logger: logger,
Scope: scope,
ApplyDisabled: false,
AzureDevopsWebhookBasicUser: user,
AzureDevopsWebhookBasicPassword: secret,
AzureDevopsRequestValidator: ado,
GithubRequestValidator: v,
Parser: p,
CommentParser: cp,
CommandRunner: cr,
PullCleaner: c,
GithubWebhookSecret: secret,
SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab, models.AzureDevops, models.Gitea},
GiteaWebhookSecret: secret,
GitlabWebhookSecret: secret,
GitlabRequestParserValidator: gl,
RepoAllowlistChecker: repoAllowlistChecker,
VCSClient: vcsmock,
}
return e, v, gl, ado, p, cr, c, vcsmock, cp
}