test coverage

This commit is contained in:
Pavel 2023-01-09 15:51:07 +03:00
parent c09319ce8c
commit 6d1ce327e7
7 changed files with 620 additions and 4 deletions

View File

@ -0,0 +1,122 @@
package healthcheck
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type AtomicCounterTest struct {
suite.Suite
}
func TestAtomicCounter(t *testing.T) {
suite.Run(t, new(AtomicCounterTest))
}
func (t *AtomicCounterTest) new() Counter {
return NewAtomicCounter("test")
}
func (t *AtomicCounterTest) Test_Name() {
t.Assert().Equal("test", t.new().Name())
}
func (t *AtomicCounterTest) Test_SetName() {
c := t.new()
c.SetName("new")
t.Assert().Equal("new", c.Name())
}
func (t *AtomicCounterTest) Test_HitSuccess() {
c := t.new()
c.HitSuccess()
t.Assert().Equal(uint32(1), c.TotalSucceeded())
c.Failed("test")
c.FailureProcessed()
c.HitSuccess()
t.Assert().Equal(uint32(2), c.TotalSucceeded())
t.Assert().False(c.IsFailed())
t.Assert().False(c.IsFailureProcessed())
t.Assert().Equal("", c.Message())
}
func (t *AtomicCounterTest) Test_HitFailure() {
c := t.new()
c.HitFailure()
t.Assert().Equal(uint32(1), c.TotalFailed())
c.HitFailure()
t.Assert().Equal(uint32(2), c.TotalFailed())
}
func (t *AtomicCounterTest) Test_Failed() {
c := t.new()
t.Require().False(c.IsFailed())
t.Require().Equal("", c.Message())
c.Failed("message")
t.Assert().True(c.IsFailed())
t.Assert().Equal("message", c.Message())
}
func (t *AtomicCounterTest) Test_CountersProcessed() {
c := t.new()
t.Require().False(c.IsCountersProcessed())
c.CountersProcessed()
t.Assert().True(c.IsCountersProcessed())
c.ClearCountersProcessed()
t.Assert().False(c.IsCountersProcessed())
}
func (t *AtomicCounterTest) Test_FlushCounters() {
c := t.new()
c.HitSuccess()
t.Require().Equal(uint32(1), c.TotalSucceeded())
c.FlushCounters()
t.Assert().Equal(uint32(1), c.TotalSucceeded())
c.(*AtomicCounter).timestamp.Store(time.Now().Add(-(DefaultResetPeriod + time.Second)))
c.FlushCounters()
t.Assert().Equal(uint32(0), c.TotalSucceeded())
}
func (t *AtomicCounterTest) Test_Concurrency() {
c := t.new()
var wg sync.WaitGroup
wg.Add(2)
go func() {
for i := 0; i < 1000; i++ {
c.HitSuccess()
}
wg.Done()
}()
go func() {
for i := 0; i < 500; i++ {
// this delay will ensure that failure is being called after success.
// technically, both have been executed concurrently because first 399 calls will not be delayed.
if i > 399 {
time.Sleep(time.Microsecond)
}
if i > 400 {
c.Failed("total failure")
continue
}
c.HitFailure()
}
c.FailureProcessed()
wg.Done()
}()
wg.Wait()
t.Assert().Equal(uint32(1000), c.TotalSucceeded())
t.Assert().Equal(uint32(401), c.TotalFailed())
t.Assert().True(c.IsFailed())
t.Assert().True(c.IsFailureProcessed())
t.Assert().Equal("total failure", c.Message())
}

View File

@ -0,0 +1,65 @@
package healthcheck
import (
"encoding/json"
"net/http"
"net/url"
"testing"
retailcrm "github.com/retailcrm/api-client-go/v2"
"github.com/retailcrm/mg-transport-core/v2/core/util/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/h2non/gock.v1"
)
func TestDefaultNotifyFunc(t *testing.T) {
apiURL := "https://test.retailcrm.pro"
apiKey := "key"
msg := "Notification"
data, err := json.Marshal(retailcrm.NotificationsSendRequest{
UserGroups: []retailcrm.UserGroupType{retailcrm.UserGroupSuperadmins},
Type: retailcrm.NotificationTypeError,
Message: msg,
})
require.NoError(t, err)
defer gock.Off()
gock.New(apiURL).
Post("/api/v5/notifications/send").
BodyString(url.Values{"notification": {string(data)}}.Encode()).
Reply(http.StatusOK).
JSON(retailcrm.SuccessfulResponse{Success: true})
assert.NoError(t, DefaultNotifyFunc(apiURL, apiKey, msg))
testutil.AssertNoUnmatchedRequests(t)
}
func TestDefaultNotifyFunc_Error(t *testing.T) {
apiURL := "https://test.retailcrm.pro"
apiKey := "key"
msg := "Notification"
data, err := json.Marshal(retailcrm.NotificationsSendRequest{
UserGroups: []retailcrm.UserGroupType{retailcrm.UserGroupSuperadmins},
Type: retailcrm.NotificationTypeError,
Message: msg,
})
require.NoError(t, err)
defer gock.Off()
gock.New(apiURL).
Post("/api/v5/notifications/send").
BodyString(url.Values{"notification": {string(data)}}.Encode()).
Reply(http.StatusForbidden).
JSON(retailcrm.ErrorResponse{
SuccessfulResponse: retailcrm.SuccessfulResponse{Success: false},
ErrorMessage: "Forbidden",
})
err = DefaultNotifyFunc(apiURL, apiKey, msg)
assert.Error(t, err)
assert.Equal(t, "Forbidden", err.Error())
testutil.AssertNoUnmatchedRequests(t)
}

View File

@ -1,13 +1,9 @@
package healthcheck
import (
"errors"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
)
var ErrNoConnection = errors.New("no connection")
const (
// DefaultMinRequests is a default minimal threshold of total requests. If Counter has less than this amount of requests
// total, it will be skipped because it can trigger false alerts otherwise.

View File

@ -0,0 +1,363 @@
package healthcheck
import (
"encoding/json"
"errors"
"fmt"
"testing"
"github.com/retailcrm/mg-transport-core/v2/core/util/testutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type CounterProcessorTest struct {
suite.Suite
apiURL string
apiKey string
lang string
}
func TestCounterProcessor(t *testing.T) {
suite.Run(t, new(CounterProcessorTest))
}
func (t *CounterProcessorTest) SetupSuite() {
t.apiURL = "https://test.retailcrm.pro"
t.apiKey = "key"
t.lang = "en"
}
func (t *CounterProcessorTest) localizer() NotifyMessageLocalizer {
loc := &localizerMock{}
loc.On("SetLocale", mock.AnythingOfType("string")).Return()
loc.On("GetLocalizedTemplateMessage",
mock.AnythingOfType("string"), mock.Anything).Return(
func(msg string, tpl map[string]interface{}) string {
data, err := json.Marshal(tpl)
if err != nil {
panic(err)
}
return fmt.Sprintf("%s [%s]", msg, string(data))
})
return loc
}
func (t *CounterProcessorTest) new(
nf NotifyFunc, pr ConnectionDataProvider, noLocalizer ...bool) (Processor, testutil.BufferedLogger) {
loc := t.localizer()
if len(noLocalizer) > 0 && noLocalizer[0] {
loc = nil
}
log := testutil.NewBufferedLogger()
return CounterProcessor{
Localizer: loc,
Logger: log,
Notifier: nf,
ConnectionDataProvider: pr,
Error: "default error",
FailureThreshold: DefaultFailureThreshold,
MinRequests: DefaultMinRequests,
Debug: true,
}, log
}
func (t *CounterProcessorTest) notifier(err ...error) *notifierMock {
if len(err) > 0 && err[0] != nil {
return &notifierMock{err: err[0]}
}
return &notifierMock{}
}
func (t *CounterProcessorTest) provider(notFound ...bool) ConnectionDataProvider {
if len(notFound) > 0 && notFound[0] {
return func(id int) (apiURL, apiKey, lang string, exists bool) {
return "", "", "", false
}
}
return func(id int) (apiURL, apiKey, lang string, exists bool) {
return t.apiURL, t.apiKey, t.lang, true
}
}
func (t *CounterProcessorTest) counter() mockedCounter {
return &counterMock{}
}
func (t *CounterProcessorTest) Test_FailureProcessed() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(true)
c.On("IsFailureProcessed").Return(true)
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "skipping counter id=1 because its failure is already processed")
}
func (t *CounterProcessorTest) Test_CounterFailed_CannotFindConnection() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider(true))
c := t.counter()
c.On("IsFailed").Return(true)
c.On("IsFailureProcessed").Return(false)
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
}
func (t *CounterProcessorTest) Test_CounterFailed_ErrWhileNotifying() {
n := t.notifier(errors.New("http status code: 500"))
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(true)
c.On("IsFailureProcessed").Return(false)
c.On("Message").Return("error message")
c.On("FailureProcessed").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: http status code: 500 (message: error message)")
t.Assert().Equal(t.apiURL, n.apiURL)
t.Assert().Equal(t.apiKey, n.apiKey)
t.Assert().Equal("error message", n.message)
}
func (t *CounterProcessorTest) Test_CounterFailed_SentNotification() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(true)
c.On("IsFailureProcessed").Return(false)
c.On("Message").Return("error message")
c.On("FailureProcessed").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Empty(log.String())
t.Assert().Equal(t.apiURL, n.apiURL)
t.Assert().Equal(t.apiKey, n.apiKey)
t.Assert().Equal("error message", n.message)
}
func (t *CounterProcessorTest) Test_TooFewRequests() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(0))
c.On("TotalSucceeded").Return(uint32(DefaultMinRequests - 1))
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(),
fmt.Sprintf("skipping counter id=%d because it has fewer than %d requests", 1, DefaultMinRequests))
}
func (t *CounterProcessorTest) Test_ThresholdNotPassed() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(20))
c.On("TotalSucceeded").Return(uint32(80))
c.On("ClearCountersProcessed").Return()
c.On("FlushCounters").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Empty(log.String())
t.Assert().Empty(n.message)
}
func (t *CounterProcessorTest) Test_ThresholdPassed_AlreadyProcessed() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(21))
c.On("TotalSucceeded").Return(uint32(79))
c.On("IsCountersProcessed").Return(true)
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Empty(log.String())
t.Assert().Empty(n.message)
}
func (t *CounterProcessorTest) Test_ThresholdPassed_NoConnectionFound() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider(true))
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(21))
c.On("TotalSucceeded").Return(uint32(79))
c.On("IsCountersProcessed").Return(false)
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot find connection data for counter id=1")
t.Assert().Empty(n.message)
}
func (t *CounterProcessorTest) Test_ThresholdPassed_NotifyingError() {
n := t.notifier(errors.New("unknown error"))
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(21))
c.On("TotalSucceeded").Return(uint32(79))
c.On("IsCountersProcessed").Return(false)
c.On("Name").Return("MockedCounter")
c.On("Message").Return("")
c.On("CountersProcessed").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Contains(log.String(), "cannot send notification for counter id=1: unknown error (message: )")
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
}
func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider())
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(21))
c.On("TotalSucceeded").Return(uint32(79))
c.On("IsCountersProcessed").Return(false)
c.On("Name").Return("MockedCounter")
c.On("CountersProcessed").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Empty(log.String())
t.Assert().Equal(`default error [{"Name":"MockedCounter"}]`, n.message)
}
func (t *CounterProcessorTest) Test_ThresholdPassed_NotificationSent_NoLocalizer() {
n := t.notifier()
p, log := t.new(n.Notify, t.provider(), true)
c := t.counter()
c.On("IsFailed").Return(false)
c.On("TotalFailed").Return(uint32(21))
c.On("TotalSucceeded").Return(uint32(79))
c.On("IsCountersProcessed").Return(false)
c.On("Name").Return("MockedCounter")
c.On("CountersProcessed").Return()
p.Process(1, c)
c.AssertExpectations(t.T())
t.Assert().Empty(log.String())
t.Assert().Equal(`default error`, n.message)
}
type localizerMock struct {
mock.Mock
}
func (l *localizerMock) SetLocale(lang string) {
l.Called(lang)
}
func (l *localizerMock) GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string {
args := l.Called(messageID, templateData)
if fn, ok := args.Get(0).(func(string, map[string]interface{}) string); ok {
return fn(messageID, templateData)
}
return args.String(0)
}
type mockedCounter interface {
Counter
On(methodName string, arguments ...interface{}) *mock.Call
AssertExpectations(t mock.TestingT) bool
}
type counterMock struct {
mock.Mock
}
func (cm *counterMock) Name() string {
args := cm.Called()
return args.String(0)
}
func (cm *counterMock) SetName(name string) {
cm.Called(name)
}
func (cm *counterMock) HitSuccess() {
cm.Called()
}
func (cm *counterMock) HitFailure() {
cm.Called()
}
func (cm *counterMock) TotalSucceeded() uint32 {
args := cm.Called()
return args.Get(0).(uint32)
}
func (cm *counterMock) TotalFailed() uint32 {
args := cm.Called()
return args.Get(0).(uint32)
}
func (cm *counterMock) Message() string {
args := cm.Called()
return args.String(0)
}
func (cm *counterMock) IsFailed() bool {
args := cm.Called()
return args.Bool(0)
}
func (cm *counterMock) Failed(message string) {
cm.Called(message)
}
func (cm *counterMock) IsFailureProcessed() bool {
args := cm.Called()
return args.Bool(0)
}
func (cm *counterMock) IsCountersProcessed() bool {
args := cm.Called()
return args.Bool(0)
}
func (cm *counterMock) FailureProcessed() {
cm.Called()
}
func (cm *counterMock) CountersProcessed() {
cm.Called()
}
func (cm *counterMock) ClearCountersProcessed() {
cm.Called()
}
func (cm *counterMock) FlushCounters() {
cm.Called()
}
type notifierMock struct {
apiURL string
apiKey string
message string
err error
}
func (n *notifierMock) Notify(apiURL, apiKey, msg string) error {
n.apiURL = apiURL
n.apiKey = apiKey
n.message = msg
return n.err
}

View File

@ -0,0 +1,68 @@
package healthcheck
import (
"sync"
"testing"
"github.com/stretchr/testify/suite"
)
type SyncMapStorageTest struct {
suite.Suite
storage Storage
}
func TestSyncMapStorage(t *testing.T) {
suite.Run(t, new(SyncMapStorageTest))
}
func (t *SyncMapStorageTest) SetupSuite() {
t.storage = NewSyncMapStorage(NewAtomicCounter)
}
func (t *SyncMapStorageTest) Test_Get() {
counter := t.storage.Get(1, "Name")
t.Assert().NotNil(counter)
t.Assert().IsType(&AtomicCounter{}, counter)
t.Assert().Equal("Name", counter.Name())
newCounter := t.storage.Get(1, "New Name")
t.Assert().Equal(counter, newCounter)
t.Assert().Equal("New Name", newCounter.Name())
}
func (t *SyncMapStorageTest) Test_Process() {
var wg sync.WaitGroup
wg.Add(1)
t.storage.Process(storageCallbackProcessor{callback: func(id int, counter Counter) bool {
t.Assert().Equal(1, id)
t.Assert().Equal("New Name", counter.Name())
wg.Done()
return false
}})
wg.Wait()
}
func (t *SyncMapStorageTest) Test_Remove() {
defer func() {
if r := recover(); r != nil {
t.Fail("unexpected panic:", r)
}
}()
t.storage.Remove(0)
t.storage.Remove(-1)
t.storage.Remove(1)
t.storage.Process(storageCallbackProcessor{callback: func(id int, counter Counter) bool {
t.Fail("did not expect any items:", id, counter)
return false
}})
}
type storageCallbackProcessor struct {
callback func(id int, counter Counter) bool
}
func (p storageCallbackProcessor) Process(id int, counter Counter) bool {
return p.callback(id, counter)
}

1
go.mod
View File

@ -69,6 +69,7 @@ require (
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect

1
go.sum
View File

@ -385,6 +385,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=