package controllers_test import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "time" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/controllers" . "github.com/runatlantis/atlantis/server/core/locking/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" . "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/models" . "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" ) const atlantisTokenHeader = "X-Atlantis-Token" const atlantisToken = "token" func TestAPIController_Plan(t *testing.T) { ac, projectCommandBuilder, projectCommandRunner := setup(t) cases := []struct { repository string ref string vcsType string pr int projects []string paths []struct { Directory string Workspace string } }{ { repository: "Repo", ref: "main", vcsType: "Gitlab", projects: []string{"default"}, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", pr: 1, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", paths: []struct { Directory string Workspace string }{ { Directory: ".", Workspace: "myworkspace", }, { Directory: "./myworkspace2", Workspace: "myworkspace2", }, }, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", pr: 1, projects: []string{"test"}, paths: []struct { Directory string Workspace string }{ { Directory: ".", Workspace: "myworkspace", }, }, }, } expectedCalls := 0 for _, c := range cases { body, _ := json.Marshal(controllers.APIRequest{ Repository: c.repository, Ref: c.ref, Type: c.vcsType, PR: c.pr, Projects: c.projects, Paths: c.paths, }) req, _ := http.NewRequest("POST", "", bytes.NewBuffer(body)) req.Header.Set(atlantisTokenHeader, atlantisToken) w := httptest.NewRecorder() ac.Plan(w, req) ResponseContains(t, w, http.StatusOK, "") expectedCalls += len(c.projects) expectedCalls += len(c.paths) } projectCommandBuilder.VerifyWasCalled(Times(expectedCalls)).BuildPlanCommands(Any[*command.Context](), Any[*events.CommentCommand]()) projectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Plan(Any[command.ProjectContext]()) } func TestAPIController_Apply(t *testing.T) { ac, projectCommandBuilder, projectCommandRunner := setup(t) cases := []struct { repository string ref string vcsType string pr int projects []string paths []struct { Directory string Workspace string } }{ { repository: "Repo", ref: "main", vcsType: "Gitlab", projects: []string{"default"}, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", pr: 1, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", paths: []struct { Directory string Workspace string }{ { Directory: ".", Workspace: "myworkspace", }, { Directory: "./myworkspace2", Workspace: "myworkspace2", }, }, }, { repository: "Repo", ref: "main", vcsType: "Gitlab", pr: 1, projects: []string{"test"}, paths: []struct { Directory string Workspace string }{ { Directory: ".", Workspace: "myworkspace", }, }, }, } expectedCalls := 0 for _, c := range cases { body, _ := json.Marshal(controllers.APIRequest{ Repository: c.repository, Ref: c.ref, Type: c.vcsType, PR: c.pr, Projects: c.projects, Paths: c.paths, }) req, _ := http.NewRequest("POST", "", bytes.NewBuffer(body)) req.Header.Set(atlantisTokenHeader, atlantisToken) w := httptest.NewRecorder() ac.Apply(w, req) ResponseContains(t, w, http.StatusOK, "") expectedCalls += len(c.projects) expectedCalls += len(c.paths) } projectCommandBuilder.VerifyWasCalled(Times(expectedCalls)).BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]()) projectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Plan(Any[command.ProjectContext]()) projectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Apply(Any[command.ProjectContext]()) } func TestAPIController_ListLocks(t *testing.T) { ac, _, _ := setup(t) time := time.Now() expected := controllers.ListLocksResult{[]controllers.LockDetail{ { Name: "lock-id", ProjectName: "terraform", ProjectRepo: "owner/repo", ProjectRepoPath: "/path", PullID: 123, PullURL: "url", User: "jdoe", Workspace: "default", Time: time, }, }, } mockLock := models.ProjectLock{ Project: models.Project{ProjectName: "terraform", RepoFullName: "owner/repo", Path: "/path"}, Pull: models.PullRequest{Num: 123, URL: "url", Author: "lkysow"}, User: models.User{Username: "jdoe"}, Workspace: "default", Time: time, } mockLocks := map[string]models.ProjectLock{ "lock-id": mockLock, } When(ac.Locker.List()).ThenReturn(mockLocks, nil) req, _ := http.NewRequest("GET", "", nil) w := httptest.NewRecorder() ac.ListLocks(w, req) response, _ := io.ReadAll(w.Result().Body) var result controllers.ListLocksResult err := json.Unmarshal(response, &result) Ok(t, err) Equals(t, expected, result) } func TestAPIController_ListLocksEmpty(t *testing.T) { ac, _, _ := setup(t) expected := controllers.ListLocksResult{} mockLocks := map[string]models.ProjectLock{} When(ac.Locker.List()).ThenReturn(mockLocks, nil) req, _ := http.NewRequest("GET", "", nil) w := httptest.NewRecorder() ac.ListLocks(w, req) response, _ := io.ReadAll(w.Result().Body) var result controllers.ListLocksResult err := json.Unmarshal(response, &result) Ok(t, err) Equals(t, expected, result) } func setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, *MockProjectCommandRunner) { RegisterMockTestingT(t) locker := NewMockLocker() logger := logging.NewNoopLogger(t) parser := NewMockEventParsing() repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") scope, _, _ := metrics.NewLoggingScope(logger, "null") vcsClient := NewMockClient() workingDir := NewMockWorkingDir() Ok(t, err) workingDirLocker := NewMockWorkingDirLocker() When(workingDirLocker.TryLock(Any[string](), Any[int](), Eq(events.DefaultWorkspace), Eq(events.DefaultRepoRelDir))). ThenReturn(func() {}, nil) projectCommandBuilder := NewMockProjectCommandBuilder() When(projectCommandBuilder.BuildPlanCommands(Any[*command.Context](), Any[*events.CommentCommand]())). ThenReturn([]command.ProjectContext{{ CommandName: command.Plan, }}, nil) When(projectCommandBuilder.BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]())). ThenReturn([]command.ProjectContext{{ CommandName: command.Apply, }}, nil) projectCommandRunner := NewMockProjectCommandRunner() When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{ PlanSuccess: &models.PlanSuccess{}, }) When(projectCommandRunner.Apply(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{ ApplySuccess: "success", }) preWorkflowHooksCommandRunner := NewMockPreWorkflowHooksCommandRunner() When(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil) postWorkflowHooksCommandRunner := NewMockPostWorkflowHooksCommandRunner() When(postWorkflowHooksCommandRunner.RunPostHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil) commitStatusUpdater := NewMockCommitStatusUpdater() When(commitStatusUpdater.UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[models.CommitStatus](), Any[command.Name]())).ThenReturn(nil) ac := controllers.APIController{ APISecret: []byte(atlantisToken), Locker: locker, Logger: logger, Scope: scope, Parser: parser, ProjectCommandBuilder: projectCommandBuilder, ProjectPlanCommandRunner: projectCommandRunner, ProjectApplyCommandRunner: projectCommandRunner, PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, VCSClient: vcsClient, RepoAllowlistChecker: repoAllowlistChecker, WorkingDir: workingDir, WorkingDirLocker: workingDirLocker, CommitStatusUpdater: commitStatusUpdater, } return ac, projectCommandBuilder, projectCommandRunner }