package controllers_test import ( "bytes" "errors" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/runatlantis/atlantis/server/controllers" "github.com/runatlantis/atlantis/server/controllers/web_templates" tMocks "github.com/runatlantis/atlantis/server/controllers/web_templates/mocks" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/core/locking" "github.com/gorilla/mux" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/core/locking/mocks" "github.com/runatlantis/atlantis/server/events/command" mocks2 "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/testing" ) func TestCreateApplyLock(t *testing.T) { t.Run("Creates apply lock", func(t *testing.T) { req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() layout := "2006-01-02T15:04:05.000Z" strLockTime := "2020-09-01T00:45:26.371Z" expLockTime := "2020-09-01 00:45:26" lockTime, _ := time.Parse(layout, strLockTime) l := mocks.NewMockApplyLocker() When(l.LockApply()).ThenReturn(locking.ApplyCommandLock{ Locked: true, Time: lockTime, }, nil) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), ApplyLocker: l, } lc.LockApply(w, req) ResponseContains(t, w, http.StatusOK, fmt.Sprintf("Apply Lock is acquired on %s", expLockTime)) }) t.Run("Apply lock creation fails", func(t *testing.T) { req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() l := mocks.NewMockApplyLocker() When(l.LockApply()).ThenReturn(locking.ApplyCommandLock{ Locked: false, }, errors.New("failed to acquire lock")) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), ApplyLocker: l, } lc.LockApply(w, req) ResponseContains(t, w, http.StatusInternalServerError, "creating apply lock failed with: failed to acquire lock") }) } func TestUnlockApply(t *testing.T) { t.Run("Apply lock deleted successfully", func(t *testing.T) { req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() l := mocks.NewMockApplyLocker() When(l.UnlockApply()).ThenReturn(nil) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), ApplyLocker: l, } lc.UnlockApply(w, req) ResponseContains(t, w, http.StatusOK, "Deleted apply lock") }) t.Run("Apply lock deletion failed", func(t *testing.T) { req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() l := mocks.NewMockApplyLocker() When(l.UnlockApply()).ThenReturn(errors.New("failed to delete lock")) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), ApplyLocker: l, } lc.UnlockApply(w, req) ResponseContains(t, w, http.StatusInternalServerError, "deleting apply lock failed with: failed to delete lock") }) } func TestGetLockRoute_NoLockID(t *testing.T) { t.Log("If there is no lock ID in the request then we should get a 400") req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), } lc.GetLock(w, req) ResponseContains(t, w, http.StatusBadRequest, "No lock id in request") } func TestGetLock_InvalidLockID(t *testing.T) { t.Log("If the lock ID is invalid then we should get a 400") lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "%A@"}) w := httptest.NewRecorder() lc.GetLock(w, req) ResponseContains(t, w, http.StatusBadRequest, "Invalid lock id") } func TestGetLock_LockerErr(t *testing.T) { t.Log("If there is an error retrieving the lock, a 500 is returned") RegisterMockTestingT(t) l := mocks.NewMockLocker() When(l.GetLock("id")).ThenReturn(nil, errors.New("err")) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), Locker: l, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.GetLock(w, req) ResponseContains(t, w, http.StatusInternalServerError, "err") } func TestGetLock_None(t *testing.T) { t.Log("If there is no lock at that ID we get a 404") RegisterMockTestingT(t) l := mocks.NewMockLocker() When(l.GetLock("id")).ThenReturn(nil, nil) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), Locker: l, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.GetLock(w, req) ResponseContains(t, w, http.StatusNotFound, "No lock found at id 'id'") } func TestGetLock_Success(t *testing.T) { t.Log("Should be able to render a lock successfully") RegisterMockTestingT(t) l := mocks.NewMockLocker() When(l.GetLock("id")).ThenReturn(&models.ProjectLock{ Project: models.Project{RepoFullName: "owner/repo", Path: "path"}, Pull: models.PullRequest{URL: "url", Author: "lkysow"}, Workspace: "workspace", }, nil) tmpl := tMocks.NewMockTemplateWriter() atlantisURL, err := url.Parse("https://example.com/basepath") Ok(t, err) lc := controllers.LocksController{ Logger: logging.NewNoopLogger(t), Locker: l, LockDetailTemplate: tmpl, AtlantisVersion: "1300135", AtlantisURL: atlantisURL, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.GetLock(w, req) tmpl.VerifyWasCalledOnce().Execute(w, web_templates.LockDetailData{ LockKeyEncoded: "id", LockKey: "id", RepoOwner: "owner", RepoName: "repo", PullRequestLink: "url", LockedBy: "lkysow", Workspace: "workspace", AtlantisVersion: "1300135", CleanedBasePath: "/basepath", }) ResponseContains(t, w, http.StatusOK, "") } func TestDeleteLock_NoLockID(t *testing.T) { t.Log("If there is no lock ID in the request then we should get a 400") req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() lc := controllers.LocksController{Logger: logging.NewNoopLogger(t)} lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusBadRequest, "No lock id in request") } func TestDeleteLock_InvalidLockID(t *testing.T) { t.Log("If the lock ID is invalid then we should get a 400") lc := controllers.LocksController{Logger: logging.NewNoopLogger(t)} req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "%A@"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusBadRequest, "Invalid lock id '%A@'") } func TestDeleteLock_LockerErr(t *testing.T) { t.Log("If there is an error retrieving the lock, a 500 is returned") RegisterMockTestingT(t) dlc := mocks2.NewMockDeleteLockCommand() When(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(nil, errors.New("err")) lc := controllers.LocksController{ DeleteLockCommand: dlc, Logger: logging.NewNoopLogger(t), } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusInternalServerError, "err") } func TestDeleteLock_None(t *testing.T) { t.Log("If there is no lock at that ID we get a 404") RegisterMockTestingT(t) dlc := mocks2.NewMockDeleteLockCommand() When(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(nil, nil) lc := controllers.LocksController{ DeleteLockCommand: dlc, Logger: logging.NewNoopLogger(t), } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusNotFound, "No lock found at id 'id'") } func TestDeleteLock_OldFormat(t *testing.T) { t.Log("If the lock doesn't have BaseRepo set it is deleted successfully") RegisterMockTestingT(t) cp := vcsmocks.NewMockClient() dlc := mocks2.NewMockDeleteLockCommand() When(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(&models.ProjectLock{}, nil) lc := controllers.LocksController{ DeleteLockCommand: dlc, Logger: logging.NewNoopLogger(t), VCSClient: cp, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusOK, "Deleted lock id 'id'") cp.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()) } func TestDeleteLock_UpdateProjectStatus(t *testing.T) { t.Log("When deleting a lock, pull status has to be updated to reflect discarded plan") RegisterMockTestingT(t) repoName := "owner/repo" projectPath := "path" workspaceName := "workspace" cp := vcsmocks.NewMockClient() l := mocks2.NewMockDeleteLockCommand() workingDir := mocks2.NewMockWorkingDir() workingDirLocker := events.NewDefaultWorkingDirLocker() pull := models.PullRequest{ BaseRepo: models.Repo{FullName: repoName}, } When(l.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(&models.ProjectLock{ Pull: pull, Workspace: workspaceName, Project: models.Project{ Path: projectPath, RepoFullName: repoName, }, }, nil) var backend locking.Backend tmp := t.TempDir() backend, err := db.New(tmp) Ok(t, err) // Seed the DB with a successful plan for that project (that is later discarded). _, err = backend.UpdatePullWithResults(pull, []command.ProjectResult{ { Command: command.Plan, RepoRelDir: projectPath, Workspace: workspaceName, PlanSuccess: &models.PlanSuccess{ TerraformOutput: "tf-output", LockURL: "lock-url", }, }, }) Ok(t, err) lc := controllers.LocksController{ DeleteLockCommand: l, Logger: logging.NewNoopLogger(t), VCSClient: cp, WorkingDirLocker: workingDirLocker, WorkingDir: workingDir, Backend: backend, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusOK, "Deleted lock id 'id'") status, err := backend.GetPullStatus(pull) Ok(t, err) Assert(t, status.Projects != nil, "status projects was nil") Equals(t, []models.ProjectStatus{ { Workspace: workspaceName, RepoRelDir: projectPath, Status: models.DiscardedPlanStatus, }, }, status.Projects) } func TestDeleteLock_CommentFailed(t *testing.T) { t.Log("If the commenting fails we still return success") RegisterMockTestingT(t) dlc := mocks2.NewMockDeleteLockCommand() When(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(&models.ProjectLock{ Pull: models.PullRequest{ BaseRepo: models.Repo{FullName: "owner/repo"}, }, }, nil) cp := vcsmocks.NewMockClient() workingDir := mocks2.NewMockWorkingDir() workingDirLocker := events.NewDefaultWorkingDirLocker() var backend locking.Backend tmp := t.TempDir() backend, err := db.New(tmp) Ok(t, err) When(cp.CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())).ThenReturn(errors.New("err")) lc := controllers.LocksController{ DeleteLockCommand: dlc, Logger: logging.NewNoopLogger(t), VCSClient: cp, WorkingDir: workingDir, WorkingDirLocker: workingDirLocker, Backend: backend, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusOK, "Deleted lock id 'id'") } func TestDeleteLock_CommentSuccess(t *testing.T) { t.Log("We should comment back on the pull request if the lock is deleted") RegisterMockTestingT(t) cp := vcsmocks.NewMockClient() dlc := mocks2.NewMockDeleteLockCommand() workingDir := mocks2.NewMockWorkingDir() workingDirLocker := events.NewDefaultWorkingDirLocker() var backend locking.Backend tmp := t.TempDir() backend, err := db.New(tmp) Ok(t, err) pull := models.PullRequest{ BaseRepo: models.Repo{FullName: "owner/repo"}, } When(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq("id"))).ThenReturn(&models.ProjectLock{ Pull: pull, Workspace: "workspace", Project: models.Project{ Path: "path", RepoFullName: "owner/repo", }, }, nil) lc := controllers.LocksController{ DeleteLockCommand: dlc, Logger: logging.NewNoopLogger(t), VCSClient: cp, Backend: backend, WorkingDir: workingDir, WorkingDirLocker: workingDirLocker, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) ResponseContains(t, w, http.StatusOK, "Deleted lock id 'id'") cp.VerifyWasCalled(Once()).CreateComment(Any[logging.SimpleLogging](), Eq(pull.BaseRepo), Eq(pull.Num), Eq("**Warning**: The plan for dir: `path` workspace: `workspace` was **discarded** via the Atlantis UI.\n\n"+ "To `apply` this plan you must run `plan` again."), Eq("")) }