Skip to content

Commit 20a5219

Browse files
Merge branch 'master' into fix/update-github-actions-sha-latest
2 parents e149ea1 + a4d3122 commit 20a5219

File tree

2 files changed

+156
-11
lines changed

2 files changed

+156
-11
lines changed

gateway/mw_mock_response.go

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"net/http"
1010
"sort"
1111
"strconv"
12+
"time"
1213

14+
"github.com/TykTechnologies/tyk-pump/analytics"
1315
"github.com/TykTechnologies/tyk/apidef/oas"
1416
"github.com/TykTechnologies/tyk/common/option"
1517
"github.com/TykTechnologies/tyk/header"
@@ -21,14 +23,22 @@ var _ TykMiddleware = (*mockResponseMiddleware)(nil)
2123

2224
type mockResponseMiddleware struct {
2325
*BaseMiddleware
26+
hitRecorder hitRecorder
2427
}
2528

2629
func newMockResponseMiddleware(base *BaseMiddleware, opts ...option.Option[mockResponseMiddleware]) TykMiddleware {
2730
return option.New(opts).Build(mockResponseMiddleware{
2831
BaseMiddleware: base,
32+
hitRecorder: &realHitRecorder{successHandler: &SuccessHandler{base.Copy()}},
2933
})
3034
}
3135

36+
func withHitRecorder(h hitRecorder) option.Option[mockResponseMiddleware] {
37+
return func(m *mockResponseMiddleware) {
38+
m.hitRecorder = h
39+
}
40+
}
41+
3242
func (m *mockResponseMiddleware) Name() string {
3343
return "MockResponseMiddleware"
3444
}
@@ -61,11 +71,13 @@ func (m *mockResponseMiddleware) forward(res *http.Response, rw http.ResponseWri
6171
}
6272

6373
func (m *mockResponseMiddleware) ProcessRequest(rw http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
74+
start := time.Now()
75+
6476
if !m.Spec.hasActiveMock() {
6577
return nil, http.StatusOK
6678
}
6779

68-
res, err := m.mockResponse(r)
80+
res, requestOverwritten, err := m.mockResponse(r)
6981

7082
if err != nil {
7183
return fmt.Errorf("failed to mock response: %w", err), http.StatusInternalServerError
@@ -86,10 +98,16 @@ func (m *mockResponseMiddleware) ProcessRequest(rw http.ResponseWriter, r *http.
8698
return fmt.Errorf("failed to forward response: %w", err), http.StatusInternalServerError
8799
}
88100

101+
m.hitRecorder.hit(rw, requestOverwritten, res, start)
102+
89103
return nil, middleware.StatusRespond
90104
}
91105

92-
func (m *mockResponseMiddleware) mockResponse(r *http.Request) (*http.Response, error) {
106+
func (m *mockResponseMiddleware) mockResponse(r *http.Request) (
107+
res *http.Response,
108+
internal *http.Request,
109+
err error,
110+
) {
93111
// Use FindSpecMatchesStatus to check if this path should be mocked
94112
// This ensures the standard regex-based path matching is used, respecting gateway configurations
95113
versionInfo, _ := m.Spec.Version(r)
@@ -99,21 +117,23 @@ func (m *mockResponseMiddleware) mockResponse(r *http.Request) (*http.Response,
99117

100118
if !found || urlSpec == nil {
101119
// No mock response configured for this path
102-
return nil, nil
120+
return nil, nil, nil
103121
}
104122

105123
mockResponse := urlSpec.OASMockResponseMeta
106124
if mockResponse == nil || !mockResponse.Enabled {
107-
return nil, nil
125+
return nil, nil, nil
108126
}
109127

110-
res := &http.Response{Header: http.Header{}}
128+
res = &http.Response{Header: http.Header{}}
129+
130+
internal = r.Clone(r.Context())
131+
internal.URL.Path = urlSpec.OASPath
111132

112133
var code int
113134
var contentType string
114135
var body []byte
115136
var headers []oas.Header
116-
var err error
117137

118138
if mockResponse.FromOASExamples != nil && mockResponse.FromOASExamples.Enabled {
119139
// Find the route using the OAS path from URLSpec, not the actual request path.
@@ -122,13 +142,12 @@ func (m *mockResponseMiddleware) mockResponse(r *http.Request) (*http.Response,
122142
route, _, routeErr := m.Spec.findRouteForOASPath(urlSpec.OASPath, urlSpec.OASMethod, strippedPath, r.URL.Path)
123143
if routeErr != nil || route == nil {
124144
log.Tracef("URL spec matched for mock response but route not found for OAS path %s: %v", urlSpec.OASPath, routeErr)
125-
return nil, nil
145+
return nil, nil, nil
126146
}
127147
code, contentType, body, headers, err = mockFromOAS(r, route.Operation, mockResponse.FromOASExamples)
128148
res.StatusCode = code
129149
if err != nil {
130-
err = fmt.Errorf("mock: %w", err)
131-
return res, err
150+
return res, internal, fmt.Errorf("mock: %w", err)
132151
}
133152
} else {
134153
code, body, headers = mockFromConfig(mockResponse)
@@ -143,12 +162,15 @@ func (m *mockResponseMiddleware) mockResponse(r *http.Request) (*http.Response,
143162
}
144163

145164
res.StatusCode = code
165+
res.Body = nopCloser{ReadSeeker: bytes.NewReader(body)}
146166

147-
res.Body = io.NopCloser(bytes.NewBuffer(body))
167+
if m.Gw.GetConfig().CloseConnections {
168+
res.Header.Set(header.Connection, "close")
169+
}
148170

149171
m.Spec.sendRateLimitHeaders(ctxGetSession(r), res)
150172

151-
return res, nil
173+
return res, internal, nil
152174
}
153175

154176
func mockFromConfig(tykMockRespOp *oas.MockResponse) (int, []byte, []oas.Header) {
@@ -268,3 +290,23 @@ func mockFromOAS(r *http.Request, operation *openapi3.Operation, fromOASExamples
268290

269291
return code, contentType, body, headers, err
270292
}
293+
294+
type hitRecorder interface {
295+
hit(rw http.ResponseWriter, r *http.Request, res *http.Response, start time.Time)
296+
}
297+
298+
type realHitRecorder struct {
299+
successHandler *SuccessHandler
300+
}
301+
302+
func (s *realHitRecorder) hit(rw http.ResponseWriter, r *http.Request, res *http.Response, start time.Time) {
303+
if s.successHandler.Spec.DoNotTrack {
304+
return
305+
}
306+
307+
ms := DurationToMillisecond(time.Since(start))
308+
latency := analytics.Latency{Total: int64(ms), Upstream: 0, Gateway: int64(ms)}
309+
s.successHandler.RecordHit(r, latency, res.StatusCode, res, true)
310+
s.successHandler.RecordAccessLog(r, res, latency)
311+
s.successHandler.Base().RecordMetrics(rw, r, res.StatusCode, latency, res)
312+
}

gateway/mw_mock_response_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package gateway
22

33
import (
44
"context"
5+
"io"
56
"net/http"
7+
"net/http/httptest"
68
"testing"
9+
"time"
710

811
"github.com/getkin/kin-openapi/openapi3"
12+
"github.com/sirupsen/logrus"
913
"github.com/stretchr/testify/assert"
1014
"github.com/stretchr/testify/require"
1115

@@ -142,6 +146,85 @@ func TestMockResponse(t *testing.T) {
142146
_, _ = g.Run(t, test.TestCase{Path: "/listen-path/get", BodyMatch: "mock: there is no example response for the content type: application/xml"})
143147
})
144148
})
149+
150+
t.Run("ProcessRequest", func(t *testing.T) {
151+
t.Run("calls hit recorder for oas API", func(t *testing.T) {
152+
spec := BuildOASAPI(func(oasDef *oas.OAS) {
153+
opId := uuid.New()
154+
155+
headers := oas.Headers{}
156+
headers.Add("Content-Type", "application/json")
157+
158+
tykExt := oasDef.GetTykExtension()
159+
tykExt.Info.ID = "test"
160+
tykExt.Info.Name = "test"
161+
tykExt.Server.ListenPath.Value = "/test"
162+
tykExt.Middleware = &oas.Middleware{}
163+
tykExt.Middleware.Operations = oas.Operations{}
164+
tykExt.Middleware.Operations[opId] = &oas.Operation{
165+
MockResponse: &oas.MockResponse{
166+
Enabled: true,
167+
Code: http.StatusCreated,
168+
Body: `{"mocked":true}`,
169+
Headers: headers,
170+
},
171+
}
172+
173+
desc := "hello world"
174+
responses := openapi3.NewResponses()
175+
responses.Delete("default")
176+
responses.Set("200", &openapi3.ResponseRef{
177+
Value: &openapi3.Response{
178+
Description: &desc,
179+
Content: openapi3.Content{
180+
"application/json": &openapi3.MediaType{},
181+
},
182+
},
183+
})
184+
185+
oasDef.Paths = openapi3.NewPaths()
186+
oasDef.Paths.Set("/mock", &openapi3.PathItem{
187+
Get: &openapi3.Operation{
188+
OperationID: opId,
189+
Responses: responses,
190+
},
191+
})
192+
})[0]
193+
194+
apiSpec := g.Gw.LoadAPI(spec)[0]
195+
196+
_, _ = g.Run(t, test.TestCase{
197+
Path: "/test/mock",
198+
Method: http.MethodGet,
199+
Code: http.StatusCreated,
200+
})
201+
202+
logger := logrus.New()
203+
logger.SetOutput(io.Discard)
204+
entry := logrus.NewEntry(logger)
205+
206+
mockRecorder := &mockHitRecorder{}
207+
208+
baseMid := NewBaseMiddleware(g.Gw, apiSpec, nil, entry)
209+
mockMw := newMockResponseMiddleware(baseMid, withHitRecorder(mockRecorder))
210+
211+
rw := httptest.NewRecorder()
212+
r := httptest.NewRequest(http.MethodGet, "/test/mock", nil)
213+
214+
err, _ := mockMw.ProcessRequest(rw, r, nil)
215+
assert.NoError(t, err)
216+
require.Len(t, mockRecorder.calls, 1, "expected to be called once")
217+
218+
req := mockRecorder.calls[0].req
219+
res := mockRecorder.calls[0].res
220+
221+
// is needed downstack during writing the log
222+
assert.Equal(t, "/mock", req.URL.Path, "req is overwritten to local api endpoint")
223+
224+
// is needed downstack; body is being read at least twice: during writing log and during proxying to the real response
225+
assert.IsType(t, nopCloser{}, res.Body, "response body is wrapped by nopCloser")
226+
})
227+
})
145228
}
146229

147230
func Test_mockFromConfig(t *testing.T) {
@@ -665,3 +748,23 @@ func TestMockResponseWithInternalRedirect(t *testing.T) {
665748
})
666749
})
667750
}
751+
752+
var _ hitRecorder = new(mockHitRecorder)
753+
754+
type (
755+
mockHitRecorder struct {
756+
calls []mockHitRecorderCall
757+
}
758+
759+
mockHitRecorderCall struct {
760+
req *http.Request
761+
res *http.Response
762+
}
763+
)
764+
765+
func (h *mockHitRecorder) hit(_ http.ResponseWriter, req *http.Request, res *http.Response, _ time.Time) {
766+
h.calls = append(h.calls, mockHitRecorderCall{
767+
req: req,
768+
res: res,
769+
})
770+
}

0 commit comments

Comments
 (0)