diff --git a/client_test.go b/client_test.go index 2118c95..b4591a9 100644 --- a/client_test.go +++ b/client_test.go @@ -83,10 +83,7 @@ func TestStubRule_ToJson(t *testing.T) { WithScheme("http"). WithPort(8080). WithBearerToken(StartsWith("token")). - WillReturnResponse( - NewResponse(). - WithStatus(http.StatusOK), - ), + WillReturnResponse(OK()), ExpectedFileName: "expected-template-bearer-auth-startsWith.json", }, { @@ -96,10 +93,7 @@ func TestStubRule_ToJson(t *testing.T) { WithScheme("http"). WithPort(8080). WithBearerToken(EqualTo("token")). - WillReturnResponse( - NewResponse(). - WithStatus(http.StatusOK), - ), + WillReturnResponse(OK()), ExpectedFileName: "expected-template-bearer-auth-equalTo.json", }, { @@ -109,10 +103,7 @@ func TestStubRule_ToJson(t *testing.T) { WithScheme("http"). WithPort(8080). WithBearerToken(Contains("token")). - WillReturnResponse( - NewResponse(). - WithStatus(http.StatusOK), - ), + WillReturnResponse(OK()), ExpectedFileName: "expected-template-bearer-auth-contains.json", }, { @@ -122,12 +113,47 @@ func TestStubRule_ToJson(t *testing.T) { WithScheme("http"). WithPort(8080). WithBearerToken(EqualTo("token123").And(StartsWith("token"))). - WillReturnResponse( - NewResponse(). - WithStatus(http.StatusOK), - ), + WillReturnResponse(OK()), ExpectedFileName: "expected-template-bearer-auth-logicalMatcher.json", }, + { + Name: "NotLogicalMatcher", + StubRule: Post(URLPathEqualTo("/example")). + WithQueryParam("firstName", Not(EqualTo("John").Or(EqualTo("Jack")))). + WillReturnResponse(OK()), + ExpectedFileName: "not-logical-expression.json", + }, + { + Name: "JsonSchemaMatcher", + StubRule: Post(URLPathEqualTo("/example")). + WithQueryParam("firstName", MatchesJsonSchema( + `{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } +}`, + "V202012", + )). + WillReturnResponse(OK()), + ExpectedFileName: "matches-Json-schema.json", + }, + { + Name: "URLPathTemplateMatcher", + StubRule: Get(URLPathTemplate("/contacts/{contactId}/addresses/{addressId}")). + WithPathParam("contactId", EqualTo("12345")). + WithPathParam("addressId", EqualTo("99876")). + WillReturnResponse(OK()), + ExpectedFileName: "url-path-templating.json", + }, } for _, tc := range testCases { @@ -142,7 +168,7 @@ func TestStubRule_ToJson(t *testing.T) { var expected map[string]interface{} err = json.Unmarshal([]byte(fmt.Sprintf(string(rawExpectedRequestBody), stubRule.uuid, stubRule.uuid)), &expected) if err != nil { - t.Fatalf("StubRole json.Unmarshal error: %v", err) + t.Fatalf("StubRule json.Unmarshal error: %v", err) } rawResult, err := json.Marshal(stubRule) diff --git a/logical_matcher.go b/logical_matcher.go index 194bc10..3b0f6df 100644 --- a/logical_matcher.go +++ b/logical_matcher.go @@ -16,6 +16,12 @@ func (m LogicalMatcher) MarshalJSON() ([]byte, error) { // ParseMatcher returns the map representation of the structure. func (m LogicalMatcher) ParseMatcher() map[string]interface{} { + if m.operator == "not" { + return map[string]interface{}{ + m.operator: m.operands[0], + } + } + return map[string]interface{}{ m.operator: m.operands, } @@ -56,3 +62,11 @@ func And(matchers ...BasicParamMatcher) LogicalMatcher { operands: matchers, } } + +// Not returns a logical NOT of the given matcher. Required wiremock version >= 3.0.0 +func Not(matcher BasicParamMatcher) LogicalMatcher { + return LogicalMatcher{ + operator: "not", + operands: []BasicParamMatcher{matcher}, + } +} diff --git a/matching.go b/matching.go index 303be48..6726dd1 100644 --- a/matching.go +++ b/matching.go @@ -2,16 +2,17 @@ package wiremock // Types of params matching. const ( - ParamEqualTo ParamMatchingStrategy = "equalTo" - ParamMatches ParamMatchingStrategy = "matches" - ParamContains ParamMatchingStrategy = "contains" - ParamEqualToXml ParamMatchingStrategy = "equalToXml" - ParamEqualToJson ParamMatchingStrategy = "equalToJson" - ParamMatchesXPath ParamMatchingStrategy = "matchesXPath" - ParamMatchesJsonPath ParamMatchingStrategy = "matchesJsonPath" - ParamAbsent ParamMatchingStrategy = "absent" - ParamDoesNotMatch ParamMatchingStrategy = "doesNotMatch" - ParamDoesNotContains ParamMatchingStrategy = "doesNotContain" + ParamEqualTo ParamMatchingStrategy = "equalTo" + ParamMatches ParamMatchingStrategy = "matches" + ParamContains ParamMatchingStrategy = "contains" + ParamEqualToXml ParamMatchingStrategy = "equalToXml" + ParamEqualToJson ParamMatchingStrategy = "equalToJson" + ParamMatchesXPath ParamMatchingStrategy = "matchesXPath" + ParamMatchesJsonPath ParamMatchingStrategy = "matchesJsonPath" + ParamAbsent ParamMatchingStrategy = "absent" + ParamDoesNotMatch ParamMatchingStrategy = "doesNotMatch" + ParamDoesNotContains ParamMatchingStrategy = "doesNotContain" + ParamMatchesJsonSchema ParamMatchingStrategy = "matchesJsonSchema" ) // Types of url matching. @@ -20,6 +21,7 @@ const ( URLPathEqualToRule URLMatchingStrategy = "urlPath" URLPathMatchingRule URLMatchingStrategy = "urlPathPattern" URLMatchingRule URLMatchingStrategy = "urlPattern" + URLPathTemplateRule URLMatchingStrategy = "urlPathTemplate" ) // Type of less strict matching flags. diff --git a/request.go b/request.go index 0bac237..7e7909e 100644 --- a/request.go +++ b/request.go @@ -13,9 +13,10 @@ type Request struct { scheme *string headers map[string]MatcherInterface queryParams map[string]MatcherInterface + pathParams map[string]MatcherInterface cookies map[string]BasicParamMatcher - bodyPatterns []BasicParamMatcher formParameters map[string]BasicParamMatcher + bodyPatterns []BasicParamMatcher multipartPatterns []MultipartPatternInterface basicAuthCredentials *struct { username string @@ -104,6 +105,16 @@ func (r *Request) WithQueryParam(param string, matcher MatcherInterface) *Reques return r } +// WithPathParam add param to path param list +func (r *Request) WithPathParam(param string, matcher MatcherInterface) *Request { + if r.pathParams == nil { + r.pathParams = map[string]MatcherInterface{} + } + + r.pathParams[param] = matcher + return r +} + // WithHeader add header to header list func (r *Request) WithHeader(header string, matcher MatcherInterface) *Request { if r.headers == nil { @@ -161,6 +172,9 @@ func (r *Request) MarshalJSON() ([]byte, error) { if len(r.queryParams) > 0 { request["queryParameters"] = r.queryParams } + if len(r.pathParams) > 0 { + request["pathParameters"] = r.pathParams + } if r.basicAuthCredentials != nil { request["basicAuthCredentials"] = map[string]string{ diff --git a/response.go b/response.go index e80830d..eedb62c 100644 --- a/response.go +++ b/response.go @@ -36,6 +36,10 @@ func NewResponse() Response { } } +func OK() Response { + return NewResponse().WithStatus(http.StatusOK) +} + // WithLogNormalRandomDelay sets log normal random delay for response func (r Response) WithLogNormalRandomDelay(median time.Duration, sigma float64) Response { r.delayDistribution = NewLogNormalRandomDelay(median, sigma) diff --git a/string_value_matcher.go b/string_value_matcher.go index 105c4a7..28539ef 100644 --- a/string_value_matcher.go +++ b/string_value_matcher.go @@ -160,6 +160,28 @@ func StartsWith(prefix string) BasicParamMatcher { return NewStringValueMatcher(ParamMatches, regex) } +type JSONSchemaMatcher struct { + StringValueMatcher + schemaVersion string +} + +// MatchesJsonSchema returns a matcher that matches when the parameter matches the specified JSON schema. +// Required wiremock version >= 3.0.0 +func MatchesJsonSchema(schema string, schemaVersion string) BasicParamMatcher { + return JSONSchemaMatcher{ + StringValueMatcher: NewStringValueMatcher(ParamMatchesJsonSchema, schema), + schemaVersion: schemaVersion, + } +} + +// MarshalJSON returns the JSON encoding of the matcher. +func (m JSONSchemaMatcher) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + string(m.strategy): m.value, + "schemaVersion": m.schemaVersion, + }) +} + func regexContainsStartAnchor(regex string) bool { return len(regex) > 0 && regex[0] == '^' } diff --git a/stub_rule.go b/stub_rule.go index 90960a8..fac34f8 100644 --- a/stub_rule.go +++ b/stub_rule.go @@ -45,6 +45,12 @@ func (s *StubRule) WithQueryParam(param string, matcher MatcherInterface) *StubR return s } +// WithPathParam adds path param and returns *StubRule +func (s *StubRule) WithPathParam(param string, matcher MatcherInterface) *StubRule { + s.request.WithPathParam(param, matcher) + return s +} + // WithPort adds port and returns *StubRule func (s *StubRule) WithPort(port int64) *StubRule { s.request.WithPort(port) diff --git a/testdata/matches-Json-schema.json b/testdata/matches-Json-schema.json new file mode 100644 index 0000000..b9e724c --- /dev/null +++ b/testdata/matches-Json-schema.json @@ -0,0 +1,17 @@ +{ + "uuid": "%s", + "id": "%s", + "request": { + "method": "POST", + "urlPath": "/example", + "queryParameters": { + "firstName": { + "matchesJsonSchema" : "{\n \"type\": \"object\",\n \"required\": [\n \"name\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"tag\": {\n \"type\": \"string\"\n }\n }\n}", + "schemaVersion" : "V202012" + } + } + }, + "response": { + "status": 200 + } +} \ No newline at end of file diff --git a/testdata/not-logical-expression.json b/testdata/not-logical-expression.json new file mode 100644 index 0000000..3f71da9 --- /dev/null +++ b/testdata/not-logical-expression.json @@ -0,0 +1,25 @@ +{ + "uuid": "%s", + "id": "%s", + "request": { + "method": "POST", + "urlPath": "/example", + "queryParameters": { + "firstName": { + "not": { + "or": [ + { + "equalTo": "John" + }, + { + "equalTo": "Jack" + } + ] + } + } + } + }, + "response": { + "status": 200 + } +} \ No newline at end of file diff --git a/testdata/url-path-templating.json b/testdata/url-path-templating.json new file mode 100644 index 0000000..7372272 --- /dev/null +++ b/testdata/url-path-templating.json @@ -0,0 +1,19 @@ +{ + "uuid": "%s", + "id": "%s", + "request" : { + "urlPathTemplate" : "/contacts/{contactId}/addresses/{addressId}", + "method" : "GET", + "pathParameters" : { + "contactId" : { + "equalTo" : "12345" + }, + "addressId" : { + "equalTo" : "99876" + } + } + }, + "response" : { + "status" : 200 + } +} \ No newline at end of file diff --git a/url_matcher.go b/url_matcher.go index 0acb152..20701fd 100644 --- a/url_matcher.go +++ b/url_matcher.go @@ -53,3 +53,14 @@ func URLMatching(url string) URLMatcher { value: url, } } + +// URLPathTemplate URL paths can be matched using URI templates, conforming to the same subset of the URI template standard as used in OpenAPI. +// Path variable matchers can also be used in the same manner as query and form parameters. +// Required wiremock >= 3.0.0 +// Example: /contacts/{contactId}/addresses/{addressId} +func URLPathTemplate(url string) URLMatcher { + return URLMatcher{ + strategy: URLPathTemplateRule, + value: url, + } +}