From 5af0c8a266837658b4b223b7dbeb9268c59ec41f Mon Sep 17 00:00:00 2001 From: Kamil Samigullin Date: Fri, 31 Jul 2020 23:04:56 +0300 Subject: [PATCH] #27: up coverage of provider (grafana) --- internal/cmd/coverage.go | 2 +- internal/cmd/queries.go | 5 +- internal/provider/grafana/contract.go | 10 ++ internal/provider/grafana/dto.go | 10 +- internal/provider/grafana/dto_test.go | 71 ++++++++++- internal/provider/grafana/mocks_test.go | 50 ++++++++ internal/provider/grafana/provider.go | 6 +- internal/provider/grafana/provider_test.go | 117 ++++++++++++++++++ .../provider/grafana/testdata/invalid.json | 1 + .../provider/grafana/testdata/success.json | 2 +- internal/provider/graphite/provider_test.go | 12 +- 11 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 internal/provider/grafana/contract.go create mode 100644 internal/provider/grafana/mocks_test.go create mode 100644 internal/provider/grafana/testdata/invalid.json diff --git a/internal/cmd/coverage.go b/internal/cmd/coverage.go index 516cb79..786f231 100644 --- a/internal/cmd/coverage.go +++ b/internal/cmd/coverage.go @@ -97,7 +97,7 @@ func NewCoverageCommand( return nil }) g.Go(func() error { - provider, err := grafana.New(config.Grafana.URL, logger) + provider, err := grafana.New(config.Grafana.URL, &http.Client{Timeout: time.Second}, logger) if err != nil { return err } diff --git a/internal/cmd/queries.go b/internal/cmd/queries.go index 271eb7d..174720f 100644 --- a/internal/cmd/queries.go +++ b/internal/cmd/queries.go @@ -1,6 +1,9 @@ package cmd import ( + "net/http" + "time" + "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -53,7 +56,7 @@ func NewQueriesCommand( return nil }, RunE: func(cmd *cobra.Command, args []string) error { - provider, err := grafana.New(config.Grafana.URL, logger) + provider, err := grafana.New(config.Grafana.URL, &http.Client{Timeout: time.Second}, logger) if err != nil { return err } diff --git a/internal/provider/grafana/contract.go b/internal/provider/grafana/contract.go new file mode 100644 index 0000000..97232fa --- /dev/null +++ b/internal/provider/grafana/contract.go @@ -0,0 +1,10 @@ +package grafana + +import "net/http" + +//go:generate mockgen -source $GOFILE -destination mocks_test.go -package ${GOPACKAGE}_test + +// Client defines HTTP client interface. +type Client interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/internal/provider/grafana/dto.go b/internal/provider/grafana/dto.go index 37df7cb..1a96b68 100644 --- a/internal/provider/grafana/dto.go +++ b/internal/provider/grafana/dto.go @@ -3,10 +3,8 @@ package grafana import "github.com/kamilsk/grafaman/internal/model" type dashboard struct { - Panels []panel `json:"panels,omitempty"` - Templating struct { - List []variable `json:"list,omitempty"` - } `json:"templating,omitempty"` + Panels []panel `json:"panels,omitempty"` + Templating templating `json:"templating,omitempty"` } type panel struct { @@ -17,6 +15,10 @@ type panel struct { Targets []target `json:"targets,omitempty"` } +type templating struct { + List []variable `json:"list,omitempty"` +} + type target struct { Query string `json:"target,omitempty"` } diff --git a/internal/provider/grafana/dto_test.go b/internal/provider/grafana/dto_test.go index 65f5350..25a3311 100644 --- a/internal/provider/grafana/dto_test.go +++ b/internal/provider/grafana/dto_test.go @@ -43,15 +43,80 @@ func TestDumpStubs(t *testing.T) { fs = afero.NewOsFs() } + type payload struct { + Dashboard dashboard `json:"dashboard,omitempty"` + } + type response struct { - Code int `json:"code,omitempty"` - Body dashboard `json:"body,omitempty"` + Code int `json:"code,omitempty"` + Body payload `json:"body,omitempty"` } t.Run("success", func(t *testing.T) { resp := response{ Code: http.StatusOK, - Body: dashboard{}, + Body: payload{ + Dashboard: dashboard{ + Panels: []panel{ + { + ID: 1, + Title: "Panel A", + Type: "singlestat", + Targets: []target{ + { + Query: "sumSeriesWithWildcards(movingSum(apps.services.*.rpc.*, '1min'), 3, 5)", + }, + }, + }, + { + ID: 2, + Title: "Error rate", + Type: "row", + Panels: []panel{ + { + ID: 3, + Title: "Panel B", + Type: "graph", + Targets: []target{ + { + Query: "aliasByNode(movingSum(apps.services.*.errors.*, '1min'), 3, 6, 5)", + }, + }, + }, + }, + }, + }, + Templating: templating{ + List: []variable{ + { + Name: "env", + Options: []option{}, + Current: currentOption{ + Text: "prod", + Value: "prod", + }, + }, + { + Name: "source", + Options: []option{ + { + Text: "All", + Value: "$__all", + }, + { + Text: "service", + Value: "service", + }, + }, + Current: currentOption{ + Text: "All", + Value: []interface{}{"$__all"}, + }, + }, + }, + }, + }, + }, } file, err := fs.Create("testdata/success.json") diff --git a/internal/provider/grafana/mocks_test.go b/internal/provider/grafana/mocks_test.go new file mode 100644 index 0000000..b681723 --- /dev/null +++ b/internal/provider/grafana/mocks_test.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: contract.go + +// Package grafana_test is a generated GoMock package. +package grafana_test + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Do mocks base method +func (m *MockClient) Do(arg0 *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", arg0) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Do indicates an expected call of Do +func (mr *MockClientMockRecorder) Do(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockClient)(nil).Do), arg0) +} diff --git a/internal/provider/grafana/provider.go b/internal/provider/grafana/provider.go index 8646ccc..396da04 100644 --- a/internal/provider/grafana/provider.go +++ b/internal/provider/grafana/provider.go @@ -20,20 +20,20 @@ import ( ) // New returns an instance of Grafana dashboard provider. -func New(endpoint string, logger *logrus.Logger) (*provider, error) { +func New(endpoint string, client Client, logger *logrus.Logger) (*provider, error) { u, err := url.Parse(endpoint) if err != nil { return nil, errors.Wrap(err, "grafana: prepare dashboard provider endpoint URL") } return &provider{ - client: &http.Client{Timeout: time.Second}, + client: client, endpoint: *u, logger: logger, }, nil } type provider struct { - client *http.Client + client Client endpoint url.URL logger *logrus.Logger } diff --git a/internal/provider/grafana/provider_test.go b/internal/provider/grafana/provider_test.go index 0b89c28..fb87c7f 100644 --- a/internal/provider/grafana/provider_test.go +++ b/internal/provider/grafana/provider_test.go @@ -1 +1,118 @@ package grafana_test + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.octolab.org/safe" + "go.octolab.org/unsafe" + + . "github.com/kamilsk/grafaman/internal/provider/grafana" +) + +func TestProvider(t *testing.T) { + ctx := context.Background() + _ = ctx + + logger := logrus.New() + logger.SetOutput(ioutil.Discard) + + t.Run("success fetch", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := NewMockClient(ctrl) + client.EXPECT(). + Do(gomock.Any()). + Return(response("testdata/success.json")) // nolint:bodyclose + + provider, err := New("test", client, logger) + require.NoError(t, err) + + dashboard, err := provider.Fetch(ctx, "dashboard") + assert.NoError(t, err) + assert.NotNil(t, dashboard) + }) + + t.Run("bad endpoint", func(t *testing.T) { + provider, err := New(":invalid", nil, logger) + assert.Error(t, err) + assert.Nil(t, provider) + }) + + t.Run("nil context", func(t *testing.T) { + provider, err := New("test", nil, logger) + require.NoError(t, err) + + dashboard, err := provider.Fetch(nil, "dashboard") // nolint:staticcheck + assert.Error(t, err) + assert.Nil(t, dashboard) + }) + + t.Run("service unavailable", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := NewMockClient(ctrl) + client.EXPECT(). + Do(gomock.Any()). + Return(nil, errors.New(http.StatusText(http.StatusServiceUnavailable))) + + provider, err := New("test", client, logger) + require.NoError(t, err) + + dashboard, err := provider.Fetch(ctx, "dashboard") + assert.Error(t, err) + assert.Nil(t, dashboard) + }) + + t.Run("bad response", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := NewMockClient(ctrl) + client.EXPECT(). + Do(gomock.Any()). + Return(response("testdata/invalid.json")) // nolint:bodyclose + + provider, err := New("test", client, logger) + require.NoError(t, err) + + dashboard, err := provider.Fetch(ctx, "dashboard") + assert.Error(t, err) + assert.Nil(t, dashboard) + }) +} + +// helpers + +func response(filename string) (*http.Response, error) { + resp := new(http.Response) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer safe.Close(file, unsafe.Ignore) + + var dto struct { + Code int `json:"code,omitempty"` + Body json.RawMessage `json:"body,omitempty"` + } + if err := json.NewDecoder(file).Decode(&dto); err != nil { + return nil, err + } + + resp.StatusCode = dto.Code + resp.Body = ioutil.NopCloser(bytes.NewReader(dto.Body)) + return resp, nil +} diff --git a/internal/provider/grafana/testdata/invalid.json b/internal/provider/grafana/testdata/invalid.json new file mode 100644 index 0000000..aff8e45 --- /dev/null +++ b/internal/provider/grafana/testdata/invalid.json @@ -0,0 +1 @@ +{"code":200,"body":"invalid"} diff --git a/internal/provider/grafana/testdata/success.json b/internal/provider/grafana/testdata/success.json index 80ab2a4..15d3e54 100644 --- a/internal/provider/grafana/testdata/success.json +++ b/internal/provider/grafana/testdata/success.json @@ -1 +1 @@ -{"code":200,"body":{"panels":null,"templating":{"list":null}}} +{"code":200,"body":{"dashboard":{"panels":[{"id":1,"title":"Panel A","type":"singlestat","targets":[{"target":"sumSeriesWithWildcards(movingSum(apps.services.*.rpc.*, '1min'), 3, 5)"}]},{"id":2,"title":"Error rate","type":"row","panels":[{"id":3,"title":"Panel B","type":"graph","targets":[{"target":"aliasByNode(movingSum(apps.services.*.errors.*, '1min'), 3, 6, 5)"}]}]}],"templating":{"list":[{"name":"env","current":{"text":"prod","value":"prod"}},{"name":"source","options":[{"text":"All","value":"$__all"},{"text":"service","value":"service"}],"current":{"text":"All","value":["$__all"]}}]}}}} diff --git a/internal/provider/graphite/provider_test.go b/internal/provider/graphite/provider_test.go index c28b32a..696bcbc 100644 --- a/internal/provider/graphite/provider_test.go +++ b/internal/provider/graphite/provider_test.go @@ -29,12 +29,6 @@ func TestProvider(t *testing.T) { logger := logrus.New() logger.SetOutput(ioutil.Discard) - t.Run("bad endpoint", func(t *testing.T) { - provider, err := New(":invalid", nil, logger) - assert.Error(t, err) - assert.Nil(t, provider) - }) - t.Run("success fetch", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -62,6 +56,12 @@ func TestProvider(t *testing.T) { }, metrics) }) + t.Run("bad endpoint", func(t *testing.T) { + provider, err := New(":invalid", nil, logger) + assert.Error(t, err) + assert.Nil(t, provider) + }) + t.Run("nil context", func(t *testing.T) { provider, err := New("test", nil, logger) require.NoError(t, err)