Skip to content

Commit

Permalink
should use array matcher when capturing multi-value header (#1058)
Browse files Browse the repository at this point in the history
* should use array matcher when capturing  multi-value header

* Update simulation schema in the docs

* add functional test for form matcher

* add docs for form matcher
  • Loading branch information
tommysitu authored Feb 20, 2023
1 parent 427fb29 commit ffb79d4
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 43 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ brew install python
Apache License version 2.0 [See LICENSE for details](https://github.com/SpectoLabs/hoverfly/blob/master/LICENSE).
(c) [SpectoLabs](https://specto.io) 2017.
(c) [SpectoLabs](https://specto.io) 2023.
[CircleCI-Image]: https://circleci.com/gh/SpectoLabs/hoverfly.svg?style=shield
[CircleCI-Url]: https://circleci.com/gh/SpectoLabs/hoverfly
43 changes: 21 additions & 22 deletions core/hoverfly_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,35 +349,16 @@ func (hf *Hoverfly) Save(request *models.RequestDetails, response *models.Respon
var requestHeaders map[string][]models.RequestFieldMatchers
if len(headers) > 0 {
requestHeaders = map[string][]models.RequestFieldMatchers{}
for headerKey, headerValues := range headers {
requestHeaders[headerKey] = []models.RequestFieldMatchers{
{
Matcher: matchers.Exact,
Value: strings.Join(headerValues, ";"),
},
}
for key, values := range headers {
requestHeaders[key] = getRequestMatcherForMultipleValues(values)
}
}

var queries *models.QueryRequestFieldMatchers
if len(request.Query) > 0 {
queries = &models.QueryRequestFieldMatchers{}
for key, values := range request.Query {
var matcher string
var value interface{}
if len(values) > 1 {
matcher = matchers.Array
value = values
} else {
matcher = matchers.Exact
value = strings.Join(values, ";")
}
queries.Add(key, []models.RequestFieldMatchers{
{
Matcher: matcher,
Value: value,
},
})
queries.Add(key, getRequestMatcherForMultipleValues(values))
}
}

Expand Down Expand Up @@ -431,3 +412,21 @@ func (hf *Hoverfly) ApplyMiddleware(pair models.RequestResponsePair) (models.Req

return pair, nil
}

func getRequestMatcherForMultipleValues(values []string) []models.RequestFieldMatchers {
var matcher string
var value interface{}
if len(values) > 1 {
matcher = matchers.Array
value = values
} else {
matcher = matchers.Exact
value = strings.Join(values, ";")
}
return []models.RequestFieldMatchers{
{
Matcher: matcher,
Value: value,
},
}
}
6 changes: 3 additions & 3 deletions core/hoverfly_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ func Test_Hoverfly_Save_SavesAllRequestHeadersWhenGivenAnAsterisk(t *testing.T)
_ = unit.Save(&models.RequestDetails{
Headers: map[string][]string{
"testheader": {"testvalue"},
"testheader2": {"testvalue2"},
"testheader2": {"testvalue2", "testvalue3"},
},
}, &models.ResponseDetails{
Body: "testresponsebody",
Expand All @@ -914,8 +914,8 @@ func Test_Hoverfly_Save_SavesAllRequestHeadersWhenGivenAnAsterisk(t *testing.T)
}))
Expect(unit.Simulation.GetMatchingPairs()[0].RequestMatcher.Headers["testheader2"]).To(HaveLen(1))
Expect(unit.Simulation.GetMatchingPairs()[0].RequestMatcher.Headers["testheader2"][0]).To(Equal(models.RequestFieldMatchers{
Matcher: "exact",
Value: "testvalue2",
Matcher: "array",
Value: []string{"testvalue2", "testvalue3"},
}))
}

Expand Down
10 changes: 5 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
master_doc = 'index'

project = u'Hoverfly'
copyright = u'2017, SpectoLabs'
copyright = u'2023, SpectoLabs'
author = u'SpectoLabs'


Expand Down Expand Up @@ -44,10 +44,10 @@
html_static_path = ['_static']

html_context = {
'css_files': [
'https://media.readthedocs.org/css/sphinx_rtd_theme.css',
'https://media.readthedocs.org/css/readthedocs-doc-embed.css',
'_static/theme_overrides.css',
'css_files': [
'https://media.readthedocs.org/css/sphinx_rtd_theme.css',
'https://media.readthedocs.org/css/readthedocs-doc-embed.css',
'_static/theme_overrides.css',
],
}

Expand Down
35 changes: 32 additions & 3 deletions docs/pages/reference/hoverfly/request_matchers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,38 @@ Example
</tbody>
</table>

Form matcher
-------------

Matches form data posted in the request payload with content type ``application/x-www-form-urlencoded``.
You can match only the form params you are interested in regardless of the order. You can also leverage
``jwt`` or ``jsonpath`` matchers if your form params contains JWT tokens or JSON document.

Please note that this matcher only works for ``body`` field.

Example
"""""""

.. code:: json
"matcher": "form",
"value": {
"grant_type": [
{
"matcher": "exact",
"value": "authorization_code"
}
],
"client_assertion": [
{
"matcher": "jwt",
"value": "{\"header\":{\"alg\":\"HS256\"},\"payload\":{\"sub\":\"1234567890\",\"name\":\"John Doe\"}}"
}
]
}
Array matcher
-----------------------
-------------

Matches an array contains exactly the given values and nothing else. This can be used to match
multi-value query param or header in the request data.
Expand Down Expand Up @@ -540,7 +569,7 @@ Example
"profile:vd"
]
JWT Matcher
JWT matcher
-----------

This matcher is primarily used for matching JWT tokens. This matcher converts base64 encoded JWT to
Expand All @@ -556,7 +585,7 @@ Example
"value": "{\"header\":{\"alg\":\"HS256\"},\"payload\":{\"sub\":\"1234567890\",\"name\":\"John Doe\"}}"
Matcher Chaining
Matcher chaining
----------------

Matcher chaining allows you to pass a matched value into another matcher to do further matching.
Expand Down
56 changes: 55 additions & 1 deletion docs/pages/reference/simulationschema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,24 @@ This is the JSON schema for v5 Hoverfly simulations.
"matcher": {
"type": "string"
},
"value": {}
"value": {},
"config": {
"properties": {
"ignoreUnknown": {
"type": "boolean"
},
"ignoreOrder": {
"type": "boolean"
},
"ignoreOccurrences": {
"type": "boolean"
}
},
"type": "object"
},
"doMatch": {
"$ref": "#/definitions/field-matchers"
}
},
"type": "object"
},
Expand All @@ -65,6 +82,16 @@ This is the JSON schema for v5 Hoverfly simulations.
},
"type": "object"
},
"literals": {
"properties": {
"name": {
"type": "string"
},
"value": {}
},
"required": ["name", "value"],
"type": "object"
},
"meta": {
"properties": {
"hoverflyVersion": {
Expand Down Expand Up @@ -205,6 +232,21 @@ This is the JSON schema for v5 Hoverfly simulations.
}
},
"type": "object"
},
"variables": {
"properties": {
"name": {
"type": "string"
},
"function": {
"type": "string"
},
"arguments": {
"type": "array"
}
},
"required": ["name", "function"],
"type": "object"
}
},
"description": "Hoverfly simulation schema",
Expand All @@ -228,11 +270,23 @@ This is the JSON schema for v5 Hoverfly simulations.
},
"type": "object"
},
"literals": {
"items": {
"$ref": "#/definitions/literals"
},
"type": "array"
},
"pairs": {
"items": {
"$ref": "#/definitions/request-response-pair"
},
"type": "array"
},
"variables": {
"items": {
"$ref": "#/definitions/variables"
},
"type": "array"
}
},
"type": "object"
Expand Down
51 changes: 44 additions & 7 deletions functional-tests/core/ft_capture_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package hoverfly_test
import (
"bytes"
"encoding/json"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -119,7 +120,7 @@ var _ = Describe("When I run Hoverfly", func() {
defer fakeServer.Close()
expectedDestination := strings.Replace(fakeServer.URL, "http://", "", 1)

existingSimBytes, err := ioutil.ReadFile("testdata/fake-server.json")
existingSimBytes, err := os.ReadFile("testdata/fake-server.json")
Expect(err).To(BeNil())
existingSim := string(existingSimBytes)
existingSim = strings.Replace(existingSim, "127.0.0.1:53751", expectedDestination, 1)
Expand Down Expand Up @@ -231,7 +232,7 @@ var _ = Describe("When I run Hoverfly", func() {
resp := hoverfly.Proxy(sling.New().Get(fakeServer.URL))
Expect(resp.StatusCode).To(Equal(200))

recordsJson, err := ioutil.ReadAll(hoverfly.GetSimulation())
recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}
Expand Down Expand Up @@ -266,7 +267,7 @@ var _ = Describe("When I run Hoverfly", func() {
resp := hoverfly.Proxy(sling.New().Get(fakeServer.URL).Add("Test", "value"))
Expect(resp.StatusCode).To(Equal(200))

recordsJson, err := ioutil.ReadAll(hoverfly.GetSimulation())
recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}
Expand All @@ -293,6 +294,42 @@ var _ = Describe("When I run Hoverfly", func() {
))
})

It("Should capture multi-value Accept request header by using array matcher", func() {
hoverfly.SetModeWithArgs("capture", v2.ModeArgumentsView{
Headers: []string{"Accept"},
})

fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Date", "date")
w.Write([]byte("Hello world"))
}))

defer fakeServer.Close()

resp := hoverfly.Proxy(sling.New().Get(fakeServer.URL).Add("Accept", "application/json").Add("Accept", "application/xml"))
Expect(resp.StatusCode).To(Equal(200))

recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}

Expect(json.Unmarshal(recordsJson, &payload)).To(Succeed())
Expect(payload.RequestResponsePairs).To(HaveLen(1))

Expect(payload.RequestResponsePairs[0].RequestMatcher.Headers).To(Equal(
map[string][]v2.MatcherViewV5{
"Accept": {
{
Matcher: "array",
Value: []interface{}{"application/json", "application/xml"},
},
},
},
))
})

It("Should capture a redirect response", func() {

fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -316,7 +353,7 @@ var _ = Describe("When I run Hoverfly", func() {

expectedRedirectDestination := strings.Replace(fakeServerRedirectUrl.String(), "http://", "", 1)

recordsJson, err := ioutil.ReadAll(hoverfly.GetSimulation())
recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}
Expand Down Expand Up @@ -362,7 +399,7 @@ var _ = Describe("When I run Hoverfly", func() {
resp := hoverfly.Proxy(sling.New().Post(fakeServer.URL).Add("Content-Type", "application/json").Body(bytes.NewBuffer([]byte(`{"title": "a todo"}`))))
Expect(resp.StatusCode).To(Equal(200))

recordsJson, err := ioutil.ReadAll(hoverfly.GetSimulation())
recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}
Expand All @@ -385,7 +422,7 @@ var _ = Describe("When I run Hoverfly", func() {
resp := hoverfly.Proxy(sling.New().Post(fakeServer.URL).Add("Content-Type", "application/xml").Body(bytes.NewBuffer([]byte(`<document/>`))))
Expect(resp.StatusCode).To(Equal(200))

recordsJson, err := ioutil.ReadAll(hoverfly.GetSimulation())
recordsJson, err := io.ReadAll(hoverfly.GetSimulation())
Expect(err).To(BeNil())

payload := v2.SimulationViewV5{}
Expand Down
Loading

0 comments on commit ffb79d4

Please sign in to comment.