Skip to content

Commit

Permalink
Delay for mocked response (#11)
Browse files Browse the repository at this point in the history
* Created mock delay bechavior

* Created added graceful shutdown support

* Disable maintidx

* Added tests for decode hook

* Added tests for context cancelation
  • Loading branch information
Evgeny Abramovich committed Mar 19, 2023
1 parent a87b8f5 commit 1cdcd43
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 4 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ linters:
- gofumpt
- nolintlint
- tagliatelle
- maintidx
linters-settings:
varnamelen:
ignore-names:
Expand Down
9 changes: 7 additions & 2 deletions internal/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package configuration
import (
"fmt"

"github.com/evg4b/uncors/internal/configuration/hooks"
"github.com/evg4b/uncors/internal/middlewares/mock"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -53,8 +55,11 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig
return nil, fmt.Errorf("filed to read config file '%s': %w", configPath, err)
}
}

if err := viperInstance.Unmarshal(configuration); err != nil {
configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
))
if err := viperInstance.Unmarshal(configuration, configOption); err != nil {
return nil, fmt.Errorf("filed parsing configuraion: %w", err)
}

Expand Down
25 changes: 25 additions & 0 deletions internal/configuration/hooks/time_decode_hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package hooks

import (
"reflect"
"strings"
"time"

"github.com/mitchellh/mapstructure"
)

func StringToTimeDurationHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}

if t != reflect.TypeOf(time.Second) {
return data, nil
}

trimmed := strings.ReplaceAll(data.(string), " ", "") //nolint: forcetypeassert

return time.ParseDuration(trimmed) //nolint:wrapcheck
}
}
103 changes: 103 additions & 0 deletions internal/configuration/hooks/time_decode_hook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package hooks_test

import (
"testing"
"time"

"github.com/evg4b/uncors/internal/configuration/hooks"
"github.com/evg4b/uncors/testing/testutils"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

func TestStringToTimeDurationHookFunc(t *testing.T) {
const key = "duration"
viperInstance := viper.New()
configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.StringToTimeDurationHookFunc(),
mapstructure.OrComposeDecodeHookFunc(
mapstructure.StringToSliceHookFunc(","),
mapstructure.StringToSliceHookFunc(", "),
),
))

t.Run("correct parse different formats", func(t *testing.T) {
tests := []struct {
name string
value string
expected time.Duration
}{
{
name: "duration with spaces",
value: "1m 4s",
expected: 1*time.Minute + 4*time.Second,
},
{
name: "duration without spaces",
value: "3h6m13s",
expected: 3*time.Hour + 6*time.Minute + 13*time.Second,
},
{
name: "duration with mixed spaces",
value: "1h 3m59s 40ms",
expected: 1*time.Hour + 3*time.Minute + 59*time.Second + 40*time.Millisecond,
},
}

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
viperInstance.Set(key, testCase.value)

durationValue := time.Duration(0)
err := viperInstance.UnmarshalKey(key, &durationValue, configOption)
testutils.CheckNoError(t, err)

assert.Equal(t, testCase.expected, durationValue)
})
}
})

t.Run("doesnt not affected other type parses", func(t *testing.T) {
t.Run("string to string", func(t *testing.T) {
viperInstance.Set(key, "value")

stringValue := ""
err := viperInstance.UnmarshalKey(key, &stringValue, configOption)
testutils.CheckNoError(t, err)

assert.Equal(t, "value", stringValue)
})

t.Run("string to []string", func(t *testing.T) {
viperInstance.Set(key, "value,value2")

stringValue := []string{}
err := viperInstance.UnmarshalKey(key, &stringValue, configOption)
testutils.CheckNoError(t, err)

assert.Equal(t, []string{"value", "value2"}, stringValue)
})

t.Run("number to string", func(t *testing.T) {
viperInstance.Set(key, 11)

stringValue := ""
err := viperInstance.UnmarshalKey(key, &stringValue, configOption)
testutils.CheckNoError(t, err)

assert.Equal(t, "11", stringValue)
})

t.Run("number to duration", func(t *testing.T) {
const expected = 14 * time.Minute
viperInstance.Set(key, int(expected))

durationValue := time.Nanosecond
err := viperInstance.UnmarshalKey(key, &durationValue, configOption)
testutils.CheckNoError(t, err)

assert.Equal(t, expected, durationValue)
})
})
}
16 changes: 16 additions & 0 deletions internal/middlewares/mock/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mock

import (
"net/http"
"time"

"github.com/evg4b/uncors/internal/contracts"
"github.com/evg4b/uncors/internal/infrastructure"
Expand All @@ -12,11 +13,26 @@ type internalHandler struct {
response Response
logger contracts.Logger
fs afero.Fs
after func(duration time.Duration) <-chan time.Time
}

func (handler *internalHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
response := handler.response
header := writer.Header()

if response.Delay > 0 {
handler.logger.Debugf("Delay %s for %s", response.Delay, request.URL.RequestURI())
ctx := request.Context()
select {
case <-ctx.Done():
writer.WriteHeader(http.StatusServiceUnavailable)
handler.logger.Debugf("Delay for %s canceled", request.URL.RequestURI())

return
case <-handler.after(response.Delay):
}
}

infrastructure.WriteCorsHeaders(header)
for key, value := range response.Headers {
header.Set(key, value)
Expand Down
105 changes: 105 additions & 0 deletions internal/middlewares/mock/handler_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package mock

import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"

"github.com/evg4b/uncors/testing/mocks"
"github.com/evg4b/uncors/testing/testutils"
Expand Down Expand Up @@ -43,6 +46,9 @@ func TestHandler(t *testing.T) {
logger: mocks.NewNoopLogger(t),
response: response,
fs: fileSystem,
after: func(duration time.Duration) <-chan time.Time {
return time.After(time.Nanosecond)
},
}
}

Expand Down Expand Up @@ -260,4 +266,103 @@ func TestHandler(t *testing.T) {
})
}
})

t.Run("mock response delay", func(t *testing.T) {
t.Run("correctly handle delay", func(t *testing.T) {
tests := []struct {
name string
response Response
shouldBeCalled bool
expected time.Duration
}{
{
name: "3s delay",
response: Response{
Code: http.StatusCreated,
Delay: 3 * time.Second,
},
shouldBeCalled: true,
expected: 3 * time.Second,
},
{
name: "15h delay",
response: Response{
Code: http.StatusCreated,
Delay: 15 * time.Hour,
},
shouldBeCalled: true,
expected: 15 * time.Hour,
},
{
name: "0s delay",
response: Response{
Code: http.StatusCreated,
Delay: 0 * time.Second,
},
shouldBeCalled: false,
},
{
name: "delay is not set",
response: Response{
Code: http.StatusCreated,
},
shouldBeCalled: false,
},
{
name: "incorrect delay",
response: Response{
Code: http.StatusCreated,
Delay: -13 * time.Minute,
},
shouldBeCalled: false,
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
called := false
handler := makeHandler(t, testCase.response)
handler.after = func(duration time.Duration) <-chan time.Time {
assert.Equal(t, duration, testCase.expected)
called = true

return time.After(time.Nanosecond)
}

request := httptest.NewRequest(http.MethodGet, "/", nil)
recorder := httptest.NewRecorder()

handler.ServeHTTP(recorder, request)

assert.Equal(t, called, testCase.shouldBeCalled)
})
}
})

t.Run("correctly cancel delay", func(t *testing.T) {
handler := makeHandler(t, Response{
Code: http.StatusOK,
Delay: 1 * time.Hour,
RawContent: "Text content",
})
handler.after = time.After

request := httptest.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
recorder := httptest.NewRecorder()

var waitGroup sync.WaitGroup
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
handler.ServeHTTP(recorder, request.WithContext(ctx))
}()

cancel()

waitGroup.Wait()

assert.Equal(t, testutils.ReadBody(t, recorder), "")
assert.Equal(t, recorder.Code, http.StatusServiceUnavailable)
})
})
}
4 changes: 3 additions & 1 deletion internal/middlewares/mock/make_mocked_routes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mock

import (
"time"

"github.com/gorilla/mux"
)

Expand Down Expand Up @@ -28,7 +30,7 @@ func (m *Middleware) makeMockedRoutes() {
}

func (m *Middleware) makeHandler(response Response) *internalHandler {
return &internalHandler{response, m.logger, m.fs}
return &internalHandler{response, m.logger, m.fs, time.After}
}

func setPath(route *mux.Route, path string) {
Expand Down
1 change: 0 additions & 1 deletion internal/middlewares/mock/middleware_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//nolint:maintidx
package mock_test

import (
Expand Down
5 changes: 5 additions & 0 deletions internal/middlewares/mock/model.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package mock

import (
"time"
)

type Response struct {
Code int `mapstructure:"code"`
Headers map[string]string `mapstructure:"headers"`
RawContent string `mapstructure:"raw-content"`
File string `mapstructure:"file"`
Delay time.Duration `mapstructure:"delay"`
}

type Mock struct {
Expand Down

0 comments on commit 1cdcd43

Please sign in to comment.