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.
Files changed (116) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +30 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +1 -1
  5. data/History.markdown +62 -50
  6. data/README.markdown +1070 -114
  7. data/TODO.markdown +39 -3
  8. data/api_matchers.gemspec +13 -6
  9. data/lib/api_matchers/collection/base.rb +65 -0
  10. data/lib/api_matchers/collection/be_sorted_by.rb +97 -0
  11. data/lib/api_matchers/collection/have_json_size.rb +54 -0
  12. data/lib/api_matchers/core/exceptions.rb +5 -0
  13. data/lib/api_matchers/core/find_in_json.rb +57 -28
  14. data/lib/api_matchers/core/http_status_codes.rb +118 -0
  15. data/lib/api_matchers/core/json_path_finder.rb +87 -0
  16. data/lib/api_matchers/core/parser.rb +4 -2
  17. data/lib/api_matchers/core/rspec_matchers.rb +130 -37
  18. data/lib/api_matchers/core/setup.rb +49 -23
  19. data/lib/api_matchers/core/value_normalizer.rb +26 -0
  20. data/lib/api_matchers/error_response/base.rb +77 -0
  21. data/lib/api_matchers/error_response/have_error.rb +69 -0
  22. data/lib/api_matchers/error_response/have_error_on.rb +173 -0
  23. data/lib/api_matchers/hateoas/base.rb +77 -0
  24. data/lib/api_matchers/hateoas/have_link.rb +102 -0
  25. data/lib/api_matchers/headers/base.rb +7 -7
  26. data/lib/api_matchers/headers/be_json.rb +2 -4
  27. data/lib/api_matchers/headers/be_xml.rb +2 -4
  28. data/lib/api_matchers/headers/have_cache_control.rb +90 -0
  29. data/lib/api_matchers/headers/have_cors_headers.rb +102 -0
  30. data/lib/api_matchers/headers/have_header.rb +102 -0
  31. data/lib/api_matchers/http_status/base.rb +48 -0
  32. data/lib/api_matchers/http_status/be_client_error.rb +17 -0
  33. data/lib/api_matchers/http_status/be_forbidden.rb +17 -0
  34. data/lib/api_matchers/http_status/be_no_content.rb +17 -0
  35. data/lib/api_matchers/http_status/be_not_found.rb +17 -0
  36. data/lib/api_matchers/http_status/be_redirect.rb +17 -0
  37. data/lib/api_matchers/http_status/be_server_error.rb +17 -0
  38. data/lib/api_matchers/http_status/be_successful.rb +17 -0
  39. data/lib/api_matchers/http_status/be_unauthorized.rb +17 -0
  40. data/lib/api_matchers/http_status/be_unprocessable.rb +17 -0
  41. data/lib/api_matchers/http_status/have_http_status.rb +39 -0
  42. data/lib/api_matchers/json_api/base.rb +83 -0
  43. data/lib/api_matchers/json_api/be_json_api_compliant.rb +158 -0
  44. data/lib/api_matchers/json_api/have_json_api_attributes.rb +62 -0
  45. data/lib/api_matchers/json_api/have_json_api_data.rb +110 -0
  46. data/lib/api_matchers/json_api/have_json_api_relationships.rb +62 -0
  47. data/lib/api_matchers/json_structure/base.rb +65 -0
  48. data/lib/api_matchers/json_structure/have_json_keys.rb +55 -0
  49. data/lib/api_matchers/json_structure/have_json_type.rb +72 -0
  50. data/lib/api_matchers/pagination/base.rb +73 -0
  51. data/lib/api_matchers/pagination/be_paginated.rb +73 -0
  52. data/lib/api_matchers/pagination/have_pagination_links.rb +74 -0
  53. data/lib/api_matchers/pagination/have_total_count.rb +77 -0
  54. data/lib/api_matchers/response_body/base.rb +10 -9
  55. data/lib/api_matchers/response_body/have_json.rb +2 -4
  56. data/lib/api_matchers/response_body/have_json_node.rb +45 -1
  57. data/lib/api_matchers/response_body/have_node.rb +2 -0
  58. data/lib/api_matchers/response_body/have_xml_node.rb +13 -14
  59. data/lib/api_matchers/response_body/match_json_schema.rb +89 -0
  60. data/lib/api_matchers/version.rb +3 -1
  61. data/lib/api_matchers.rb +75 -14
  62. data/spec/api_matchers/collection/be_sorted_by_spec.rb +110 -0
  63. data/spec/api_matchers/collection/have_json_size_spec.rb +101 -0
  64. data/spec/api_matchers/error_response/have_error_on_spec.rb +123 -0
  65. data/spec/api_matchers/error_response/have_error_spec.rb +108 -0
  66. data/spec/api_matchers/hateoas/have_link_spec.rb +105 -0
  67. data/spec/api_matchers/headers/base_spec.rb +8 -3
  68. data/spec/api_matchers/headers/be_json_spec.rb +1 -1
  69. data/spec/api_matchers/headers/be_xml_spec.rb +1 -1
  70. data/spec/api_matchers/headers/have_cache_control_spec.rb +102 -0
  71. data/spec/api_matchers/headers/have_cors_headers_spec.rb +74 -0
  72. data/spec/api_matchers/headers/have_header_spec.rb +88 -0
  73. data/spec/api_matchers/http_status/be_client_error_spec.rb +53 -0
  74. data/spec/api_matchers/http_status/be_forbidden_spec.rb +33 -0
  75. data/spec/api_matchers/http_status/be_no_content_spec.rb +33 -0
  76. data/spec/api_matchers/http_status/be_not_found_spec.rb +39 -0
  77. data/spec/api_matchers/http_status/be_redirect_spec.rb +55 -0
  78. data/spec/api_matchers/http_status/be_server_error_spec.rb +49 -0
  79. data/spec/api_matchers/http_status/be_successful_spec.rb +78 -0
  80. data/spec/api_matchers/http_status/be_unauthorized_spec.rb +33 -0
  81. data/spec/api_matchers/http_status/be_unprocessable_spec.rb +39 -0
  82. data/spec/api_matchers/http_status/have_http_status_spec.rb +81 -0
  83. data/spec/api_matchers/json_api/be_json_api_compliant_spec.rb +109 -0
  84. data/spec/api_matchers/json_api/have_json_api_attributes_spec.rb +61 -0
  85. data/spec/api_matchers/json_api/have_json_api_data_spec.rb +95 -0
  86. data/spec/api_matchers/json_api/have_json_api_relationships_spec.rb +61 -0
  87. data/spec/api_matchers/json_structure/have_json_keys_spec.rb +81 -0
  88. data/spec/api_matchers/json_structure/have_json_type_spec.rb +134 -0
  89. data/spec/api_matchers/pagination/be_paginated_spec.rb +95 -0
  90. data/spec/api_matchers/pagination/have_pagination_links_spec.rb +80 -0
  91. data/spec/api_matchers/pagination/have_total_count_spec.rb +85 -0
  92. data/spec/api_matchers/response_body/base_spec.rb +15 -7
  93. data/spec/api_matchers/response_body/have_json_node_spec.rb +57 -0
  94. data/spec/api_matchers/response_body/match_json_schema_spec.rb +86 -0
  95. metadata +152 -47
  96. data/.rvmrc.example +0 -1
  97. data/.travis.yml +0 -12
  98. data/Gemfile.lock +0 -46
  99. data/lib/api_matchers/http_status_code/base.rb +0 -32
  100. data/lib/api_matchers/http_status_code/be_bad_request.rb +0 -25
  101. data/lib/api_matchers/http_status_code/be_forbidden.rb +0 -21
  102. data/lib/api_matchers/http_status_code/be_internal_server_error.rb +0 -25
  103. data/lib/api_matchers/http_status_code/be_not_found.rb +0 -25
  104. data/lib/api_matchers/http_status_code/be_ok.rb +0 -25
  105. data/lib/api_matchers/http_status_code/be_unauthorized.rb +0 -25
  106. data/lib/api_matchers/http_status_code/be_unprocessable_entity.rb +0 -25
  107. data/lib/api_matchers/http_status_code/create_resource.rb +0 -25
  108. data/spec/api_matchers/http_status_code/base_spec.rb +0 -12
  109. data/spec/api_matchers/http_status_code/be_bad_request_spec.rb +0 -49
  110. data/spec/api_matchers/http_status_code/be_forbidden_spec.rb +0 -49
  111. data/spec/api_matchers/http_status_code/be_internal_server_error_spec.rb +0 -49
  112. data/spec/api_matchers/http_status_code/be_not_found_spec.rb +0 -49
  113. data/spec/api_matchers/http_status_code/be_ok_spec.rb +0 -49
  114. data/spec/api_matchers/http_status_code/be_unauthorized_spec.rb +0 -49
  115. data/spec/api_matchers/http_status_code/be_unprocessable_entity_spec.rb +0 -27
  116. data/spec/api_matchers/http_status_code/create_resource_spec.rb +0 -49
data/lib/api_matchers.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "api_matchers/version"
2
4
  require "active_support/core_ext/object"
3
5
  require "active_support/core_ext/class"
@@ -5,26 +7,15 @@ require "active_support/core_ext/class"
5
7
  module APIMatchers
6
8
  autoload :RSpecMatchers, 'api_matchers/core/rspec_matchers'
7
9
 
8
- # HTTP Status Code Matchers
9
- #
10
- module HTTPStatusCode
11
- autoload :Base, 'api_matchers/http_status_code/base'
12
- autoload :BeBadRequest, 'api_matchers/http_status_code/be_bad_request'
13
- autoload :BeNotFound, 'api_matchers/http_status_code/be_not_found'
14
- autoload :BeInternalServerError, 'api_matchers/http_status_code/be_internal_server_error'
15
- autoload :BeUnauthorized, 'api_matchers/http_status_code/be_unauthorized'
16
- autoload :BeOk, 'api_matchers/http_status_code/be_ok'
17
- autoload :BeUnprocessableEntity, 'api_matchers/http_status_code/be_unprocessable_entity'
18
- autoload :BeForbidden, 'api_matchers/http_status_code/be_forbidden'
19
- autoload :CreateResource, 'api_matchers/http_status_code/create_resource'
20
- end
21
-
22
10
  # Content Type Matchers
23
11
  #
24
12
  module Headers
25
13
  autoload :Base, 'api_matchers/headers/base'
26
14
  autoload :BeXML, 'api_matchers/headers/be_xml'
27
15
  autoload :BeJSON, 'api_matchers/headers/be_json'
16
+ autoload :HaveHeader, 'api_matchers/headers/have_header'
17
+ autoload :HaveCorsHeaders, 'api_matchers/headers/have_cors_headers'
18
+ autoload :HaveCacheControl, 'api_matchers/headers/have_cache_control'
28
19
  end
29
20
 
30
21
  # Response Body Matchers
@@ -35,15 +26,85 @@ module APIMatchers
35
26
  autoload :HaveJson, 'api_matchers/response_body/have_json'
36
27
  autoload :HaveXmlNode, 'api_matchers/response_body/have_xml_node'
37
28
  autoload :HaveNode, 'api_matchers/response_body/have_node'
29
+ autoload :MatchJsonSchema, 'api_matchers/response_body/match_json_schema'
30
+ end
31
+
32
+ # HTTP Status Matchers
33
+ #
34
+ module HTTPStatus
35
+ autoload :Base, 'api_matchers/http_status/base'
36
+ autoload :HaveHttpStatus, 'api_matchers/http_status/have_http_status'
37
+ autoload :BeSuccessful, 'api_matchers/http_status/be_successful'
38
+ autoload :BeRedirect, 'api_matchers/http_status/be_redirect'
39
+ autoload :BeClientError, 'api_matchers/http_status/be_client_error'
40
+ autoload :BeServerError, 'api_matchers/http_status/be_server_error'
41
+ autoload :BeNotFound, 'api_matchers/http_status/be_not_found'
42
+ autoload :BeUnauthorized, 'api_matchers/http_status/be_unauthorized'
43
+ autoload :BeForbidden, 'api_matchers/http_status/be_forbidden'
44
+ autoload :BeUnprocessable, 'api_matchers/http_status/be_unprocessable'
45
+ autoload :BeNoContent, 'api_matchers/http_status/be_no_content'
46
+ end
47
+
48
+ # JSON Structure Matchers
49
+ #
50
+ module JsonStructure
51
+ autoload :Base, 'api_matchers/json_structure/base'
52
+ autoload :HaveJsonKeys, 'api_matchers/json_structure/have_json_keys'
53
+ autoload :HaveJsonType, 'api_matchers/json_structure/have_json_type'
54
+ end
55
+
56
+ # Collection Matchers
57
+ #
58
+ module Collection
59
+ autoload :Base, 'api_matchers/collection/base'
60
+ autoload :HaveJsonSize, 'api_matchers/collection/have_json_size'
61
+ autoload :BeSortedBy, 'api_matchers/collection/be_sorted_by'
62
+ end
63
+
64
+ # Pagination Matchers
65
+ #
66
+ module Pagination
67
+ autoload :Base, 'api_matchers/pagination/base'
68
+ autoload :BePaginated, 'api_matchers/pagination/be_paginated'
69
+ autoload :HavePaginationLinks, 'api_matchers/pagination/have_pagination_links'
70
+ autoload :HaveTotalCount, 'api_matchers/pagination/have_total_count'
71
+ end
72
+
73
+ # Error Response Matchers
74
+ #
75
+ module ErrorResponse
76
+ autoload :Base, 'api_matchers/error_response/base'
77
+ autoload :HaveError, 'api_matchers/error_response/have_error'
78
+ autoload :HaveErrorOn, 'api_matchers/error_response/have_error_on'
79
+ end
80
+
81
+ # JSON:API Matchers
82
+ #
83
+ module JsonApi
84
+ autoload :Base, 'api_matchers/json_api/base'
85
+ autoload :BeJsonApiCompliant, 'api_matchers/json_api/be_json_api_compliant'
86
+ autoload :HaveJsonApiData, 'api_matchers/json_api/have_json_api_data'
87
+ autoload :HaveJsonApiAttributes, 'api_matchers/json_api/have_json_api_attributes'
88
+ autoload :HaveJsonApiRelationships, 'api_matchers/json_api/have_json_api_relationships'
89
+ end
90
+
91
+ # HATEOAS Matchers
92
+ #
93
+ module Hateoas
94
+ autoload :Base, 'api_matchers/hateoas/base'
95
+ autoload :HaveLink, 'api_matchers/hateoas/have_link'
38
96
  end
39
97
 
40
98
  # Core
41
99
  #
42
100
  module Core
43
101
  autoload :FindInJSON, 'api_matchers/core/find_in_json'
102
+ autoload :ValueNormalizer, 'api_matchers/core/value_normalizer'
44
103
  autoload :Parser, 'api_matchers/core/parser'
45
104
  autoload :Setup, 'api_matchers/core/setup'
46
105
  autoload :Exceptions, 'api_matchers/core/exceptions'
106
+ autoload :HTTPStatusCodes, 'api_matchers/core/http_status_codes'
107
+ autoload :JsonPathFinder, 'api_matchers/core/json_path_finder'
47
108
  end
48
109
  include ::APIMatchers::Core::Exceptions
49
110
 
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::Collection::BeSortedBy do
4
+ describe "actual.to be_sorted_by" do
5
+ context "ascending order (default)" do
6
+ it "passes when array is sorted ascending" do
7
+ json = '[{"id": 1}, {"id": 2}, {"id": 3}]'
8
+ expect(json).to be_sorted_by(:id)
9
+ end
10
+
11
+ it "passes when array is sorted ascending (explicit)" do
12
+ json = '[{"id": 1}, {"id": 2}, {"id": 3}]'
13
+ expect(json).to be_sorted_by(:id).ascending
14
+ end
15
+
16
+ it "fails when array is not sorted ascending" do
17
+ json = '[{"id": 3}, {"id": 1}, {"id": 2}]'
18
+ expect {
19
+ expect(json).to be_sorted_by(:id).ascending
20
+ }.to fail_with(/expected JSON array at 'root' to be sorted by 'id' ascending/)
21
+ end
22
+ end
23
+
24
+ context "descending order" do
25
+ it "passes when array is sorted descending" do
26
+ json = '[{"id": 3}, {"id": 2}, {"id": 1}]'
27
+ expect(json).to be_sorted_by(:id).descending
28
+ end
29
+
30
+ it "fails when array is not sorted descending" do
31
+ json = '[{"id": 1}, {"id": 2}, {"id": 3}]'
32
+ expect {
33
+ expect(json).to be_sorted_by(:id).descending
34
+ }.to fail_with(/expected JSON array at 'root' to be sorted by 'id' descending/)
35
+ end
36
+ end
37
+
38
+ context "with at_path" do
39
+ it "checks sorting at specified path" do
40
+ json = '{"users": [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]}'
41
+ expect(json).to be_sorted_by(:name).at_path("users")
42
+ end
43
+ end
44
+
45
+ context "with string values" do
46
+ it "sorts strings alphabetically" do
47
+ json = '[{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]'
48
+ expect(json).to be_sorted_by(:name)
49
+ end
50
+ end
51
+
52
+ context "with date strings" do
53
+ it "sorts date strings" do
54
+ json = '[{"created_at": "2023-01-01"}, {"created_at": "2023-06-15"}, {"created_at": "2023-12-31"}]'
55
+ expect(json).to be_sorted_by(:created_at)
56
+ end
57
+
58
+ it "checks descending date order" do
59
+ json = '[{"created_at": "2023-12-31"}, {"created_at": "2023-06-15"}, {"created_at": "2023-01-01"}]'
60
+ expect(json).to be_sorted_by(:created_at).descending
61
+ end
62
+ end
63
+
64
+ context "with empty array" do
65
+ it "passes for empty array" do
66
+ json = '[]'
67
+ expect(json).to be_sorted_by(:id)
68
+ end
69
+ end
70
+
71
+ context "with single element" do
72
+ it "passes for single element array" do
73
+ json = '[{"id": 1}]'
74
+ expect(json).to be_sorted_by(:id)
75
+ end
76
+ end
77
+
78
+ context "when path does not exist" do
79
+ it "fails with descriptive message" do
80
+ json = '{"data": []}'
81
+ expect {
82
+ expect(json).to be_sorted_by(:id).at_path("users")
83
+ }.to fail_with(/but path was not found/)
84
+ end
85
+ end
86
+
87
+ context "when value is not an array" do
88
+ it "fails with descriptive message" do
89
+ json = '{"user": {"id": 1}}'
90
+ expect {
91
+ expect(json).to be_sorted_by(:id).at_path("user")
92
+ }.to fail_with(/path was not found or value was not an array/)
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "actual.not_to be_sorted_by" do
98
+ it "passes when not sorted" do
99
+ json = '[{"id": 3}, {"id": 1}, {"id": 2}]'
100
+ expect(json).not_to be_sorted_by(:id)
101
+ end
102
+
103
+ it "fails when sorted" do
104
+ json = '[{"id": 1}, {"id": 2}, {"id": 3}]'
105
+ expect {
106
+ expect(json).not_to be_sorted_by(:id)
107
+ }.to fail_with(/expected JSON array at 'root' NOT to be sorted by 'id' ascending/)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::Collection::HaveJsonSize do
4
+ describe "actual.to have_json_size" do
5
+ context "with array at root" do
6
+ it "passes when size matches" do
7
+ json = '[1, 2, 3]'
8
+ expect(json).to have_json_size(3)
9
+ end
10
+
11
+ it "fails when size does not match" do
12
+ json = '[1, 2, 3]'
13
+ expect {
14
+ expect(json).to have_json_size(5)
15
+ }.to fail_with(/expected JSON collection at 'root' to have size 5. Got size: 3/)
16
+ end
17
+ end
18
+
19
+ context "with array at path" do
20
+ it "passes when size matches" do
21
+ json = '{"users": [{"id": 1}, {"id": 2}, {"id": 3}]}'
22
+ expect(json).to have_json_size(3).at_path("users")
23
+ end
24
+
25
+ it "fails when size does not match" do
26
+ json = '{"users": [{"id": 1}, {"id": 2}]}'
27
+ expect {
28
+ expect(json).to have_json_size(5).at_path("users")
29
+ }.to fail_with(/expected JSON collection at 'users' to have size 5. Got size: 2/)
30
+ end
31
+ end
32
+
33
+ context "with empty array" do
34
+ it "passes for size 0" do
35
+ json = '{"items": []}'
36
+ expect(json).to have_json_size(0).at_path("items")
37
+ end
38
+
39
+ it "fails when expecting non-zero size" do
40
+ json = '{"items": []}'
41
+ expect {
42
+ expect(json).to have_json_size(5).at_path("items")
43
+ }.to fail_with(/Got size: 0/)
44
+ end
45
+ end
46
+
47
+ context "with hash (keys count)" do
48
+ it "checks size of hash keys" do
49
+ json = '{"a": 1, "b": 2, "c": 3}'
50
+ expect(json).to have_json_size(3)
51
+ end
52
+ end
53
+
54
+ context "when path does not exist" do
55
+ it "fails with descriptive message" do
56
+ json = '{"data": []}'
57
+ expect {
58
+ expect(json).to have_json_size(5).at_path("users")
59
+ }.to fail_with(/but path was not found/)
60
+ end
61
+ end
62
+
63
+ context "when value is not a collection" do
64
+ it "fails with descriptive message" do
65
+ json = '{"count": "not a collection"}'
66
+ expect {
67
+ expect(json).to have_json_size(5).at_path("count")
68
+ }.to fail_with(/path was not found or value was not a collection/)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe "actual.not_to have_json_size" do
74
+ it "passes when size does not match" do
75
+ json = '[1, 2, 3]'
76
+ expect(json).not_to have_json_size(5)
77
+ end
78
+
79
+ it "fails when size matches" do
80
+ json = '[1, 2, 3]'
81
+ expect {
82
+ expect(json).not_to have_json_size(3)
83
+ }.to fail_with(/expected JSON collection at 'root' NOT to have size 3/)
84
+ end
85
+ end
86
+
87
+ describe "with configuration" do
88
+ before do
89
+ APIMatchers.setup { |config| config.response_body_method = :body }
90
+ end
91
+
92
+ after do
93
+ APIMatchers.setup { |config| config.response_body_method = nil }
94
+ end
95
+
96
+ it "extracts body from response object" do
97
+ response = OpenStruct.new(body: '{"items": [1, 2, 3]}')
98
+ expect(response).to have_json_size(3).at_path("items")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::ErrorResponse::HaveErrorOn do
4
+ describe "actual.to have_error_on" do
5
+ context "with array of error objects (standard API style)" do
6
+ it "passes when error exists on field" do
7
+ json = '{"errors": [{"field": "email", "message": "is invalid"}]}'
8
+ expect(json).to have_error_on(:email)
9
+ end
10
+
11
+ it "fails when no error on field" do
12
+ json = '{"errors": [{"field": "name", "message": "is required"}]}'
13
+ expect {
14
+ expect(json).to have_error_on(:email)
15
+ }.to fail_with(/Found errors on: \["name"\]/)
16
+ end
17
+ end
18
+
19
+ context "with Rails-style errors hash" do
20
+ it "passes when error exists on field" do
21
+ json = '{"email": ["is invalid", "is already taken"]}'
22
+ expect(json).to have_error_on(:email)
23
+ end
24
+
25
+ it "fails when no error on field" do
26
+ json = '{"name": ["is required"]}'
27
+ expect {
28
+ expect(json).to have_error_on(:email)
29
+ }.to fail_with(/Found errors on: \["name"\]/)
30
+ end
31
+ end
32
+
33
+ context "with with_message" do
34
+ it "passes when message matches" do
35
+ json = '{"errors": [{"field": "email", "message": "is invalid"}]}'
36
+ expect(json).to have_error_on(:email).with_message("is invalid")
37
+ end
38
+
39
+ it "fails when message does not match" do
40
+ json = '{"errors": [{"field": "email", "message": "is invalid"}]}'
41
+ expect {
42
+ expect(json).to have_error_on(:email).with_message("can't be blank")
43
+ }.to fail_with(/expected error on 'email' to have message/)
44
+ end
45
+
46
+ it "works with Rails-style errors" do
47
+ json = '{"email": ["is invalid", "is already taken"]}'
48
+ expect(json).to have_error_on(:email).with_message("is invalid")
49
+ end
50
+ end
51
+
52
+ context "with matching (regex)" do
53
+ it "passes when message matches pattern" do
54
+ json = '{"errors": [{"field": "email", "message": "Email format is invalid"}]}'
55
+ expect(json).to have_error_on(:email).matching(/invalid/i)
56
+ end
57
+
58
+ it "fails when message does not match pattern" do
59
+ json = '{"errors": [{"field": "email", "message": "is required"}]}'
60
+ expect {
61
+ expect(json).to have_error_on(:email).matching(/invalid/)
62
+ }.to fail_with(/expected error on 'email' to match/)
63
+ end
64
+ end
65
+
66
+ context "when no errors found" do
67
+ it "fails with descriptive message" do
68
+ json = '{"data": {"id": 1}}'
69
+ expect {
70
+ expect(json).to have_error_on(:email)
71
+ }.to fail_with(/but no errors were found/)
72
+ end
73
+ end
74
+
75
+ context "with attribute key instead of field" do
76
+ it "works with attribute key" do
77
+ json = '{"errors": [{"attribute": "email", "message": "is invalid"}]}'
78
+ expect(json).to have_error_on(:email)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "actual.not_to have_error_on" do
84
+ it "passes when no error on field" do
85
+ json = '{"errors": [{"field": "name", "message": "is required"}]}'
86
+ expect(json).not_to have_error_on(:email)
87
+ end
88
+
89
+ it "fails when error exists on field" do
90
+ json = '{"errors": [{"field": "email", "message": "is invalid"}]}'
91
+ expect {
92
+ expect(json).not_to have_error_on(:email)
93
+ }.to fail_with(/expected NOT to have error on 'email'/)
94
+ end
95
+ end
96
+
97
+ describe "with configuration" do
98
+ before do
99
+ APIMatchers.setup do |config|
100
+ config.response_body_method = :body
101
+ config.errors_path = 'response.errors'
102
+ config.error_field_key = 'attribute'
103
+ config.error_message_key = 'detail'
104
+ end
105
+ end
106
+
107
+ after do
108
+ APIMatchers.setup do |config|
109
+ config.response_body_method = nil
110
+ config.errors_path = nil
111
+ config.error_field_key = nil
112
+ config.error_message_key = nil
113
+ end
114
+ end
115
+
116
+ it "uses configured paths and keys" do
117
+ response = OpenStruct.new(
118
+ body: '{"response": {"errors": [{"attribute": "email", "detail": "is invalid"}]}}'
119
+ )
120
+ expect(response).to have_error_on(:email).with_message("is invalid")
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::ErrorResponse::HaveError do
4
+ describe "actual.to have_error" do
5
+ context "with errors array" do
6
+ it "passes when errors array is present" do
7
+ json = '{"errors": [{"message": "Name is required"}]}'
8
+ expect(json).to have_error
9
+ end
10
+
11
+ it "passes when errors array has multiple errors" do
12
+ json = '{"errors": [{"message": "Name is required"}, {"message": "Email is invalid"}]}'
13
+ expect(json).to have_errors
14
+ end
15
+
16
+ it "fails when errors array is empty" do
17
+ json = '{"errors": []}'
18
+ expect {
19
+ expect(json).to have_error
20
+ }.to fail_with(/expected response to have error/)
21
+ end
22
+ end
23
+
24
+ context "with error object" do
25
+ it "passes when error key is present" do
26
+ json = '{"error": "Something went wrong"}'
27
+ expect(json).to have_error
28
+ end
29
+
30
+ it "passes when error is an object" do
31
+ json = '{"error": {"code": "INVALID", "message": "Invalid input"}}'
32
+ expect(json).to have_error
33
+ end
34
+ end
35
+
36
+ context "with message key" do
37
+ it "passes when message is present" do
38
+ json = '{"message": "Resource not found"}'
39
+ expect(json).to have_error
40
+ end
41
+ end
42
+
43
+ context "with errors at configured path" do
44
+ before do
45
+ APIMatchers.setup { |config| config.errors_path = 'response.errors' }
46
+ end
47
+
48
+ after do
49
+ APIMatchers.setup { |config| config.errors_path = nil }
50
+ end
51
+
52
+ it "finds errors at configured path" do
53
+ json = '{"response": {"errors": [{"message": "Error"}]}}'
54
+ expect(json).to have_error
55
+ end
56
+ end
57
+
58
+ context "when no errors found" do
59
+ it "fails with descriptive message" do
60
+ json = '{"data": {"id": 1}}'
61
+ expect {
62
+ expect(json).to have_error
63
+ }.to fail_with(/expected response to have error/)
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "actual.not_to have_error" do
69
+ it "passes when no errors present" do
70
+ json = '{"data": {"id": 1}}'
71
+ expect(json).not_to have_error
72
+ end
73
+
74
+ it "passes when errors array is empty" do
75
+ json = '{"errors": []}'
76
+ expect(json).not_to have_errors
77
+ end
78
+
79
+ it "fails when errors are present" do
80
+ json = '{"errors": [{"message": "Error"}]}'
81
+ expect {
82
+ expect(json).not_to have_error
83
+ }.to fail_with(/expected response NOT to have error/)
84
+ end
85
+ end
86
+
87
+ describe "alias have_errors" do
88
+ it "works as alias for have_error" do
89
+ json = '{"errors": [{"message": "Error"}]}'
90
+ expect(json).to have_errors
91
+ end
92
+ end
93
+
94
+ describe "with configuration" do
95
+ before do
96
+ APIMatchers.setup { |config| config.response_body_method = :body }
97
+ end
98
+
99
+ after do
100
+ APIMatchers.setup { |config| config.response_body_method = nil }
101
+ end
102
+
103
+ it "extracts body from response object" do
104
+ response = OpenStruct.new(body: '{"errors": [{"message": "Error"}]}')
105
+ expect(response).to have_error
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::Hateoas::HaveLink do
4
+ describe "actual.to have_link" do
5
+ context "with HAL-style links" do
6
+ it "passes when link is present" do
7
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
8
+ expect(json).to have_link(:self)
9
+ end
10
+
11
+ it "fails when link is not present" do
12
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
13
+ expect {
14
+ expect(json).to have_link(:next)
15
+ }.to fail_with(/Available links: \["self"\]/)
16
+ end
17
+ end
18
+
19
+ context "with simple links object" do
20
+ it "passes when link is present" do
21
+ json = '{"links": {"self": "/users/1"}}'
22
+ expect(json).to have_link(:self)
23
+ end
24
+ end
25
+
26
+ context "with with_href (exact match)" do
27
+ it "passes when href matches exactly" do
28
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
29
+ expect(json).to have_link(:self).with_href("/users/1")
30
+ end
31
+
32
+ it "fails when href does not match" do
33
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
34
+ expect {
35
+ expect(json).to have_link(:self).with_href("/users/2")
36
+ }.to fail_with(/expected link 'self' to have href '\/users\/2'. Got: '\/users\/1'/)
37
+ end
38
+ end
39
+
40
+ context "with with_href (regex)" do
41
+ it "passes when href matches pattern" do
42
+ json = '{"_links": {"self": {"href": "/users/123"}}}'
43
+ expect(json).to have_link(:self).with_href(/\/users\/\d+/)
44
+ end
45
+
46
+ it "fails when href does not match pattern" do
47
+ json = '{"_links": {"self": {"href": "/posts/123"}}}'
48
+ expect {
49
+ expect(json).to have_link(:self).with_href(/\/users\/\d+/)
50
+ }.to fail_with(/expected link 'self' href to match/)
51
+ end
52
+ end
53
+
54
+ context "with simple string href" do
55
+ it "extracts href from string value" do
56
+ json = '{"links": {"self": "/users/1"}}'
57
+ expect(json).to have_link(:self).with_href("/users/1")
58
+ end
59
+ end
60
+
61
+ context "when links not found" do
62
+ it "fails with descriptive message" do
63
+ json = '{"data": {"id": 1}}'
64
+ expect {
65
+ expect(json).to have_link(:self)
66
+ }.to fail_with(/but no links were found/)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "actual.not_to have_link" do
72
+ it "passes when link is not present" do
73
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
74
+ expect(json).not_to have_link(:next)
75
+ end
76
+
77
+ it "fails when link is present" do
78
+ json = '{"_links": {"self": {"href": "/users/1"}}}'
79
+ expect {
80
+ expect(json).not_to have_link(:self)
81
+ }.to fail_with(/expected response NOT to have link 'self', but it was present/)
82
+ end
83
+ end
84
+
85
+ describe "with configuration" do
86
+ before do
87
+ APIMatchers.setup do |config|
88
+ config.response_body_method = :body
89
+ config.links_path = 'links'
90
+ end
91
+ end
92
+
93
+ after do
94
+ APIMatchers.setup do |config|
95
+ config.response_body_method = nil
96
+ config.links_path = nil
97
+ end
98
+ end
99
+
100
+ it "uses configured links_path" do
101
+ response = OpenStruct.new(body: '{"links": {"self": {"href": "/users/1"}}}')
102
+ expect(response).to have_link(:self)
103
+ end
104
+ end
105
+ end
@@ -1,12 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe APIMatchers::Headers::Base do
4
- let(:setup) { OpenStruct.new }
5
- subject { APIMatchers::Headers::Base.new(setup) }
4
+ subject { APIMatchers::Headers::Base.new }
6
5
 
7
6
  describe "#matches?" do
8
7
  it "should raise Not Implement Exception" do
9
8
  expect { subject.matches?('application/xml') }.to raise_error(NotImplementedError, "not implemented on #{subject}")
10
9
  end
11
10
  end
12
- end
11
+
12
+ describe "#setup" do
13
+ it "returns the global Setup class" do
14
+ expect(subject.setup).to eq APIMatchers::Core::Setup
15
+ end
16
+ end
17
+ end