diff --git a/core/templating/template_helpers.go b/core/templating/template_helpers.go index 2edd98729..b5b522f29 100644 --- a/core/templating/template_helpers.go +++ b/core/templating/template_helpers.go @@ -498,6 +498,24 @@ func (t templateHelpers) setStatusCode(statusCode string, options *raymond.Optio return "" } +func (t templateHelpers) setHeader(headerName string, headerValue string, options *raymond.Options) string { + if headerName == "" { + log.Error("header name cannot be empty") + return "" + } + internalVars := options.ValueFromAllCtx("InternalVars").(map[string]interface{}) + var headers map[string][]string + if h, ok := internalVars["setHeaders"]; ok { + headers = h.(map[string][]string) + } else { + headers = make(map[string][]string) + } + // Replace or add the header + headers[headerName] = []string{headerValue} + internalVars["setHeaders"] = headers + return "" +} + func (t templateHelpers) sum(numbers []string, format string) string { return sumNumbers(numbers, format) } diff --git a/core/templating/template_helpers_test.go b/core/templating/template_helpers_test.go index 905272506..4594c7fdb 100644 --- a/core/templating/template_helpers_test.go +++ b/core/templating/template_helpers_test.go @@ -3,10 +3,24 @@ package templating import ( "testing" "time" - . "github.com/onsi/gomega" ) + + +// mockRaymondOptions is a minimal mock for raymond.Options for testing +type mockRaymondOptions struct { + internalVars map[string]interface{} +} + +func (m *mockRaymondOptions) ValueFromAllCtx(key string) interface{} { + if key == "InternalVars" { + return m.internalVars + } + return nil +} + + func testNow() time.Time { parsedTime, _ := time.Parse("2006-01-02T15:04:05Z", "2018-01-01T00:00:00Z") return parsedTime diff --git a/core/templating/templating.go b/core/templating/templating.go index 801122d3e..9270e95be 100644 --- a/core/templating/templating.go +++ b/core/templating/templating.go @@ -105,6 +105,7 @@ func NewEnrichedTemplator(journal *journal.Journal) *Templator { helperMethodMap["journal"] = t.parseJournalBasedOnIndex helperMethodMap["hasJournalKey"] = t.hasJournalKey helperMethodMap["setStatusCode"] = t.setStatusCode + helperMethodMap["setHeader"] = t.setHeader helperMethodMap["sum"] = t.sum helperMethodMap["add"] = t.add helperMethodMap["subtract"] = t.subtract @@ -146,11 +147,20 @@ func (t *Templator) RenderTemplate(tpl *raymond.Template, requestDetails *models ctx := t.NewTemplatingData(requestDetails, literals, vars, state) result, err := tpl.Exec(ctx) - if err == nil { - statusCode, ok := ctx.InternalVars["statusCode"] - if ok && response != nil { + if err == nil && response != nil { + // Set status code if present + if statusCode, ok := ctx.InternalVars["statusCode"]; ok { response.Status = statusCode.(int) } + // Set headers if present + if setHeaders, ok := ctx.InternalVars["setHeaders"]; ok { + if response.Headers == nil { + response.Headers = make(map[string][]string) + } + for k, v := range setHeaders.(map[string][]string) { + response.Headers[k] = v + } + } } return result, err } diff --git a/core/templating/templating_test.go b/core/templating/templating_test.go index cbbbb2bf1..3cfd09747 100644 --- a/core/templating/templating_test.go +++ b/core/templating/templating_test.go @@ -917,6 +917,22 @@ func Test_ApplyTemplate_setStatusCode_should_handle_nil_response(t *testing.T) { Expect(template).To(Equal("")) } +func Test_ApplyTemplate_setHeader(t *testing.T) { + RegisterTestingT(t) + + templator := templating.NewTemplator() + + template, err := templator.ParseTemplate(`{{ setHeader "X-Test-Header" "HeaderValue" }}`) + Expect(err).To(BeNil()) + + response := &models.ResponseDetails{Headers: map[string][]string{}} + result, err := templator.RenderTemplate(template, &models.RequestDetails{}, response, &models.Literals{}, &models.Variables{}, make(map[string]string)) + + Expect(err).To(BeNil()) + Expect(result).To(Equal("")) + Expect(response.Headers).To(HaveKeyWithValue("X-Test-Header", []string{"HeaderValue"})) +} + func toInterfaceSlice(arguments []string) []interface{} { argumentsArray := make([]interface{}, len(arguments)) diff --git a/docs/pages/keyconcepts/templating/templating.rst b/docs/pages/keyconcepts/templating/templating.rst index 20e460061..3e1933689 100644 --- a/docs/pages/keyconcepts/templating/templating.rst +++ b/docs/pages/keyconcepts/templating/templating.rst @@ -654,6 +654,51 @@ To learn about more advanced templating functionality, such as looping and condi Global Literals and Variables ----------------------------- +Setting properties on the response +---------------------------------- + +Hoverfly provides helper functions to set properties on the HTTP response directly from your templates. This allows you to dynamically control the status code and headers based on request data or logic in your template. + +Setting the Status Code +~~~~~~~~~~~~~~~~~~~~~~~ +You can set the HTTP status code of the response using the ``setStatusCode`` helper. This is useful for conditional logic, such as returning a 404 if a resource is not found, or a 200 if an operation succeeds. + +.. code:: handlebars + + {{ setStatusCode 404 }} + +You can use this helper inside conditional blocks: + +.. code:: handlebars + + {{#equal (csvDeleteRows 'pets' 'category' 'cats' true) '0'}} + {{ setStatusCode 404 }} + {"Message":"Error no cats found"} + {{else}} + {{ setStatusCode 200 }} + {"Message":"All cats deleted"} + {{/equal}} + +If you provide an invalid status code (e.g., outside the range 100-599), it will be ignored. + +Setting Response Headers +~~~~~~~~~~~~~~~~~~~~~~~ +You can set or override HTTP response headers using the ``setHeader`` helper. This is useful for adding custom headers, controlling caching, or setting content types dynamically. + +.. code:: handlebars + + {{ setHeader "X-Custom-Header" "HeaderValue" }} + +You can use this helper multiple times to set different headers, or inside conditional blocks to set headers based on logic: + +.. code:: handlebars + + {{ setHeader "Content-Type" "application/json" }} + {{ setHeader "X-Request-Id" (randomUuid) }} + +If the header already exists, it will be overwritten with the new value. + +Both helpers do not output anything to the template result; they only affect the response properties. You can define global literals and variables for templated response. This comes in handy when you have a lot of templated responses that share the same constant values or helper methods. diff --git a/functional-tests/hoverctl/.hoverfly/.gitkeep b/functional-tests/hoverctl/.hoverfly/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/functional-tests/hoverctl/.hoverfly/cache/.gitkeep b/functional-tests/hoverctl/.hoverfly/cache/.gitkeep deleted file mode 100644 index e69de29bb..000000000