Skip to content

Commit

Permalink
chore: Creates mock generation infrastructure (#1763)
Browse files Browse the repository at this point in the history
* use mockery

* don't include test utilities in build

* Revert "don't include test utilities in build"

This reverts commit b41ba5e.

* TEMPORARY: testing mock change detection

* Revert "TEMPORARY: testing mock change detection"

This reverts commit 0cbacbc.

* centralize lint version in make file

* fix call to mockery

* timeout in linter

* TEMPORARY - delete mock file

* Revert "TEMPORARY - delete mock file"

This reverts commit d171e9b.

* use assert

* TEMPORARY - failing unit test

* Revert "TEMPORARY - failing unit test"

This reverts commit 05ed39b.

* use testify mock

* change service name

* use state

* disable version mockery generated files

* refactor response

* responses in one line each

* rename err

* clarify file detection message

* add info in contributing file

* move timeout to config file

* TEMPORARY - linter fail

* Revert "TEMPORARY - linter fail"

This reverts commit 5ef6390.

* remove asdf comment

* revert lint changes
  • Loading branch information
lantoli committed Dec 19, 2023
1 parent fb58b34 commit d8643b3
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 123 deletions.
31 changes: 18 additions & 13 deletions .github/workflows/code-health.yml
Expand Up @@ -16,22 +16,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Mock generation
run: make tools && mockery
- name: Check for uncommited files
run: |
export FILES=$(git ls-files -o -m --directory --exclude-standard --no-empty-directory)
export LINES=$(echo "$FILES" | awk 'NF' | wc -l)
if [ $LINES -ne 0 ]; then
echo "Detected files that need to be committed:"
echo "$FILES" | sed -e "s/^/ /"
echo ""
echo "Mock skeletons are not up-to-date, you may have forgotten to run mockery before committing your changes."
exit 1
fi
- name: Build
run: make build
unit-test:
needs: build
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Unit Test
Expand Down Expand Up @@ -70,14 +78,11 @@ jobs:
uses: golangci/golangci-lint-action@v3.7.0
with:
version: v1.55.0
args: --timeout 10m
website-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: website lint
Expand Down
3 changes: 1 addition & 2 deletions .golangci.yml
Expand Up @@ -145,9 +145,8 @@ issues:
text: "^hugeParam: req is heavy"

run:
timeout: 10m
tests: true
build-tags:
- integration
skip-dirs:
- internal/mocks
modules-download-mode: readonly
11 changes: 11 additions & 0 deletions .mockery.yaml
@@ -0,0 +1,11 @@
with-expecter: false
disable-version-string: true
dir: internal/testutil/mocksvc
outpkg: mocksvc
filename: "{{ .InterfaceName | snakecase }}.go"
mockname: "{{.InterfaceName}}"

packages:
? github.com/mongodb/terraform-provider-mongodbatlas/internal/service/searchdeployment
: interfaces:
DeploymentService:
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -303,6 +303,8 @@ To do this you can:
- Helper methods must have their own tests, e.g. `common_advanced_cluster_test.go` has tests for `common_advanced_cluster.go`.
- `internal/testutils/acc` contains helper test methods for Acceptance and Migration tests.
- Tests that need the provider binary like End-to-End tests don’t belong to the source code packages and go in `test/e2e`.
- [Testify Mock](https://pkg.go.dev/github.com/stretchr/testify/mock) and [Mockery](https://github.com/vektra/mockery) are used for test doubles in unit tests. Mocked interfaces are generated in folder `internal/testutil/mocksvc`.


### Creating New Resource and Data Sources

Expand Down
2 changes: 2 additions & 0 deletions GNUmakefile
Expand Up @@ -14,6 +14,7 @@ VERSION=$(GITTAG:v%=%)
LINKER_FLAGS=-s -w -X 'github.com/mongodb/terraform-provider-mongodbatlas/version.ProviderVersion=${VERSION}'

GOLANGCI_VERSION=v1.55.0
MOCKERY_VERSION=v2.38.0

export PATH := $(shell go env GOPATH)/bin:$(PATH)
export SHELL := env PATH=$(PATH) /bin/bash
Expand Down Expand Up @@ -74,6 +75,7 @@ tools: ## Install dev tools
go install github.com/terraform-linters/tflint@v0.49.0
go install github.com/rhysd/actionlint/cmd/actionlint@latest
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
go install github.com/vektra/mockery/v2@$(MOCKERY_VERSION)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin $(GOLANGCI_VERSION)

.PHONY: check
Expand Down
Expand Up @@ -3,155 +3,126 @@ package searchdeployment_test
import (
"context"
"errors"
"log"
"net/http"
"reflect"
"testing"
"time"

"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/retrystrategy"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/searchdeployment"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/mocksvc"
"github.com/stretchr/testify/assert"
"go.mongodb.org/atlas-sdk/v20231115002/admin"
)

type stateTransitionTestCase struct {
expectedResult *admin.ApiSearchDeploymentResponse
name string
mockResponses []SearchDeploymentResponse
expectedError bool
var (
updating = "UPDATING"
idle = "IDLE"
unknown = ""
sc400 = conversion.IntPtr(400)
sc500 = conversion.IntPtr(500)
sc503 = conversion.IntPtr(503)
)

type testCase struct {
expectedState *string
name string
mockResponses []response
expectedError bool
}

func TestSearchDeploymentStateTransition(t *testing.T) {
testCases := []stateTransitionTestCase{
testCases := []testCase{
{
name: "Successful transition to IDLE",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: responseWithState("UPDATING"),
},
{
DeploymentResp: responseWithState("IDLE"),
},
mockResponses: []response{
{state: &updating},
{state: &idle},
},
expectedResult: responseWithState("IDLE"),
expectedError: false,
expectedState: &idle,
expectedError: false,
},
{
name: "Successful transition to IDLE with 503 error in between",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: responseWithState("UPDATING"),
},
{
DeploymentResp: nil,
HTTPResponse: &http.Response{StatusCode: 503},
Err: errors.New("Service Unavailable"),
},
{
DeploymentResp: responseWithState("IDLE"),
},
mockResponses: []response{
{state: &updating},
{statusCode: sc503, err: errors.New("Service Unavailable")},
{state: &idle},
},
expectedResult: responseWithState("IDLE"),
expectedError: false,
expectedState: &idle,
expectedError: false,
},
{
name: "Error when transitioning to an unknown state",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: responseWithState("UPDATING"),
},
{
DeploymentResp: responseWithState(""),
},
mockResponses: []response{
{state: &updating},
{state: &unknown},
},
expectedResult: nil,
expectedError: true,
expectedState: nil,
expectedError: true,
},
{
name: "Error when API responds with error",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: nil,
HTTPResponse: &http.Response{StatusCode: 500},
Err: errors.New("Internal server error"),
},
mockResponses: []response{
{statusCode: sc500, err: errors.New("Internal server error")},
},
expectedResult: nil,
expectedError: true,
expectedState: nil,
expectedError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockService := MockSearchDeploymentService{
MockResponses: tc.mockResponses,
}

resp, err := searchdeployment.WaitSearchNodeStateTransition(context.Background(), dummyProjectID, "Cluster0", &mockService, testTimeoutConfig)

if (err != nil) != tc.expectedError {
t.Errorf("Case %s: Received unexpected error: %v", tc.name, err)
}

if !reflect.DeepEqual(tc.expectedResult, resp) {
t.Errorf("Case %s: Response did not match expected output", tc.name)
svc := mocksvc.NewDeploymentService(t)
ctx := context.Background()
for _, resp := range tc.mockResponses {
svc.On("GetAtlasSearchDeployment", ctx, dummyProjectID, clusterName).Return(resp.get()...).Once()
}
resp, err := searchdeployment.WaitSearchNodeStateTransition(ctx, dummyProjectID, "Cluster0", svc, testTimeoutConfig)
assert.Equal(t, tc.expectedError, err != nil)
assert.Equal(t, responseWithState(tc.expectedState), resp)
svc.AssertExpectations(t)
})
}
}

func TestSearchDeploymentStateTransitionForDelete(t *testing.T) {
testCases := []stateTransitionTestCase{
testCases := []testCase{
{
name: "Regular transition to DELETED",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: responseWithState("UPDATING"),
},
{
DeploymentResp: nil,
HTTPResponse: &http.Response{StatusCode: 400},
Err: errors.New(searchdeployment.SearchDeploymentDoesNotExistsError),
},
mockResponses: []response{
{state: &updating},
{statusCode: sc400, err: errors.New(searchdeployment.SearchDeploymentDoesNotExistsError)},
},
expectedError: false,
},
{
name: "Error when API responds with error",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: nil,
HTTPResponse: &http.Response{StatusCode: 500},
Err: errors.New("Internal server error"),
},
mockResponses: []response{
{statusCode: sc500, err: errors.New("Internal server error")},
},
expectedError: true,
},
{
name: "Failed delete when responding with unknown state",
mockResponses: []SearchDeploymentResponse{
{
DeploymentResp: responseWithState("UPDATING"),
},
{
DeploymentResp: responseWithState(""),
},
mockResponses: []response{
{state: &updating},
{state: &unknown},
},
expectedError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockService := MockSearchDeploymentService{
MockResponses: tc.mockResponses,
}

err := searchdeployment.WaitSearchNodeDelete(context.Background(), dummyProjectID, clusterName, &mockService, testTimeoutConfig)

if (err != nil) != tc.expectedError {
t.Errorf("Case %s: Received unexpected error: %v", tc.name, err)
svc := mocksvc.NewDeploymentService(t)
ctx := context.Background()
for _, resp := range tc.mockResponses {
svc.On("GetAtlasSearchDeployment", ctx, dummyProjectID, clusterName).Return(resp.get()...).Once()
}
err := searchdeployment.WaitSearchNodeDelete(ctx, dummyProjectID, clusterName, svc, testTimeoutConfig)
assert.Equal(t, tc.expectedError, err != nil)
svc.AssertExpectations(t)
})
}
}
Expand All @@ -162,7 +133,10 @@ var testTimeoutConfig = retrystrategy.TimeConfig{
Delay: 0,
}

func responseWithState(state string) *admin.ApiSearchDeploymentResponse {
func responseWithState(state *string) *admin.ApiSearchDeploymentResponse {
if state == nil {
return nil
}
return &admin.ApiSearchDeploymentResponse{
GroupId: admin.PtrString(dummyProjectID),
Id: admin.PtrString(dummyDeploymentID),
Expand All @@ -172,26 +146,20 @@ func responseWithState(state string) *admin.ApiSearchDeploymentResponse {
NodeCount: nodeCount,
},
},
StateName: admin.PtrString(state),
StateName: state,
}
}

type MockSearchDeploymentService struct {
MockResponses []SearchDeploymentResponse
index int
type response struct {
state *string
statusCode *int
err error
}

func (a *MockSearchDeploymentService) GetAtlasSearchDeployment(ctx context.Context, groupID, clusterName string) (*admin.ApiSearchDeploymentResponse, *http.Response, error) {
if a.index >= len(a.MockResponses) {
log.Fatal(errors.New("no more mocked responses available"))
func (r *response) get() []interface{} {
var httpResp *http.Response
if r.statusCode != nil {
httpResp = &http.Response{StatusCode: *r.statusCode}
}
resp := a.MockResponses[a.index]
a.index++
return resp.DeploymentResp, resp.HTTPResponse, resp.Err
}

type SearchDeploymentResponse struct {
DeploymentResp *admin.ApiSearchDeploymentResponse
HTTPResponse *http.Response
Err error
return []interface{}{responseWithState(r.state), httpResp, r.err}
}

0 comments on commit d8643b3

Please sign in to comment.