Skip to content

Commit

Permalink
expose request time with testing and cache support
Browse files Browse the repository at this point in the history
  • Loading branch information
StephanHCB committed Apr 10, 2022
1 parent 3a0a6bd commit 0adfae6
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 2 deletions.
3 changes: 3 additions & 0 deletions api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aurestclientapi
import (
"context"
"net/http"
"time"
)

var ContentTypeApplicationJson = "application/json"
Expand All @@ -13,6 +14,8 @@ type ParsedResponse struct {
Body interface{}
Status int
Header http.Header
// Time is set to the time the request was made. Even when it comes from cache, it will be set to the original time.
Time time.Time
}

// Client is a utility class representing a http client.
Expand Down
8 changes: 6 additions & 2 deletions implementation/caching/caching.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type CachingImpl struct {
CacheKeyFunction aurestclientapi.CacheKeyFunction
RetentionTime time.Duration
Cache *tinylru.LRU
// Now is exposed so tests can fixate the time by overwriting this field
Now func() time.Time
}

type CacheEntry struct {
Expand Down Expand Up @@ -49,6 +51,7 @@ func New(
CacheKeyFunction: cacheKeyFunction,
RetentionTime: retentionTime,
Cache: cache,
Now: time.Now,
}
}

Expand All @@ -64,11 +67,12 @@ func (c *CachingImpl) Perform(ctx context.Context, method string, requestUrl str
if ok {
cachedResponse, ok := cachedResponseRaw.(CacheEntry)
if ok {
age := time.Now().Sub(cachedResponse.Recorded)
age := c.Now().Sub(cachedResponse.Recorded)
if age < c.RetentionTime {
err := json.Unmarshal(cachedResponse.ResponseBodyJson, response.Body)
err2 := json.Unmarshal(cachedResponse.ResponseHeaderJson, &response.Header)
response.Status = cachedResponse.ResponseStatus
response.Time = cachedResponse.Recorded
if err == nil && err2 == nil {
// cache successfully used
aulogging.Logger.Ctx(ctx).Info().Printf("downstream %s %s -> %d cached %d seconds ago", method, requestUrl, response.Status, age.Milliseconds()/1000)
Expand All @@ -90,7 +94,7 @@ func (c *CachingImpl) Perform(ctx context.Context, method string, requestUrl str
status := response.Status
if err == nil && err2 == nil {
_, _ = c.Cache.Set(key, CacheEntry{
Recorded: time.Now(),
Recorded: response.Time,
ResponseBodyJson: bodyJson,
ResponseHeaderJson: headerJson,
ResponseStatus: status,
Expand Down
39 changes: 39 additions & 0 deletions implementation/caching/caching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,42 @@ func TestCacheDeletion(t *testing.T) {
require.Equal(t, "&[first second]", fmt.Sprintf("%v", response.Body))
require.Equal(t, []string{"GET http://cache-me <nil>"}, aurestcapture.GetRecording(mock))
}

func TestCacheDeletionCorruptJson(t *testing.T) {
aulogging.SetupNoLoggerForTesting()

mock := tstMock()
cut := tstCut(mock)

aurestcapture.ResetRecording(mock)
response := &aurestclientapi.ParsedResponse{
Body: &[]string{},
}
err := cut.Perform(context.Background(), "GET", "http://cache-me", nil, response)
require.Nil(t, err)
require.Equal(t, "&[first second]", fmt.Sprintf("%v", response.Body))
require.Equal(t, []string{"GET http://cache-me <nil>"}, aurestcapture.GetRecording(mock))

// now let's manipulate the cache so the json is syntactically invalid
cache := cut.(*CachingImpl).Cache
key := defaultKeyFunction(nil, "GET", "http://cache-me", nil)
entryRaw, ok := cache.Get(key)
require.True(t, ok)
entry := entryRaw.(CacheEntry)
entry.ResponseBodyJson = []byte("not a valid json")
cache.Set(key, entry)

// now try a second time. Since the cache entry is invalid json, parsing it will fail and the request
// will go out despite the cache entry
aurestcapture.ResetRecording(mock)
response = &aurestclientapi.ParsedResponse{
Body: &[]string{},
}
err = cut.Perform(context.Background(), "GET", "http://cache-me", nil, response)
require.Nil(t, err)
require.Equal(t, "&[first second]", fmt.Sprintf("%v", response.Body))
require.Equal(t, []string{"GET http://cache-me <nil>"}, aurestcapture.GetRecording(mock))

// and the entry should have been removed from the cache (but we can't test this because it will just
// have been added again)
}
6 changes: 6 additions & 0 deletions implementation/httpclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type HttpClientImpl struct {
HttpClient *http.Client
RequestManipulator aurestclientapi.RequestManipulatorCallback
Timeout time.Duration
// Now is exposed so tests can fixate the time by overwriting this field
Now func() time.Time
}

// New builds a new http client.
Expand Down Expand Up @@ -48,13 +50,15 @@ func New(timeout time.Duration, customCACert []byte, requestManipulator aurestcl
Timeout: timeout,
},
RequestManipulator: requestManipulator,
Now: time.Now,
}, nil
} else {
return &HttpClientImpl{
HttpClient: &http.Client{
Timeout: timeout,
},
RequestManipulator: requestManipulator,
Now: time.Now,
}, nil
}
}
Expand All @@ -78,6 +82,8 @@ func (c *HttpClientImpl) Perform(ctx context.Context, method string, requestUrl
c.RequestManipulator(ctx, req)
}

response.Time = c.Now()

responseInternal, err := c.HttpClient.Do(req)
if err != nil {
switch err.(type) {
Expand Down
5 changes: 5 additions & 0 deletions implementation/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import (
"errors"
"fmt"
aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api"
"time"
)

type MockImpl struct {
mockResponses map[string]aurestclientapi.ParsedResponse
mockErrors map[string]error
// Now is exposed so tests can fixate the time by overwriting this field
Now func() time.Time
}

func New(mockResponses map[string]aurestclientapi.ParsedResponse, mockErrors map[string]error) aurestclientapi.Client {
return &MockImpl{
mockResponses: mockResponses,
mockErrors: mockErrors,
Now: time.Now,
}
}

Expand All @@ -32,6 +36,7 @@ func (c *MockImpl) Perform(ctx context.Context, method string, requestUrl string
if ok {
response.Header = mockResponse.Header
response.Status = mockResponse.Status
response.Time = c.Now()
if response.Body != nil && mockResponse.Body != nil {
// copy over through json round trip
marshalled, _ := json.Marshal(mockResponse.Body)
Expand Down
5 changes: 5 additions & 0 deletions implementation/playback/playback.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import (
aurestrecorder "github.com/StephanHCB/go-autumn-restclient/implementation/recorder"
"os"
"strings"
"time"
)

type PlaybackImpl struct {
RecorderPath string
// Now is exposed so tests can fixate the time by overwriting this field
Now func() time.Time
}

// New builds a new http client simulator based on playback.
Expand All @@ -24,6 +27,7 @@ func New(recorderPath string) aurestclientapi.Client {
}
return &PlaybackImpl{
RecorderPath: recorderPath,
Now: time.Now,
}
}

Expand All @@ -46,6 +50,7 @@ func (c *PlaybackImpl) Perform(_ context.Context, method string, requestUrl stri

response.Header = recording.ParsedResponse.Header
response.Status = recording.ParsedResponse.Status
response.Time = c.Now()

// cannot just assign the body, need to re-parse into the existing pointer - using a json round trip
bodyJsonBytes, err := json.Marshal(recording.ParsedResponse.Body)
Expand Down

0 comments on commit 0adfae6

Please sign in to comment.