// 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 ( "errors" "fmt" "regexp" "strings" "testing" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/logging" "github.com/google/go-github/v31/github" . "github.com/petergtz/pegomock" lockingmocks "github.com/runatlantis/atlantis/server/core/locking/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/mocks" eventmocks "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/mocks/matchers" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/models/fixtures" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" . "github.com/runatlantis/atlantis/testing" ) var projectCommandBuilder *mocks.MockProjectCommandBuilder var projectCommandRunner *mocks.MockProjectCommandRunner var eventParsing *mocks.MockEventParsing var azuredevopsGetter *mocks.MockAzureDevopsPullGetter var githubGetter *mocks.MockGithubPullGetter var gitlabGetter *mocks.MockGitlabMergeRequestGetter var ch events.DefaultCommandRunner var workingDir events.WorkingDir var pendingPlanFinder *mocks.MockPendingPlanFinder var drainer *events.Drainer var deleteLockCommand *mocks.MockDeleteLockCommand var commitUpdater *mocks.MockCommitStatusUpdater // TODO: refactor these into their own unit tests. // these were all split out from default command runner in an effort to improve // readability however the tests were kept as is. var dbUpdater *events.DBUpdater var pullUpdater *events.PullUpdater var autoMerger *events.AutoMerger var policyCheckCommandRunner *events.PolicyCheckCommandRunner var approvePoliciesCommandRunner *events.ApprovePoliciesCommandRunner var planCommandRunner *events.PlanCommandRunner var applyLockChecker *lockingmocks.MockApplyLockChecker var applyCommandRunner *events.ApplyCommandRunner var unlockCommandRunner *events.UnlockCommandRunner var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner var postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner func setup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) projectCommandBuilder = mocks.NewMockProjectCommandBuilder() eventParsing = mocks.NewMockEventParsing() vcsClient := vcsmocks.NewMockClient() githubGetter = mocks.NewMockGithubPullGetter() gitlabGetter = mocks.NewMockGitlabMergeRequestGetter() azuredevopsGetter = mocks.NewMockAzureDevopsPullGetter() logger = logging.NewNoopLogger(t) projectCommandRunner = mocks.NewMockProjectCommandRunner() workingDir = mocks.NewMockWorkingDir() pendingPlanFinder = mocks.NewMockPendingPlanFinder() commitUpdater = mocks.NewMockCommitStatusUpdater() tmp, cleanup := TempDir(t) defer cleanup() defaultBoltDB, err := db.New(tmp) Ok(t, err) drainer = &events.Drainer{} deleteLockCommand = eventmocks.NewMockDeleteLockCommand() applyLockChecker = lockingmocks.NewMockApplyLockChecker() dbUpdater = &events.DBUpdater{ DB: defaultBoltDB, } pullUpdater = &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, MarkdownRenderer: &events.MarkdownRenderer{}, } autoMerger = &events.AutoMerger{ VCSClient: vcsClient, GlobalAutomerge: false, } parallelPoolSize := 1 SilenceNoProjects := false policyCheckCommandRunner = events.NewPolicyCheckCommandRunner( dbUpdater, pullUpdater, commitUpdater, projectCommandRunner, parallelPoolSize, false, ) planCommandRunner = events.NewPlanCommandRunner( false, false, vcsClient, pendingPlanFinder, workingDir, commitUpdater, projectCommandBuilder, projectCommandRunner, dbUpdater, pullUpdater, policyCheckCommandRunner, autoMerger, parallelPoolSize, SilenceNoProjects, defaultBoltDB, ) pullReqStatusFetcher := vcs.NewPullReqStatusFetcher(vcsClient) applyCommandRunner = events.NewApplyCommandRunner( vcsClient, false, applyLockChecker, commitUpdater, projectCommandBuilder, projectCommandRunner, autoMerger, pullUpdater, dbUpdater, defaultBoltDB, parallelPoolSize, SilenceNoProjects, false, pullReqStatusFetcher, ) approvePoliciesCommandRunner = events.NewApprovePoliciesCommandRunner( commitUpdater, projectCommandBuilder, projectCommandRunner, pullUpdater, dbUpdater, SilenceNoProjects, false, ) unlockCommandRunner = events.NewUnlockCommandRunner( deleteLockCommand, vcsClient, SilenceNoProjects, ) versionCommandRunner := events.NewVersionCommandRunner( pullUpdater, projectCommandBuilder, projectCommandRunner, parallelPoolSize, SilenceNoProjects, ) commentCommandRunnerByCmd := map[models.CommandName]events.CommentCommandRunner{ models.PlanCommand: planCommandRunner, models.ApplyCommand: applyCommandRunner, models.ApprovePoliciesCommand: approvePoliciesCommandRunner, models.UnlockCommand: unlockCommandRunner, models.VersionCommand: versionCommandRunner, } preWorkflowHooksCommandRunner = mocks.NewMockPreWorkflowHooksCommandRunner() When(preWorkflowHooksCommandRunner.RunPreHooks(matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil) postWorkflowHooksCommandRunner = mocks.NewMockPostWorkflowHooksCommandRunner() When(postWorkflowHooksCommandRunner.RunPostHooks(matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil) globalCfg := valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{}) ch = events.DefaultCommandRunner{ VCSClient: vcsClient, CommentCommandRunnerByCmd: commentCommandRunnerByCmd, EventParser: eventParsing, GithubPullGetter: githubGetter, GitlabMergeRequestGetter: gitlabGetter, AzureDevopsPullGetter: azuredevopsGetter, Logger: logger, GlobalCfg: globalCfg, AllowForkPRs: false, AllowForkPRsFlag: "allow-fork-prs-flag", Drainer: drainer, PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, PullStatusFetcher: defaultBoltDB, } return vcsClient } func TestRunCommentCommand_LogPanics(t *testing.T) { t.Log("if there is a panic it is commented back on the pull request") vcsClient := setup(t) When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, 1, &events.CommentCommand{Name: models.PlanCommand}) _, _, comment, _ := vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()).GetCapturedArguments() Assert(t, strings.Contains(comment, "Error: goroutine panic"), fmt.Sprintf("comment should be about a goroutine panic but was %q", comment)) } func TestRunCommentCommand_GithubPullErr(t *testing.T) { t.Log("if getting the github pull request fails an error should be logged") vcsClient := setup(t) When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, nil) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "`Error: making pull request API call to GitHub: err`", "") } func TestRunCommentCommand_GitlabMergeRequestErr(t *testing.T) { t.Log("if getting the gitlab merge request fails an error should be logged") vcsClient := setup(t) When(gitlabGetter.GetMergeRequest(fixtures.GitlabRepo.FullName, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) ch.RunCommentCommand(fixtures.GitlabRepo, &fixtures.GitlabRepo, nil, fixtures.User, fixtures.Pull.Num, nil) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GitlabRepo, fixtures.Pull.Num, "`Error: making merge request API call to GitLab: err`", "") } func TestRunCommentCommand_GithubPullParseErr(t *testing.T) { t.Log("if parsing the returned github pull request fails an error should be logged") vcsClient := setup(t) var pull github.PullRequest When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, fixtures.GitlabRepo, errors.New("err")) ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, nil) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "`Error: extracting required fields from comment data: err`", "") } func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { t.Run("nil checker", func(t *testing.T) { vcsClient := setup(t) // by default these are false so don't need to reset ch.TeamAllowlistChecker = nil var pull github.PullRequest modelPull := models.PullRequest{ BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, } When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.PlanCommand}) vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(fixtures.GithubRepo, fixtures.User) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Ran Plan for 0 projects:\n\n\n\n", "plan") }) t.Run("no rules", func(t *testing.T) { vcsClient := setup(t) // by default these are false so don't need to reset ch.TeamAllowlistChecker = &events.TeamAllowlistChecker{} var pull github.PullRequest modelPull := models.PullRequest{ BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, } When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.PlanCommand}) vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(fixtures.GithubRepo, fixtures.User) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Ran Plan for 0 projects:\n\n\n\n", "plan") }) } func TestRunCommentCommand_ForkPRDisabled(t *testing.T) { t.Log("if a command is run on a forked pull request and this is disabled atlantis should" + " comment saying that this is not allowed") vcsClient := setup(t) // by default these are false so don't need to reset ch.AllowForkPRs = false ch.SilenceForkPRErrors = false var pull github.PullRequest modelPull := models.PullRequest{ BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, } When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) headRepo := fixtures.GithubRepo headRepo.FullName = "forkrepo/atlantis" headRepo.Owner = "forkrepo" When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, headRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, nil) commentMessage := fmt.Sprintf("Atlantis commands can't be run on fork pull requests. To enable, set --%s or, to disable this message, set --%s", ch.AllowForkPRsFlag, ch.SilenceForkPRErrorsFlag) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, commentMessage, "") } func TestRunCommentCommand_ForkPRDisabled_SilenceEnabled(t *testing.T) { t.Log("if a command is run on a forked pull request and forks are disabled and we are silencing errors do not comment with error") vcsClient := setup(t) ch.AllowForkPRs = false // by default it's false so don't need to reset ch.SilenceForkPRErrors = true var pull github.PullRequest modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) headRepo := fixtures.GithubRepo headRepo.FullName = "forkrepo/atlantis" headRepo.Owner = "forkrepo" When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, headRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, nil) vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()) } func TestRunCommentCommandPlan_NoProjects_SilenceEnabled(t *testing.T) { t.Log("if a plan command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project") vcsClient := setup(t) planCommandRunner.SilenceNoProjects = true var pull github.PullRequest modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.PlanCommand}) vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()) commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount( matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.EqModelsCommitStatus(models.SuccessCommitStatus), matchers.EqModelsCommandName(models.PlanCommand), EqInt(0), EqInt(0), ) } func TestRunCommentCommandApply_NoProjects_SilenceEnabled(t *testing.T) { t.Log("if an apply command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project") vcsClient := setup(t) applyCommandRunner.SilenceNoProjects = true var pull github.PullRequest modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand}) vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()) commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount( matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.EqModelsCommitStatus(models.SuccessCommitStatus), matchers.EqModelsCommandName(models.ApplyCommand), EqInt(0), EqInt(0), ) } func TestRunCommentCommandApprovePolicy_NoProjects_SilenceEnabled(t *testing.T) { t.Log("if an approve_policy command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project") vcsClient := setup(t) approvePoliciesCommandRunner.SilenceNoProjects = true var pull github.PullRequest modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApprovePoliciesCommand}) vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()) commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount( matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.EqModelsCommitStatus(models.SuccessCommitStatus), matchers.EqModelsCommandName(models.PolicyCheckCommand), EqInt(0), EqInt(0), ) } func TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) { t.Log("if an unlock command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project") vcsClient := setup(t) unlockCommandRunner.SilenceNoProjects = true var pull github.PullRequest modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.UnlockCommand}) vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()) } func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) { t.Log("if \"atlantis apply\" is run and this is disabled atlantis should" + " comment saying that this is not allowed") vcsClient := setup(t) applyCommandRunner.DisableApplyAll = true pull := &github.PullRequest{ State: github.String("open"), } modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num} When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, modelPull.Num, &events.CommentCommand{Name: models.ApplyCommand}) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` without flags is disabled. You must specify which project to apply via the `-d