Skip to content

Commit

Permalink
Add templating function to dynamically change status code according t…
Browse files Browse the repository at this point in the history
…o response body
  • Loading branch information
tommysitu committed May 31, 2024
1 parent 9570500 commit c81076b
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 107 deletions.
6 changes: 3 additions & 3 deletions core/hoverfly_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func (hf *Hoverfly) applyTransitionsStateTemplating(requestDetails *models.Reque
state := make(map[string]string)

for k, v := range stateTemplates {
state[k], err = hf.templator.RenderTemplate(v, requestDetails, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)
state[k], err = hf.templator.RenderTemplate(v, requestDetails, response, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)
if err != nil {
return nil, err
}
Expand All @@ -240,7 +240,7 @@ func (hf *Hoverfly) applyBodyTemplating(requestDetails *models.RequestDetails, r
}
}

return hf.templator.RenderTemplate(template, requestDetails, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)
return hf.templator.RenderTemplate(template, requestDetails, response, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)
}

func (hf *Hoverfly) applyHeadersTemplating(requestDetails *models.RequestDetails, response *models.ResponseDetails, cachedResponse *models.CachedResponse) (map[string][]string, error) {
Expand Down Expand Up @@ -275,7 +275,7 @@ func (hf *Hoverfly) applyHeadersTemplating(requestDetails *models.RequestDetails
for k, v := range headersTemplates {
header = make([]string, len(v))
for i, h := range v {
header[i], err = hf.templator.RenderTemplate(h, requestDetails, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)
header[i], err = hf.templator.RenderTemplate(h, requestDetails, response, hf.Simulation.Literals, hf.Simulation.Vars, hf.state.State, hf.Journal)

if err != nil {
return nil, err
Expand Down
6 changes: 6 additions & 0 deletions core/templating/template_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ func (t templateHelpers) parseJournalBasedOnIndex(indexName, keyValue, dataSourc
return getEvaluationString("journal", options)
}

func (t templateHelpers) setStatusCode(statusCode int, options *raymond.Options) string {
internalVars := options.ValueFromAllCtx("InternalVars").(map[string]interface{})
internalVars["statusCode"] = statusCode
return ""
}

func (t templateHelpers) sum(numbers []string, format string) string {
return sumNumbers(numbers, format)
}
Expand Down
18 changes: 14 additions & 4 deletions core/templating/templating.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type TemplatingData struct {
Vars map[string]interface{}
Journal Journal
Kvs map[string]interface{}
InternalVars map[string]interface{} // data store used internally by templating helpers
}

type Request struct {
Expand Down Expand Up @@ -90,6 +91,7 @@ func NewTemplator() *Templator {
helperMethodMap["requestBody"] = t.requestBody
helperMethodMap["csv"] = t.parseCsv
helperMethodMap["journal"] = t.parseJournalBasedOnIndex
helperMethodMap["setStatusCode"] = t.setStatusCode
helperMethodMap["sum"] = t.sum
helperMethodMap["add"] = t.add
helperMethodMap["subtract"] = t.subtract
Expand All @@ -115,20 +117,27 @@ func (*Templator) ParseTemplate(responseBody string) (*raymond.Template, error)
return raymond.Parse(responseBody)
}

func (t *Templator) RenderTemplate(tpl *raymond.Template, requestDetails *models.RequestDetails, literals *models.Literals, vars *models.Variables, state map[string]string, journal *journal.Journal) (string, error) {
func (t *Templator) RenderTemplate(tpl *raymond.Template, requestDetails *models.RequestDetails, response *models.ResponseDetails, literals *models.Literals, vars *models.Variables, state map[string]string, journal *journal.Journal) (string, error) {
if tpl == nil {
return "", fmt.Errorf("template cannot be nil")
}

ctx := t.NewTemplatingData(requestDetails, literals, vars, state, journal)
return tpl.Exec(ctx)
ctx := t.NewTemplatingData(requestDetails, response, literals, vars, state, journal)
result, err := tpl.Exec(ctx)
if err == nil {
statusCode, ok := ctx.InternalVars["statusCode"]
if ok {
response.Status = statusCode.(int)
}
}
return result, err
}

func (templator *Templator) GetSupportedMethodMap() map[string]interface{} {
return templator.SupportedMethodMap
}

func (t *Templator) NewTemplatingData(requestDetails *models.RequestDetails, literals *models.Literals, vars *models.Variables, state map[string]string, journal *journal.Journal) *TemplatingData {
func (t *Templator) NewTemplatingData(requestDetails *models.RequestDetails, response *models.ResponseDetails, literals *models.Literals, vars *models.Variables, state map[string]string, journal *journal.Journal) *TemplatingData {

literalMap := make(map[string]interface{})
if literals != nil {
Expand Down Expand Up @@ -176,6 +185,7 @@ func (t *Templator) NewTemplatingData(requestDetails *models.RequestDetails, lit
return a1 + " " + a2 + " " + a3
},
Kvs: kvs,
InternalVars: make(map[string]interface{}),
}

}
Expand Down
144 changes: 45 additions & 99 deletions core/templating/templating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,55 +106,37 @@ func Test_ApplyTemplate_EachBlockWithCsvTemplatingFunctionAndLargeInteger(t *tes
func Test_ShouldCreateTemplatingDataPathsFromRequest(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Path: "/foo/bar",
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Path: "/foo/bar",
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.Path).To(ConsistOf("foo", "bar"))
}

func Test_ShouldCreateTemplatingDataPathsFromRequestWithNoPaths(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.Path).To(BeEmpty())
}

func Test_ShouldCreateTemplatingDataQueryParamsFromRequest(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Query: map[string][]string{
"cheese": {"1", "3"},
"ham": {"2"},
},
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Query: map[string][]string{
"cheese": {"1", "3"},
"ham": {"2"},
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.QueryParam).To(HaveKeyWithValue("cheese", []string{"1", "3"}))
Expect(actual.Request.QueryParam).To(HaveKeyWithValue("ham", []string{"2"}))
Expand All @@ -164,54 +146,36 @@ func Test_ShouldCreateTemplatingDataQueryParamsFromRequest(t *testing.T) {
func Test_ShouldCreateTemplatingDataQueryParamsFromRequestWithNoQueryParams(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.QueryParam).To(BeEmpty())
}

func Test_ShouldCreateTemplatingDataHttpScheme(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.Scheme).To(Equal("http"))
}

func Test_ShouldCreateTemplatingDataHeaderFromRequest(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Headers: map[string][]string{
"cheese": {"1", "3"},
"ham": {"2"},
},
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
Headers: map[string][]string{
"cheese": {"1", "3"},
"ham": {"2"},
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.Header).To(HaveKeyWithValue("cheese", []string{"1", "3"}))
Expect(actual.Request.Header).To(HaveKeyWithValue("ham", []string{"2"}))
Expand All @@ -221,16 +185,10 @@ func Test_ShouldCreateTemplatingDataHeaderFromRequest(t *testing.T) {
func Test_ShouldCreateTemplatingDataHeaderFromRequestWithNoHeader(t *testing.T) {
RegisterTestingT(t)

actual := templating.NewTemplator().NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
},
&models.Literals{},
&models.Variables{},
make(map[string]string),
&journal.Journal{},
)
actual := templating.NewTemplator().NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
}, nil, &models.Literals{}, &models.Variables{}, make(map[string]string), &journal.Journal{})

Expect(actual.Request.Header).To(BeEmpty())
}
Expand Down Expand Up @@ -554,16 +512,10 @@ func Test_VarSetToNilInCaseOfInvalidArgsPassed(t *testing.T) {
},
}

actual := templator.NewTemplatingData(
&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
},
&models.Literals{},
vars,
make(map[string]string),
&journal.Journal{},
)
actual := templator.NewTemplatingData(&models.RequestDetails{
Scheme: "http",
Destination: "test.com",
}, nil, &models.Literals{}, vars, make(map[string]string), &journal.Journal{})

Expect(actual.Vars["varOne"]).To(BeNil())

Expand All @@ -581,15 +533,9 @@ func Test_VarSetToProperValueInCaseOfRequestDetailsPassedAsArgument(t *testing.T
},
}

actual := templator.NewTemplatingData(
&models.RequestDetails{
Path: "/part1/foo,bar",
},
&models.Literals{},
vars,
make(map[string]string),
&journal.Journal{},
)
actual := templator.NewTemplatingData(&models.RequestDetails{
Path: "/part1/foo,bar",
}, nil, &models.Literals{}, vars, make(map[string]string), &journal.Journal{})

Expect(actual.Vars["splitRequestPath"]).ToNot(BeNil())
Expect(len(actual.Vars["splitRequestPath"].([]string))).To(Equal(2))
Expand Down Expand Up @@ -690,13 +636,13 @@ func Test_ApplyTemplate_Arithmetic_Ops_With_Each_Block(t *testing.T) {

template, _ := templator.ParseTemplate(responseBody)
state := make(map[string]string)
result, err := templator.RenderTemplate(template, requestDetails, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})
result, err := templator.RenderTemplate(template, requestDetails, nil, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})

Expect(err).To(BeNil())
Expect(result).To(Equal(` 3.5 9 total: 12.50`))

// Running the second time should produce the same result because each execution has its own context data.
result, err = templator.RenderTemplate(template, requestDetails, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})
result, err = templator.RenderTemplate(template, requestDetails, nil, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})
Expect(err).To(BeNil())
Expect(result).To(Equal(` 3.5 9 total: 12.50`))
}
Expand Down Expand Up @@ -743,5 +689,5 @@ func ApplyTemplate(requestDetails *models.RequestDetails, state map[string]strin

template, err := templator.ParseTemplate(responseBody)
Expect(err).To(BeNil())
return templator.RenderTemplate(template, requestDetails, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})
return templator.RenderTemplate(template, requestDetails, nil, &models.Literals{}, &models.Variables{}, state, &journal.Journal{})
}
14 changes: 13 additions & 1 deletion functional-tests/core/ft_templated_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,19 @@ var _ = Describe("When I run Hoverfly", func() {
Expect(string(body)).To(Equal("test-server.com"))
})

It("Gloabl literals and variables", func() {
It("SetStatusCode", func() {
hoverfly.ImportSimulation(testdata.TemplatingRequest)

resp := hoverfly.Proxy(sling.New().Get("http://test-server.com/SetStatusCode"))
Expect(resp.StatusCode).To(Equal(202))

body, err := io.ReadAll(resp.Body)
Expect(err).To(BeNil())

Expect(string(body)).To(Equal(""))
})

It("Global literals and variables", func() {
hoverfly.ImportSimulation(testdata.TemplatingRequest)
resp := hoverfly.Proxy(sling.New().Post("http://test-server.com/global").BodyJSON(map[string]string{
"city": "London",
Expand Down
16 changes: 16 additions & 0 deletions functional-tests/testdata/templating_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,22 @@ var TemplatingRequest = `{
"templated": true
}
},
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/SetStatusCode"
}
]
},
"response": {
"status": 200,
"body": "{{ setStatusCode 202 }}",
"encodedBody": false,
"templated": true
}
},
{
"request": {
"path": [
Expand Down

0 comments on commit c81076b

Please sign in to comment.