diff --git a/core/cmd/hoverfly/main.go b/core/cmd/hoverfly/main.go index bfd6a4000..b9c61a9a2 100644 --- a/core/cmd/hoverfly/main.go +++ b/core/cmd/hoverfly/main.go @@ -38,6 +38,7 @@ import ( hvc "github.com/SpectoLabs/hoverfly/core/certs" cs "github.com/SpectoLabs/hoverfly/core/cors" "github.com/SpectoLabs/hoverfly/core/handlers" + v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" "github.com/SpectoLabs/hoverfly/core/matching" mw "github.com/SpectoLabs/hoverfly/core/middleware" "github.com/SpectoLabs/hoverfly/core/modes" @@ -77,6 +78,7 @@ var ( synthesize = flag.Bool("synthesize", false, "Start Hoverfly in synthesize mode (middleware is required)") modify = flag.Bool("modify", false, "Start Hoverfly in modify mode - applies middleware (required) to both outgoing and incoming HTTP traffic") spy = flag.Bool("spy", false, "Start Hoverfly in spy mode, similar to simulate but calls real server when cache miss") + captureOnMiss = flag.Bool("capture-on-miss", false, "Capture requests that don't match a simulation when in spy mode") diff = flag.Bool("diff", false, "Start Hoverfly in diff mode - calls real server and compares the actual response with the expected simulation config if present") middleware = flag.String("middleware", "", "Set middleware by passing the name of the binary and the path of the middleware script separated by space. (i.e. '-middleware \"python script.py\"')") proxyPort = flag.String("pp", "", "Proxy port - run proxy on another port (i.e. '-pp 9999' to run proxy on port 9999)") @@ -678,6 +680,19 @@ func main() { hoverfly.CacheMatcher.PreloadCache(hoverfly.Simulation) } + if *captureOnMiss && !*spy { + log.Fatal("-capture-on-miss can only be used with -spy mode") + } + + if *spy && *captureOnMiss { + if err := hoverfly.SetModeWithArguments(v2.ModeView{ + Mode: modes.Spy, + Arguments: v2.ModeArgumentsView{CaptureOnMiss: true}, + }); err != nil { + log.WithError(err).Fatal("Failed to set spy mode with captureOnMiss") + } + } + // start metrics registry flush if *metrics { hoverfly.Counter.Init() diff --git a/core/hoverfly_service_test.go b/core/hoverfly_service_test.go index 1a55e6cee..6cd6e2257 100644 --- a/core/hoverfly_service_test.go +++ b/core/hoverfly_service_test.go @@ -985,6 +985,22 @@ func Test_Hoverfly_SetModeWithArguments_OverwriteDuplicate(t *testing.T) { Expect(storedMode.Arguments.OverwriteDuplicate).To(BeTrue()) } +func Test_Hoverfly_SetModeWithArguments_SpyCaptureOnMiss(t *testing.T) { + RegisterTestingT(t) + + unit := NewHoverflyWithConfiguration(&Configuration{}) + + Expect(unit.SetModeWithArguments(v2.ModeView{ + Mode: "spy", + Arguments: v2.ModeArgumentsView{ + CaptureOnMiss: true, + }, + })).To(Succeed()) + + storedMode := unit.modeMap[modes.Spy].View() + Expect(storedMode.Arguments.CaptureOnMiss).To(BeTrue()) +} + func Test_Hoverfly_AddDiff_AddEntry(t *testing.T) { RegisterTestingT(t) diff --git a/core/modes/spy_mode_test.go b/core/modes/spy_mode_test.go index 5a38134ab..50180b661 100644 --- a/core/modes/spy_mode_test.go +++ b/core/modes/spy_mode_test.go @@ -14,10 +14,14 @@ import ( . "github.com/onsi/gomega" ) -type hoverflySpyStub struct{} +type hoverflySpyStub struct { + savedRequest *models.RequestDetails + savedResponse *models.ResponseDetails + savedArguments *modes.ModeArguments +} // DoRequest - Stub implementation of modes.HoverflySpy interface -func (this hoverflySpyStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { +func (this *hoverflySpyStub) DoRequest(request *http.Request) (*http.Response, *time.Duration, error) { response := &http.Response{} if request.Host == "error.com" { return nil, nil, fmt.Errorf("Could not reach error.com") @@ -30,7 +34,7 @@ func (this hoverflySpyStub) DoRequest(request *http.Request) (*http.Response, *t return response, &duration, nil } -func (this hoverflySpyStub) GetResponse(requestDetails models.RequestDetails) (*models.ResponseDetails, *errors.HoverflyError) { +func (this *hoverflySpyStub) GetResponse(requestDetails models.RequestDetails) (*models.ResponseDetails, *errors.HoverflyError) { if requestDetails.Destination == "positive-match.com" { return &models.ResponseDetails{ Status: 200, @@ -42,14 +46,17 @@ func (this hoverflySpyStub) GetResponse(requestDetails models.RequestDetails) (* } } -func (this hoverflySpyStub) ApplyMiddleware(pair models.RequestResponsePair) (models.RequestResponsePair, error) { +func (this *hoverflySpyStub) ApplyMiddleware(pair models.RequestResponsePair) (models.RequestResponsePair, error) { if pair.Request.Path == "middleware-error" { return pair, fmt.Errorf("middleware-error") } return pair, nil } -func (this hoverflySpyStub) Save(request *models.RequestDetails, response *models.ResponseDetails, arguments *modes.ModeArguments) error { +func (this *hoverflySpyStub) Save(request *models.RequestDetails, response *models.ResponseDetails, arguments *modes.ModeArguments) error { + this.savedRequest = request + this.savedResponse = response + this.savedArguments = arguments return nil } @@ -57,7 +64,7 @@ func Test_SpyMode_WhenGivenAMatchingRequestItReturnsTheCorrectResponse(t *testin RegisterTestingT(t) unit := &modes.SpyMode{ - Hoverfly: hoverflySpyStub{}, + Hoverfly: &hoverflySpyStub{}, } request := models.RequestDetails{ @@ -74,7 +81,7 @@ func Test_SpyMode_WhenGivenANonMatchingRequestItWillMakeTheRequestAndReturnIt(t RegisterTestingT(t) unit := &modes.SpyMode{ - Hoverfly: hoverflySpyStub{}, + Hoverfly: &hoverflySpyStub{}, } requestDetails := models.RequestDetails{ @@ -100,7 +107,7 @@ func Test_SpyMode_WhenGivenAMatchingRequesAndMiddlewareFaislItReturnsAnError(t * RegisterTestingT(t) unit := &modes.SpyMode{ - Hoverfly: hoverflySpyStub{}, + Hoverfly: &hoverflySpyStub{}, } request := models.RequestDetails{ @@ -124,7 +131,7 @@ func Test_SpyMode_ShouldReturnErrorOnRemoteServiceCall(t *testing.T) { RegisterTestingT(t) unit := &modes.SpyMode{ - Hoverfly: hoverflySpyStub{}, + Hoverfly: &hoverflySpyStub{}, } requestDetails := models.RequestDetails{ @@ -147,3 +154,57 @@ func Test_SpyMode_ShouldReturnErrorOnRemoteServiceCall(t *testing.T) { Expect(string(responseBody)).To(ContainSubstring("Could not reach error.com")) } + +func Test_SpyMode_OnCacheMiss_WhenCaptureOnMissEnabled_SavesRequestAndResponse(t *testing.T) { + RegisterTestingT(t) + + stub := &hoverflySpyStub{} + unit := &modes.SpyMode{ + Hoverfly: stub, + Arguments: modes.ModeArguments{CaptureOnMiss: true}, + } + + requestDetails := models.RequestDetails{ + Scheme: "http", + Destination: "negative-match.com", + } + + request, err := http.NewRequest("GET", "http://negative-match.com", nil) + Expect(err).To(BeNil()) + + result, err := unit.Process(request, requestDetails) + Expect(err).To(BeNil()) + Expect(result.Response.StatusCode).To(Equal(200)) + + Expect(stub.savedRequest).ToNot(BeNil()) + Expect(stub.savedResponse).ToNot(BeNil()) + Expect(stub.savedResponse.Status).To(Equal(200)) + Expect(stub.savedResponse.Body).To(Equal("test")) + Expect(stub.savedArguments).ToNot(BeNil()) + Expect(stub.savedArguments.CaptureOnMiss).To(BeTrue()) +} + +func Test_SpyMode_OnCacheMiss_WhenCaptureOnMissDisabled_DoesNotSave(t *testing.T) { + RegisterTestingT(t) + + stub := &hoverflySpyStub{} + unit := &modes.SpyMode{ + Hoverfly: stub, + Arguments: modes.ModeArguments{CaptureOnMiss: false}, + } + + requestDetails := models.RequestDetails{ + Scheme: "http", + Destination: "negative-match.com", + } + + request, err := http.NewRequest("GET", "http://negative-match.com", nil) + Expect(err).To(BeNil()) + + result, err := unit.Process(request, requestDetails) + Expect(err).To(BeNil()) + Expect(result.Response.StatusCode).To(Equal(200)) + + Expect(stub.savedRequest).To(BeNil()) + Expect(stub.savedResponse).To(BeNil()) +} diff --git a/functional-tests/core/ft_modes_test.go b/functional-tests/core/ft_modes_test.go index 4e7117b4f..163ac9e05 100644 --- a/functional-tests/core/ft_modes_test.go +++ b/functional-tests/core/ft_modes_test.go @@ -8,6 +8,7 @@ import ( "os" "strings" + "github.com/SpectoLabs/hoverfly/core/handlers/v2" "github.com/SpectoLabs/hoverfly/functional-tests" "github.com/dghubble/sling" . "github.com/onsi/ginkgo" @@ -127,6 +128,66 @@ var _ = Describe("Running Hoverfly in various modes", func() { Expect(string(body)).To(Equal("Simulated")) }) }) + + Context("With capture-on-miss enabled", func() { + + var fakeServer *httptest.Server + + BeforeEach(func() { + hoverfly.Start() + + fakeServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("Real response")) + })) + + hoverfly.SetModeWithArgs("spy", v2.ModeArgumentsView{CaptureOnMiss: true}) + hoverfly.ImportSimulation(`{ + "data": { + "pairs": [ + { + "request": { + "headers": { + "X-API-TEST": [ { "value": "test", "matcher": "exact" } ] + } + }, + "response": { + "status": 200, + "body": "Simulated" + } + } + ] + }, + "meta": { "schemaVersion": "v5" } + }`) + }) + + AfterEach(func() { + fakeServer.Close() + }) + + It("Should save the request/response pair when there is a cache miss", func() { + resp := hoverfly.Proxy(sling.New().Get(fakeServer.URL)) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + body, err := ioutil.ReadAll(resp.Body) + Expect(err).To(BeNil()) + Expect(string(body)).To(Equal("Real response")) + + simulation := hoverfly.ExportSimulation() + Expect(simulation.RequestResponsePairs).To(HaveLen(2)) + }) + + It("Should not save the request/response pair when there is a cache hit", func() { + resp := hoverfly.Proxy(sling.New().Get(fakeServer.URL).Set("X-API-TEST", "test")) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + body, err := ioutil.ReadAll(resp.Body) + Expect(err).To(BeNil()) + Expect(string(body)).To(Equal("Simulated")) + + simulation := hoverfly.ExportSimulation() + Expect(simulation.RequestResponsePairs).To(HaveLen(1)) + }) + }) }) Context("When running in synthesise mode", func() {