diff --git a/README.md b/README.md index 40f0eb4..6f71c28 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,9 @@ Mock definition: "host": "example.com", "method": "GET|POST|PUT|PATCH|...", "path": "/your/path/:variable", + "pathVariables": { + "variable": "\\d*" + }, "queryStringParameters": { "name": ["value"], "name": ["value", "value"] @@ -201,6 +204,7 @@ A core feature of Mmock is the ability to return canned HTTP responses for reque * *host*: Request http host. (without port) * *method*: Request http method. It allows more than one separated by pipes "|" **Mandatory** * *path*: Resource identifier. It allows :value matching. **Mandatory** +* *pathVariables*: A map of path variables to regexp that they need to match against. { "value": "\\d*" } * *queryStringParameters*: Array of query strings. It allows more than one value for the same key. * *headers*: Array of headers. It allows more than one value for the same key. **Case sensitive!** * *cookies*: Array of cookies. @@ -228,6 +232,7 @@ Query strings and headers support also global matches (*) in the header/paramete Regexp matching is available for: - body - query strings +- path variables See https://pkg.go.dev/regexp/syntax for regexp syntax @@ -265,6 +270,10 @@ You can use variable data in response. The variables will be defined as tags lik - URI - description +**Enviroment variables:** You can access the server's environment variables using + + * env."*VARIABLE_NAME*" + **Request data:** Use them if you want to add request data in your response. - request.scheme @@ -631,6 +640,7 @@ You can always disable this behavior adding the following flag `-server-statisti - Improved logging with levels thanks to [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) - Support for Regular Expressions for QueryStringParameters [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) - Support for URI and Description tags [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) +- Support for Regular Expressions for Path Variables [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) ### Contributing diff --git a/pkg/match/request.go b/pkg/match/request.go index 6ffe3c1..22d7b29 100644 --- a/pkg/match/request.go +++ b/pkg/match/request.go @@ -19,6 +19,7 @@ var ( ErrCookiesNotMatch = errors.New("Cookies not match") ErrScenarioNotMatch = errors.New("Scenario state not match") ErrPathNotMatch = errors.New("Path not match") + ErrPathVariablesNotMatch = errors.New("Path variables not match") ) func NewTester(comparator *payload.Comparator, scenario ScenearioStorer) *Request { @@ -189,9 +190,15 @@ func (mm Request) Match(req *mock.Request, mock *mock.Definition, scenarioAware return false, fmt.Errorf("Fragment not match. Actual: %s, Expected: %s", req.Fragment, mock.Request.Fragment) } - if !glob.Glob(mock.Request.Path, req.Path) && route.Match(req.Path) == nil { + var pathMatch = route.Match(req.Path) + + if !glob.Glob(mock.Request.Path, req.Path) && pathMatch == nil { return false, fmt.Errorf("%w Actual: %s, Expected: %s", ErrPathNotMatch, req.Path, mock.Request.Path) } + + if pathMatch != nil && mock.Request.PathVariables != nil && !route.MatchPathVariables(pathMatch, mock.Request.PathVariables) { + return false, fmt.Errorf("%w Actual: %s Expected: %v", ErrPathVariablesNotMatch, mock.Request.PathVariables, pathMatch.Params) + } if !mm.mockIncludesMethod(req.Method, &mock.Request) { return false, fmt.Errorf("Method not match. Actual: %s, Expected: %s", req.Method, mock.Request.Method) diff --git a/pkg/match/request_test.go b/pkg/match/request_test.go index 59a2620..ac533c4 100644 --- a/pkg/match/request_test.go +++ b/pkg/match/request_test.go @@ -94,6 +94,39 @@ func TestPathVars(t *testing.T) { } } +func TestPathVariables(t *testing.T) { + req := mock.Request{} + req.Path = "/a/b/c" + + m := mock.Definition{} + m.Request.Path = "/a/:b/:c" + m.Request.PathVariables = map[string]string{ + "b": "[a-z]", + "c": "[a-z]", + } + + mm := Request{} + + // match + if b, err := mm.Match(&req, &m, true); !b { + t.Error(err) + } + + // not match + req.Path = "a/b/3" + + if b, err := mm.Match(&req, &m, true); b { + t.Error(err) + } + + // not present + req.Path = "a/b" + + if b, err := mm.Match(&req, &m, true); b { + t.Error(err) + } +} + func TestPathGlob(t *testing.T) { req := mock.Request{} req.Path = "/a/b/c" diff --git a/pkg/mock/definition.go b/pkg/mock/definition.go index c5c909c..8c3d281 100644 --- a/pkg/mock/definition.go +++ b/pkg/mock/definition.go @@ -11,6 +11,8 @@ type Values map[string][]string type Cookies map[string]string +type PathValues map[string]string + type HttpHeaders struct { Headers Values `json:"headers"` Cookies Cookies `json:"cookies"` @@ -22,6 +24,7 @@ type Request struct { Port string `json:"port"` Method string `json:"method"` Path string `json:"path"` + PathVariables PathValues `json:"pathVariables"` QueryStringParameters Values `json:"queryStringParameters"` Fragment string `json:"fragment"` HttpHeaders diff --git a/pkg/route/route.go b/pkg/route/route.go index dffab94..61bfb26 100644 --- a/pkg/route/route.go +++ b/pkg/route/route.go @@ -4,8 +4,11 @@ import ( "fmt" "regexp" "strings" + "github.com/jmartin82/mmock/v3/internal/config/logger" ) +var log = logger.Log + type Route struct { Keys []string Regex *regexp.Regexp @@ -40,6 +43,28 @@ func (route *Route) Match(url string) *Match { return &Match{route.Params(url), route.Pattern} } +func (route *Route) MatchPathVariables(match *Match, patterns map[string]string) bool{ + for key, value := range match.Params { + + pattern, exists := patterns[key] + + if !exists { + log.Debugf("pathVariable %v isn't in request", key) + return false + } + + log.Debugf("comparing pathVariable %v that has a value of %v against pattern %v", key, value, pattern) + matched, err := regexp.MatchString(pattern, value) + + if !matched || err != nil { + log.Debugf("PathVariable %v with a value of %v either doesn't match %v or %v", key, value, pattern, err) + return false + } + } + + return true +} + func NewRoute(pattern string) *Route { regex, keys := pathToRegex(pattern) return &Route{keys, regex, pattern}