diff --git a/core/handlers/v2/views.go b/core/handlers/v2/views.go index c7feb994e..b29a419ed 100644 --- a/core/handlers/v2/views.go +++ b/core/handlers/v2/views.go @@ -39,6 +39,7 @@ type ModeArgumentsView struct { Stateful bool `json:"stateful,omitempty"` OverwriteDuplicate bool `json:"overwriteDuplicate,omitempty"` CaptureOnMiss bool `json:"captureOnMiss,omitempty"` + CaptureDelay bool `json:"captureDelay,omitempty"` } type IsWebServerView struct { diff --git a/core/hoverfly_funcs.go b/core/hoverfly_funcs.go index da97baa6a..f6e5824bc 100644 --- a/core/hoverfly_funcs.go +++ b/core/hoverfly_funcs.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/SpectoLabs/hoverfly/core/errors" v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" @@ -19,19 +20,21 @@ import ( ) // DoRequest - performs request and returns response that should be returned to client and error -func (hf *Hoverfly) DoRequest(request *http.Request) (*http.Response, error) { +func (hf *Hoverfly) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { // We can't have this set. And it only contains "/pkg/net/http/" anyway request.RequestURI = "" client, err := GetHttpClient(hf, request.Host) if err != nil { - return nil, err + return nil, nil, err } + start := time.Now() resp, err := client.Do(request) + elapsed := time.Since(start) if err != nil { - return nil, err + return nil, nil, err } resp.Header.Set("Hoverfly", "Was-Here") @@ -40,8 +43,7 @@ func (hf *Hoverfly) DoRequest(request *http.Request) (*http.Response, error) { resp.Header.Add("Hoverfly", "Forwarded") } - return resp, nil - + return resp, &elapsed, nil } // GetResponse returns stored response from cache diff --git a/core/hoverfly_funcs_test.go b/core/hoverfly_funcs_test.go index 5cf4f2b2f..cb8ca6d1b 100644 --- a/core/hoverfly_funcs_test.go +++ b/core/hoverfly_funcs_test.go @@ -26,8 +26,9 @@ func Test_Hoverfly_DoRequest_DoesNotPanicWhenCannotMakeRequest(t *testing.T) { request, err := http.NewRequest("GET", "w.specto.fake", ioutil.NopCloser(bytes.NewBuffer([]byte("")))) Expect(err).To(BeNil()) - response, err := unit.DoRequest(request) + response, time, err := unit.DoRequest(request) Expect(response).To(BeNil()) + Expect(time).To(BeNil()) Expect(err).ToNot(BeNil()) } @@ -43,7 +44,7 @@ func Test_Hoverfly_DoRequest_FailedHTTP(t *testing.T) { req, err := http.NewRequest("POST", "http://capture_body.com", body) Expect(err).To(BeNil()) - _, err = unit.DoRequest(req) + _, _, err = unit.DoRequest(req) Expect(err).ToNot(BeNil()) } diff --git a/core/hoverfly_service.go b/core/hoverfly_service.go index cd9b33c67..2533fde61 100644 --- a/core/hoverfly_service.go +++ b/core/hoverfly_service.go @@ -109,6 +109,7 @@ func (hf *Hoverfly) SetModeWithArguments(modeView v2.ModeView) error { Stateful: modeView.Arguments.Stateful, OverwriteDuplicate: modeView.Arguments.OverwriteDuplicate, CaptureOnMiss: modeView.Arguments.CaptureOnMiss, + CaptureDelay: modeView.Arguments.CaptureDelay, } hf.modeMap[hf.Cfg.GetMode()].SetArguments(modeArguments) diff --git a/core/modes/capture_mode.go b/core/modes/capture_mode.go index c4f812556..25f4c1a4e 100644 --- a/core/modes/capture_mode.go +++ b/core/modes/capture_mode.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "net/http" + "time" "github.com/SpectoLabs/hoverfly/core/models" "github.com/SpectoLabs/hoverfly/core/util" @@ -14,7 +15,7 @@ import ( type HoverflyCapture interface { ApplyMiddleware(models.RequestResponsePair) (models.RequestResponsePair, error) - DoRequest(*http.Request) (*http.Response, error) + DoRequest(*http.Request) (*http.Response, *time.Duration, error) Save(*models.RequestDetails, *models.ResponseDetails, *ModeArguments) error } @@ -30,6 +31,7 @@ func (this *CaptureMode) View() v2.ModeView { Headers: this.Arguments.Headers, Stateful: this.Arguments.Stateful, OverwriteDuplicate: this.Arguments.OverwriteDuplicate, + CaptureDelay: this.Arguments.CaptureDelay, }, } } @@ -59,7 +61,7 @@ func (this CaptureMode) Process(request *http.Request, details models.RequestDet return ReturnErrorAndLog(request, err, &pair, "There was an error when preparing request for pass through", Capture) } - response, err := this.Hoverfly.DoRequest(modifiedRequest) + response, duration, err := this.Hoverfly.DoRequest(modifiedRequest) if err != nil { return ReturnErrorAndLog(request, err, &pair, "There was an error when forwarding the request to the intended destination", Capture) } @@ -67,10 +69,16 @@ func (this CaptureMode) Process(request *http.Request, details models.RequestDet respBody, _ := util.GetResponseBody(response) respHeaders := util.GetResponseHeaders(response) + delayInMs := 0 + if this.Arguments.CaptureDelay { + delayInMs = int(duration.Milliseconds()) + } + responseObj := &models.ResponseDetails{ - Status: response.StatusCode, - Body: respBody, - Headers: respHeaders, + Status: response.StatusCode, + Body: respBody, + Headers: respHeaders, + FixedDelay: delayInMs, } if this.Arguments.Headers == nil { diff --git a/core/modes/capture_mode_test.go b/core/modes/capture_mode_test.go index efc97b1a9..007a4d1f6 100644 --- a/core/modes/capture_mode_test.go +++ b/core/modes/capture_mode_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/SpectoLabs/hoverfly/core/models" "github.com/SpectoLabs/hoverfly/core/modes" @@ -25,10 +26,10 @@ func (this hoverflyCaptureStub) ApplyMiddleware(pair models.RequestResponsePair) } // DoRequest - Stub implementation of modes.HoverflyCapture interface -func (this hoverflyCaptureStub) DoRequest(request *http.Request) (*http.Response, error) { +func (this hoverflyCaptureStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { response := &http.Response{} if request.Host == "error.com" { - return nil, errors.New("Could not reach error.com") + return nil, nil, errors.New("Could not reach error.com") } response.StatusCode = 200 @@ -42,7 +43,8 @@ func (this hoverflyCaptureStub) DoRequest(request *http.Request) (*http.Response response.Trailer.Set("X-Bin-Id", "xyz") } - return response, nil + duration := 1 * time.Second + return response, &duration, nil } // Save - Stub implementation of modes.HoverflyCapture interface diff --git a/core/modes/diff_mode.go b/core/modes/diff_mode.go index 03415a556..1db97dbd4 100644 --- a/core/modes/diff_mode.go +++ b/core/modes/diff_mode.go @@ -21,7 +21,7 @@ import ( type HoverflyDiff interface { GetResponse(models.RequestDetails) (*models.ResponseDetails, *errors.HoverflyError) - DoRequest(*http.Request) (*http.Response, error) + DoRequest(*http.Request) (*http.Response, *time.Duration, error) AddDiff(requestView v2.SimpleRequestDefinitionView, diffReport v2.DiffReport) } @@ -44,7 +44,7 @@ func (this *DiffMode) SetArguments(arguments ModeArguments) { this.Arguments = arguments } -//TODO: We should only need one of these two parameters +// TODO: We should only need one of these two parameters func (this *DiffMode) Process(request *http.Request, details models.RequestDetails) (ProcessResult, error) { this.DiffReport = v2.DiffReport{Timestamp: time.Now().Format(time.RFC3339)} @@ -60,7 +60,7 @@ func (this *DiffMode) Process(request *http.Request, details models.RequestDetai return ReturnErrorAndLog(request, err, &actualPair, "There was an error when reconstructing the request.", Diff) } - actualResponse, err := this.Hoverfly.DoRequest(modifiedRequest) + actualResponse, _, err := this.Hoverfly.DoRequest(modifiedRequest) if err != nil { return ReturnErrorAndLog(request, err, &actualPair, "There was an error when forwarding the request to the intended destination", Diff) } diff --git a/core/modes/diff_mode_test.go b/core/modes/diff_mode_test.go index 44eef9bd2..e9c5cc5a1 100644 --- a/core/modes/diff_mode_test.go +++ b/core/modes/diff_mode_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "bytes" "encoding/json" @@ -17,40 +18,41 @@ import ( type hoverflyDiffStub struct{} -func (this hoverflyDiffStub) DoRequest(request *http.Request) (*http.Response, error) { +func (this hoverflyDiffStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { + duration := 1 * time.Second switch request.Host { case "error.com": - return nil, fmt.Errorf("Could not reach error.com") + return nil, nil, fmt.Errorf("Could not reach error.com") case "positive-match-with-same-response.com": return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("expected")), Header: map[string][]string{"header": {"expected"}, "source": {"service"}}, - }, nil + }, &duration, nil case "positive-match-with-different-response.com": return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("actual")), Header: map[string][]string{"header": {"actual"}, "source": {"service"}}, - }, nil + }, &duration, nil case "negative-match.com": return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("actual")), Header: map[string][]string{"header": {"actual"}, "source": {"service"}}, - }, nil + }, &duration, nil case "positive-match-with-different-trailers.com": return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("actual")), Header: map[string][]string{"header": {"actual"}}, Trailer: map[string][]string{"trailer1": {"actual"}}, - }, nil + }, &duration, nil default: return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("test")), - }, nil + }, &duration, nil } } diff --git a/core/modes/modes.go b/core/modes/modes.go index 36468cf84..8d8d2c634 100644 --- a/core/modes/modes.go +++ b/core/modes/modes.go @@ -46,6 +46,7 @@ type ModeArguments struct { Stateful bool OverwriteDuplicate bool CaptureOnMiss bool + CaptureDelay bool } type ProcessResult struct { diff --git a/core/modes/modify_mode.go b/core/modes/modify_mode.go index 4ec4bef3b..ae5c25614 100644 --- a/core/modes/modify_mode.go +++ b/core/modes/modify_mode.go @@ -3,6 +3,7 @@ package modes import ( "io/ioutil" "net/http" + "time" "github.com/SpectoLabs/hoverfly/core/handlers/v2" "github.com/SpectoLabs/hoverfly/core/models" @@ -10,7 +11,7 @@ import ( type HoverflyModify interface { ApplyMiddleware(models.RequestResponsePair) (models.RequestResponsePair, error) - DoRequest(*http.Request) (*http.Response, error) + DoRequest(*http.Request) (*http.Response, *time.Duration, error) } type ModifyMode struct { @@ -36,7 +37,7 @@ func (this ModifyMode) Process(request *http.Request, details models.RequestDeta return ReturnErrorAndLog(request, err, &pair, "There was an error when rebuilding the modified http request", Modify) } - resp, err := this.Hoverfly.DoRequest(modifiedRequest) + resp, _, err := this.Hoverfly.DoRequest(modifiedRequest) if err != nil { return ReturnErrorAndLog(request, err, &pair, "There was an error when forwarding the request to the intended destination", Modify) } diff --git a/core/modes/modify_mode_test.go b/core/modes/modify_mode_test.go index e98c27845..a40347439 100644 --- a/core/modes/modify_mode_test.go +++ b/core/modes/modify_mode_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/SpectoLabs/hoverfly/core/models" "github.com/SpectoLabs/hoverfly/core/modes" @@ -14,10 +15,10 @@ import ( type hoverflyModifyStub struct{} -func (this hoverflyModifyStub) DoRequest(request *http.Request) (*http.Response, error) { +func (this hoverflyModifyStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { response := &http.Response{} if request.Host == "error.com" { - return nil, errors.New("Could not reach error.com") + return nil, nil, errors.New("Could not reach error.com") } request.Host = "modified.com" @@ -25,7 +26,8 @@ func (this hoverflyModifyStub) DoRequest(request *http.Request) (*http.Response, response.StatusCode = 200 response.Body = ioutil.NopCloser(bytes.NewBufferString("test")) - return response, nil + duration := 1 * time.Second + return response, &duration, nil } func (this hoverflyModifyStub) ApplyMiddleware(pair models.RequestResponsePair) (models.RequestResponsePair, error) { diff --git a/core/modes/spy_mode.go b/core/modes/spy_mode.go index 18166fc06..f31f05d99 100644 --- a/core/modes/spy_mode.go +++ b/core/modes/spy_mode.go @@ -2,6 +2,7 @@ package modes import ( "net/http" + "time" "github.com/SpectoLabs/hoverfly/core/errors" v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" @@ -15,7 +16,7 @@ import ( type HoverflySpy interface { GetResponse(models.RequestDetails) (*models.ResponseDetails, *errors.HoverflyError) ApplyMiddleware(models.RequestResponsePair) (models.RequestResponsePair, error) - DoRequest(*http.Request) (*http.Response, error) + DoRequest(*http.Request) (*http.Response, *time.Duration, error) Save(*models.RequestDetails, *models.ResponseDetails, *ModeArguments) error } @@ -33,6 +34,7 @@ func (this *SpyMode) View() v2.ModeView { Stateful: this.Arguments.Stateful, Headers: this.Arguments.Headers, OverwriteDuplicate: this.Arguments.OverwriteDuplicate, + CaptureDelay: this.Arguments.CaptureDelay, }, } } @@ -50,6 +52,7 @@ func (this *SpyMode) SetArguments(arguments ModeArguments) { Stateful: arguments.Stateful, OverwriteDuplicate: arguments.OverwriteDuplicate, CaptureOnMiss: arguments.CaptureOnMiss, + CaptureDelay: arguments.CaptureDelay, } } @@ -67,17 +70,21 @@ func (this SpyMode) Process(request *http.Request, details models.RequestDetails if err != nil { return ReturnErrorAndLog(request, err, &pair, "There was an error when reconstructing the request.", Spy) } - response, err := this.Hoverfly.DoRequest(modifiedRequest) + response, duration, err := this.Hoverfly.DoRequest(modifiedRequest) if err == nil { if this.Arguments.CaptureOnMiss { respBody, _ := util.GetResponseBody(response) respHeaders := util.GetResponseHeaders(response) - + delayInMs := 0 + if this.Arguments.CaptureDelay { + delayInMs = int(duration.Milliseconds()) + } responseObj := &models.ResponseDetails{ - Status: response.StatusCode, - Body: respBody, - Headers: respHeaders, + Status: response.StatusCode, + Body: respBody, + Headers: respHeaders, + FixedDelay: delayInMs, } if this.Arguments.Headers == nil { this.Arguments.Headers = []string{} diff --git a/core/modes/spy_mode_test.go b/core/modes/spy_mode_test.go index b43ff614f..d2ee50110 100644 --- a/core/modes/spy_mode_test.go +++ b/core/modes/spy_mode_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/SpectoLabs/hoverfly/core/errors" "github.com/SpectoLabs/hoverfly/core/models" @@ -16,16 +17,17 @@ import ( type hoverflySpyStub struct{} // DoRequest - Stub implementation of modes.HoverflySpy interface -func (this hoverflySpyStub) DoRequest(request *http.Request) (*http.Response, error) { +func (this hoverflySpyStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { response := &http.Response{} if request.Host == "error.com" { - return nil, fmt.Errorf("Could not reach error.com") + return nil, nil, fmt.Errorf("Could not reach error.com") } response.StatusCode = 200 response.Body = ioutil.NopCloser(bytes.NewBufferString("test")) - return response, nil + duration := 1 * time.Second + return response, &duration, nil } func (this hoverflySpyStub) GetResponse(requestDetails models.RequestDetails) (*models.ResponseDetails, *errors.HoverflyError) { diff --git a/functional-tests/core/ft_capture_mode_test.go b/functional-tests/core/ft_capture_mode_test.go index bde93b5a8..74fd0eeb4 100644 --- a/functional-tests/core/ft_capture_mode_test.go +++ b/functional-tests/core/ft_capture_mode_test.go @@ -22,10 +22,9 @@ import ( type FormData struct { FirstName string `url:"first_name"` - LastName string `url:"last_name"` + LastName string `url:"last_name"` } - var _ = Describe("When I run Hoverfly", func() { var ( @@ -455,7 +454,6 @@ var _ = Describe("When I run Hoverfly", func() { LastName: "Doe", } - resp := hoverfly.Proxy(sling.New().Post(fakeServer.URL).BodyForm(formData)) Expect(resp.StatusCode).To(Equal(200)) @@ -494,7 +492,7 @@ var _ = Describe("When I run Hoverfly", func() { { Matcher: "form", Value: map[string]interface{}{ - "first_name":[]interface{}{ + "first_name": []interface{}{ map[string]interface{}{ "matcher": matchers.Exact, "value": "John", diff --git a/hoverctl/cmd/mode.go b/hoverctl/cmd/mode.go index 5795c184e..637d42fd4 100644 --- a/hoverctl/cmd/mode.go +++ b/hoverctl/cmd/mode.go @@ -16,6 +16,7 @@ var stateful bool var overwriteDuplicate bool var matchingStrategy string var captureOnMiss bool +var captureDelay bool var modeCmd = &cobra.Command{ Use: "mode [capture|diff|simulate|spy|modify|synthesize (optional)]", @@ -51,6 +52,7 @@ mode is shown. case modes.Capture: modeView.Arguments.Stateful = stateful modeView.Arguments.OverwriteDuplicate = overwriteDuplicate + modeView.Arguments.CaptureDelay = captureDelay setHeaderArgument(modeView) break case modes.Diff: @@ -61,6 +63,7 @@ mode is shown. modeView.Arguments.Stateful = stateful modeView.Arguments.OverwriteDuplicate = overwriteDuplicate modeView.Arguments.CaptureOnMiss = captureOnMiss + modeView.Arguments.CaptureDelay = captureDelay setHeaderArgument(modeView) break } @@ -98,6 +101,9 @@ func getExtraInfo(mode *v2.ModeView) string { extraInfo = fmt.Sprintf("and will capture the following request headers: %s", mode.Arguments.Headers) } } + if captureDelay { + extraInfo = fmt.Sprintf(" and will also capture the delay") + } break case modes.Diff: if len(mode.Arguments.Headers) > 0 { @@ -119,6 +125,9 @@ func getExtraInfo(mode *v2.ModeView) string { extraInfo = fmt.Sprintf("and also will capture the following request headers: %s", mode.Arguments.Headers) } } + if captureDelay { + extraInfo = fmt.Sprintf(" and will also capture the delay") + } break } @@ -139,4 +148,5 @@ func init() { modeCmd.PersistentFlags().BoolVar(&overwriteDuplicate, "overwrite-duplicate", false, "Overwrite duplicate requests in capture mode") modeCmd.PersistentFlags().BoolVar(&captureOnMiss, "capture-on-miss", false, "Capture the request on miss in spy mode") + modeCmd.PersistentFlags().BoolVar(&captureDelay, "capture-delay", false, "Capture the request delay in capture and spy mode") }