api_matchers 0.6.2 → 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 -0
- data/Gemfile +1 -1
- data/History.markdown +62 -50
- 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 +152 -47
- data/.rvmrc.example +0 -1
- data/.travis.yml +0 -12
- data/Gemfile.lock +0 -46
- 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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module APIMatchers
|
|
6
|
+
module Hateoas
|
|
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 links_path
|
|
58
|
+
setup.links_path || '_links'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hateoas_links
|
|
62
|
+
# Try configured path first
|
|
63
|
+
links = parsed_json[links_path] || parsed_json[links_path.to_sym]
|
|
64
|
+
return links if links
|
|
65
|
+
|
|
66
|
+
# Try common HATEOAS link locations
|
|
67
|
+
parsed_json['_links'] || parsed_json[:_links] ||
|
|
68
|
+
parsed_json['links'] || parsed_json[:links]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def truncate_json(json)
|
|
72
|
+
str = json.to_json
|
|
73
|
+
str.length > 200 ? "#{str[0..200]}..." : str
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module Hateoas
|
|
5
|
+
class HaveLink < Base
|
|
6
|
+
def initialize(rel)
|
|
7
|
+
@rel = rel.to_s
|
|
8
|
+
@expected_href = nil
|
|
9
|
+
@href_pattern = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def with_href(href_or_pattern)
|
|
13
|
+
case href_or_pattern
|
|
14
|
+
when Regexp
|
|
15
|
+
@href_pattern = href_or_pattern
|
|
16
|
+
else
|
|
17
|
+
@expected_href = href_or_pattern.to_s
|
|
18
|
+
end
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failure_message
|
|
23
|
+
links = hateoas_links
|
|
24
|
+
|
|
25
|
+
if links.nil?
|
|
26
|
+
"expected response to have link '#{@rel}', but no links were found. " \
|
|
27
|
+
"Looked at '#{links_path}'. Got: #{truncate_json(parsed_json)}"
|
|
28
|
+
elsif !link_exists?(links)
|
|
29
|
+
"expected response to have link '#{@rel}'. " \
|
|
30
|
+
"Available links: #{available_rels(links).inspect}"
|
|
31
|
+
elsif @expected_href
|
|
32
|
+
"expected link '#{@rel}' to have href '#{@expected_href}'. " \
|
|
33
|
+
"Got: '#{extract_href(links)}'"
|
|
34
|
+
elsif @href_pattern
|
|
35
|
+
"expected link '#{@rel}' href to match #{@href_pattern.inspect}. " \
|
|
36
|
+
"Got: '#{extract_href(links)}'"
|
|
37
|
+
else
|
|
38
|
+
"expected response to have link '#{@rel}'"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def failure_message_when_negated
|
|
43
|
+
if @expected_href
|
|
44
|
+
"expected link '#{@rel}' NOT to have href '#{@expected_href}', but it did"
|
|
45
|
+
elsif @href_pattern
|
|
46
|
+
"expected link '#{@rel}' NOT to match #{@href_pattern.inspect}, but it did"
|
|
47
|
+
else
|
|
48
|
+
"expected response NOT to have link '#{@rel}', but it was present"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def description
|
|
53
|
+
desc = "have link '#{@rel}'"
|
|
54
|
+
desc += " with href '#{@expected_href}'" if @expected_href
|
|
55
|
+
desc += " matching #{@href_pattern.inspect}" if @href_pattern
|
|
56
|
+
desc
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
protected
|
|
60
|
+
|
|
61
|
+
def perform_match
|
|
62
|
+
links = hateoas_links
|
|
63
|
+
return false unless links.is_a?(Hash)
|
|
64
|
+
return false unless link_exists?(links)
|
|
65
|
+
|
|
66
|
+
href = extract_href(links)
|
|
67
|
+
|
|
68
|
+
if @expected_href
|
|
69
|
+
href == @expected_href
|
|
70
|
+
elsif @href_pattern
|
|
71
|
+
href.to_s.match?(@href_pattern)
|
|
72
|
+
else
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def link_exists?(links)
|
|
80
|
+
links.key?(@rel) || links.key?(@rel.to_sym)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extract_href(links)
|
|
84
|
+
link = links[@rel] || links[@rel.to_sym]
|
|
85
|
+
return nil unless link
|
|
86
|
+
|
|
87
|
+
case link
|
|
88
|
+
when Hash
|
|
89
|
+
link['href'] || link[:href]
|
|
90
|
+
when String
|
|
91
|
+
link
|
|
92
|
+
else
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def available_rels(links)
|
|
98
|
+
links.keys.map(&:to_s)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module APIMatchers
|
|
2
4
|
module Headers
|
|
3
5
|
class Base
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def initialize(setup)
|
|
7
|
-
@setup = setup
|
|
6
|
+
def setup
|
|
7
|
+
::APIMatchers::Core::Setup
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def matches?(actual)
|
|
@@ -14,9 +14,9 @@ module APIMatchers
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def content_type_response
|
|
17
|
-
if
|
|
18
|
-
headers = @actual.send(
|
|
19
|
-
headers[
|
|
17
|
+
if setup.header_method.present? && setup.header_content_type_key.present?
|
|
18
|
+
headers = @actual.send(setup.header_method)
|
|
19
|
+
headers.present? ? (headers[setup.header_content_type_key] || headers) : nil
|
|
20
20
|
else
|
|
21
21
|
@actual
|
|
22
22
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module APIMatchers
|
|
2
4
|
module Headers
|
|
3
5
|
class BeJSON < Base
|
|
@@ -16,10 +18,6 @@ module APIMatchers
|
|
|
16
18
|
def description
|
|
17
19
|
"be in JSON"
|
|
18
20
|
end
|
|
19
|
-
|
|
20
|
-
# RSpec 2 compatibility:
|
|
21
|
-
alias_method :failure_message_for_should, :failure_message
|
|
22
|
-
alias_method :failure_message_for_should_not, :failure_message_when_negated
|
|
23
21
|
end
|
|
24
22
|
end
|
|
25
23
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module APIMatchers
|
|
2
4
|
module Headers
|
|
3
5
|
class BeXML < Base
|
|
@@ -16,10 +18,6 @@ module APIMatchers
|
|
|
16
18
|
def description
|
|
17
19
|
"be in XML"
|
|
18
20
|
end
|
|
19
|
-
|
|
20
|
-
# RSpec 2 compatibility:
|
|
21
|
-
alias_method :failure_message_for_should, :failure_message
|
|
22
|
-
alias_method :failure_message_for_should_not, :failure_message_when_negated
|
|
23
21
|
end
|
|
24
22
|
end
|
|
25
23
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module Headers
|
|
5
|
+
class HaveCacheControl
|
|
6
|
+
VALID_DIRECTIVES = %w[
|
|
7
|
+
public private no-cache no-store no-transform must-revalidate
|
|
8
|
+
proxy-revalidate max-age s-maxage immutable stale-while-revalidate
|
|
9
|
+
stale-if-error
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :actual
|
|
13
|
+
|
|
14
|
+
def initialize(*directives)
|
|
15
|
+
@expected_directives = normalize_directives(directives.flatten)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup
|
|
19
|
+
::APIMatchers::Core::Setup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def matches?(actual)
|
|
23
|
+
@actual = actual
|
|
24
|
+
cache_control = fetch_cache_control
|
|
25
|
+
return false if cache_control.nil?
|
|
26
|
+
|
|
27
|
+
@actual_directives = parse_cache_control(cache_control)
|
|
28
|
+
|
|
29
|
+
@expected_directives.all? do |directive|
|
|
30
|
+
@actual_directives.include?(directive) ||
|
|
31
|
+
@actual_directives.any? { |d| d.start_with?("#{directive}=") }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failure_message
|
|
36
|
+
cache_control = fetch_cache_control
|
|
37
|
+
|
|
38
|
+
if cache_control.nil?
|
|
39
|
+
"expected response to have Cache-Control header with #{@expected_directives.inspect}, " \
|
|
40
|
+
"but Cache-Control header was not present"
|
|
41
|
+
else
|
|
42
|
+
"expected Cache-Control to include #{@expected_directives.inspect}. " \
|
|
43
|
+
"Got: '#{cache_control}' (parsed: #{@actual_directives.inspect})"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def failure_message_when_negated
|
|
48
|
+
"expected Cache-Control NOT to include #{@expected_directives.inspect}, " \
|
|
49
|
+
"but it did: '#{fetch_cache_control}'"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def description
|
|
53
|
+
"have Cache-Control with #{@expected_directives.inspect}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize_directives(directives)
|
|
59
|
+
directives.map do |d|
|
|
60
|
+
d.to_s.tr('_', '-')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_cache_control
|
|
65
|
+
headers = available_headers
|
|
66
|
+
return nil unless headers
|
|
67
|
+
|
|
68
|
+
headers['Cache-Control'] ||
|
|
69
|
+
headers['cache-control'] ||
|
|
70
|
+
headers.find { |k, _| k.to_s.downcase == 'cache-control' }&.last
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_cache_control(value)
|
|
74
|
+
value.to_s.split(',').map(&:strip).map(&:downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def available_headers
|
|
78
|
+
if setup.header_method.present?
|
|
79
|
+
@actual.send(setup.header_method)
|
|
80
|
+
elsif @actual.respond_to?(:headers)
|
|
81
|
+
@actual.headers
|
|
82
|
+
elsif @actual.is_a?(Hash)
|
|
83
|
+
@actual
|
|
84
|
+
else
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module Headers
|
|
5
|
+
class HaveCorsHeaders
|
|
6
|
+
CORS_HEADERS = %w[
|
|
7
|
+
Access-Control-Allow-Origin
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
OPTIONAL_CORS_HEADERS = %w[
|
|
11
|
+
Access-Control-Allow-Methods
|
|
12
|
+
Access-Control-Allow-Headers
|
|
13
|
+
Access-Control-Allow-Credentials
|
|
14
|
+
Access-Control-Expose-Headers
|
|
15
|
+
Access-Control-Max-Age
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :actual
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@expected_origin = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def setup
|
|
25
|
+
::APIMatchers::Core::Setup
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def for_origin(origin)
|
|
29
|
+
@expected_origin = origin
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def matches?(actual)
|
|
34
|
+
@actual = actual
|
|
35
|
+
headers = available_headers
|
|
36
|
+
return false unless headers
|
|
37
|
+
|
|
38
|
+
has_required_headers = CORS_HEADERS.all? { |h| header_present?(headers, h) }
|
|
39
|
+
return false unless has_required_headers
|
|
40
|
+
|
|
41
|
+
if @expected_origin
|
|
42
|
+
origin_value = fetch_header(headers, 'Access-Control-Allow-Origin')
|
|
43
|
+
return false unless origin_value == @expected_origin || origin_value == '*'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failure_message
|
|
50
|
+
headers = available_headers
|
|
51
|
+
missing = CORS_HEADERS.reject { |h| header_present?(headers, h) }
|
|
52
|
+
|
|
53
|
+
if missing.any?
|
|
54
|
+
"expected response to have CORS headers. Missing: #{missing.inspect}. " \
|
|
55
|
+
"Available headers: #{headers&.keys&.inspect}"
|
|
56
|
+
elsif @expected_origin
|
|
57
|
+
origin_value = fetch_header(headers, 'Access-Control-Allow-Origin')
|
|
58
|
+
"expected Access-Control-Allow-Origin to be '#{@expected_origin}' or '*'. " \
|
|
59
|
+
"Got: '#{origin_value}'"
|
|
60
|
+
else
|
|
61
|
+
"expected response to have CORS headers"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def failure_message_when_negated
|
|
66
|
+
"expected response NOT to have CORS headers, but they were present"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def description
|
|
70
|
+
desc = "have CORS headers"
|
|
71
|
+
desc += " for origin '#{@expected_origin}'" if @expected_origin
|
|
72
|
+
desc
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def available_headers
|
|
78
|
+
if setup.header_method.present?
|
|
79
|
+
@actual.send(setup.header_method)
|
|
80
|
+
elsif @actual.respond_to?(:headers)
|
|
81
|
+
@actual.headers
|
|
82
|
+
elsif @actual.is_a?(Hash)
|
|
83
|
+
@actual
|
|
84
|
+
else
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def header_present?(headers, name)
|
|
90
|
+
fetch_header(headers, name) != nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def fetch_header(headers, name)
|
|
94
|
+
return nil unless headers
|
|
95
|
+
|
|
96
|
+
headers[name] ||
|
|
97
|
+
headers[name.downcase] ||
|
|
98
|
+
headers.find { |k, _| k.to_s.downcase == name.downcase }&.last
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module Headers
|
|
5
|
+
class HaveHeader
|
|
6
|
+
attr_reader :actual
|
|
7
|
+
|
|
8
|
+
def initialize(header_name)
|
|
9
|
+
@header_name = header_name.to_s
|
|
10
|
+
@expected_value = nil
|
|
11
|
+
@value_pattern = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def setup
|
|
15
|
+
::APIMatchers::Core::Setup
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with_value(value)
|
|
19
|
+
@expected_value = value
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def matching(pattern)
|
|
24
|
+
@value_pattern = pattern
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def matches?(actual)
|
|
29
|
+
@actual = actual
|
|
30
|
+
header_value = fetch_header_value
|
|
31
|
+
|
|
32
|
+
return false if header_value.nil?
|
|
33
|
+
|
|
34
|
+
if @expected_value
|
|
35
|
+
header_value == @expected_value
|
|
36
|
+
elsif @value_pattern
|
|
37
|
+
header_value.to_s.match?(@value_pattern)
|
|
38
|
+
else
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def failure_message
|
|
44
|
+
header_value = fetch_header_value
|
|
45
|
+
|
|
46
|
+
if header_value.nil?
|
|
47
|
+
"expected response to have header '#{@header_name}', but it was not present. " \
|
|
48
|
+
"Available headers: #{available_headers.keys.inspect}"
|
|
49
|
+
elsif @expected_value
|
|
50
|
+
"expected header '#{@header_name}' to have value '#{@expected_value}'. " \
|
|
51
|
+
"Got: '#{header_value}'"
|
|
52
|
+
elsif @value_pattern
|
|
53
|
+
"expected header '#{@header_name}' to match #{@value_pattern.inspect}. " \
|
|
54
|
+
"Got: '#{header_value}'"
|
|
55
|
+
else
|
|
56
|
+
"expected response to have header '#{@header_name}'"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def failure_message_when_negated
|
|
61
|
+
if @expected_value
|
|
62
|
+
"expected header '#{@header_name}' NOT to have value '#{@expected_value}', but it did"
|
|
63
|
+
elsif @value_pattern
|
|
64
|
+
"expected header '#{@header_name}' NOT to match #{@value_pattern.inspect}, but it did"
|
|
65
|
+
else
|
|
66
|
+
"expected response NOT to have header '#{@header_name}', but it was present"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def description
|
|
71
|
+
desc = "have header '#{@header_name}'"
|
|
72
|
+
desc += " with value '#{@expected_value}'" if @expected_value
|
|
73
|
+
desc += " matching #{@value_pattern.inspect}" if @value_pattern
|
|
74
|
+
desc
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def fetch_header_value
|
|
80
|
+
headers = available_headers
|
|
81
|
+
return nil unless headers
|
|
82
|
+
|
|
83
|
+
# Try exact match first, then case-insensitive
|
|
84
|
+
headers[@header_name] ||
|
|
85
|
+
headers[@header_name.downcase] ||
|
|
86
|
+
headers.find { |k, _| k.to_s.downcase == @header_name.downcase }&.last
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def available_headers
|
|
90
|
+
if setup.header_method.present?
|
|
91
|
+
@actual.send(setup.header_method)
|
|
92
|
+
elsif @actual.respond_to?(:headers)
|
|
93
|
+
@actual.headers
|
|
94
|
+
elsif @actual.is_a?(Hash)
|
|
95
|
+
@actual
|
|
96
|
+
else
|
|
97
|
+
{}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :actual
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
::APIMatchers::Core::Setup
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def matches?(actual)
|
|
13
|
+
@actual = actual
|
|
14
|
+
expected_status_match?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def failure_message
|
|
18
|
+
"expected response to #{expectation_description}. Got: #{actual_status}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failure_message_when_negated
|
|
22
|
+
"expected response NOT to #{expectation_description}. Got: #{actual_status}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def description
|
|
26
|
+
expectation_description
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
def expected_status_match?
|
|
32
|
+
raise NotImplementedError, "Subclasses must implement #expected_status_match?"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def expectation_description
|
|
36
|
+
raise NotImplementedError, "Subclasses must implement #expectation_description"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def actual_status
|
|
40
|
+
if setup.http_status_method.present?
|
|
41
|
+
@actual.send(setup.http_status_method)
|
|
42
|
+
else
|
|
43
|
+
@actual
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeClientError < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
::APIMatchers::Core::HTTPStatusCodes.client_error?(actual_status)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be client error (4xx)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeForbidden < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
actual_status.to_i == 403
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be forbidden (403)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeNoContent < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
actual_status.to_i == 204
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be no content (204)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeNotFound < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
actual_status.to_i == 404
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be not found (404)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeRedirect < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
::APIMatchers::Core::HTTPStatusCodes.redirect?(actual_status)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be redirect (3xx)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module APIMatchers
|
|
4
|
+
module HTTPStatus
|
|
5
|
+
class BeServerError < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def expected_status_match?
|
|
9
|
+
::APIMatchers::Core::HTTPStatusCodes.server_error?(actual_status)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def expectation_description
|
|
13
|
+
"be server error (5xx)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|