diff --git a/core/hoverfly_funcs.go b/core/hoverfly_funcs.go index 2cb36654a..181d2f135 100644 --- a/core/hoverfly_funcs.go +++ b/core/hoverfly_funcs.go @@ -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 } @@ -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) { @@ -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 diff --git a/core/templating/template_helpers.go b/core/templating/template_helpers.go index 06c1887e7..2d5a66546 100644 --- a/core/templating/template_helpers.go +++ b/core/templating/template_helpers.go @@ -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) } diff --git a/core/templating/templating.go b/core/templating/templating.go index 86f3bb7e2..f69c4261b 100644 --- a/core/templating/templating.go +++ b/core/templating/templating.go @@ -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 { @@ -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 @@ -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 { @@ -176,6 +185,7 @@ func (t *Templator) NewTemplatingData(requestDetails *models.RequestDetails, lit return a1 + " " + a2 + " " + a3 }, Kvs: kvs, + InternalVars: make(map[string]interface{}), } } diff --git a/core/templating/templating_test.go b/core/templating/templating_test.go index 5ebc36e56..c64ad16b4 100644 --- a/core/templating/templating_test.go +++ b/core/templating/templating_test.go @@ -106,17 +106,11 @@ 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")) } @@ -124,16 +118,10 @@ func Test_ShouldCreateTemplatingDataPathsFromRequest(t *testing.T) { 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()) } @@ -141,20 +129,14 @@ func Test_ShouldCreateTemplatingDataPathsFromRequestWithNoPaths(t *testing.T) { 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"})) @@ -164,16 +146,10 @@ 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()) } @@ -181,16 +157,10 @@ func Test_ShouldCreateTemplatingDataQueryParamsFromRequestWithNoQueryParams(t *t 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")) } @@ -198,20 +168,14 @@ func Test_ShouldCreateTemplatingDataHttpScheme(t *testing.T) { 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"})) @@ -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()) } @@ -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()) @@ -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)) @@ -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`)) } @@ -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{}) } diff --git a/functional-tests/core/ft_templated_response_test.go b/functional-tests/core/ft_templated_response_test.go index 82dc9bbb3..be2a0b440 100644 --- a/functional-tests/core/ft_templated_response_test.go +++ b/functional-tests/core/ft_templated_response_test.go @@ -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", diff --git a/functional-tests/testdata/templating_request.go b/functional-tests/testdata/templating_request.go index a1f6844c0..dab1fb920 100644 --- a/functional-tests/testdata/templating_request.go +++ b/functional-tests/testdata/templating_request.go @@ -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": [