From 089e8bbbe3a526ad15d79c1cd3facd04ff80f632 Mon Sep 17 00:00:00 2001 From: Ruslan Isamukhametov Date: Tue, 1 Aug 2023 11:05:02 +0300 Subject: [PATCH 1/2] Add multiple values matchers and logical matchers (#14) --- README.md | 2 +- client_test.go | 12 ++- doc.go | 5 +- expected-template.json | 35 ++++++++- logical_matcher.go | 54 +++++++++++++ matching.go | 168 ++-------------------------------------- multi_value_matcher.go | 33 ++++++++ multipart_pattern.go | 36 ++------- request.go | 104 ++++++++++++------------- string_value_matcher.go | 108 ++++++++++++++++++++++++++ stub_rule.go | 37 ++++++--- url_matcher.go | 49 ++++++++++++ 12 files changed, 381 insertions(+), 262 deletions(-) create mode 100644 logical_matcher.go create mode 100644 multi_value_matcher.go create mode 100644 string_value_matcher.go create mode 100644 url_matcher.go diff --git a/README.md b/README.md index 0e9c48d..f297163 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ func TestSome(t *testing.T) { // stubbing POST http://0.0.0.0:8080/example wiremockClient.StubFor(wiremock.Post(wiremock.URLPathEqualTo("/example")). - WithQueryParam("firstName", wiremock.EqualTo("Jhon")). + WithQueryParam("firstName", wiremock.EqualTo("John")). WithQueryParam("lastName", wiremock.NotMatching("Black")). WithBodyPattern(wiremock.EqualToJson(`{"meta": "information"}`)). WithHeader("x-session", wiremock.Matching("^\\S+fingerprint\\S+$")). diff --git a/client_test.go b/client_test.go index 24f54f1..0331c79 100644 --- a/client_test.go +++ b/client_test.go @@ -22,9 +22,14 @@ func TestStubRule_ToJson(t *testing.T) { } postStubRule := Post(URLPathEqualTo("/example")). - WithQueryParam("firstName", EqualTo("Jhon")). + WithHost(EqualTo("localhost")). + WithScheme("http"). + WithPort(8080). + WithQueryParam("firstName", EqualTo("John").Or(EqualTo("Jack"))). WithQueryParam("lastName", NotMatching("Black")). - WithQueryParam("nickname", EqualToIgnoreCase("jhonBlack")). + WithQueryParam("nickname", EqualToIgnoreCase("johnBlack")). + WithQueryParam("address", Includes(EqualTo("1"), Contains("2"), NotContains("3"))). + WithQueryParam("id", Contains("1").And(NotContains("2"))). WithBodyPattern(EqualToJson(`{"meta": "information"}`, IgnoreArrayOrder, IgnoreExtraElements)). WithBodyPattern(Contains("information")). WithMultipartPattern( @@ -53,15 +58,18 @@ func TestStubRule_ToJson(t *testing.T) { if err != nil { t.Fatalf("failed to read expected-template.json %v", err) } + rawResult, err := json.Marshal(postStubRule) if err != nil { t.Fatalf("StubRole json.Marshal error: %v", err) } + var expected map[string]interface{} err = json.Unmarshal([]byte(fmt.Sprintf(string(rawExpectedRequestBody), postStubRule.uuid, postStubRule.uuid)), &expected) if err != nil { t.Fatalf("StubRole json.Unmarshal error: %v", err) } + var parsedResult map[string]interface{} err = json.Unmarshal(rawResult, &parsedResult) if err != nil { diff --git a/doc.go b/doc.go index 71c2f55..4d39fd5 100644 --- a/doc.go +++ b/doc.go @@ -8,7 +8,7 @@ Some might consider it a service virtualization tool or a mock server. HTTP request: - POST /example?firstName=Jhon&lastName=Any string other than "Gray" HTTP/1.1 + POST /example?firstName=John&lastName=Any string other than "Gray" HTTP/1.1 Host: 0.0.0.0:8080 x-session: somefingerprintsome Content-Type: application/json @@ -26,7 +26,7 @@ Stub: client := wiremock.NewClient("http://0.0.0.0:8080") client.StubFor(wiremock.Post(wiremock.URLPathEqualTo("/example")). - WithQueryParam("firstName", wiremock.EqualTo("Jhon")). + WithQueryParam("firstName", wiremock.EqualTo("John")). WithQueryParam("lastName", wiremock.NotMatching("Gray")). WithBodyPattern(wiremock.EqualToJson(`{"meta": "information"}`)). WithHeader("x-session", wiremock.Matching("^\\S+fingerprint\\S+$")). @@ -61,6 +61,5 @@ You can verify if a request has been made that matches the mapping. client.StubFor(exampleStubRule) // ... client.Verify(exampleStubRule.Request(), 1) - */ package wiremock diff --git a/expected-template.json b/expected-template.json index 4042ba4..d60dbc7 100644 --- a/expected-template.json +++ b/expected-template.json @@ -7,6 +7,11 @@ "newScenarioState": "Stopped", "request": { "method": "POST", + "scheme": "http", + "host": { + "equalTo": "localhost" + }, + "port": 8080, "basicAuthCredentials": { "password": "password", "username": "username" @@ -58,14 +63,40 @@ }, "queryParameters": { "firstName": { - "equalTo": "Jhon" + "or": [ + { + "equalTo": "John" + }, + { + "equalTo": "Jack" + } + ] }, "lastName": { "doesNotMatch": "Black" }, "nickname": { - "equalTo": "jhonBlack", + "equalTo": "johnBlack", "caseInsensitive": true + }, + "address": { + "includes" : [ + { + "equalTo": "1" + }, + { + "contains": "2" + }, + { + "doesNotContain": "3" + } + ] + }, + "id": { + "and": [ + {"contains": "1"}, + {"doesNotContain": "2"} + ] } }, "urlPath": "/example" diff --git a/logical_matcher.go b/logical_matcher.go new file mode 100644 index 0000000..973081d --- /dev/null +++ b/logical_matcher.go @@ -0,0 +1,54 @@ +package wiremock + +import ( + "encoding/json" +) + +type LogicalMatcher struct { + operator string + operands []BasicParamMatcher +} + +func (m LogicalMatcher) MarshalJSON() ([]byte, error) { + jsonMap := map[string]interface{}{ + m.operator: m.operands, + } + + return json.Marshal(jsonMap) +} + +// Or returns a logical OR of the current matcher and the given matcher. +func (m LogicalMatcher) Or(matcher BasicParamMatcher) BasicParamMatcher { + if m.operator == "or" { + m.operands = append(m.operands, matcher) + return m + } + + return Or(m, matcher) +} + +// And returns a logical AND of the current matcher and the given matcher. +func (m LogicalMatcher) And(matcher BasicParamMatcher) BasicParamMatcher { + if m.operator == "and" { + m.operands = append(m.operands, matcher) + return m + } + + return And(m, matcher) +} + +// Or returns a logical OR of the list of matchers. +func Or(matchers ...BasicParamMatcher) LogicalMatcher { + return LogicalMatcher{ + operator: "or", + operands: matchers, + } +} + +// And returns a logical AND of the list of matchers. +func And(matchers ...BasicParamMatcher) LogicalMatcher { + return LogicalMatcher{ + operator: "and", + operands: matchers, + } +} diff --git a/matching.go b/matching.go index 8f2153b..303be48 100644 --- a/matching.go +++ b/matching.go @@ -11,6 +11,7 @@ const ( ParamMatchesJsonPath ParamMatchingStrategy = "matchesJsonPath" ParamAbsent ParamMatchingStrategy = "absent" ParamDoesNotMatch ParamMatchingStrategy = "doesNotMatch" + ParamDoesNotContains ParamMatchingStrategy = "doesNotContain" ) // Types of url matching. @@ -27,6 +28,11 @@ const ( IgnoreExtraElements EqualFlag = "ignoreExtraElements" ) +const ( + ParamHasExactly MultiValueMatchingStrategy = "hasExactly" + ParamIncludes MultiValueMatchingStrategy = "includes" +) + // EqualFlag is enum of less strict matching flag. type EqualFlag string @@ -36,163 +42,5 @@ type URLMatchingStrategy string // ParamMatchingStrategy is enum params matching type. type ParamMatchingStrategy string -// URLMatcher is structure for defining the type of url matching. -type URLMatcher struct { - strategy URLMatchingStrategy - value string -} - -// Strategy returns URLMatchingStrategy of URLMatcher. -func (m URLMatcher) Strategy() URLMatchingStrategy { - return m.strategy -} - -// Value returns value of URLMatcher. -func (m URLMatcher) Value() string { - return m.value -} - -// URLEqualTo returns URLMatcher with URLEqualToRule matching strategy. -func URLEqualTo(url string) URLMatcher { - return URLMatcher{ - strategy: URLEqualToRule, - value: url, - } -} - -// URLPathEqualTo returns URLMatcher with URLPathEqualToRule matching strategy. -func URLPathEqualTo(url string) URLMatcher { - return URLMatcher{ - strategy: URLPathEqualToRule, - value: url, - } -} - -// URLPathMatching returns URLMatcher with URLPathMatchingRule matching strategy. -func URLPathMatching(url string) URLMatcher { - return URLMatcher{ - strategy: URLPathMatchingRule, - value: url, - } -} - -// URLMatching returns URLMatcher with URLMatchingRule matching strategy. -func URLMatching(url string) URLMatcher { - return URLMatcher{ - strategy: URLMatchingRule, - value: url, - } -} - -// ParamMatcher is structure for defining the type of params. -type ParamMatcher struct { - strategy ParamMatchingStrategy - value string - flags map[string]bool -} - -// Strategy returns ParamMatchingStrategy of ParamMatcher. -func (m ParamMatcher) Strategy() ParamMatchingStrategy { - return m.strategy -} - -// Value returns value of ParamMatcher. -func (m ParamMatcher) Value() string { - return m.value -} - -// Flags return value of ParamMatcher. -func (m ParamMatcher) Flags() map[string]bool { - return m.flags -} - -// EqualTo returns ParamMatcher with ParamEqualTo matching strategy. -func EqualTo(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamEqualTo, - value: param, - } -} - -// EqualToIgnoreCase returns ParamMatcher with ParamEqualToIgnoreCase matching strategy -func EqualToIgnoreCase(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamEqualTo, - value: param, - flags: map[string]bool{ - "caseInsensitive": true, - }, - } -} - -// Matching returns ParamMatcher with ParamMatches matching strategy. -func Matching(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamMatches, - value: param, - } -} - -// Contains returns ParamMatcher with ParamContains matching strategy. -func Contains(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamContains, - value: param, - } -} - -// EqualToXml returns ParamMatcher with ParamEqualToXml matching strategy. -func EqualToXml(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamEqualToXml, - value: param, - } -} - -// EqualToJson returns ParamMatcher with ParamEqualToJson matching strategy. -func EqualToJson(param string, flags ...EqualFlag) ParamMatcher { - mflags := make(map[string]bool, len(flags)) - for _, flag := range flags { - mflags[string(flag)] = true - } - - return ParamMatcher{ - strategy: ParamEqualToJson, - value: param, - flags: mflags, - } -} - -// MatchingXPath returns ParamMatcher with ParamMatchesXPath matching strategy. -func MatchingXPath(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamMatchesXPath, - value: param, - } -} - -// MatchingJsonPath returns ParamMatcher with ParamMatchesJsonPath matching strategy. -func MatchingJsonPath(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamMatchesJsonPath, - value: param, - } -} - -// NotMatching returns ParamMatcher with ParamDoesNotMatch matching strategy. -func NotMatching(param string) ParamMatcher { - return ParamMatcher{ - strategy: ParamDoesNotMatch, - value: param, - } -} - -func Absent() ParamMatcher { - return ParamMatcher{ - strategy: ParamAbsent, - value: "", - flags: map[string]bool{ - string(ParamAbsent): true, - }, - } -} +// MultiValueMatchingStrategy is enum multi value matching type. +type MultiValueMatchingStrategy string diff --git a/multi_value_matcher.go b/multi_value_matcher.go new file mode 100644 index 0000000..dc380ff --- /dev/null +++ b/multi_value_matcher.go @@ -0,0 +1,33 @@ +package wiremock + +import "encoding/json" + +type MultiValueMatcher struct { + strategy MultiValueMatchingStrategy + matchers []BasicParamMatcher +} + +// MarshalJSON returns the JSON encoding of the matcher. +func (m MultiValueMatcher) MarshalJSON() ([]byte, error) { + jsonMap := map[string]interface{}{ + string(m.strategy): m.matchers, + } + + return json.Marshal(jsonMap) +} + +// HasExactly returns a matcher that matches when the parameter has exactly the specified values. +func HasExactly(matchers ...BasicParamMatcher) MultiValueMatcher { + return MultiValueMatcher{ + strategy: ParamHasExactly, + matchers: matchers, + } +} + +// Includes returns a matcher that matches when the parameter includes the specified values. +func Includes(matchers ...BasicParamMatcher) MultiValueMatcher { + return MultiValueMatcher{ + strategy: ParamIncludes, + matchers: matchers, + } +} diff --git a/multipart_pattern.go b/multipart_pattern.go index 48a640b..8755b41 100644 --- a/multipart_pattern.go +++ b/multipart_pattern.go @@ -14,8 +14,8 @@ type MultipartMatchingType string type MultipartPattern struct { matchingType MultipartMatchingType - headers map[string]ParamMatcherInterface - bodyPatterns []ParamMatcher + headers map[string]json.Marshaler + bodyPatterns []BasicParamMatcher } func NewMultipartPattern() *MultipartPattern { @@ -26,7 +26,7 @@ func NewMultipartPattern() *MultipartPattern { func (m *MultipartPattern) WithName(name string) *MultipartPattern { if m.headers == nil { - m.headers = map[string]ParamMatcherInterface{} + m.headers = map[string]json.Marshaler{} } m.headers["Content-Disposition"] = Contains(fmt.Sprintf(`name="%s"`, name)) @@ -48,14 +48,14 @@ func (m *MultipartPattern) WithAnyMatchingType() *MultipartPattern { return m } -func (m *MultipartPattern) WithBodyPattern(matcher ParamMatcher) *MultipartPattern { +func (m *MultipartPattern) WithBodyPattern(matcher BasicParamMatcher) *MultipartPattern { m.bodyPatterns = append(m.bodyPatterns, matcher) return m } -func (m *MultipartPattern) WithHeader(header string, matcher ParamMatcherInterface) *MultipartPattern { +func (m *MultipartPattern) WithHeader(header string, matcher json.Marshaler) *MultipartPattern { if m.headers == nil { - m.headers = map[string]ParamMatcherInterface{} + m.headers = map[string]json.Marshaler{} } m.headers[header] = matcher @@ -69,31 +69,11 @@ func (m *MultipartPattern) MarshalJSON() ([]byte, error) { } if len(m.bodyPatterns) > 0 { - bodyPatterns := make([]map[string]interface{}, len(m.bodyPatterns)) - for i, bodyPattern := range m.bodyPatterns { - bodyPatterns[i] = map[string]interface{}{ - string(bodyPattern.Strategy()): bodyPattern.Value(), - } - - for flag, value := range bodyPattern.flags { - bodyPatterns[i][flag] = value - } - } - multipart["bodyPatterns"] = bodyPatterns + multipart["bodyPatterns"] = m.bodyPatterns } if len(m.headers) > 0 { - headers := make(map[string]map[string]interface{}, len(m.headers)) - for key, header := range m.headers { - headers[key] = map[string]interface{}{ - string(header.Strategy()): header.Value(), - } - - for flag, value := range header.Flags() { - headers[key][flag] = value - } - } - multipart["headers"] = headers + multipart["headers"] = m.headers } return json.Marshal(multipart) diff --git a/request.go b/request.go index f624355..bef1ef6 100644 --- a/request.go +++ b/request.go @@ -8,10 +8,13 @@ import ( type Request struct { urlMatcher URLMatcherInterface method string - headers map[string]ParamMatcherInterface - queryParams map[string]ParamMatcherInterface - cookies map[string]ParamMatcherInterface - bodyPatterns []ParamMatcher + host BasicParamMatcher + port *int64 + scheme *string + headers map[string]json.Marshaler + queryParams map[string]json.Marshaler + cookies map[string]BasicParamMatcher + bodyPatterns []BasicParamMatcher multipartPatterns []*MultipartPattern basicAuthCredentials *struct { username string @@ -27,6 +30,24 @@ func NewRequest(method string, urlMatcher URLMatcherInterface) *Request { } } +// WithPort is fluent-setter for port +func (r *Request) WithPort(port int64) *Request { + r.port = &port + return r +} + +// WithScheme is fluent-setter for scheme +func (r *Request) WithScheme(scheme string) *Request { + r.scheme = &scheme + return r +} + +// WithHost is fluent-setter for host +func (r *Request) WithHost(host BasicParamMatcher) *Request { + r.host = host + return r +} + // WithMethod is fluent-setter for http verb func (r *Request) WithMethod(method string) *Request { r.method = method @@ -40,7 +61,7 @@ func (r *Request) WithURLMatched(urlMatcher URLMatcherInterface) *Request { } // WithBodyPattern adds body pattern to list -func (r *Request) WithBodyPattern(matcher ParamMatcher) *Request { +func (r *Request) WithBodyPattern(matcher BasicParamMatcher) *Request { r.bodyPatterns = append(r.bodyPatterns, matcher) return r } @@ -64,9 +85,9 @@ func (r *Request) WithBasicAuth(username, password string) *Request { } // WithQueryParam add param to query param list -func (r *Request) WithQueryParam(param string, matcher ParamMatcherInterface) *Request { +func (r *Request) WithQueryParam(param string, matcher json.Marshaler) *Request { if r.queryParams == nil { - r.queryParams = map[string]ParamMatcherInterface{} + r.queryParams = map[string]json.Marshaler{} } r.queryParams[param] = matcher @@ -74,9 +95,9 @@ func (r *Request) WithQueryParam(param string, matcher ParamMatcherInterface) *R } // WithHeader add header to header list -func (r *Request) WithHeader(header string, matcher ParamMatcherInterface) *Request { +func (r *Request) WithHeader(header string, matcher json.Marshaler) *Request { if r.headers == nil { - r.headers = map[string]ParamMatcherInterface{} + r.headers = map[string]json.Marshaler{} } r.headers[header] = matcher @@ -84,9 +105,9 @@ func (r *Request) WithHeader(header string, matcher ParamMatcherInterface) *Requ } // WithCookie is fluent-setter for cookie -func (r *Request) WithCookie(cookie string, matcher ParamMatcherInterface) *Request { +func (r *Request) WithCookie(cookie string, matcher BasicParamMatcher) *Request { if r.cookies == nil { - r.cookies = map[string]ParamMatcherInterface{} + r.cookies = map[string]BasicParamMatcher{} } r.cookies[cookie] = matcher @@ -99,60 +120,33 @@ func (r *Request) MarshalJSON() ([]byte, error) { "method": r.method, string(r.urlMatcher.Strategy()): r.urlMatcher.Value(), } + + if r.scheme != nil { + request["scheme"] = r.scheme + } + + if r.host != nil { + request["host"] = r.host + } + + if r.port != nil { + request["port"] = r.port + } + if len(r.bodyPatterns) > 0 { - bodyPatterns := make([]map[string]interface{}, len(r.bodyPatterns)) - for i, bodyPattern := range r.bodyPatterns { - bodyPatterns[i] = map[string]interface{}{ - string(bodyPattern.Strategy()): bodyPattern.Value(), - } - - for flag, value := range bodyPattern.flags { - bodyPatterns[i][flag] = value - } - } - request["bodyPatterns"] = bodyPatterns + request["bodyPatterns"] = r.bodyPatterns } if len(r.multipartPatterns) > 0 { request["multipartPatterns"] = r.multipartPatterns } if len(r.headers) > 0 { - headers := make(map[string]map[string]interface{}, len(r.headers)) - for key, header := range r.headers { - headers[key] = map[string]interface{}{ - string(header.Strategy()): header.Value(), - } - - for flag, value := range header.Flags() { - headers[key][flag] = value - } - } - request["headers"] = headers + request["headers"] = r.headers } if len(r.cookies) > 0 { - cookies := make(map[string]map[string]interface{}, len(r.cookies)) - for key, cookie := range r.cookies { - cookies[key] = map[string]interface{}{ - string(cookie.Strategy()): cookie.Value(), - } - - for flag, value := range cookie.Flags() { - cookies[key][flag] = value - } - } - request["cookies"] = cookies + request["cookies"] = r.cookies } if len(r.queryParams) > 0 { - params := make(map[string]map[string]interface{}, len(r.queryParams)) - for key, param := range r.queryParams { - params[key] = map[string]interface{}{ - string(param.Strategy()): param.Value(), - } - - for flag, value := range param.Flags() { - params[key][flag] = value - } - } - request["queryParameters"] = params + request["queryParameters"] = r.queryParams } if r.basicAuthCredentials != nil { diff --git a/string_value_matcher.go b/string_value_matcher.go new file mode 100644 index 0000000..1f9a95a --- /dev/null +++ b/string_value_matcher.go @@ -0,0 +1,108 @@ +package wiremock + +import "encoding/json" + +type BasicParamMatcher interface { + json.Marshaler + Or(stringMatcher BasicParamMatcher) BasicParamMatcher + And(stringMatcher BasicParamMatcher) BasicParamMatcher +} + +type StringValueMatcher struct { + strategy ParamMatchingStrategy + value string + flags []string +} + +func (m StringValueMatcher) MarshalJSON() ([]byte, error) { + jsonMap := make(map[string]interface{}, 1+len(m.flags)) + if m.strategy != "" { + jsonMap[string(m.strategy)] = m.value + } + for _, flag := range m.flags { + jsonMap[flag] = true + } + + return json.Marshal(jsonMap) +} + +// Or returns a logical OR of the two matchers. +func (m StringValueMatcher) Or(matcher BasicParamMatcher) BasicParamMatcher { + return Or(m, matcher) +} + +// And returns a logical AND of the two matchers. +func (m StringValueMatcher) And(matcher BasicParamMatcher) BasicParamMatcher { + return And(m, matcher) +} + +// NewStringValueMatcher creates a new StringValueMatcher. +func NewStringValueMatcher(strategy ParamMatchingStrategy, value string, flags ...string) StringValueMatcher { + return StringValueMatcher{ + strategy: strategy, + value: value, + flags: flags, + } +} + +// EqualTo returns a matcher that matches when the parameter equals the specified value. +func EqualTo(value string) BasicParamMatcher { + return NewStringValueMatcher(ParamEqualTo, value) +} + +// EqualToIgnoreCase returns a matcher that matches when the parameter equals the specified value, ignoring case. +func EqualToIgnoreCase(value string) BasicParamMatcher { + return NewStringValueMatcher(ParamEqualTo, value, "caseInsensitive") +} + +// Matching returns a matcher that matches when the parameter matches the specified regular expression. +func Matching(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamMatches, param) +} + +// EqualToXml returns a matcher that matches when the parameter is equal to the specified XML. +func EqualToXml(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamEqualToXml, param) +} + +// EqualToJson returns a matcher that matches when the parameter is equal to the specified JSON. +func EqualToJson(param string, equalJsonFlags ...EqualFlag) BasicParamMatcher { + flags := make([]string, len(equalJsonFlags)) + for i, flag := range equalJsonFlags { + flags[i] = string(flag) + } + + return NewStringValueMatcher(ParamEqualToJson, param, flags...) +} + +// MatchingXPath returns a matcher that matches when the parameter matches the specified XPath. +func MatchingXPath(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamMatchesXPath, param) +} + +// MatchingJsonPath returns a matcher that matches when the parameter matches the specified JSON path. +func MatchingJsonPath(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamMatchesJsonPath, param) +} + +// NotMatching returns a matcher that matches when the parameter does not match the specified regular expression. +func NotMatching(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamDoesNotMatch, param) +} + +// Absent returns a matcher that matches when the parameter is absent. +func Absent() BasicParamMatcher { + return StringValueMatcher{ + flags: []string{string(ParamAbsent)}, + } +} + +// Contains returns a matcher that matches when the parameter contains the specified value. +func Contains(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamContains, param) +} + +// NotContains returns a matcher that matches when the parameter does not contain the specified value. +func NotContains(param string) BasicParamMatcher { + return NewStringValueMatcher(ParamDoesNotContains, param) +} diff --git a/stub_rule.go b/stub_rule.go index 1068e7d..669bd8a 100644 --- a/stub_rule.go +++ b/stub_rule.go @@ -11,13 +11,6 @@ import ( const ScenarioStateStarted = "Started" -// ParamMatcherInterface is pair ParamMatchingStrategy and string matched value -type ParamMatcherInterface interface { - Strategy() ParamMatchingStrategy - Value() string - Flags() map[string]bool -} - // URLMatcherInterface is pair URLMatchingStrategy and string matched value type URLMatcherInterface interface { Strategy() URLMatchingStrategy @@ -34,6 +27,10 @@ type response struct { fixedDelayMilliseconds time.Duration } +type Matcher interface { + StringValueMatcher | MultiValueMatcher +} + // StubRule is struct of http Request body to WireMock type StubRule struct { uuid string @@ -63,25 +60,43 @@ func (s *StubRule) Request() *Request { } // WithQueryParam adds query param and returns *StubRule -func (s *StubRule) WithQueryParam(param string, matcher ParamMatcherInterface) *StubRule { +func (s *StubRule) WithQueryParam(param string, matcher json.Marshaler) *StubRule { s.request.WithQueryParam(param, matcher) return s } +// WithPort adds port and returns *StubRule +func (s *StubRule) WithPort(port int64) *StubRule { + s.request.WithPort(port) + return s +} + +// WithScheme adds scheme and returns *StubRule +func (s *StubRule) WithScheme(scheme string) *StubRule { + s.request.WithScheme(scheme) + return s +} + +// WithHost adds host and returns *StubRule +func (s *StubRule) WithHost(host BasicParamMatcher) *StubRule { + s.request.WithHost(host) + return s +} + // WithHeader adds header to Headers and returns *StubRule -func (s *StubRule) WithHeader(header string, matcher ParamMatcherInterface) *StubRule { +func (s *StubRule) WithHeader(header string, matcher json.Marshaler) *StubRule { s.request.WithHeader(header, matcher) return s } // WithCookie adds cookie and returns *StubRule -func (s *StubRule) WithCookie(cookie string, matcher ParamMatcherInterface) *StubRule { +func (s *StubRule) WithCookie(cookie string, matcher BasicParamMatcher) *StubRule { s.request.WithCookie(cookie, matcher) return s } // WithBodyPattern adds body pattern and returns *StubRule -func (s *StubRule) WithBodyPattern(matcher ParamMatcher) *StubRule { +func (s *StubRule) WithBodyPattern(matcher BasicParamMatcher) *StubRule { s.request.WithBodyPattern(matcher) return s } diff --git a/url_matcher.go b/url_matcher.go new file mode 100644 index 0000000..cfa75d5 --- /dev/null +++ b/url_matcher.go @@ -0,0 +1,49 @@ +package wiremock + +// URLMatcher is structure for defining the type of url matching. +type URLMatcher struct { + strategy URLMatchingStrategy + value string +} + +// Strategy returns URLMatchingStrategy of URLMatcher. +func (m URLMatcher) Strategy() URLMatchingStrategy { + return m.strategy +} + +// Value returns value of URLMatcher. +func (m URLMatcher) Value() string { + return m.value +} + +// URLEqualTo returns URLMatcher with URLEqualToRule matching strategy. +func URLEqualTo(url string) URLMatcher { + return URLMatcher{ + strategy: URLEqualToRule, + value: url, + } +} + +// URLPathEqualTo returns URLMatcher with URLPathEqualToRule matching strategy. +func URLPathEqualTo(url string) URLMatcher { + return URLMatcher{ + strategy: URLPathEqualToRule, + value: url, + } +} + +// URLPathMatching returns URLMatcher with URLPathMatchingRule matching strategy. +func URLPathMatching(url string) URLMatcher { + return URLMatcher{ + strategy: URLPathMatchingRule, + value: url, + } +} + +// URLMatching returns URLMatcher with URLMatchingRule matching strategy. +func URLMatching(url string) URLMatcher { + return URLMatcher{ + strategy: URLMatchingRule, + value: url, + } +} From 555626936c0e9eda9161cd5321e9d0458e7de952 Mon Sep 17 00:00:00 2001 From: Ruslan Isamukhametov Date: Fri, 11 Aug 2023 00:18:32 +0300 Subject: [PATCH 2/2] Refactoring: changed internal work with Response of StubRule. (#16) * Refactoring: changed internal work with Response of StubRule. Deprecated: WillReturn, WillReturnBinary, WillReturnFileContent, WillReturnJSON, WithFixedDelayMilliseconds. Add: WillReturnResponse method for extended response with delays and faults. --- README.md | 80 ++++++++++--------- client.go | 12 +-- client_test.go | 13 ++-- delay.go | 58 ++++++++++++++ doc.go | 9 ++- expected-template.json | 6 +- logical_matcher.go | 10 ++- multi_value_matcher.go | 9 ++- multipart_pattern.go | 19 +++-- request.go | 14 ++-- response.go | 168 ++++++++++++++++++++++++++++++++++++++++ string_value_matcher.go | 15 +++- stub_rule.go | 116 ++++++++++----------------- url_matcher.go | 6 ++ 14 files changed, 391 insertions(+), 144 deletions(-) create mode 100644 delay.go create mode 100644 response.go diff --git a/README.md b/README.md index f297163..421b290 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,25 @@ # go-wiremock + [![Actions Status](https://github.com/walkerus/go-wiremock/workflows/build/badge.svg)](https://github.com/walkerus/go-wiremock/actions?query=workflow%3Abuild) [![Go Report Card](https://goreportcard.com/badge/github.com/walkerus/go-wiremock)](https://goreportcard.com/report/github.com/walkerus/go-wiremock) The simple package to stub HTTP resource using [WireMock admin](http://wiremock.org/docs/api/) ## Documentation + [![GoDoc](https://godoc.org/github.com/walkerus/go-wiremock?status.svg)](http://godoc.org/github.com/walkerus/go-wiremock) ## Usage + ```shell docker run -it --rm -p 8080:8080 wiremock/wiremock ``` + ```go package main import ( + "net/http" "testing" "github.com/walkerus/go-wiremock" @@ -23,60 +28,63 @@ import ( func TestSome(t *testing.T) { wiremockClient := wiremock.NewClient("http://0.0.0.0:8080") defer wiremockClient.Reset() - + // stubbing POST http://0.0.0.0:8080/example wiremockClient.StubFor(wiremock.Post(wiremock.URLPathEqualTo("/example")). - WithQueryParam("firstName", wiremock.EqualTo("John")). - WithQueryParam("lastName", wiremock.NotMatching("Black")). - WithBodyPattern(wiremock.EqualToJson(`{"meta": "information"}`)). - WithHeader("x-session", wiremock.Matching("^\\S+fingerprint\\S+$")). - WillReturnJSON( - map[string]interface{}{ - "code": 400, + WithQueryParam("firstName", wiremock.EqualTo("John")). + WithQueryParam("lastName", wiremock.NotMatching("Black")). + WithBodyPattern(wiremock.EqualToJson(`{"meta": "information"}`)). + WithHeader("x-session", wiremock.Matching("^\\S+fingerprint\\S+$")). + WillReturnResponse( + wiremock.NewResponse(). + WithJSONBody(map[string]interface{}{ + "code": 400, "detail": "detail", - }, - map[string]string{"Content-Type": "application/json"}, - 400, - ). - AtPriority(1)) + }). + WithHeader("Content-Type", "application/json"). + WithStatus(http.StatusBadRequest), + ). + AtPriority(1)) // scenario defer wiremockClient.ResetAllScenarios() wiremockClient.StubFor(wiremock.Get(wiremock.URLPathEqualTo("/status")). - WillReturnJSON( - map[string]interface{}{ - "status": nil, - }, - map[string]string{"Content-Type": "application/json"}, - 200, - ). - InScenario("Set status"). + WillReturnResponse( + wiremock.NewResponse(). + WithJSONBody(map[string]interface{}{ + "status": nil, + }). + WithHeader("Content-Type", "application/json"). + WithStatus(http.StatusOK), + ). + InScenario("Set status"). WhenScenarioStateIs(wiremock.ScenarioStateStarted)) wiremockClient.StubFor(wiremock.Post(wiremock.URLPathEqualTo("/state")). - WithBodyPattern(wiremock.EqualToJson(`{"status": "started"}`)). - InScenario("Set status"). - WillSetStateTo("Status started")) + WithBodyPattern(wiremock.EqualToJson(`{"status": "started"}`)). + InScenario("Set status"). + WillSetStateTo("Status started")) statusStub := wiremock.Get(wiremock.URLPathEqualTo("/status")). - WillReturnJSON( - map[string]interface{}{ - "status": "started", - }, - map[string]string{"Content-Type": "application/json"}, - 200, - ). - InScenario("Set status"). - WhenScenarioStateIs("Status started") + WillReturnResponse( + wiremock.NewResponse(). + WithJSONBody(map[string]interface{}{ + "status": "started", + }). + WithHeader("Content-Type", "application/json"). + WithStatus(http.StatusOK), + ). + InScenario("Set status"). + WhenScenarioStateIs("Status started") wiremockClient.StubFor(statusStub) //testing code... - + verifyResult, _ := wiremockClient.Verify(statusStub.Request(), 1) if !verifyResult { - //... + //... } - + wiremockClient.DeleteStub(statusStub) } ``` \ No newline at end of file diff --git a/client.go b/client.go index 151278b..5a61820 100644 --- a/client.go +++ b/client.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -37,7 +37,7 @@ func (c *Client) StubFor(stubRule *StubRule) error { defer res.Body.Close() if res.StatusCode != http.StatusCreated { - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("read response error: %s", err.Error()) } @@ -77,7 +77,7 @@ func (c *Client) Reset() error { defer res.Body.Close() if res.StatusCode != http.StatusOK { - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("read response error: %s", err.Error()) } @@ -97,7 +97,7 @@ func (c *Client) ResetAllScenarios() error { defer res.Body.Close() if res.StatusCode != http.StatusOK { - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("read response error: %s", err.Error()) } @@ -121,7 +121,7 @@ func (c *Client) GetCountRequests(r *Request) (int64, error) { } defer res.Body.Close() - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { return 0, fmt.Errorf("get count requests: read response error: %s", err.Error()) } @@ -167,7 +167,7 @@ func (c *Client) DeleteStubByID(id string) error { defer res.Body.Close() if res.StatusCode != http.StatusOK { - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("read response error: %s", err.Error()) } diff --git a/client_test.go b/client_test.go index 0331c79..70223c1 100644 --- a/client_test.go +++ b/client_test.go @@ -3,6 +3,7 @@ package wiremock import ( "encoding/json" "fmt" + "net/http" "os" "reflect" "testing" @@ -43,12 +44,14 @@ func TestStubRule_ToJson(t *testing.T) { WithCookie("absentcookie", Absent()). WithHeader("x-session", Matching("^\\S+@\\S+$")). WithCookie("session", EqualToXml("")). - WillReturn( - `{"code": 400, "detail": "detail"}`, - map[string]string{"Content-Type": "application/json"}, - 400, + WillReturnResponse( + NewResponse(). + WithStatus(http.StatusBadRequest). + WithHeader("Content-Type", "application/json"). + WithBody(`{"code": 400, "detail": "detail"}`). + WithFault(FaultConnectionResetByPeer). + WithFixedDelay(time.Second * 5), ). - WithFixedDelayMilliseconds(time.Second * 5). AtPriority(1). InScenario("Scenario"). WhenScenarioStateIs("Started"). diff --git a/delay.go b/delay.go new file mode 100644 index 0000000..50cb95d --- /dev/null +++ b/delay.go @@ -0,0 +1,58 @@ +package wiremock + +import "encoding/json" + +type DelayInterface interface { + ParseDelay() map[string]interface{} +} + +type fixedDelay struct { + milliseconds int64 +} + +func (d fixedDelay) ParseDelay() map[string]interface{} { + return map[string]interface{}{ + "type": "fixed", + "milliseconds": d.milliseconds, + } +} + +type logNormalRandomDelay struct { + median int64 + sigma float64 +} + +func (d logNormalRandomDelay) ParseDelay() map[string]interface{} { + return map[string]interface{}{ + "type": "lognormal", + "median": d.median, + "sigma": d.sigma, + } +} + +type uniformRandomDelay struct { + lower int64 + upper int64 +} + +func (d uniformRandomDelay) ParseDelay() map[string]interface{} { + return map[string]interface{}{ + "type": "uniform", + "lower": d.lower, + "upper": d.upper, + } +} + +type chunkedDribbleDelay struct { + numberOfChunks int64 + totalDuration int64 +} + +func (d chunkedDribbleDelay) MarshalJSON() ([]byte, error) { + jsonMap := map[string]interface{}{ + "numberOfChunks": d.numberOfChunks, + "totalDuration": d.totalDuration, + } + + return json.Marshal(jsonMap) +} diff --git a/doc.go b/doc.go index 4d39fd5..7eacff6 100644 --- a/doc.go +++ b/doc.go @@ -30,10 +30,11 @@ Stub: WithQueryParam("lastName", wiremock.NotMatching("Gray")). WithBodyPattern(wiremock.EqualToJson(`{"meta": "information"}`)). WithHeader("x-session", wiremock.Matching("^\\S+fingerprint\\S+$")). - WillReturn( - `{"code": 400, "detail": "detail"}`, - map[string]string{"Content-Type": "application/json"}, - 400, + WillReturnResponse( + wiremock.NewResponse(). + WithStatus(http.StatusBadRequest). + WithHeader("Content-Type", "application/json"). + WithBody(`{"code": 400, "detail": "detail"}`), ). AtPriority(1)) diff --git a/expected-template.json b/expected-template.json index d60dbc7..d9cc545 100644 --- a/expected-template.json +++ b/expected-template.json @@ -107,6 +107,10 @@ "Content-Type": "application/json" }, "status": 400, - "fixedDelayMilliseconds": 5000 + "delayDistribution": { + "type": "fixed", + "milliseconds": 5000 + }, + "fault": "CONNECTION_RESET_BY_PEER" } } \ No newline at end of file diff --git a/logical_matcher.go b/logical_matcher.go index 973081d..194bc10 100644 --- a/logical_matcher.go +++ b/logical_matcher.go @@ -9,12 +9,16 @@ type LogicalMatcher struct { operands []BasicParamMatcher } +// MarshalJSON returns the JSON encoding of the matcher. func (m LogicalMatcher) MarshalJSON() ([]byte, error) { - jsonMap := map[string]interface{}{ + return json.Marshal(m.ParseMatcher()) +} + +// ParseMatcher returns the map representation of the structure. +func (m LogicalMatcher) ParseMatcher() map[string]interface{} { + return map[string]interface{}{ m.operator: m.operands, } - - return json.Marshal(jsonMap) } // Or returns a logical OR of the current matcher and the given matcher. diff --git a/multi_value_matcher.go b/multi_value_matcher.go index dc380ff..b5f2680 100644 --- a/multi_value_matcher.go +++ b/multi_value_matcher.go @@ -9,11 +9,14 @@ type MultiValueMatcher struct { // MarshalJSON returns the JSON encoding of the matcher. func (m MultiValueMatcher) MarshalJSON() ([]byte, error) { - jsonMap := map[string]interface{}{ + return json.Marshal(m.ParseMatcher()) +} + +// ParseMatcher returns the map representation of the structure. +func (m MultiValueMatcher) ParseMatcher() map[string]interface{} { + return map[string]interface{}{ string(m.strategy): m.matchers, } - - return json.Marshal(jsonMap) } // HasExactly returns a matcher that matches when the parameter has exactly the specified values. diff --git a/multipart_pattern.go b/multipart_pattern.go index 8755b41..a59134d 100644 --- a/multipart_pattern.go +++ b/multipart_pattern.go @@ -12,9 +12,14 @@ const ( type MultipartMatchingType string +type MultipartPatternInterface interface { + json.Marshaler + ParseMultipartPattern() map[string]interface{} +} + type MultipartPattern struct { matchingType MultipartMatchingType - headers map[string]json.Marshaler + headers map[string]MatcherInterface bodyPatterns []BasicParamMatcher } @@ -26,7 +31,7 @@ func NewMultipartPattern() *MultipartPattern { func (m *MultipartPattern) WithName(name string) *MultipartPattern { if m.headers == nil { - m.headers = map[string]json.Marshaler{} + m.headers = map[string]MatcherInterface{} } m.headers["Content-Disposition"] = Contains(fmt.Sprintf(`name="%s"`, name)) @@ -53,9 +58,9 @@ func (m *MultipartPattern) WithBodyPattern(matcher BasicParamMatcher) *Multipart return m } -func (m *MultipartPattern) WithHeader(header string, matcher json.Marshaler) *MultipartPattern { +func (m *MultipartPattern) WithHeader(header string, matcher MatcherInterface) *MultipartPattern { if m.headers == nil { - m.headers = map[string]json.Marshaler{} + m.headers = map[string]MatcherInterface{} } m.headers[header] = matcher @@ -64,6 +69,10 @@ func (m *MultipartPattern) WithHeader(header string, matcher json.Marshaler) *Mu // MarshalJSON gives valid JSON or error. func (m *MultipartPattern) MarshalJSON() ([]byte, error) { + return json.Marshal(m.ParseMultipartPattern()) +} + +func (m *MultipartPattern) ParseMultipartPattern() map[string]interface{} { multipart := map[string]interface{}{ "matchingType": m.matchingType, } @@ -76,5 +85,5 @@ func (m *MultipartPattern) MarshalJSON() ([]byte, error) { multipart["headers"] = m.headers } - return json.Marshal(multipart) + return multipart } diff --git a/request.go b/request.go index bef1ef6..daa5d67 100644 --- a/request.go +++ b/request.go @@ -11,11 +11,11 @@ type Request struct { host BasicParamMatcher port *int64 scheme *string - headers map[string]json.Marshaler - queryParams map[string]json.Marshaler + headers map[string]MatcherInterface + queryParams map[string]MatcherInterface cookies map[string]BasicParamMatcher bodyPatterns []BasicParamMatcher - multipartPatterns []*MultipartPattern + multipartPatterns []MultipartPatternInterface basicAuthCredentials *struct { username string password string @@ -85,9 +85,9 @@ func (r *Request) WithBasicAuth(username, password string) *Request { } // WithQueryParam add param to query param list -func (r *Request) WithQueryParam(param string, matcher json.Marshaler) *Request { +func (r *Request) WithQueryParam(param string, matcher MatcherInterface) *Request { if r.queryParams == nil { - r.queryParams = map[string]json.Marshaler{} + r.queryParams = map[string]MatcherInterface{} } r.queryParams[param] = matcher @@ -95,9 +95,9 @@ func (r *Request) WithQueryParam(param string, matcher json.Marshaler) *Request } // WithHeader add header to header list -func (r *Request) WithHeader(header string, matcher json.Marshaler) *Request { +func (r *Request) WithHeader(header string, matcher MatcherInterface) *Request { if r.headers == nil { - r.headers = map[string]json.Marshaler{} + r.headers = map[string]MatcherInterface{} } r.headers[header] = matcher diff --git a/response.go b/response.go new file mode 100644 index 0000000..1270b13 --- /dev/null +++ b/response.go @@ -0,0 +1,168 @@ +package wiremock + +import ( + "net/http" + "time" +) + +type Fault string + +const ( + FaultEmptyResponse Fault = "EMPTY_RESPONSE" + FaultMalformedResponseChunk Fault = "MALFORMED_RESPONSE_CHUNK" + FaultRandomDataThenClose Fault = "RANDOM_DATA_THEN_CLOSE" + FaultConnectionResetByPeer Fault = "CONNECTION_RESET_BY_PEER" +) + +type ResponseInterface interface { + ParseResponse() map[string]interface{} +} + +type Response struct { + body *string + base64Body []byte + bodyFileName *string + jsonBody interface{} + headers map[string]string + status int64 + delayDistribution DelayInterface + chunkedDribbleDelay *chunkedDribbleDelay + fault *Fault +} + +func NewResponse() Response { + return Response{ + status: http.StatusOK, + } +} + +// WithLogNormalRandomDelay sets log normal random delay for response +func (r Response) WithLogNormalRandomDelay(mediana time.Duration, sigma float64) Response { + r.delayDistribution = logNormalRandomDelay{ + median: mediana.Milliseconds(), + sigma: sigma, + } + + return r +} + +// WithUniformRandomDelay sets uniform random delay for response +func (r Response) WithUniformRandomDelay(lower, upper time.Duration) Response { + r.delayDistribution = uniformRandomDelay{ + lower: lower.Milliseconds(), + upper: upper.Milliseconds(), + } + + return r +} + +// WithFixedDelay sets fixed delay milliseconds for response +func (r Response) WithFixedDelay(time time.Duration) Response { + r.delayDistribution = fixedDelay{ + milliseconds: time.Milliseconds(), + } + + return r +} + +// WithChunkedDribbleDelay sets chunked dribble delay for response +func (r Response) WithChunkedDribbleDelay(numberOfChunks int64, totalDuration time.Duration) Response { + r.chunkedDribbleDelay = &chunkedDribbleDelay{ + numberOfChunks: numberOfChunks, + totalDuration: totalDuration.Milliseconds(), + } + + return r +} + +// WithStatus sets status for response +func (r Response) WithStatus(status int64) Response { + r.status = status + return r +} + +// WithHeader sets header for response +func (r Response) WithHeader(key, value string) Response { + if r.headers == nil { + r.headers = make(map[string]string) + } + + r.headers[key] = value + + return r +} + +// WithHeaders sets headers for response +func (r Response) WithHeaders(headers map[string]string) Response { + r.headers = headers + return r +} + +func (r Response) WithFault(fault Fault) Response { + r.fault = &fault + return r +} + +// WithBody sets body for response +func (r Response) WithBody(body string) Response { + r.body = &body + return r +} + +// WithBinaryBody sets binary body for response +func (r Response) WithBinaryBody(body []byte) Response { + r.base64Body = body + return r +} + +// WithJSONBody sets json body for response +func (r Response) WithJSONBody(body interface{}) Response { + r.jsonBody = body + return r +} + +// WithBodyFile sets body file name for response +func (r Response) WithBodyFile(fileName string) Response { + r.bodyFileName = &fileName + return r +} + +func (r Response) ParseResponse() map[string]interface{} { + jsonMap := map[string]interface{}{ + "status": r.status, + } + + if r.body != nil { + jsonMap["body"] = *r.body + } + + if r.base64Body != nil { + jsonMap["base64Body"] = r.base64Body + } + + if r.bodyFileName != nil { + jsonMap["bodyFileName"] = *r.bodyFileName + } + + if r.jsonBody != nil { + jsonMap["jsonBody"] = r.jsonBody + } + + if r.headers != nil { + jsonMap["headers"] = r.headers + } + + if r.delayDistribution != nil { + jsonMap["delayDistribution"] = r.delayDistribution.ParseDelay() + } + + if r.chunkedDribbleDelay != nil { + jsonMap["chunkedDribbleDelay"] = r.chunkedDribbleDelay + } + + if r.fault != nil { + jsonMap["fault"] = *r.fault + } + + return jsonMap +} diff --git a/string_value_matcher.go b/string_value_matcher.go index 1f9a95a..f144b8e 100644 --- a/string_value_matcher.go +++ b/string_value_matcher.go @@ -2,8 +2,14 @@ package wiremock import "encoding/json" +type MatcherInterface interface { + json.Marshaler + ParseMatcher() map[string]interface{} +} + type BasicParamMatcher interface { json.Marshaler + ParseMatcher() map[string]interface{} Or(stringMatcher BasicParamMatcher) BasicParamMatcher And(stringMatcher BasicParamMatcher) BasicParamMatcher } @@ -14,16 +20,23 @@ type StringValueMatcher struct { flags []string } +// MarshalJSON returns the JSON encoding of the matcher. func (m StringValueMatcher) MarshalJSON() ([]byte, error) { + return json.Marshal(m.ParseMatcher()) +} + +// ParseMatcher returns the map representation of the structure. +func (m StringValueMatcher) ParseMatcher() map[string]interface{} { jsonMap := make(map[string]interface{}, 1+len(m.flags)) if m.strategy != "" { jsonMap[string(m.strategy)] = m.value } + for _, flag := range m.flags { jsonMap[flag] = true } - return json.Marshal(jsonMap) + return jsonMap } // Or returns a logical OR of the two matchers. diff --git a/stub_rule.go b/stub_rule.go index 669bd8a..bbfc993 100644 --- a/stub_rule.go +++ b/stub_rule.go @@ -1,7 +1,6 @@ package wiremock import ( - "encoding/base64" "encoding/json" "net/http" "time" @@ -11,46 +10,29 @@ import ( const ScenarioStateStarted = "Started" -// URLMatcherInterface is pair URLMatchingStrategy and string matched value -type URLMatcherInterface interface { - Strategy() URLMatchingStrategy - Value() string -} - -type response struct { - body *string - base64Body []byte - bodyFileName *string - jsonBody interface{} - headers map[string]string - status int64 - fixedDelayMilliseconds time.Duration -} - type Matcher interface { StringValueMatcher | MultiValueMatcher } // StubRule is struct of http Request body to WireMock type StubRule struct { - uuid string - request *Request - response response - priority *int64 - scenarioName *string - requiredScenarioState *string - newScenarioState *string + uuid string + request *Request + response ResponseInterface + fixedDelayMilliseconds *int64 + priority *int64 + scenarioName *string + requiredScenarioState *string + newScenarioState *string } // NewStubRule returns a new *StubRule. func NewStubRule(method string, urlMatcher URLMatcher) *StubRule { uuid, _ := uuidPkg.NewRandom() return &StubRule{ - uuid: uuid.String(), - request: NewRequest(method, urlMatcher), - response: response{ - status: http.StatusOK, - }, + uuid: uuid.String(), + request: NewRequest(method, urlMatcher), + response: NewResponse(), } } @@ -60,7 +42,7 @@ func (s *StubRule) Request() *Request { } // WithQueryParam adds query param and returns *StubRule -func (s *StubRule) WithQueryParam(param string, matcher json.Marshaler) *StubRule { +func (s *StubRule) WithQueryParam(param string, matcher MatcherInterface) *StubRule { s.request.WithQueryParam(param, matcher) return s } @@ -84,7 +66,7 @@ func (s *StubRule) WithHost(host BasicParamMatcher) *StubRule { } // WithHeader adds header to Headers and returns *StubRule -func (s *StubRule) WithHeader(header string, matcher json.Marshaler) *StubRule { +func (s *StubRule) WithHeader(header string, matcher MatcherInterface) *StubRule { s.request.WithHeader(header, matcher) return s } @@ -107,41 +89,45 @@ func (s *StubRule) WithMultipartPattern(pattern *MultipartPattern) *StubRule { return s } +// Deprecated: Use WillReturnResponse(NewResponse().WithBody(body).WithHeaders(headers).WithStatus(status)) instead // WillReturn sets response and returns *StubRule func (s *StubRule) WillReturn(body string, headers map[string]string, status int64) *StubRule { - s.response.body = &body - s.response.headers = headers - s.response.status = status + s.response = NewResponse().WithBody(body).WithStatus(status).WithHeaders(headers) return s } +// Deprecated: Use WillReturnResponse(NewResponse().WithBinaryBody(body).WithHeaders(headers).WithStatus(status)) instead // WillReturnBinary sets response with binary body and returns *StubRule func (s *StubRule) WillReturnBinary(body []byte, headers map[string]string, status int64) *StubRule { - s.response.base64Body = body - s.response.headers = headers - s.response.status = status + s.response = NewResponse().WithBinaryBody(body).WithStatus(status).WithHeaders(headers) return s } +// Deprecated: Use WillReturnResponse(NewResponse().WithBodyFile(file).WithHeaders(headers).WithStatus(status)) instead // WillReturnFileContent sets response with some file content and returns *StubRule func (s *StubRule) WillReturnFileContent(bodyFileName string, headers map[string]string, status int64) *StubRule { - s.response.bodyFileName = &bodyFileName - s.response.headers = headers - s.response.status = status + s.response = NewResponse().WithBodyFile(bodyFileName).WithStatus(status).WithHeaders(headers) return s } +// Deprecated: Use WillReturnResponse(NewResponse().WithJsonBody(json).WithHeaders(headers).WithStatus(status)) instead // WillReturnJSON sets response with json body and returns *StubRule func (s *StubRule) WillReturnJSON(json interface{}, headers map[string]string, status int64) *StubRule { - s.response.jsonBody = json - s.response.headers = headers - s.response.status = status + s.response = NewResponse().WithJSONBody(json).WithStatus(status).WithHeaders(headers) + return s +} + +// Deprecated: Use WillReturnResponse(NewResponse().WithFixedDelay(time.Second)) instead +// WithFixedDelayMilliseconds adds delay to response and returns *StubRule +func (s *StubRule) WithFixedDelayMilliseconds(delay time.Duration) *StubRule { + milliseconds := delay.Milliseconds() + s.fixedDelayMilliseconds = &milliseconds return s } -// WithFixedDelayMilliseconds sets fixed delay milliseconds for response -func (s *StubRule) WithFixedDelayMilliseconds(time time.Duration) *StubRule { - s.response.fixedDelayMilliseconds = time +// WillReturnResponse sets response and returns *StubRule +func (s *StubRule) WillReturnResponse(response ResponseInterface) *StubRule { + s.response = response return s } @@ -208,41 +194,25 @@ func Patch(urlMatchingPair URLMatcher) *StubRule { // MarshalJSON makes json body for http Request func (s *StubRule) MarshalJSON() ([]byte, error) { jsonStubRule := struct { - UUID string `json:"uuid,omitempty"` - ID string `json:"id,omitempty"` - Priority *int64 `json:"priority,omitempty"` - ScenarioName *string `json:"scenarioName,omitempty"` - RequiredScenarioScenarioState *string `json:"requiredScenarioState,omitempty"` - NewScenarioState *string `json:"newScenarioState,omitempty"` - Request *Request `json:"request"` - Response struct { - Body string `json:"body,omitempty"` - Base64Body string `json:"base64Body,omitempty"` - BodyFileName string `json:"bodyFileName,omitempty"` - JSONBody interface{} `json:"jsonBody,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Status int64 `json:"status,omitempty"` - FixedDelayMilliseconds int `json:"fixedDelayMilliseconds,omitempty"` - } `json:"response"` + UUID string `json:"uuid,omitempty"` + ID string `json:"id,omitempty"` + Priority *int64 `json:"priority,omitempty"` + ScenarioName *string `json:"scenarioName,omitempty"` + RequiredScenarioScenarioState *string `json:"requiredScenarioState,omitempty"` + NewScenarioState *string `json:"newScenarioState,omitempty"` + Request *Request `json:"request"` + Response map[string]interface{} `json:"response"` }{} jsonStubRule.Priority = s.priority jsonStubRule.ScenarioName = s.scenarioName jsonStubRule.RequiredScenarioScenarioState = s.requiredScenarioState jsonStubRule.NewScenarioState = s.newScenarioState + jsonStubRule.Response = s.response.ParseResponse() - if s.response.body != nil { - jsonStubRule.Response.Body = *s.response.body - } else if len(s.response.base64Body) > 0 { - jsonStubRule.Response.Base64Body = base64.StdEncoding.EncodeToString(s.response.base64Body) - } else if s.response.bodyFileName != nil { - jsonStubRule.Response.BodyFileName = *s.response.bodyFileName - } else if s.response.jsonBody != nil { - jsonStubRule.Response.JSONBody = s.response.jsonBody + if s.fixedDelayMilliseconds != nil { + jsonStubRule.Response["fixedDelayMilliseconds"] = *s.fixedDelayMilliseconds } - jsonStubRule.Response.Headers = s.response.headers - jsonStubRule.Response.Status = s.response.status - jsonStubRule.Response.FixedDelayMilliseconds = int(s.response.fixedDelayMilliseconds.Milliseconds()) jsonStubRule.Request = s.request jsonStubRule.ID = s.uuid jsonStubRule.UUID = s.uuid diff --git a/url_matcher.go b/url_matcher.go index cfa75d5..0acb152 100644 --- a/url_matcher.go +++ b/url_matcher.go @@ -1,5 +1,11 @@ package wiremock +// URLMatcherInterface is pair URLMatchingStrategy and string matched value +type URLMatcherInterface interface { + Strategy() URLMatchingStrategy + Value() string +} + // URLMatcher is structure for defining the type of url matching. type URLMatcher struct { strategy URLMatchingStrategy