From b5087da164493e23ec53d8eba010828f46b24ed5 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 4 Dec 2023 16:33:10 -0500 Subject: [PATCH] refactor(GCS): Migrate to using mockery/testify (#25) * Adds `mockery` generated mocks for `cloudcost-exporter` specific interfaces * Removes using mocked interfaces for `pkg/gcp/bucket.go` and favors intercepting HTTP requests * Updates uncaught errors for the List commands in `pkg/gcp/bucket.go` --- .mockery.yaml | 11 ++ go.mod | 1 + go.sum | 1 + main.go | 3 + .../pkg/google/gcs/StorageClientInterface.go | 86 +++++++++ mocks/pkg/provider/Collector.go | 171 ++++++++++++++++++ mocks/pkg/provider/Provider.go | 126 +++++++++++++ pkg/google/gcs/bucket.go | 8 +- pkg/google/gcs/bucket_test.go | 70 ++++--- 9 files changed, 448 insertions(+), 29 deletions(-) create mode 100644 .mockery.yaml create mode 100644 main.go create mode 100644 mocks/pkg/google/gcs/StorageClientInterface.go create mode 100644 mocks/pkg/provider/Collector.go create mode 100644 mocks/pkg/provider/Provider.go diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 00000000..31d1c863 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,11 @@ +with-expecter: True +inpackage: True +dir: mocks/{{ replaceAll .InterfaceDirRelative "internal" "internal_" }} +mockname: "{{.InterfaceName}}" +outpkg: "{{.PackageName}}" +filename: "{{.InterfaceName}}.go" +all: True +packages: + github.com/grafana/cloudcost-exporter: + config: + recursive: True diff --git a/go.mod b/go.mod index 4c32ddb2..c7a05923 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect diff --git a/go.sum b/go.sum index 6d027372..c024dfb0 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,7 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/main.go b/main.go new file mode 100644 index 00000000..957df9f1 --- /dev/null +++ b/main.go @@ -0,0 +1,3 @@ +package cloudcost_exporter + +// This is required for mockery to work: https://vektra.github.io/mockery/v2.38/notes/#error-no-go-files-found-in-root-search-path diff --git a/mocks/pkg/google/gcs/StorageClientInterface.go b/mocks/pkg/google/gcs/StorageClientInterface.go new file mode 100644 index 00000000..988dbcb0 --- /dev/null +++ b/mocks/pkg/google/gcs/StorageClientInterface.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package gcs + +import ( + context "context" + + storage "cloud.google.com/go/storage" + mock "github.com/stretchr/testify/mock" +) + +// StorageClientInterface is an autogenerated mock type for the StorageClientInterface type +type StorageClientInterface struct { + mock.Mock +} + +type StorageClientInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *StorageClientInterface) EXPECT() *StorageClientInterface_Expecter { + return &StorageClientInterface_Expecter{mock: &_m.Mock} +} + +// Buckets provides a mock function with given fields: ctx, projectID +func (_m *StorageClientInterface) Buckets(ctx context.Context, projectID string) *storage.BucketIterator { + ret := _m.Called(ctx, projectID) + + if len(ret) == 0 { + panic("no return value specified for Buckets") + } + + var r0 *storage.BucketIterator + if rf, ok := ret.Get(0).(func(context.Context, string) *storage.BucketIterator); ok { + r0 = rf(ctx, projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*storage.BucketIterator) + } + } + + return r0 +} + +// StorageClientInterface_Buckets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Buckets' +type StorageClientInterface_Buckets_Call struct { + *mock.Call +} + +// Buckets is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +func (_e *StorageClientInterface_Expecter) Buckets(ctx interface{}, projectID interface{}) *StorageClientInterface_Buckets_Call { + return &StorageClientInterface_Buckets_Call{Call: _e.mock.On("Buckets", ctx, projectID)} +} + +func (_c *StorageClientInterface_Buckets_Call) Run(run func(ctx context.Context, projectID string)) *StorageClientInterface_Buckets_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *StorageClientInterface_Buckets_Call) Return(_a0 *storage.BucketIterator) *StorageClientInterface_Buckets_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *StorageClientInterface_Buckets_Call) RunAndReturn(run func(context.Context, string) *storage.BucketIterator) *StorageClientInterface_Buckets_Call { + _c.Call.Return(run) + return _c +} + +// NewStorageClientInterface creates a new instance of StorageClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStorageClientInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *StorageClientInterface { + mock := &StorageClientInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/pkg/provider/Collector.go b/mocks/pkg/provider/Collector.go new file mode 100644 index 00000000..fff36751 --- /dev/null +++ b/mocks/pkg/provider/Collector.go @@ -0,0 +1,171 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package provider + +import ( + prometheus "github.com/prometheus/client_golang/prometheus" + mock "github.com/stretchr/testify/mock" +) + +// Collector is an autogenerated mock type for the Collector type +type Collector struct { + mock.Mock +} + +type Collector_Expecter struct { + mock *mock.Mock +} + +func (_m *Collector) EXPECT() *Collector_Expecter { + return &Collector_Expecter{mock: &_m.Mock} +} + +// Collect provides a mock function with given fields: +func (_m *Collector) Collect() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Collect") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Collector_Collect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Collect' +type Collector_Collect_Call struct { + *mock.Call +} + +// Collect is a helper method to define mock.On call +func (_e *Collector_Expecter) Collect() *Collector_Collect_Call { + return &Collector_Collect_Call{Call: _e.mock.On("Collect")} +} + +func (_c *Collector_Collect_Call) Run(run func()) *Collector_Collect_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Collector_Collect_Call) Return(_a0 error) *Collector_Collect_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Collector_Collect_Call) RunAndReturn(run func() error) *Collector_Collect_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with given fields: +func (_m *Collector) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Collector_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type Collector_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *Collector_Expecter) Name() *Collector_Name_Call { + return &Collector_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *Collector_Name_Call) Run(run func()) *Collector_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Collector_Name_Call) Return(_a0 string) *Collector_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Collector_Name_Call) RunAndReturn(run func() string) *Collector_Name_Call { + _c.Call.Return(run) + return _c +} + +// Register provides a mock function with given fields: _a0 +func (_m *Collector) Register(_a0 *prometheus.Registry) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Register") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*prometheus.Registry) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Collector_Register_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Register' +type Collector_Register_Call struct { + *mock.Call +} + +// Register is a helper method to define mock.On call +// - _a0 *prometheus.Registry +func (_e *Collector_Expecter) Register(_a0 interface{}) *Collector_Register_Call { + return &Collector_Register_Call{Call: _e.mock.On("Register", _a0)} +} + +func (_c *Collector_Register_Call) Run(run func(_a0 *prometheus.Registry)) *Collector_Register_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*prometheus.Registry)) + }) + return _c +} + +func (_c *Collector_Register_Call) Return(_a0 error) *Collector_Register_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Collector_Register_Call) RunAndReturn(run func(*prometheus.Registry) error) *Collector_Register_Call { + _c.Call.Return(run) + return _c +} + +// NewCollector creates a new instance of Collector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCollector(t interface { + mock.TestingT + Cleanup(func()) +}) *Collector { + mock := &Collector{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/pkg/provider/Provider.go b/mocks/pkg/provider/Provider.go new file mode 100644 index 00000000..c1471fbd --- /dev/null +++ b/mocks/pkg/provider/Provider.go @@ -0,0 +1,126 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package provider + +import ( + prometheus "github.com/prometheus/client_golang/prometheus" + mock "github.com/stretchr/testify/mock" +) + +// Provider is an autogenerated mock type for the Provider type +type Provider struct { + mock.Mock +} + +type Provider_Expecter struct { + mock *mock.Mock +} + +func (_m *Provider) EXPECT() *Provider_Expecter { + return &Provider_Expecter{mock: &_m.Mock} +} + +// CollectMetrics provides a mock function with given fields: +func (_m *Provider) CollectMetrics() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CollectMetrics") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Provider_CollectMetrics_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CollectMetrics' +type Provider_CollectMetrics_Call struct { + *mock.Call +} + +// CollectMetrics is a helper method to define mock.On call +func (_e *Provider_Expecter) CollectMetrics() *Provider_CollectMetrics_Call { + return &Provider_CollectMetrics_Call{Call: _e.mock.On("CollectMetrics")} +} + +func (_c *Provider_CollectMetrics_Call) Run(run func()) *Provider_CollectMetrics_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Provider_CollectMetrics_Call) Return(_a0 error) *Provider_CollectMetrics_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Provider_CollectMetrics_Call) RunAndReturn(run func() error) *Provider_CollectMetrics_Call { + _c.Call.Return(run) + return _c +} + +// RegisterCollectors provides a mock function with given fields: registry +func (_m *Provider) RegisterCollectors(registry *prometheus.Registry) error { + ret := _m.Called(registry) + + if len(ret) == 0 { + panic("no return value specified for RegisterCollectors") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*prometheus.Registry) error); ok { + r0 = rf(registry) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Provider_RegisterCollectors_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterCollectors' +type Provider_RegisterCollectors_Call struct { + *mock.Call +} + +// RegisterCollectors is a helper method to define mock.On call +// - registry *prometheus.Registry +func (_e *Provider_Expecter) RegisterCollectors(registry interface{}) *Provider_RegisterCollectors_Call { + return &Provider_RegisterCollectors_Call{Call: _e.mock.On("RegisterCollectors", registry)} +} + +func (_c *Provider_RegisterCollectors_Call) Run(run func(registry *prometheus.Registry)) *Provider_RegisterCollectors_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*prometheus.Registry)) + }) + return _c +} + +func (_c *Provider_RegisterCollectors_Call) Return(_a0 error) *Provider_RegisterCollectors_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Provider_RegisterCollectors_Call) RunAndReturn(run func(*prometheus.Registry) error) *Provider_RegisterCollectors_Call { + _c.Call.Return(run) + return _c +} + +// NewProvider creates a new instance of Provider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *Provider { + mock := &Provider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/google/gcs/bucket.go b/pkg/google/gcs/bucket.go index d768552d..bd10d6f4 100644 --- a/pkg/google/gcs/bucket.go +++ b/pkg/google/gcs/bucket.go @@ -2,6 +2,7 @@ package gcs import ( "context" + "errors" "log" "cloud.google.com/go/storage" @@ -23,16 +24,16 @@ func NewBucketClient(client StorageClientInterface) *BucketClient { } func (bc *BucketClient) list(ctx context.Context, project string) ([]*storage.BucketAttrs, error) { - var buckets []*storage.BucketAttrs log.Printf("Listing buckets for project %s", project) + buckets := make([]*storage.BucketAttrs, 0) it := bc.client.Buckets(ctx, project) for { bucketAttrs, err := it.Next() - if err == iterator.Done { + if errors.Is(err, iterator.Done) { break } if err != nil { - return nil, err + return buckets, err } buckets = append(buckets, bucketAttrs) } @@ -40,6 +41,7 @@ func (bc *BucketClient) list(ctx context.Context, project string) ([]*storage.Bu return buckets, nil } +// TODO: Return an interface of the storage.BucketAttrs func (bc *BucketClient) List(ctx context.Context, project string) ([]*storage.BucketAttrs, error) { return bc.list(ctx, project) } diff --git a/pkg/google/gcs/bucket_test.go b/pkg/google/gcs/bucket_test.go index d33d7426..12d1e55c 100644 --- a/pkg/google/gcs/bucket_test.go +++ b/pkg/google/gcs/bucket_test.go @@ -2,9 +2,16 @@ package gcs import ( "context" + "net/http" + "net/http/httptest" "testing" "cloud.google.com/go/storage" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + + "github.com/grafana/cloudcost-exporter/mocks/pkg/google/gcs" ) func TestNewBucketClient(t *testing.T) { @@ -12,7 +19,7 @@ func TestNewBucketClient(t *testing.T) { client StorageClientInterface }{ "Empty client": { - client: &MockStorageClient{}, + client: gcs.NewStorageClientInterface(t), }, } for name, test := range tests { @@ -25,42 +32,53 @@ func TestNewBucketClient(t *testing.T) { } } -type MockStorageClient struct { - BucketsFunc func(ctx context.Context, projectID string) *storage.BucketIterator -} - -func (m *MockStorageClient) Buckets(ctx context.Context, projectID string) *storage.BucketIterator { - return m.BucketsFunc(ctx, projectID) -} - func TestBucketClient_List(t *testing.T) { - mockClient := &MockStorageClient{ - BucketsFunc: func(ctx context.Context, projectID string) *storage.BucketIterator { - return &storage.BucketIterator{} - }, - } tests := map[string]struct { - client *BucketClient + server *httptest.Server projects []string - want []*storage.BucketAttrs + want int + wantErr bool }{ "no projects should result in no results": { - client: NewBucketClient(mockClient), - projects: []string{}, + projects: []string{"project-1"}, + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"items": []}`)) + })), + want: 0, + wantErr: false, + }, + "one item should result in one bucket": { + projects: []string{"project-1"}, + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"items": [{"name": "testing-123"}]}`)) + })), + want: 1, + wantErr: false, + }, + "An error should be handled": { + projects: []string{"project-1"}, + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(``)) + }, + )), + want: 0, + wantErr: true, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { for _, project := range test.projects { - got, err := test.client.List(context.Background(), project) - if err != nil { - t.Errorf("unexpected error: %s", err) - } - if len(got) != len(test.want) { - t.Errorf("expected %d buckets, got %d", len(test.want), len(got)) - } + sc, err := storage.NewClient(context.Background(), option.WithEndpoint(test.server.URL), option.WithAPIKey("hunter2")) + require.NoError(t, err) + bc := NewBucketClient(sc) + got, err := bc.List(context.Background(), project) + assert.Equal(t, test.wantErr, err != nil) + assert.NotNil(t, got) + assert.Equal(t, test.want, len(got)) } - }) } }