From ed78ba2d80c785b1e00c847927520f71d30bf657 Mon Sep 17 00:00:00 2001 From: stuioco Date: Wed, 17 Jul 2024 12:08:53 +0100 Subject: [PATCH] Template helper methods for type validation, string functions and regex --- core/templating/template_helpers.go | 86 +++++- core/templating/template_helpers_test.go | 255 ++++++++++++++++++ core/templating/templating.go | 19 +- .../keyconcepts/templating/templating.rst | 40 ++- 4 files changed, 391 insertions(+), 9 deletions(-) diff --git a/core/templating/template_helpers.go b/core/templating/template_helpers.go index 6dd9df1bf..014240ce1 100644 --- a/core/templating/template_helpers.go +++ b/core/templating/template_helpers.go @@ -2,13 +2,15 @@ package templating import ( "fmt" - "github.com/SpectoLabs/hoverfly/core/journal" "math" "reflect" + "regexp" "strconv" "strings" "time" + "github.com/SpectoLabs/hoverfly/core/journal" + "github.com/SpectoLabs/raymond" "github.com/pborman/uuid" @@ -117,6 +119,88 @@ func (t templateHelpers) concat(val1, val2 string) string { return val1 + val2 } +func (t templateHelpers) isNumeric(stringToCheck string) bool { + _, err := strconv.ParseFloat(stringToCheck, 64) + //return fmt.Sprintf("%t", err == nil) + return err == nil +} + +func (t templateHelpers) isAlphanumeric(s string) bool { + regex := regexp.MustCompile("^[a-zA-Z0-9]+$") + return regex.MatchString(s) +} + +func (t templateHelpers) isBool(s string) bool { + _, err := strconv.ParseBool(s) + return err == nil +} + +func (t templateHelpers) isGreaterThan(valueToCheck, minimumValue string) bool { + num1, err := strconv.ParseFloat(valueToCheck, 64) + if err != nil { + return false + } + num2, err := strconv.ParseFloat(minimumValue, 64) + if err != nil { + return false + } + return num1 > num2 +} + +func (t templateHelpers) isLessThan(valueToCheck, maximumValue string) bool { + num1, err := strconv.ParseFloat(valueToCheck, 64) + if err != nil { + return false + } + num2, err := strconv.ParseFloat(maximumValue, 64) + if err != nil { + return false + } + return num1 < num2 +} + +func (t templateHelpers) isBetween(valueToCheck, minimumValue, maximumValue string) bool { + return t.isGreaterThan(valueToCheck, minimumValue) && t.isLessThan(valueToCheck, maximumValue) +} + +func (t templateHelpers) matchesRegex(valueToCheck, pattern string) bool { + re, err := regexp.Compile(pattern) + if err != nil { + return false + } + return re.MatchString(valueToCheck) +} + +func (t templateHelpers) length(stringToCheck string) string { + return strconv.Itoa(len(stringToCheck)) +} + +func (t templateHelpers) substring(str, startStr, endStr string) string { + start, err := strconv.Atoi(startStr) + if err != nil { + return "" + } + end, err := strconv.Atoi(endStr) + if err != nil { + return "" + } + if start < 0 || end > len(str) || start > end { + return "" + } + return str[start:end] +} + +func (t templateHelpers) rightmostCharacters(str, countStr string) string { + count, err := strconv.Atoi(countStr) + if err != nil { + return "" + } + if count < 0 || count > len(str) { + return "" + } + return str[len(str)-count:] +} + func (t templateHelpers) faker(fakerType string) []reflect.Value { if t.fakerSource == nil { diff --git a/core/templating/template_helpers_test.go b/core/templating/template_helpers_test.go index 0928e3c32..905272506 100644 --- a/core/templating/template_helpers_test.go +++ b/core/templating/template_helpers_test.go @@ -100,6 +100,261 @@ func Test_concat(t *testing.T) { Expect(unit.concat("one", " two")).To(Equal("one two")) } +func Test_length(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.length("onelongstring")).To(Equal("13")) +} + +func Test_substring(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.substring("onelongstring", "3", "7")).To(Equal("long")) +} + +func Test_substring_withInvalidStart(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.substring("onelongstring", "-3", "6")).To(Equal("")) +} + +func Test_substring_withInvalidEnd(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.substring("onelongstring", "3", "the end")).To(Equal("")) +} + +func Test_rightmostCharacters(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.rightmostCharacters("onelongstring", "3")).To(Equal("ing")) +} + +func Test_rightmostCharacters_withInvalidCount(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.rightmostCharacters("onelongstring", "30")).To(Equal("")) +} + +func Test_isNumeric_withInteger(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isNumeric("123")).To(Equal(true)) +} + +func Test_isNumeric_withFloat(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isNumeric("45.67")).To(Equal(true)) +} + +func Test_isNumeric_withScientific(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isNumeric("1e10")).To(Equal(true)) +} + +func Test_isNumeric_withNegative(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isNumeric("-5")).To(Equal(true)) +} + +func Test_isNumeric_withString(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isNumeric("hello")).To(Equal(false)) +} + +func Test_isAlphanumeric_withAlphanumeric(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isAlphanumeric("ABC123")).To(Equal(true)) +} + +func Test_isAlphanumeric_withNumeric(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isAlphanumeric("123")).To(Equal(true)) +} + +func Test_isAlphanumeric_withAlpha(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isAlphanumeric("ABC")).To(Equal(true)) +} + +func Test_isAlphanumeric_withInvalidAlphanumeric(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isAlphanumeric("ABC!@123")).To(Equal(false)) +} + +func Test_isBool_withtrue(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBool("true")).To(Equal(true)) +} + +func Test_isBool_withfalse(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBool("false")).To(Equal(true)) +} + +func Test_isBool_with1(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBool("1")).To(Equal(true)) +} + +func Test_isBool_with0(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBool("0")).To(Equal(true)) +} + +func Test_isBool_withInvalidValue(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBool("maybe")).To(Equal(false)) +} + +func Test_isGreaterThan_withPositiveResult(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isGreaterThan("11", "10")).To(Equal(true)) +} + +func Test_isGreaterThan_withNegativeResult(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isGreaterThan("10", "11")).To(Equal(false)) +} + +func Test_isGreaterThan_withInvalidNumber(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isGreaterThan("abc", "11")).To(Equal(false)) +} + +func Test_isLessThan_withPositiveResult(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isLessThan("10", "11")).To(Equal(true)) +} + +func Test_isLessThan_withNegativeResult(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isLessThan("11", "10")).To(Equal(false)) +} + +func Test_isLessThan_withInvalidNumber(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isLessThan("abc", "11")).To(Equal(false)) +} + +func Test_isBetween_withPositiveOutcome(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBetween("5", "3", "7")).To(Equal(true)) +} + +func Test_isBetween_withNegativeOutcome(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBetween("5", "6", "7")).To(Equal(false)) +} + +func Test_isBetween_withInvalidArgument(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.isBetween("e", "6", "7")).To(Equal(false)) +} + +func Test_matchesRegex_withPositiveOutcome(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.matchesRegex("{\"someField\": \"a\", \"transactionId\": 1000, \"anotherField\": \"b\", \"store\": \"c\", \"clientUniqueId\": \"12345\", \"items\": [\"item1\", \"item2\", \"item3\"], \"extraField\": \"d\"}", "(?s).*(\"transactionId\": 1000).*store.*clientUniqueId.*items.*")).To(Equal(true)) +} +func Test_matchesRegex_withNegativeOutcome(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.matchesRegex("{\"someField\": \"a\", \"transactionNumber\": 1000, \"anotherField\": \"b\", \"store\": \"c\", \"clientUniqueId\": \"12345\", \"items\": [\"item1\", \"item2\", \"item3\"], \"extraField\": \"d\"}", "(?s).*(\"transactionId\": 1000).*store.*clientUniqueId.*items.*")).To(Equal(false)) +} + +func Test_matchesRegex_withInvalidArgument(t *testing.T) { + RegisterTestingT(t) + + unit := templateHelpers{} + + Expect(unit.matchesRegex("I am looking for this string", "&^%$£!@:<>+_!¬")).To(Equal(false)) +} + func Test_faker(t *testing.T) { RegisterTestingT(t) diff --git a/core/templating/templating.go b/core/templating/templating.go index 0e278fd04..60b6e29a9 100644 --- a/core/templating/templating.go +++ b/core/templating/templating.go @@ -2,12 +2,13 @@ package templating import ( "fmt" - "github.com/SpectoLabs/hoverfly/core/journal" - "github.com/SpectoLabs/hoverfly/core/util" "reflect" "strings" "time" + "github.com/SpectoLabs/hoverfly/core/journal" + "github.com/SpectoLabs/hoverfly/core/util" + "github.com/brianvoe/gofakeit/v6" "github.com/SpectoLabs/hoverfly/core/models" @@ -26,7 +27,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 + InternalVars map[string]interface{} // data store used internally by templating helpers } type Request struct { @@ -87,6 +88,16 @@ func NewTemplator() *Templator { helperMethodMap["replace"] = t.replace helperMethodMap["split"] = t.split helperMethodMap["concat"] = t.concat + helperMethodMap["length"] = t.length + helperMethodMap["substring"] = t.substring + helperMethodMap["rightmostCharacters"] = t.rightmostCharacters + helperMethodMap["isNumeric"] = t.isNumeric + helperMethodMap["isAlphanumeric"] = t.isAlphanumeric + helperMethodMap["isBool"] = t.isBool + helperMethodMap["isGreaterThan"] = t.isGreaterThan + helperMethodMap["isLessThan"] = t.isLessThan + helperMethodMap["isBetween"] = t.isBetween + helperMethodMap["matchesRegex"] = t.matchesRegex helperMethodMap["faker"] = t.faker helperMethodMap["requestBody"] = t.requestBody helperMethodMap["csv"] = t.parseCsv @@ -185,7 +196,7 @@ func (t *Templator) NewTemplatingData(requestDetails *models.RequestDetails, res CurrentDateTime: func(a1, a2, a3 string) string { return a1 + " " + a2 + " " + a3 }, - Kvs: kvs, + Kvs: kvs, InternalVars: make(map[string]interface{}), } diff --git a/docs/pages/keyconcepts/templating/templating.rst b/docs/pages/keyconcepts/templating/templating.rst index edd0ce2f2..f481e7098 100644 --- a/docs/pages/keyconcepts/templating/templating.rst +++ b/docs/pages/keyconcepts/templating/templating.rst @@ -281,8 +281,8 @@ is used to control whether the set value is returned. Each templating session has its own key value store, which means all the data you set will be cleared after the current response is rendered. -Maths Operation -~~~~~~~~~~~~~~~ +Maths Operations +~~~~~~~~~~~~~~~~ The basic maths operations are currently supported: add, subtract, multiply and divide. These functions take three parameters: two values it operates on and the precision. The precision is given in a string @@ -332,8 +332,8 @@ We can get the total price of all the line items using this templating function: ``{{ addToArray 'subtotal' (multiply (this.price) (this.quantity) '') false }} {{/each}}`` ``total: {{ sum (getArray 'subtotal') '0.00' }}`` -String Operation -~~~~~~~~~~~~~~~~ +String Operations +~~~~~~~~~~~~~~~~~ You can use the following helper methods to join, split or replace string values. @@ -350,7 +350,39 @@ You can use the following helper methods to join, split or replace string values | | | | | | ``{"text":"to be or not to be"}`` | to mock or not to mock | +-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Return a substring of a string | ``{{substring 'thisisalongstring' 7 11}}`` | long | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Return the length of a string | ``{{length 'thisisaverylongstring'}}`` | 21 | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Return the rightmost characters of a string | ``{{rightmostCharacters 'thisisalongstring' 3}}`` | ing | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ + +Validation Operations +~~~~~~~~~~~~~~~~~~~~~ +You can use the following helper methods to validate various types, compare value, and perform regular expression matching on strings. + ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Description | Example | Result | ++===========================================================+===========================================================+=========================================+ +| Is the value numeric | ``{{isNumeric '12.3'}}`` | true | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Is the value alphanumeric | ``{{isAlphanumeric 'abc!@123'}}`` | false | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Is the value a boolean | ``{{isBool (Request.Body 'jsonpath' '$.paidInFull')}}`` | true | +| | Where the payload is {"paidInFull":"false"} | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Is one value greater than another | ``{{isGreater (Request.Body 'jsonpath' '$.age') 25}`` | false | +| | Where the payload is {"age":"19"} | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Is one value less than another | ``{{isLess (Request.Body 'jsonpath' '$.age') 25}`` | true | +| | Where the payload is {"age":"19"} | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Is a value between two values | ``{{isBetween (Request.Body 'jsonpath' '$.age') 25 35}`` | false | +| | Where the payload is {"age":"19"} | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Does a string match a regular expression | ``{{matchesRegex '2022-09-27' '^\d{4}-\d{2}-\d{2}$'}}`` | true | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ Conditional Templating, Looping and More ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~