From ade9b315931a62cfda91c4e38844e39f5a3c7c35 Mon Sep 17 00:00:00 2001 From: Paris Morali Date: Mon, 11 May 2020 15:13:36 +0100 Subject: [PATCH] Add BoltDB interface and some relevant tests With BoltDB interface we are able to mock/stub and improve test coverage. Some other refactoring was required to make code aware and compatible with the new interface --- server/events/command_runner.go | 2 +- server/events/command_runner_test.go | 55 +++++ server/events/db/boltdb.go | 49 ++-- server/events/db/boltdb_test.go | 4 +- .../matchers/models_projectplanstatus.go | 20 ++ .../db/mocks/matchers/models_pullrequest.go | 20 ++ .../db/mocks/matchers/models_pullstatus.go | 20 ++ .../matchers/slice_of_models_projectresult.go | 20 ++ server/events/db/mocks/mock_boltdb.go | 209 ++++++++++++++++++ server/events/pull_closed_executor.go | 2 +- server/locks_controller.go | 2 +- server/locks_controller_test.go | 42 ++++ 12 files changed, 420 insertions(+), 25 deletions(-) create mode 100644 server/events/db/mocks/matchers/models_projectplanstatus.go create mode 100644 server/events/db/mocks/matchers/models_pullrequest.go create mode 100644 server/events/db/mocks/matchers/models_pullstatus.go create mode 100644 server/events/db/mocks/matchers/slice_of_models_projectresult.go create mode 100644 server/events/db/mocks/mock_boltdb.go diff --git a/server/events/command_runner.go b/server/events/command_runner.go index c00853475..80e164ec0 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -97,7 +97,7 @@ type DefaultCommandRunner struct { GlobalAutomerge bool PendingPlanFinder PendingPlanFinder WorkingDir WorkingDir - DB *db.BoltDB + DB db.BoltDB } // RunAutoplanCommand runs plan when a pull request is opened or updated. diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index d5efac686..482c3c50a 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -24,6 +24,8 @@ import ( "github.com/google/go-github/v28/github" . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/events" + dbmocks "github.com/runatlantis/atlantis/server/events/db/mocks" + dbmatchers "github.com/runatlantis/atlantis/server/events/db/mocks/matchers" "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/mocks/matchers" "github.com/runatlantis/atlantis/server/events/models" @@ -43,6 +45,7 @@ var ch events.DefaultCommandRunner var pullLogger *logging.SimpleLogger var workingDir events.WorkingDir var pendingPlanFinder *mocks.MockPendingPlanFinder +var boltDB *dbmocks.MockBoltDB func setup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) @@ -57,6 +60,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { projectCommandRunner = mocks.NewMockProjectCommandRunner() workingDir = mocks.NewMockWorkingDir() pendingPlanFinder = mocks.NewMockPendingPlanFinder() + boltDB = dbmocks.NewMockBoltDB() When(logger.GetLevel()).ThenReturn(logging.Info) When(logger.NewLogger("runatlantis/atlantis#1", true, logging.Info)). ThenReturn(pullLogger) @@ -76,6 +80,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { PendingPlanFinder: pendingPlanFinder, WorkingDir: workingDir, DisableApplyAll: false, + DB: boltDB, } return vcsClient } @@ -234,3 +239,53 @@ func TestRunAutoplanCommand_DeletePlans(t *testing.T) { ch.RunAutoplanCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) pendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp) } + +func TestApplyWithAutoMerge_VSCMerge(t *testing.T) { + t.Log("if \"atlantis apply\" is run with automerge and at least one project" + + " has a discarded plan, automerge should not take place") + + vcsClient := setup(t) + pull := &github.PullRequest{ + State: github.String("open"), + } + modelPull := models.PullRequest{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.GlobalAutomerge = true + + ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand}) + vcsClient.VerifyWasCalledOnce().MergePull(modelPull) +} + +func TestApplyWithAutoMerge_DiscardedPlan(t *testing.T) { + t.Log("if \"atlantis apply\" is run with automerge and at least one project" + + " has a discarded plan, automerge should not take place") + + setup(t) + pull := &github.PullRequest{ + State: github.String("open"), + } + modelPull := models.PullRequest{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.GlobalAutomerge = true + + ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand}) + projectStatuses := []models.ProjectStatus{ + { + RepoRelDir: ".", + Workspace: "default", + ProjectName: "automerge-test", + Status: models.DiscardedPlanStatus, + }, + } + pullStatus := models.PullStatus{ + Projects: projectStatuses, + Pull: modelPull, + } + //When(boltDB.UpdatePullWithResults(modelPull, nil)).ThenReturn(pullStatus, nil) + When(boltDB.UpdatePullWithResults(dbmatchers.EqModelsPullRequest(modelPull), dbmatchers.EqSliceOfModelsProjectResult(nil))).ThenReturn(pullStatus, nil) + // TODO: stubbing here doesn't seem to work? pullStatus defined here is not actually returned and so I cannot uncomment the next two lines which would verify our scenario here + //vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "not automerging because project at dir %q, workspace %q has status %q") + //VerifyWasCalled(Never()).MergePull(modelPull) +} diff --git a/server/events/db/boltdb.go b/server/events/db/boltdb.go index 18d8e26cb..221446c60 100644 --- a/server/events/db/boltdb.go +++ b/server/events/db/boltdb.go @@ -15,8 +15,17 @@ import ( bolt "go.etcd.io/bbolt" ) -// BoltDB is a database using BoltDB -type BoltDB struct { +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_boltdb.go BoltDB + +// BoltDB interface defines the set of methods the DB implements. Use this to allow DB mocking when testing +type BoltDB interface { + UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (models.PullStatus, error) + DeletePullStatus(pull models.PullRequest) error + UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) error +} + +// DefaultBoltDB is a database using BoltDB +type DefaultBoltDB struct { db *bolt.DB locksBucketName []byte pullsBucketName []byte @@ -30,7 +39,7 @@ const ( // New returns a valid locker. We need to be able to write to dataDir // since bolt stores its data as a file -func New(dataDir string) (*BoltDB, error) { +func New(dataDir string) (*DefaultBoltDB, error) { if err := os.MkdirAll(dataDir, 0700); err != nil { return nil, errors.Wrap(err, "creating data dir") } @@ -56,19 +65,19 @@ func New(dataDir string) (*BoltDB, error) { return nil, errors.Wrap(err, "starting BoltDB") } // todo: close BoltDB when server is sigtermed - return &BoltDB{db: db, locksBucketName: []byte(locksBucketName), pullsBucketName: []byte(pullsBucketName)}, nil + return &DefaultBoltDB{db: db, locksBucketName: []byte(locksBucketName), pullsBucketName: []byte(pullsBucketName)}, nil } // NewWithDB is used for testing. -func NewWithDB(db *bolt.DB, bucket string) (*BoltDB, error) { - return &BoltDB{db: db, locksBucketName: []byte(bucket), pullsBucketName: []byte(pullsBucketName)}, nil +func NewWithDB(db *bolt.DB, bucket string) (*DefaultBoltDB, error) { + return &DefaultBoltDB{db: db, locksBucketName: []byte(bucket), pullsBucketName: []byte(pullsBucketName)}, nil } // TryLock attempts to create a new lock. If the lock is // acquired, it will return true and the lock returned will be newLock. // If the lock is not acquired, it will return false and the current // lock that is preventing this lock from being acquired. -func (b *BoltDB) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, error) { +func (b *DefaultBoltDB) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, error) { var lockAcquired bool var currLock models.ProjectLock key := b.lockKey(newLock.Project, newLock.Workspace) @@ -105,7 +114,7 @@ func (b *BoltDB) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, // If there is no lock, then it will return a nil pointer. // If there is a lock, then it will delete it, and then return a pointer // to the deleted lock. -func (b *BoltDB) Unlock(p models.Project, workspace string) (*models.ProjectLock, error) { +func (b *DefaultBoltDB) Unlock(p models.Project, workspace string) (*models.ProjectLock, error) { var lock models.ProjectLock foundLock := false key := b.lockKey(p, workspace) @@ -128,7 +137,7 @@ func (b *BoltDB) Unlock(p models.Project, workspace string) (*models.ProjectLock } // List lists all current locks. -func (b *BoltDB) List() ([]models.ProjectLock, error) { +func (b *DefaultBoltDB) List() ([]models.ProjectLock, error) { var locks []models.ProjectLock var locksBytes [][]byte err := b.db.View(func(tx *bolt.Tx) error { @@ -156,7 +165,7 @@ func (b *BoltDB) List() ([]models.ProjectLock, error) { } // UnlockByPull deletes all locks associated with that pull request and returns them. -func (b *BoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) { +func (b *DefaultBoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) { var locks []models.ProjectLock err := b.db.View(func(tx *bolt.Tx) error { c := tx.Bucket(b.locksBucketName).Cursor() @@ -188,7 +197,7 @@ func (b *BoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec // GetLock returns a pointer to the lock for that project and workspace. // If there is no lock, it returns a nil pointer. -func (b *BoltDB) GetLock(p models.Project, workspace string) (*models.ProjectLock, error) { +func (b *DefaultBoltDB) GetLock(p models.Project, workspace string) (*models.ProjectLock, error) { key := b.lockKey(p, workspace) var lockBytes []byte err := b.db.View(func(tx *bolt.Tx) error { @@ -216,7 +225,7 @@ func (b *BoltDB) GetLock(p models.Project, workspace string) (*models.ProjectLoc // UpdatePullWithResults updates pull's status with the latest project results. // It returns the new PullStatus object. -func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (models.PullStatus, error) { +func (b *DefaultBoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (models.PullStatus, error) { key, err := b.pullKey(pull) if err != nil { return models.PullStatus{}, err @@ -281,7 +290,7 @@ func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []mod // GetPullStatus returns the status for pull. // If there is no status, returns a nil pointer. -func (b *BoltDB) GetPullStatus(pull models.PullRequest) (*models.PullStatus, error) { +func (b *DefaultBoltDB) GetPullStatus(pull models.PullRequest) (*models.PullStatus, error) { key, err := b.pullKey(pull) if err != nil { return nil, err @@ -297,7 +306,7 @@ func (b *BoltDB) GetPullStatus(pull models.PullRequest) (*models.PullStatus, err } // DeletePullStatus deletes the status for pull. -func (b *BoltDB) DeletePullStatus(pull models.PullRequest) error { +func (b *DefaultBoltDB) DeletePullStatus(pull models.PullRequest) error { key, err := b.pullKey(pull) if err != nil { return err @@ -311,7 +320,7 @@ func (b *BoltDB) DeletePullStatus(pull models.PullRequest) error { // UpdateProjectStatus updates all project statuses under pull that match // workspace and repoRelDir. -func (b *BoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) error { +func (b *DefaultBoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) error { key, err := b.pullKey(pull) if err != nil { return err @@ -344,7 +353,7 @@ func (b *BoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, return errors.Wrap(err, "DB transaction failed") } -func (b *BoltDB) pullKey(pull models.PullRequest) ([]byte, error) { +func (b *DefaultBoltDB) pullKey(pull models.PullRequest) ([]byte, error) { hostname := pull.BaseRepo.VCSHost.Hostname if strings.Contains(hostname, pullKeySeparator) { return nil, fmt.Errorf("vcs hostname %q contains illegal string %q", hostname, pullKeySeparator) @@ -358,11 +367,11 @@ func (b *BoltDB) pullKey(pull models.PullRequest) ([]byte, error) { nil } -func (b *BoltDB) lockKey(p models.Project, workspace string) string { +func (b *DefaultBoltDB) lockKey(p models.Project, workspace string) string { return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace) } -func (b *BoltDB) getPullFromBucket(bucket *bolt.Bucket, key []byte) (*models.PullStatus, error) { +func (b *DefaultBoltDB) getPullFromBucket(bucket *bolt.Bucket, key []byte) (*models.PullStatus, error) { serialized := bucket.Get(key) if serialized == nil { return nil, nil @@ -375,7 +384,7 @@ func (b *BoltDB) getPullFromBucket(bucket *bolt.Bucket, key []byte) (*models.Pul return &p, nil } -func (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models.PullStatus) error { +func (b *DefaultBoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models.PullStatus) error { serialized, err := json.Marshal(pull) if err != nil { return errors.Wrap(err, "serializing") @@ -383,7 +392,7 @@ func (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models. return bucket.Put(key, serialized) } -func (b *BoltDB) projectResultToProject(p models.ProjectResult) models.ProjectStatus { +func (b *DefaultBoltDB) projectResultToProject(p models.ProjectResult) models.ProjectStatus { return models.ProjectStatus{ Workspace: p.Workspace, RepoRelDir: p.RepoRelDir, diff --git a/server/events/db/boltdb_test.go b/server/events/db/boltdb_test.go index 7129bb3ee..71b44e6ef 100644 --- a/server/events/db/boltdb_test.go +++ b/server/events/db/boltdb_test.go @@ -692,7 +692,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) { } // newTestDB returns a TestDB using a temporary path. -func newTestDB() (*bolt.DB, *db.BoltDB) { +func newTestDB() (*bolt.DB, *db.DefaultBoltDB) { // Retrieve a temporary path. f, err := ioutil.TempFile("", "") if err != nil { @@ -718,7 +718,7 @@ func newTestDB() (*bolt.DB, *db.BoltDB) { return boltDB, b } -func newTestDB2(t *testing.T) (*db.BoltDB, func()) { +func newTestDB2(t *testing.T) (*db.DefaultBoltDB, func()) { tmp, cleanup := TempDir(t) boltDB, err := db.New(tmp) Ok(t, err) diff --git a/server/events/db/mocks/matchers/models_projectplanstatus.go b/server/events/db/mocks/matchers/models_projectplanstatus.go new file mode 100644 index 000000000..9a9f2ced6 --- /dev/null +++ b/server/events/db/mocks/matchers/models_projectplanstatus.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsProjectPlanStatus() models.ProjectPlanStatus { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.ProjectPlanStatus))(nil)).Elem())) + var nullValue models.ProjectPlanStatus + return nullValue +} + +func EqModelsProjectPlanStatus(value models.ProjectPlanStatus) models.ProjectPlanStatus { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.ProjectPlanStatus + return nullValue +} diff --git a/server/events/db/mocks/matchers/models_pullrequest.go b/server/events/db/mocks/matchers/models_pullrequest.go new file mode 100644 index 000000000..dd1fb0d4e --- /dev/null +++ b/server/events/db/mocks/matchers/models_pullrequest.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsPullRequest() models.PullRequest { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.PullRequest))(nil)).Elem())) + var nullValue models.PullRequest + return nullValue +} + +func EqModelsPullRequest(value models.PullRequest) models.PullRequest { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.PullRequest + return nullValue +} diff --git a/server/events/db/mocks/matchers/models_pullstatus.go b/server/events/db/mocks/matchers/models_pullstatus.go new file mode 100644 index 000000000..920d2c5f5 --- /dev/null +++ b/server/events/db/mocks/matchers/models_pullstatus.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsPullStatus() models.PullStatus { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.PullStatus))(nil)).Elem())) + var nullValue models.PullStatus + return nullValue +} + +func EqModelsPullStatus(value models.PullStatus) models.PullStatus { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.PullStatus + return nullValue +} diff --git a/server/events/db/mocks/matchers/slice_of_models_projectresult.go b/server/events/db/mocks/matchers/slice_of_models_projectresult.go new file mode 100644 index 000000000..687753508 --- /dev/null +++ b/server/events/db/mocks/matchers/slice_of_models_projectresult.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnySliceOfModelsProjectResult() []models.ProjectResult { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]models.ProjectResult))(nil)).Elem())) + var nullValue []models.ProjectResult + return nullValue +} + +func EqSliceOfModelsProjectResult(value []models.ProjectResult) []models.ProjectResult { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue []models.ProjectResult + return nullValue +} diff --git a/server/events/db/mocks/mock_boltdb.go b/server/events/db/mocks/mock_boltdb.go new file mode 100644 index 000000000..f395896d5 --- /dev/null +++ b/server/events/db/mocks/mock_boltdb.go @@ -0,0 +1,209 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events/db (interfaces: BoltDB) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockBoltDB struct { + fail func(message string, callerSkip ...int) +} + +func NewMockBoltDB(options ...pegomock.Option) *MockBoltDB { + mock := &MockBoltDB{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockBoltDB) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockBoltDB) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockBoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (models.PullStatus, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockBoltDB().") + } + params := []pegomock.Param{pull, newResults} + result := pegomock.GetGenericMockFrom(mock).Invoke("UpdatePullWithResults", params, []reflect.Type{reflect.TypeOf((*models.PullStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullStatus + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullStatus) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockBoltDB) DeletePullStatus(pull models.PullRequest) error { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockBoltDB().") + } + params := []pegomock.Param{pull} + result := pegomock.GetGenericMockFrom(mock).Invoke("DeletePullStatus", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(error) + } + } + return ret0 +} + +func (mock *MockBoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) error { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockBoltDB().") + } + params := []pegomock.Param{pull, workspace, repoRelDir, targetStatus} + result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateProjectStatus", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(error) + } + } + return ret0 +} + +func (mock *MockBoltDB) VerifyWasCalledOnce() *VerifierMockBoltDB { + return &VerifierMockBoltDB{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockBoltDB) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockBoltDB { + return &VerifierMockBoltDB{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockBoltDB) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockBoltDB { + return &VerifierMockBoltDB{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockBoltDB) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockBoltDB { + return &VerifierMockBoltDB{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockBoltDB struct { + mock *MockBoltDB + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockBoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) *MockBoltDB_UpdatePullWithResults_OngoingVerification { + params := []pegomock.Param{pull, newResults} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdatePullWithResults", params, verifier.timeout) + return &MockBoltDB_UpdatePullWithResults_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockBoltDB_UpdatePullWithResults_OngoingVerification struct { + mock *MockBoltDB + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockBoltDB_UpdatePullWithResults_OngoingVerification) GetCapturedArguments() (models.PullRequest, []models.ProjectResult) { + pull, newResults := c.GetAllCapturedArguments() + return pull[len(pull)-1], newResults[len(newResults)-1] +} + +func (c *MockBoltDB_UpdatePullWithResults_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest, _param1 [][]models.ProjectResult) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.PullRequest) + } + _param1 = make([][]models.ProjectResult, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.([]models.ProjectResult) + } + } + return +} + +func (verifier *VerifierMockBoltDB) DeletePullStatus(pull models.PullRequest) *MockBoltDB_DeletePullStatus_OngoingVerification { + params := []pegomock.Param{pull} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeletePullStatus", params, verifier.timeout) + return &MockBoltDB_DeletePullStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockBoltDB_DeletePullStatus_OngoingVerification struct { + mock *MockBoltDB + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockBoltDB_DeletePullStatus_OngoingVerification) GetCapturedArguments() models.PullRequest { + pull := c.GetAllCapturedArguments() + return pull[len(pull)-1] +} + +func (c *MockBoltDB_DeletePullStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.PullRequest) + } + } + return +} + +func (verifier *VerifierMockBoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) *MockBoltDB_UpdateProjectStatus_OngoingVerification { + params := []pegomock.Param{pull, workspace, repoRelDir, targetStatus} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateProjectStatus", params, verifier.timeout) + return &MockBoltDB_UpdateProjectStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockBoltDB_UpdateProjectStatus_OngoingVerification struct { + mock *MockBoltDB + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockBoltDB_UpdateProjectStatus_OngoingVerification) GetCapturedArguments() (models.PullRequest, string, string, models.ProjectPlanStatus) { + pull, workspace, repoRelDir, targetStatus := c.GetAllCapturedArguments() + return pull[len(pull)-1], workspace[len(workspace)-1], repoRelDir[len(repoRelDir)-1], targetStatus[len(targetStatus)-1] +} + +func (c *MockBoltDB_UpdateProjectStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest, _param1 []string, _param2 []string, _param3 []models.ProjectPlanStatus) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.PullRequest) + } + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(string) + } + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(string) + } + _param3 = make([]models.ProjectPlanStatus, len(c.methodInvocations)) + for u, param := range params[3] { + _param3[u] = param.(models.ProjectPlanStatus) + } + } + return +} diff --git a/server/events/pull_closed_executor.go b/server/events/pull_closed_executor.go index b8a5a5219..309aaa993 100644 --- a/server/events/pull_closed_executor.go +++ b/server/events/pull_closed_executor.go @@ -46,7 +46,7 @@ type PullClosedExecutor struct { VCSClient vcs.Client WorkingDir WorkingDir Logger logging.SimpleLogging - DB *db.BoltDB + DB db.BoltDB } type templatedProject struct { diff --git a/server/locks_controller.go b/server/locks_controller.go index 540d6608a..2995c2675 100644 --- a/server/locks_controller.go +++ b/server/locks_controller.go @@ -25,7 +25,7 @@ type LocksController struct { LockDetailTemplate TemplateWriter WorkingDir events.WorkingDir WorkingDirLocker events.WorkingDirLocker - DB *db.BoltDB + DB db.BoltDB } // GetLock is the GET /locks/{id} route. It renders the lock detail view. diff --git a/server/locks_controller_test.go b/server/locks_controller_test.go index 1b159018d..2bfa7488e 100644 --- a/server/locks_controller_test.go +++ b/server/locks_controller_test.go @@ -15,6 +15,8 @@ import ( . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server" "github.com/runatlantis/atlantis/server/events" + dbmocks "github.com/runatlantis/atlantis/server/events/db/mocks" + "github.com/runatlantis/atlantis/server/events/locking/mocks" mocks2 "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/models" @@ -192,6 +194,46 @@ func TestDeleteLock_OldFormat(t *testing.T) { cp.VerifyWasCalled(Never()).CreateComment(AnyRepo(), AnyInt(), AnyString()) } +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 := mocks.NewMockLocker() + workingDir := mocks2.NewMockWorkingDir() + workingDirLocker := events.NewDefaultWorkingDirLocker() + pull := models.PullRequest{ + BaseRepo: models.Repo{FullName: repoName}, + } + When(l.Unlock("id")).ThenReturn(&models.ProjectLock{ + Pull: pull, + Workspace: workspaceName, + Project: models.Project{ + Path: projectPath, + RepoFullName: repoName, + }, + }, nil) + db := dbmocks.NewMockBoltDB() + lc := server.LocksController{ + Locker: l, + Logger: logging.NewNoopLogger(), + VCSClient: cp, + WorkingDirLocker: workingDirLocker, + WorkingDir: workingDir, + DB: db, + } + 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\"") + db.VerifyWasCalledOnce().UpdateProjectStatus(pull, workspaceName, projectPath, models.DiscardedPlanStatus) +} + func TestDeleteLock_CommentFailed(t *testing.T) { t.Log("If the commenting fails we return an error") RegisterMockTestingT(t)