Files
atlantis/server/events_controller_test.go
2018-06-15 16:16:31 +01:00

491 lines
21 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 server_test
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/lkysow/go-gitlab"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server"
"github.com/runatlantis/atlantis/server/events"
emocks "github.com/runatlantis/atlantis/server/events/mocks"
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
"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/mocks"
. "github.com/runatlantis/atlantis/testing"
)
const githubHeader = "X-Github-Event"
const gitlabHeader = "X-Gitlab-Event"
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_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_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.Validate(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_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.Validate(req, secret)).ThenReturn([]byte(`{"not an event": ""}`), nil)
e.Post(w, req)
responseContains(t, w, http.StatusOK, "Ignoring unsupported 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(matchers.AnyPtrToGithubIssueCommentEvent())).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.Validate(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, _, _, _, 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(matchers.AnyPtrToGithubIssueCommentEvent())).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: \"\"")
}
func TestPost_GitlabCommentNotWhitelisted(t *testing.T) {
t.Log("when the event is a gitlab comment from a repo that isn't whitelisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClientProxy()
e := server.EventsController{
Logger: logging.NewNoopLogger(),
CommentParser: &events.CommentParser{},
GitlabRequestParser: &server.DefaultGitlabRequestParser{},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Gitlab},
RepoWhitelist: &events.RepoWhitelist{},
VCSClient: vcsClient,
}
requestJSON, err := ioutil.ReadFile(filepath.Join("testfixtures", "gitlabMergeCommentEvent_notWhitelisted.json"))
Ok(t, err)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(requestJSON))
req.Header.Set(gitlabHeader, "Note Hook")
w := httptest.NewRecorder()
e.Post(w, req)
Equals(t, http.StatusForbidden, w.Result().StatusCode)
body, _ := ioutil.ReadAll(w.Result().Body)
exp := "Repo not whitelisted"
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(expRepo, 1, "```\nError: This repo is not whitelisted for Atlantis.\n```")
}
func TestPost_GithubCommentNotWhitelisted(t *testing.T) {
t.Log("when the event is a github comment from a repo that isn't whitelisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClientProxy()
e := server.EventsController{
Logger: logging.NewNoopLogger(),
GithubRequestValidator: &server.DefaultGithubRequestValidator{},
CommentParser: &events.CommentParser{},
Parser: &events.EventParser{},
SupportedVCSHosts: []models.VCSHostType{models.Github},
RepoWhitelist: &events.RepoWhitelist{},
VCSClient: vcsClient,
}
requestJSON, err := ioutil.ReadFile(filepath.Join("testfixtures", "githubIssueCommentEvent_notWhitelisted.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)
Equals(t, http.StatusForbidden, w.Result().StatusCode)
body, _ := ioutil.ReadAll(w.Result().Body)
exp := "Repo not whitelisted"
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(expRepo, 2, "```\nError: This repo is not whitelisted for Atlantis.\n```")
}
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.Validate(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(models.Repo{}, 0, "a comment")
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(matchers.AnyPtrToGithubIssueCommentEvent())).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(baseRepo, 1, "a comment")
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, _, _, _ := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(gitlabHeader, "value")
When(gl.Validate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)
w := httptest.NewRecorder()
e.Post(w, req)
responseContains(t, w, http.StatusOK, "Processing...")
// wait for 200ms so goroutine is called
time.Sleep(200 * time.Millisecond)
cr.VerifyWasCalledOnce().ExecuteCommand(models.Repo{}, models.Repo{}, models.User{}, 0, nil)
}
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.Command{}
When(p.ParseGithubIssueCommentEvent(matchers.AnyPtrToGithubIssueCommentEvent())).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...")
// wait for 200ms so goroutine is called
time.Sleep(200 * time.Millisecond)
cr.VerifyWasCalledOnce().ExecuteCommand(baseRepo, baseRepo, user, 1, &cmd)
}
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.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
responseContains(t, w, http.StatusBadRequest, "Error parsing pull data: err")
}
func TestPost_GithubPullRequestInvalidRepo(t *testing.T) {
t.Log("when the event is a github pull request with invalid repo 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.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, nil)
When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(models.Repo{}, errors.New("err"))
w := httptest.NewRecorder()
e.Post(w, req)
responseContains(t, w, http.StatusBadRequest, "Error parsing repo data: err")
}
func TestPost_GithubPullRequestNotWhitelisted(t *testing.T) {
t.Log("when the event is a github pull request to a non-whitelisted repo we return a 400")
e, v, _, p, _, _, _, _ := setup(t)
e.RepoWhitelist = &events.RepoWhitelist{Whitelist: "github.com/nevermatch"}
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.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, nil)
When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(models.Repo{}, nil)
w := httptest.NewRecorder()
e.Post(w, req)
responseContains(t, w, http.StatusForbidden, "Ignoring pull request event from non-whitelisted repo")
}
func TestPost_GithubPullRequestErrCleaningPull(t *testing.T) {
t.Log("when the event is a 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.Closed}
When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(pull, repo, nil)
When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(repo, nil)
When(c.CleanUpPull(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_GitlabMergeRequestErrCleaningPull(t *testing.T) {
t.Log("when the event is a 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")
When(gl.Validate(req, secret)).ThenReturn(gitlabMergeEvent, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.Closed}
When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, nil)
When(c.CleanUpPull(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_GithubPullRequestSuccess(t *testing.T) {
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.Closed}
When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(pull, repo, nil)
When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(repo, nil)
When(c.CleanUpPull(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.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.Validate(req, secret)).ThenReturn(gitlabMergeEvent, nil)
repo := models.Repo{}
pullRequest := models.PullRequest{State: models.Closed}
When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, nil)
w := httptest.NewRecorder()
e.Post(w, req)
responseContains(t, w, http.StatusOK, "Pull request cleaned successfully")
}
func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParser, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClientProxy, *emocks.MockCommentParsing) {
RegisterMockTestingT(t)
v := mocks.NewMockGithubRequestValidator()
gl := mocks.NewMockGitlabRequestParser()
p := emocks.NewMockEventParsing()
cp := emocks.NewMockCommentParsing()
cr := emocks.NewMockCommandRunner()
c := emocks.NewMockPullCleaner()
vcsmock := vcsmocks.NewMockClientProxy()
e := server.EventsController{
Logger: logging.NewNoopLogger(),
GithubRequestValidator: v,
Parser: p,
CommentParser: cp,
CommandRunner: cr,
PullCleaner: c,
GithubWebHookSecret: secret,
SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab},
GitlabWebHookSecret: secret,
GitlabRequestParser: gl,
RepoWhitelist: &events.RepoWhitelist{
Whitelist: "*",
},
VCSClient: vcsmock,
}
return e, v, gl, p, cr, c, vcsmock, cp
}
var gitlabMergeEvent = gitlab.MergeEvent{
ObjectAttributes: struct {
ID int `json:"id"`
TargetBranch string `json:"target_branch"`
SourceBranch string `json:"source_branch"`
SourceProjectID int `json:"source_project_id"`
AuthorID int `json:"author_id"`
AssigneeID int `json:"assignee_id"`
Title string `json:"title"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
StCommits []*gitlab.Commit `json:"st_commits"`
StDiffs []*gitlab.Diff `json:"st_diffs"`
MilestoneID int `json:"milestone_id"`
State string `json:"state"`
MergeStatus string `json:"merge_status"`
TargetProjectID int `json:"target_project_id"`
IID int `json:"iid"`
Description string `json:"description"`
Position int `json:"position"`
LockedAt string `json:"locked_at"`
UpdatedByID int `json:"updated_by_id"`
MergeError string `json:"merge_error"`
MergeParams struct {
ForceRemoveSourceBranch string `json:"force_remove_source_branch"`
} `json:"merge_params"`
MergeWhenBuildSucceeds bool `json:"merge_when_build_succeeds"`
MergeUserID int `json:"merge_user_id"`
MergeCommitSha string `json:"merge_commit_sha"`
DeletedAt string `json:"deleted_at"`
ApprovalsBeforeMerge string `json:"approvals_before_merge"`
RebaseCommitSha string `json:"rebase_commit_sha"`
InProgressMergeCommitSha string `json:"in_progress_merge_commit_sha"`
LockVersion int `json:"lock_version"`
TimeEstimate int `json:"time_estimate"`
Source *gitlab.Repository `json:"source"`
Target *gitlab.Repository `json:"target"`
LastCommit struct {
ID string `json:"id"`
Message string `json:"message"`
Timestamp *time.Time `json:"timestamp"`
URL string `json:"url"`
Author *gitlab.Author `json:"author"`
} `json:"last_commit"`
WorkInProgress bool `json:"work_in_progress"`
URL string `json:"url"`
Action string `json:"action"`
Assignee struct {
Name string `json:"name"`
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
} `json:"assignee"`
}{
Action: "merge",
},
}