api_matchers 0.6.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +1 -1
- data/Gemfile +1 -1
- data/History.markdown +62 -46
- data/README.markdown +1070 -114
- data/TODO.markdown +39 -3
- data/api_matchers.gemspec +13 -6
- data/lib/api_matchers/collection/base.rb +65 -0
- data/lib/api_matchers/collection/be_sorted_by.rb +97 -0
- data/lib/api_matchers/collection/have_json_size.rb +54 -0
- data/lib/api_matchers/core/exceptions.rb +5 -0
- data/lib/api_matchers/core/find_in_json.rb +57 -28
- data/lib/api_matchers/core/http_status_codes.rb +118 -0
- data/lib/api_matchers/core/json_path_finder.rb +87 -0
- data/lib/api_matchers/core/parser.rb +4 -2
- data/lib/api_matchers/core/rspec_matchers.rb +130 -37
- data/lib/api_matchers/core/setup.rb +49 -23
- data/lib/api_matchers/core/value_normalizer.rb +26 -0
- data/lib/api_matchers/error_response/base.rb +77 -0
- data/lib/api_matchers/error_response/have_error.rb +69 -0
- data/lib/api_matchers/error_response/have_error_on.rb +173 -0
- data/lib/api_matchers/hateoas/base.rb +77 -0
- data/lib/api_matchers/hateoas/have_link.rb +102 -0
- data/lib/api_matchers/headers/base.rb +7 -7
- data/lib/api_matchers/headers/be_json.rb +2 -4
- data/lib/api_matchers/headers/be_xml.rb +2 -4
- data/lib/api_matchers/headers/have_cache_control.rb +90 -0
- data/lib/api_matchers/headers/have_cors_headers.rb +102 -0
- data/lib/api_matchers/headers/have_header.rb +102 -0
- data/lib/api_matchers/http_status/base.rb +48 -0
- data/lib/api_matchers/http_status/be_client_error.rb +17 -0
- data/lib/api_matchers/http_status/be_forbidden.rb +17 -0
- data/lib/api_matchers/http_status/be_no_content.rb +17 -0
- data/lib/api_matchers/http_status/be_not_found.rb +17 -0
- data/lib/api_matchers/http_status/be_redirect.rb +17 -0
- data/lib/api_matchers/http_status/be_server_error.rb +17 -0
- data/lib/api_matchers/http_status/be_successful.rb +17 -0
- data/lib/api_matchers/http_status/be_unauthorized.rb +17 -0
- data/lib/api_matchers/http_status/be_unprocessable.rb +17 -0
- data/lib/api_matchers/http_status/have_http_status.rb +39 -0
- data/lib/api_matchers/json_api/base.rb +83 -0
- data/lib/api_matchers/json_api/be_json_api_compliant.rb +158 -0
- data/lib/api_matchers/json_api/have_json_api_attributes.rb +62 -0
- data/lib/api_matchers/json_api/have_json_api_data.rb +110 -0
- data/lib/api_matchers/json_api/have_json_api_relationships.rb +62 -0
- data/lib/api_matchers/json_structure/base.rb +65 -0
- data/lib/api_matchers/json_structure/have_json_keys.rb +55 -0
- data/lib/api_matchers/json_structure/have_json_type.rb +72 -0
- data/lib/api_matchers/pagination/base.rb +73 -0
- data/lib/api_matchers/pagination/be_paginated.rb +73 -0
- data/lib/api_matchers/pagination/have_pagination_links.rb +74 -0
- data/lib/api_matchers/pagination/have_total_count.rb +77 -0
- data/lib/api_matchers/response_body/base.rb +10 -9
- data/lib/api_matchers/response_body/have_json.rb +2 -4
- data/lib/api_matchers/response_body/have_json_node.rb +45 -1
- data/lib/api_matchers/response_body/have_node.rb +2 -0
- data/lib/api_matchers/response_body/have_xml_node.rb +13 -14
- data/lib/api_matchers/response_body/match_json_schema.rb +89 -0
- data/lib/api_matchers/version.rb +3 -1
- data/lib/api_matchers.rb +75 -14
- data/spec/api_matchers/collection/be_sorted_by_spec.rb +110 -0
- data/spec/api_matchers/collection/have_json_size_spec.rb +101 -0
- data/spec/api_matchers/error_response/have_error_on_spec.rb +123 -0
- data/spec/api_matchers/error_response/have_error_spec.rb +108 -0
- data/spec/api_matchers/hateoas/have_link_spec.rb +105 -0
- data/spec/api_matchers/headers/base_spec.rb +8 -3
- data/spec/api_matchers/headers/be_json_spec.rb +1 -1
- data/spec/api_matchers/headers/be_xml_spec.rb +1 -1
- data/spec/api_matchers/headers/have_cache_control_spec.rb +102 -0
- data/spec/api_matchers/headers/have_cors_headers_spec.rb +74 -0
- data/spec/api_matchers/headers/have_header_spec.rb +88 -0
- data/spec/api_matchers/http_status/be_client_error_spec.rb +53 -0
- data/spec/api_matchers/http_status/be_forbidden_spec.rb +33 -0
- data/spec/api_matchers/http_status/be_no_content_spec.rb +33 -0
- data/spec/api_matchers/http_status/be_not_found_spec.rb +39 -0
- data/spec/api_matchers/http_status/be_redirect_spec.rb +55 -0
- data/spec/api_matchers/http_status/be_server_error_spec.rb +49 -0
- data/spec/api_matchers/http_status/be_successful_spec.rb +78 -0
- data/spec/api_matchers/http_status/be_unauthorized_spec.rb +33 -0
- data/spec/api_matchers/http_status/be_unprocessable_spec.rb +39 -0
- data/spec/api_matchers/http_status/have_http_status_spec.rb +81 -0
- data/spec/api_matchers/json_api/be_json_api_compliant_spec.rb +109 -0
- data/spec/api_matchers/json_api/have_json_api_attributes_spec.rb +61 -0
- data/spec/api_matchers/json_api/have_json_api_data_spec.rb +95 -0
- data/spec/api_matchers/json_api/have_json_api_relationships_spec.rb +61 -0
- data/spec/api_matchers/json_structure/have_json_keys_spec.rb +81 -0
- data/spec/api_matchers/json_structure/have_json_type_spec.rb +134 -0
- data/spec/api_matchers/pagination/be_paginated_spec.rb +95 -0
- data/spec/api_matchers/pagination/have_pagination_links_spec.rb +80 -0
- data/spec/api_matchers/pagination/have_total_count_spec.rb +85 -0
- data/spec/api_matchers/response_body/base_spec.rb +15 -7
- data/spec/api_matchers/response_body/have_json_node_spec.rb +57 -0
- data/spec/api_matchers/response_body/match_json_schema_spec.rb +86 -0
- metadata +154 -48
- data/.rvmrc.example +0 -1
- data/.travis.yml +0 -12
- data/lib/api_matchers/http_status_code/base.rb +0 -32
- data/lib/api_matchers/http_status_code/be_bad_request.rb +0 -25
- data/lib/api_matchers/http_status_code/be_forbidden.rb +0 -21
- data/lib/api_matchers/http_status_code/be_internal_server_error.rb +0 -25
- data/lib/api_matchers/http_status_code/be_not_found.rb +0 -25
- data/lib/api_matchers/http_status_code/be_ok.rb +0 -25
- data/lib/api_matchers/http_status_code/be_unauthorized.rb +0 -25
- data/lib/api_matchers/http_status_code/be_unprocessable_entity.rb +0 -25
- data/lib/api_matchers/http_status_code/create_resource.rb +0 -25
- data/spec/api_matchers/http_status_code/base_spec.rb +0 -12
- data/spec/api_matchers/http_status_code/be_bad_request_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_forbidden_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_internal_server_error_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_not_found_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_ok_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_unauthorized_spec.rb +0 -49
- data/spec/api_matchers/http_status_code/be_unprocessable_entity_spec.rb +0 -27
- data/spec/api_matchers/http_status_code/create_resource_spec.rb +0 -49
|
@@ -1,69 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module APIMatchers
|
|
2
4
|
module RSpecMatchers
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
# Content Type Matchers
|
|
6
|
+
def be_xml
|
|
7
|
+
::APIMatchers::Headers::BeXML.new
|
|
5
8
|
end
|
|
6
|
-
alias :
|
|
9
|
+
alias :be_in_xml :be_xml
|
|
7
10
|
|
|
8
|
-
def
|
|
9
|
-
::APIMatchers::
|
|
11
|
+
def be_json
|
|
12
|
+
::APIMatchers::Headers::BeJSON.new
|
|
10
13
|
end
|
|
14
|
+
alias :be_in_json :be_json
|
|
15
|
+
alias :be_a_json :be_json
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
# Response Body Matchers
|
|
18
|
+
def have_json_node(expected_node)
|
|
19
|
+
::APIMatchers::ResponseBody::HaveJsonNode.new(expected_node: expected_node)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def have_xml_node(expected_node)
|
|
23
|
+
::APIMatchers::ResponseBody::HaveXmlNode.new(expected_node: expected_node)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def have_json(expected_json)
|
|
27
|
+
::APIMatchers::ResponseBody::HaveJson.new(expected_json)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def have_node(expected_node)
|
|
31
|
+
if ::APIMatchers::Core::Setup.have_node_matcher.equal?(:json)
|
|
32
|
+
have_json_node(expected_node)
|
|
33
|
+
else
|
|
34
|
+
have_xml_node(expected_node)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def match_json_schema(schema)
|
|
39
|
+
::APIMatchers::ResponseBody::MatchJsonSchema.new(schema: schema)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# HTTP Status Matchers
|
|
43
|
+
def have_http_status(expected)
|
|
44
|
+
::APIMatchers::HTTPStatus::HaveHttpStatus.new(expected)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def be_successful
|
|
48
|
+
::APIMatchers::HTTPStatus::BeSuccessful.new
|
|
49
|
+
end
|
|
50
|
+
alias :be_success :be_successful
|
|
51
|
+
|
|
52
|
+
def be_redirect
|
|
53
|
+
::APIMatchers::HTTPStatus::BeRedirect.new
|
|
54
|
+
end
|
|
55
|
+
alias :be_redirection :be_redirect
|
|
56
|
+
|
|
57
|
+
def be_client_error
|
|
58
|
+
::APIMatchers::HTTPStatus::BeClientError.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def be_server_error
|
|
62
|
+
::APIMatchers::HTTPStatus::BeServerError.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def be_not_found
|
|
66
|
+
::APIMatchers::HTTPStatus::BeNotFound.new
|
|
14
67
|
end
|
|
15
|
-
alias :be_an_internal_server_error :be_internal_server_error
|
|
16
68
|
|
|
17
69
|
def be_unauthorized
|
|
18
|
-
::APIMatchers::
|
|
70
|
+
::APIMatchers::HTTPStatus::BeUnauthorized.new
|
|
19
71
|
end
|
|
20
72
|
|
|
21
73
|
def be_forbidden
|
|
22
|
-
::APIMatchers::
|
|
74
|
+
::APIMatchers::HTTPStatus::BeForbidden.new
|
|
23
75
|
end
|
|
24
76
|
|
|
25
|
-
def
|
|
26
|
-
::APIMatchers::
|
|
77
|
+
def be_unprocessable
|
|
78
|
+
::APIMatchers::HTTPStatus::BeUnprocessable.new
|
|
27
79
|
end
|
|
80
|
+
alias :be_unprocessable_entity :be_unprocessable
|
|
28
81
|
|
|
29
|
-
def
|
|
30
|
-
::APIMatchers::
|
|
82
|
+
def be_no_content
|
|
83
|
+
::APIMatchers::HTTPStatus::BeNoContent.new
|
|
31
84
|
end
|
|
32
85
|
|
|
33
|
-
|
|
34
|
-
|
|
86
|
+
# JSON Structure Matchers
|
|
87
|
+
def have_json_keys(*keys)
|
|
88
|
+
::APIMatchers::JsonStructure::HaveJsonKeys.new(*keys)
|
|
35
89
|
end
|
|
36
|
-
alias :created_resource :create_resource
|
|
37
90
|
|
|
38
|
-
def
|
|
39
|
-
::APIMatchers::
|
|
91
|
+
def have_json_type(expected_type)
|
|
92
|
+
::APIMatchers::JsonStructure::HaveJsonType.new(expected_type)
|
|
40
93
|
end
|
|
41
|
-
alias :be_in_xml :be_xml
|
|
42
94
|
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
# Collection Matchers
|
|
96
|
+
def have_json_size(expected_size)
|
|
97
|
+
::APIMatchers::Collection::HaveJsonSize.new(expected_size)
|
|
45
98
|
end
|
|
46
|
-
alias :be_in_json :be_json
|
|
47
|
-
alias :be_a_json :be_json
|
|
48
99
|
|
|
49
|
-
def
|
|
50
|
-
::APIMatchers::
|
|
100
|
+
def be_sorted_by(field)
|
|
101
|
+
::APIMatchers::Collection::BeSortedBy.new(field)
|
|
51
102
|
end
|
|
52
103
|
|
|
53
|
-
|
|
54
|
-
|
|
104
|
+
# Header Matchers
|
|
105
|
+
def have_header(header_name)
|
|
106
|
+
::APIMatchers::Headers::HaveHeader.new(header_name)
|
|
55
107
|
end
|
|
56
108
|
|
|
57
|
-
def
|
|
58
|
-
::APIMatchers::
|
|
109
|
+
def have_cors_headers
|
|
110
|
+
::APIMatchers::Headers::HaveCorsHeaders.new
|
|
59
111
|
end
|
|
60
112
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
def have_cache_control(*directives)
|
|
114
|
+
::APIMatchers::Headers::HaveCacheControl.new(*directives)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Pagination Matchers
|
|
118
|
+
def be_paginated
|
|
119
|
+
::APIMatchers::Pagination::BePaginated.new
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def have_pagination_links(*link_types)
|
|
123
|
+
::APIMatchers::Pagination::HavePaginationLinks.new(*link_types)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def have_total_count(expected_count)
|
|
127
|
+
::APIMatchers::Pagination::HaveTotalCount.new(expected_count)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Error Response Matchers
|
|
131
|
+
def have_error
|
|
132
|
+
::APIMatchers::ErrorResponse::HaveError.new
|
|
133
|
+
end
|
|
134
|
+
alias :have_errors :have_error
|
|
135
|
+
|
|
136
|
+
def have_error_on(field)
|
|
137
|
+
::APIMatchers::ErrorResponse::HaveErrorOn.new(field)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# JSON:API Matchers
|
|
141
|
+
def be_json_api_compliant
|
|
142
|
+
::APIMatchers::JsonApi::BeJsonApiCompliant.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def have_json_api_data
|
|
146
|
+
::APIMatchers::JsonApi::HaveJsonApiData.new
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def have_json_api_attributes(*attributes)
|
|
150
|
+
::APIMatchers::JsonApi::HaveJsonApiAttributes.new(*attributes)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def have_json_api_relationships(*relationships)
|
|
154
|
+
::APIMatchers::JsonApi::HaveJsonApiRelationships.new(*relationships)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# HATEOAS Matchers
|
|
158
|
+
def have_link(rel)
|
|
159
|
+
::APIMatchers::Hateoas::HaveLink.new(rel)
|
|
67
160
|
end
|
|
68
161
|
end
|
|
69
|
-
end
|
|
162
|
+
end
|
|
@@ -1,28 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module APIMatchers
|
|
2
4
|
module Core
|
|
3
5
|
class Setup
|
|
4
|
-
# The http status method that will be called when you call http status matchers
|
|
5
|
-
#
|
|
6
|
-
# ==== Examples
|
|
7
|
-
#
|
|
8
|
-
# response.status.should create_resource
|
|
9
|
-
# response.status.should be_bad_request
|
|
10
|
-
# response.status.should be_unauthorized
|
|
11
|
-
#
|
|
12
|
-
# # Instead calling #status everytime, you can configure:
|
|
13
|
-
#
|
|
14
|
-
# APIMatchers.setup do |config|
|
|
15
|
-
# config.http_status_method = :status
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
|
-
# Then:
|
|
19
|
-
#
|
|
20
|
-
# response.should create_resource
|
|
21
|
-
# response.should be_bad_request
|
|
22
|
-
# response.should be_unauthorized
|
|
23
|
-
#
|
|
24
|
-
cattr_accessor :http_status_method
|
|
25
|
-
|
|
26
6
|
# The response body method that will be called when you call the have_node matchers
|
|
27
7
|
#
|
|
28
8
|
# ==== Examples
|
|
@@ -34,7 +14,7 @@ module APIMatchers
|
|
|
34
14
|
# # Instead calling #body everytime, you can configure:
|
|
35
15
|
#
|
|
36
16
|
# APIMatchers.setup do |config|
|
|
37
|
-
# config.
|
|
17
|
+
# config.response_body_method = :body
|
|
38
18
|
# end
|
|
39
19
|
#
|
|
40
20
|
# Then:
|
|
@@ -75,6 +55,52 @@ module APIMatchers
|
|
|
75
55
|
#
|
|
76
56
|
cattr_accessor :header_method
|
|
77
57
|
cattr_accessor :header_content_type_key
|
|
58
|
+
|
|
59
|
+
# HTTP status method - for extracting status codes from response objects
|
|
60
|
+
#
|
|
61
|
+
# ==== Examples
|
|
62
|
+
#
|
|
63
|
+
# APIMatchers.setup do |config|
|
|
64
|
+
# config.http_status_method = :status # or :code, :status_code
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
cattr_accessor :http_status_method
|
|
68
|
+
|
|
69
|
+
# Pagination configuration
|
|
70
|
+
#
|
|
71
|
+
# ==== Examples
|
|
72
|
+
#
|
|
73
|
+
# APIMatchers.setup do |config|
|
|
74
|
+
# config.pagination_meta_path = 'meta' # path to pagination metadata
|
|
75
|
+
# config.pagination_links_path = 'links' # path to pagination links
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
cattr_accessor :pagination_meta_path
|
|
79
|
+
cattr_accessor :pagination_links_path
|
|
80
|
+
|
|
81
|
+
# Error response configuration
|
|
82
|
+
#
|
|
83
|
+
# ==== Examples
|
|
84
|
+
#
|
|
85
|
+
# APIMatchers.setup do |config|
|
|
86
|
+
# config.errors_path = 'errors' # path to errors array
|
|
87
|
+
# config.error_message_key = 'message' # key for error message
|
|
88
|
+
# config.error_field_key = 'field' # key for error field name
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
cattr_accessor :errors_path
|
|
92
|
+
cattr_accessor :error_message_key
|
|
93
|
+
cattr_accessor :error_field_key
|
|
94
|
+
|
|
95
|
+
# HATEOAS links configuration
|
|
96
|
+
#
|
|
97
|
+
# ==== Examples
|
|
98
|
+
#
|
|
99
|
+
# APIMatchers.setup do |config|
|
|
100
|
+
# config.links_path = '_links' # path to HATEOAS links (HAL style)
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
cattr_accessor :links_path
|
|
78
104
|
end
|
|
79
105
|
end
|
|
80
106
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module Core
|
|
5
|
+
class ValueNormalizer
|
|
6
|
+
def self.normalize(value)
|
|
7
|
+
new(value).normalize
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def normalize
|
|
15
|
+
case @value
|
|
16
|
+
when DateTime, Date
|
|
17
|
+
@value.to_s
|
|
18
|
+
when Time
|
|
19
|
+
@value.to_datetime.to_s
|
|
20
|
+
else
|
|
21
|
+
@value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module APIMatchers
|
|
6
|
+
module ErrorResponse
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :actual
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
::APIMatchers::Core::Setup
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def matches?(actual)
|
|
15
|
+
@actual = actual
|
|
16
|
+
perform_match
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def failure_message
|
|
20
|
+
raise NotImplementedError, "Subclasses must implement #failure_message"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failure_message_when_negated
|
|
24
|
+
raise NotImplementedError, "Subclasses must implement #failure_message_when_negated"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def perform_match
|
|
30
|
+
raise NotImplementedError, "Subclasses must implement #perform_match"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def response_body
|
|
34
|
+
if setup.response_body_method.present?
|
|
35
|
+
@actual.send(setup.response_body_method)
|
|
36
|
+
else
|
|
37
|
+
@actual
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parsed_json
|
|
42
|
+
@parsed_json ||= begin
|
|
43
|
+
body = response_body
|
|
44
|
+
case body
|
|
45
|
+
when Hash, Array
|
|
46
|
+
body
|
|
47
|
+
when String
|
|
48
|
+
JSON.parse(body)
|
|
49
|
+
else
|
|
50
|
+
raise ::APIMatchers::InvalidJSON, "Invalid JSON: '#{body}'"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
rescue JSON::ParserError
|
|
54
|
+
raise ::APIMatchers::InvalidJSON, "Invalid JSON: '#{response_body}'"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def json_at_path(path)
|
|
58
|
+
return parsed_json if path.nil? || path.empty?
|
|
59
|
+
|
|
60
|
+
finder = ::APIMatchers::Core::JsonPathFinder.new(parsed_json)
|
|
61
|
+
finder.find(path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def errors_path
|
|
65
|
+
setup.errors_path || 'errors'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def error_message_key
|
|
69
|
+
setup.error_message_key || 'message'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def error_field_key
|
|
73
|
+
setup.error_field_key || 'field'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module ErrorResponse
|
|
5
|
+
class HaveError < Base
|
|
6
|
+
COMMON_ERROR_KEYS = %w[error errors message messages].freeze
|
|
7
|
+
|
|
8
|
+
def failure_message
|
|
9
|
+
"expected response to have error(s). " \
|
|
10
|
+
"Looked for keys: #{COMMON_ERROR_KEYS.inspect} and at path '#{errors_path}'. " \
|
|
11
|
+
"Got: #{truncate_json(parsed_json)}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def failure_message_when_negated
|
|
15
|
+
"expected response NOT to have error(s), but errors were found"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def description
|
|
19
|
+
"have error(s)"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def perform_match
|
|
25
|
+
has_errors_at_path? || has_error_keys_at_root?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def has_errors_at_path?
|
|
31
|
+
errors = safe_json_at_path(errors_path)
|
|
32
|
+
errors_present?(errors)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def has_error_keys_at_root?
|
|
36
|
+
return false unless parsed_json.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
COMMON_ERROR_KEYS.any? do |key|
|
|
39
|
+
value = parsed_json[key] || parsed_json[key.to_sym]
|
|
40
|
+
errors_present?(value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def errors_present?(value)
|
|
45
|
+
case value
|
|
46
|
+
when Array
|
|
47
|
+
value.any?
|
|
48
|
+
when Hash
|
|
49
|
+
value.any?
|
|
50
|
+
when String
|
|
51
|
+
value.present?
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def safe_json_at_path(path)
|
|
58
|
+
json_at_path(path)
|
|
59
|
+
rescue ::APIMatchers::Core::Exceptions::PathNotFound
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def truncate_json(json)
|
|
64
|
+
str = json.to_json
|
|
65
|
+
str.length > 200 ? "#{str[0..200]}..." : str
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module ErrorResponse
|
|
5
|
+
class HaveErrorOn < Base
|
|
6
|
+
def initialize(field)
|
|
7
|
+
@field = field.to_s
|
|
8
|
+
@expected_message = nil
|
|
9
|
+
@message_pattern = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def with_message(message)
|
|
13
|
+
@expected_message = message
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def matching(pattern)
|
|
18
|
+
@message_pattern = pattern
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failure_message
|
|
23
|
+
errors = find_errors
|
|
24
|
+
|
|
25
|
+
if errors.nil? || errors.empty?
|
|
26
|
+
"expected response to have error on '#{@field}', but no errors were found. " \
|
|
27
|
+
"Got: #{truncate_json(parsed_json)}"
|
|
28
|
+
elsif !field_has_error?(errors)
|
|
29
|
+
"expected response to have error on '#{@field}'. " \
|
|
30
|
+
"Found errors on: #{error_fields(errors).inspect}"
|
|
31
|
+
elsif @expected_message
|
|
32
|
+
"expected error on '#{@field}' to have message '#{@expected_message}'. " \
|
|
33
|
+
"Got messages: #{field_messages(errors).inspect}"
|
|
34
|
+
elsif @message_pattern
|
|
35
|
+
"expected error on '#{@field}' to match #{@message_pattern.inspect}. " \
|
|
36
|
+
"Got messages: #{field_messages(errors).inspect}"
|
|
37
|
+
else
|
|
38
|
+
"expected response to have error on '#{@field}'"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def failure_message_when_negated
|
|
43
|
+
if @expected_message
|
|
44
|
+
"expected NOT to have error on '#{@field}' with message '#{@expected_message}', but it was found"
|
|
45
|
+
elsif @message_pattern
|
|
46
|
+
"expected NOT to have error on '#{@field}' matching #{@message_pattern.inspect}, but it was found"
|
|
47
|
+
else
|
|
48
|
+
"expected NOT to have error on '#{@field}', but it was found"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def description
|
|
53
|
+
desc = "have error on '#{@field}'"
|
|
54
|
+
desc += " with message '#{@expected_message}'" if @expected_message
|
|
55
|
+
desc += " matching #{@message_pattern.inspect}" if @message_pattern
|
|
56
|
+
desc
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
protected
|
|
60
|
+
|
|
61
|
+
def perform_match
|
|
62
|
+
errors = find_errors
|
|
63
|
+
return false if errors.nil? || errors.empty?
|
|
64
|
+
return false unless field_has_error?(errors)
|
|
65
|
+
|
|
66
|
+
if @expected_message
|
|
67
|
+
message_matches?(errors, @expected_message)
|
|
68
|
+
elsif @message_pattern
|
|
69
|
+
message_matches_pattern?(errors, @message_pattern)
|
|
70
|
+
else
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def find_errors
|
|
78
|
+
# Try errors path first
|
|
79
|
+
errors = safe_json_at_path(errors_path)
|
|
80
|
+
return errors if errors.is_a?(Array) || errors.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
# Try root level
|
|
83
|
+
if parsed_json.is_a?(Hash)
|
|
84
|
+
# Rails-style errors: { field: ["message1", "message2"] }
|
|
85
|
+
return parsed_json if parsed_json.key?(@field) || parsed_json.key?(@field.to_sym)
|
|
86
|
+
return parsed_json['errors'] if parsed_json['errors']
|
|
87
|
+
return parsed_json[:errors] if parsed_json[:errors]
|
|
88
|
+
# Check if root has any field-like structure
|
|
89
|
+
return parsed_json if parsed_json.values.any? { |v| v.is_a?(Array) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def field_has_error?(errors)
|
|
96
|
+
case errors
|
|
97
|
+
when Array
|
|
98
|
+
errors.any? { |e| error_for_field?(e) }
|
|
99
|
+
when Hash
|
|
100
|
+
# Rails-style: { field: ["message1", "message2"] }
|
|
101
|
+
errors.key?(@field) || errors.key?(@field.to_sym) ||
|
|
102
|
+
# Or hash with field key
|
|
103
|
+
errors.any? { |_, v| v.is_a?(Hash) && error_for_field?(v) }
|
|
104
|
+
else
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def error_for_field?(error)
|
|
110
|
+
return false unless error.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
field_key = error_field_key
|
|
113
|
+
field_value = error[field_key] || error[field_key.to_sym] ||
|
|
114
|
+
error['field'] || error[:field] ||
|
|
115
|
+
error['attribute'] || error[:attribute]
|
|
116
|
+
|
|
117
|
+
field_value.to_s == @field
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def error_fields(errors)
|
|
121
|
+
case errors
|
|
122
|
+
when Array
|
|
123
|
+
errors.filter_map do |e|
|
|
124
|
+
next unless e.is_a?(Hash)
|
|
125
|
+
e[error_field_key] || e[error_field_key.to_sym] ||
|
|
126
|
+
e['field'] || e[:field] ||
|
|
127
|
+
e['attribute'] || e[:attribute]
|
|
128
|
+
end.map(&:to_s).uniq
|
|
129
|
+
when Hash
|
|
130
|
+
errors.keys.map(&:to_s)
|
|
131
|
+
else
|
|
132
|
+
[]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def field_messages(errors)
|
|
137
|
+
case errors
|
|
138
|
+
when Array
|
|
139
|
+
errors.filter_map do |e|
|
|
140
|
+
next unless e.is_a?(Hash) && error_for_field?(e)
|
|
141
|
+
e[error_message_key] || e[error_message_key.to_sym] ||
|
|
142
|
+
e['message'] || e[:message]
|
|
143
|
+
end
|
|
144
|
+
when Hash
|
|
145
|
+
# Rails-style: { field: ["message1", "message2"] }
|
|
146
|
+
messages = errors[@field] || errors[@field.to_sym]
|
|
147
|
+
messages.is_a?(Array) ? messages : [messages].compact
|
|
148
|
+
else
|
|
149
|
+
[]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def message_matches?(errors, expected)
|
|
154
|
+
field_messages(errors).any? { |m| m.to_s == expected.to_s }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def message_matches_pattern?(errors, pattern)
|
|
158
|
+
field_messages(errors).any? { |m| m.to_s.match?(pattern) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def safe_json_at_path(path)
|
|
162
|
+
json_at_path(path)
|
|
163
|
+
rescue ::APIMatchers::Core::Exceptions::PathNotFound
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def truncate_json(json)
|
|
168
|
+
str = json.to_json
|
|
169
|
+
str.length > 200 ? "#{str[0..200]}..." : str
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|