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.
Files changed (115) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +30 -0
  3. data/.gitignore +1 -1
  4. data/Gemfile +1 -1
  5. data/History.markdown +62 -46
  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 +154 -48
  96. data/.rvmrc.example +0 -1
  97. data/.travis.yml +0 -12
  98. data/lib/api_matchers/http_status_code/base.rb +0 -32
  99. data/lib/api_matchers/http_status_code/be_bad_request.rb +0 -25
  100. data/lib/api_matchers/http_status_code/be_forbidden.rb +0 -21
  101. data/lib/api_matchers/http_status_code/be_internal_server_error.rb +0 -25
  102. data/lib/api_matchers/http_status_code/be_not_found.rb +0 -25
  103. data/lib/api_matchers/http_status_code/be_ok.rb +0 -25
  104. data/lib/api_matchers/http_status_code/be_unauthorized.rb +0 -25
  105. data/lib/api_matchers/http_status_code/be_unprocessable_entity.rb +0 -25
  106. data/lib/api_matchers/http_status_code/create_resource.rb +0 -25
  107. data/spec/api_matchers/http_status_code/base_spec.rb +0 -12
  108. data/spec/api_matchers/http_status_code/be_bad_request_spec.rb +0 -49
  109. data/spec/api_matchers/http_status_code/be_forbidden_spec.rb +0 -49
  110. data/spec/api_matchers/http_status_code/be_internal_server_error_spec.rb +0 -49
  111. data/spec/api_matchers/http_status_code/be_not_found_spec.rb +0 -49
  112. data/spec/api_matchers/http_status_code/be_ok_spec.rb +0 -49
  113. data/spec/api_matchers/http_status_code/be_unauthorized_spec.rb +0 -49
  114. data/spec/api_matchers/http_status_code/be_unprocessable_entity_spec.rb +0 -27
  115. data/spec/api_matchers/http_status_code/create_resource_spec.rb +0 -49
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::HTTPStatus::BeUnprocessable do
4
+ describe "actual.to be_unprocessable" do
5
+ it "passes for status 422" do
6
+ expect(422).to be_unprocessable
7
+ end
8
+
9
+ it "fails for status 200" do
10
+ expect {
11
+ expect(200).to be_unprocessable
12
+ }.to fail_with("expected response to be unprocessable (422). Got: 200")
13
+ end
14
+
15
+ it "fails for status 400" do
16
+ expect {
17
+ expect(400).to be_unprocessable
18
+ }.to fail_with("expected response to be unprocessable (422). Got: 400")
19
+ end
20
+ end
21
+
22
+ describe "actual.not_to be_unprocessable" do
23
+ it "passes for non-422 status" do
24
+ expect(200).not_to be_unprocessable
25
+ end
26
+
27
+ it "fails for 422 status" do
28
+ expect {
29
+ expect(422).not_to be_unprocessable
30
+ }.to fail_with("expected response NOT to be unprocessable (422). Got: 422")
31
+ end
32
+ end
33
+
34
+ describe "alias be_unprocessable_entity" do
35
+ it "works as alias for be_unprocessable" do
36
+ expect(422).to be_unprocessable_entity
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::HTTPStatus::HaveHttpStatus do
4
+ describe "actual.to have_http_status" do
5
+ context "with integer status code" do
6
+ it "passes when the status matches" do
7
+ expect(200).to have_http_status(200)
8
+ end
9
+
10
+ it "fails when the status does not match" do
11
+ expect {
12
+ expect(404).to have_http_status(200)
13
+ }.to fail_with("expected response to have HTTP status 200. Got: 404")
14
+ end
15
+ end
16
+
17
+ context "with symbol status code" do
18
+ it "passes when the status matches :ok" do
19
+ expect(200).to have_http_status(:ok)
20
+ end
21
+
22
+ it "passes when the status matches :created" do
23
+ expect(201).to have_http_status(:created)
24
+ end
25
+
26
+ it "passes when the status matches :not_found" do
27
+ expect(404).to have_http_status(:not_found)
28
+ end
29
+
30
+ it "passes when the status matches :unprocessable_entity" do
31
+ expect(422).to have_http_status(:unprocessable_entity)
32
+ end
33
+
34
+ it "fails when the status does not match" do
35
+ expect {
36
+ expect(500).to have_http_status(:ok)
37
+ }.to fail_with("expected response to have HTTP status 200 (ok). Got: 500")
38
+ end
39
+
40
+ it "raises error for unknown symbol" do
41
+ expect {
42
+ expect(200).to have_http_status(:unknown_status)
43
+ }.to raise_error(ArgumentError, /Unknown status code symbol/)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "actual.not_to have_http_status" do
49
+ it "passes when the status does not match" do
50
+ expect(404).not_to have_http_status(200)
51
+ end
52
+
53
+ it "fails when the status matches" do
54
+ expect {
55
+ expect(200).not_to have_http_status(200)
56
+ }.to fail_with("expected response NOT to have HTTP status 200. Got: 200")
57
+ end
58
+ end
59
+
60
+ describe "with configuration" do
61
+ before do
62
+ APIMatchers.setup { |config| config.http_status_method = :status }
63
+ end
64
+
65
+ after do
66
+ APIMatchers.setup { |config| config.http_status_method = nil }
67
+ end
68
+
69
+ it "extracts status from response object" do
70
+ response = OpenStruct.new(status: 200)
71
+ expect(response).to have_http_status(:ok)
72
+ end
73
+
74
+ it "fails when response status does not match" do
75
+ response = OpenStruct.new(status: 404)
76
+ expect {
77
+ expect(response).to have_http_status(:ok)
78
+ }.to fail_with(/expected response to have HTTP status 200/)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonApi::BeJsonApiCompliant do
4
+ describe "actual.to be_json_api_compliant" do
5
+ context "with valid JSON:API structure" do
6
+ it "passes with data object" do
7
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John"}}}'
8
+ expect(json).to be_json_api_compliant
9
+ end
10
+
11
+ it "passes with data array" do
12
+ json = '{"data": [{"id": "1", "type": "users"}, {"id": "2", "type": "users"}]}'
13
+ expect(json).to be_json_api_compliant
14
+ end
15
+
16
+ it "passes with null data" do
17
+ json = '{"data": null}'
18
+ expect(json).to be_json_api_compliant
19
+ end
20
+
21
+ it "passes with errors array" do
22
+ json = '{"errors": [{"status": "404", "title": "Not Found"}]}'
23
+ expect(json).to be_json_api_compliant
24
+ end
25
+
26
+ it "passes with meta only" do
27
+ json = '{"meta": {"total": 100}}'
28
+ expect(json).to be_json_api_compliant
29
+ end
30
+
31
+ it "passes with relationships" do
32
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}}}}'
33
+ expect(json).to be_json_api_compliant
34
+ end
35
+ end
36
+
37
+ context "with invalid JSON:API structure" do
38
+ it "fails when top level is not an object" do
39
+ json = '[1, 2, 3]'
40
+ expect {
41
+ expect(json).to be_json_api_compliant
42
+ }.to fail_with(/top-level must be an object/)
43
+ end
44
+
45
+ it "fails when missing data, errors, and meta" do
46
+ json = '{"included": []}'
47
+ expect {
48
+ expect(json).to be_json_api_compliant
49
+ }.to fail_with(/must contain at least one of: data, errors, or meta/)
50
+ end
51
+
52
+ it "fails when data and errors coexist" do
53
+ json = '{"data": {"id": "1", "type": "users"}, "errors": []}'
54
+ expect {
55
+ expect(json).to be_json_api_compliant
56
+ }.to fail_with(/data and errors must not coexist/)
57
+ end
58
+
59
+ it "fails when resource is missing type" do
60
+ json = '{"data": {"id": "1"}}'
61
+ expect {
62
+ expect(json).to be_json_api_compliant
63
+ }.to fail_with(/must contain 'type'/)
64
+ end
65
+
66
+ it "fails when type is not a string" do
67
+ json = '{"data": {"id": "1", "type": 123}}'
68
+ expect {
69
+ expect(json).to be_json_api_compliant
70
+ }.to fail_with(/type must be a string/)
71
+ end
72
+
73
+ it "fails when attributes is not an object" do
74
+ json = '{"data": {"id": "1", "type": "users", "attributes": "invalid"}}'
75
+ expect {
76
+ expect(json).to be_json_api_compliant
77
+ }.to fail_with(/attributes must be an object/)
78
+ end
79
+
80
+ it "fails when relationships is not an object" do
81
+ json = '{"data": {"id": "1", "type": "users", "relationships": "invalid"}}'
82
+ expect {
83
+ expect(json).to be_json_api_compliant
84
+ }.to fail_with(/relationships must be an object/)
85
+ end
86
+
87
+ it "fails when errors is not an array" do
88
+ json = '{"errors": {"message": "error"}}'
89
+ expect {
90
+ expect(json).to be_json_api_compliant
91
+ }.to fail_with(/errors must be an array/)
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "actual.not_to be_json_api_compliant" do
97
+ it "passes when not compliant" do
98
+ json = '{"result": "success"}'
99
+ expect(json).not_to be_json_api_compliant
100
+ end
101
+
102
+ it "fails when compliant" do
103
+ json = '{"data": {"id": "1", "type": "users"}}'
104
+ expect {
105
+ expect(json).not_to be_json_api_compliant
106
+ }.to fail_with(/expected response NOT to be JSON:API compliant/)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonApi::HaveJsonApiAttributes do
4
+ describe "actual.to have_json_api_attributes" do
5
+ context "with single resource" do
6
+ it "passes when all attributes are present" do
7
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John", "email": "john@example.com"}}}'
8
+ expect(json).to have_json_api_attributes(:name, :email)
9
+ end
10
+
11
+ it "passes when checking subset of attributes" do
12
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John", "email": "john@example.com", "age": 30}}}'
13
+ expect(json).to have_json_api_attributes(:name)
14
+ end
15
+
16
+ it "fails when attribute is missing" do
17
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John"}}}'
18
+ expect {
19
+ expect(json).to have_json_api_attributes(:name, :email)
20
+ }.to fail_with(/Missing: \["email"\]/)
21
+ end
22
+ end
23
+
24
+ context "with array of resources" do
25
+ it "checks attributes of first resource" do
26
+ json = '{"data": [{"id": "1", "type": "users", "attributes": {"name": "John"}}, {"id": "2", "type": "users", "attributes": {"name": "Jane"}}]}'
27
+ expect(json).to have_json_api_attributes(:name)
28
+ end
29
+ end
30
+
31
+ context "when no attributes found" do
32
+ it "fails when data has no attributes" do
33
+ json = '{"data": {"id": "1", "type": "users"}}'
34
+ expect {
35
+ expect(json).to have_json_api_attributes(:name)
36
+ }.to fail_with(/but no attributes were found/)
37
+ end
38
+
39
+ it "fails when data is missing" do
40
+ json = '{"errors": []}'
41
+ expect {
42
+ expect(json).to have_json_api_attributes(:name)
43
+ }.to fail_with(/but no attributes were found/)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "actual.not_to have_json_api_attributes" do
49
+ it "passes when not all attributes are present" do
50
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John"}}}'
51
+ expect(json).not_to have_json_api_attributes(:email)
52
+ end
53
+
54
+ it "fails when all attributes are present" do
55
+ json = '{"data": {"id": "1", "type": "users", "attributes": {"name": "John", "email": "john@example.com"}}}'
56
+ expect {
57
+ expect(json).not_to have_json_api_attributes(:name, :email)
58
+ }.to fail_with(/expected JSON:API data NOT to have attributes/)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonApi::HaveJsonApiData do
4
+ describe "actual.to have_json_api_data" do
5
+ context "basic data presence" do
6
+ it "passes when data is present" do
7
+ json = '{"data": {"id": "1", "type": "users"}}'
8
+ expect(json).to have_json_api_data
9
+ end
10
+
11
+ it "passes when data is null" do
12
+ json = '{"data": null}'
13
+ expect(json).to have_json_api_data
14
+ end
15
+
16
+ it "passes when data is an array" do
17
+ json = '{"data": [{"id": "1", "type": "users"}]}'
18
+ expect(json).to have_json_api_data
19
+ end
20
+
21
+ it "fails when data is missing" do
22
+ json = '{"errors": []}'
23
+ expect {
24
+ expect(json).to have_json_api_data
25
+ }.to fail_with(/expected response to have JSON:API data/)
26
+ end
27
+ end
28
+
29
+ context "with of_type" do
30
+ it "passes when type matches" do
31
+ json = '{"data": {"id": "1", "type": "users"}}'
32
+ expect(json).to have_json_api_data.of_type("users")
33
+ end
34
+
35
+ it "passes when type matches for array" do
36
+ json = '{"data": [{"id": "1", "type": "users"}, {"id": "2", "type": "users"}]}'
37
+ expect(json).to have_json_api_data.of_type(:users)
38
+ end
39
+
40
+ it "fails when type does not match" do
41
+ json = '{"data": {"id": "1", "type": "posts"}}'
42
+ expect {
43
+ expect(json).to have_json_api_data.of_type("users")
44
+ }.to fail_with(/expected JSON:API data to have type 'users'. Got type: 'posts'/)
45
+ end
46
+ end
47
+
48
+ context "with with_id" do
49
+ it "passes when id matches" do
50
+ json = '{"data": {"id": "123", "type": "users"}}'
51
+ expect(json).to have_json_api_data.with_id("123")
52
+ end
53
+
54
+ it "passes when id is in array" do
55
+ json = '{"data": [{"id": "1", "type": "users"}, {"id": "2", "type": "users"}]}'
56
+ expect(json).to have_json_api_data.with_id("2")
57
+ end
58
+
59
+ it "fails when id does not match" do
60
+ json = '{"data": {"id": "123", "type": "users"}}'
61
+ expect {
62
+ expect(json).to have_json_api_data.with_id("456")
63
+ }.to fail_with(/expected JSON:API data to have id '456'. Got id: '123'/)
64
+ end
65
+ end
66
+
67
+ context "combining of_type and with_id" do
68
+ it "passes when both match" do
69
+ json = '{"data": {"id": "123", "type": "users"}}'
70
+ expect(json).to have_json_api_data.of_type("users").with_id("123")
71
+ end
72
+
73
+ it "fails when type does not match" do
74
+ json = '{"data": {"id": "123", "type": "posts"}}'
75
+ expect {
76
+ expect(json).to have_json_api_data.of_type("users").with_id("123")
77
+ }.to fail_with(/expected JSON:API data to have type 'users'/)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "actual.not_to have_json_api_data" do
83
+ it "passes when data is missing" do
84
+ json = '{"errors": []}'
85
+ expect(json).not_to have_json_api_data
86
+ end
87
+
88
+ it "fails when data is present" do
89
+ json = '{"data": {"id": "1", "type": "users"}}'
90
+ expect {
91
+ expect(json).not_to have_json_api_data
92
+ }.to fail_with(/expected response NOT to have JSON:API data/)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonApi::HaveJsonApiRelationships do
4
+ describe "actual.to have_json_api_relationships" do
5
+ context "with single resource" do
6
+ it "passes when all relationships are present" do
7
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}, "comments": {"data": []}}}}'
8
+ expect(json).to have_json_api_relationships(:author, :comments)
9
+ end
10
+
11
+ it "passes when checking subset of relationships" do
12
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}, "comments": {"data": []}}}}'
13
+ expect(json).to have_json_api_relationships(:author)
14
+ end
15
+
16
+ it "fails when relationship is missing" do
17
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}}}}'
18
+ expect {
19
+ expect(json).to have_json_api_relationships(:author, :comments)
20
+ }.to fail_with(/Missing: \["comments"\]/)
21
+ end
22
+ end
23
+
24
+ context "with array of resources" do
25
+ it "checks relationships of first resource" do
26
+ json = '{"data": [{"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}}}, {"id": "2", "type": "posts", "relationships": {"author": {"data": {"id": "2", "type": "users"}}}}]}'
27
+ expect(json).to have_json_api_relationships(:author)
28
+ end
29
+ end
30
+
31
+ context "when no relationships found" do
32
+ it "fails when data has no relationships" do
33
+ json = '{"data": {"id": "1", "type": "posts"}}'
34
+ expect {
35
+ expect(json).to have_json_api_relationships(:author)
36
+ }.to fail_with(/but no relationships were found/)
37
+ end
38
+
39
+ it "fails when data is missing" do
40
+ json = '{"errors": []}'
41
+ expect {
42
+ expect(json).to have_json_api_relationships(:author)
43
+ }.to fail_with(/but no relationships were found/)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "actual.not_to have_json_api_relationships" do
49
+ it "passes when not all relationships are present" do
50
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}}}}'
51
+ expect(json).not_to have_json_api_relationships(:comments)
52
+ end
53
+
54
+ it "fails when all relationships are present" do
55
+ json = '{"data": {"id": "1", "type": "posts", "relationships": {"author": {"data": {"id": "1", "type": "users"}}, "comments": {"data": []}}}}'
56
+ expect {
57
+ expect(json).not_to have_json_api_relationships(:author, :comments)
58
+ }.to fail_with(/expected JSON:API data NOT to have relationships/)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonStructure::HaveJsonKeys do
4
+ describe "actual.to have_json_keys" do
5
+ context "with JSON string" do
6
+ it "passes when all keys are present" do
7
+ json = '{"id": 1, "name": "John", "email": "john@example.com"}'
8
+ expect(json).to have_json_keys(:id, :name, :email)
9
+ end
10
+
11
+ it "passes when checking subset of keys" do
12
+ json = '{"id": 1, "name": "John", "email": "john@example.com"}'
13
+ expect(json).to have_json_keys(:id, :name)
14
+ end
15
+
16
+ it "fails when a key is missing" do
17
+ json = '{"id": 1, "name": "John"}'
18
+ expect {
19
+ expect(json).to have_json_keys(:id, :name, :email)
20
+ }.to fail_with(/Missing: \["email"\]/)
21
+ end
22
+
23
+ it "fails when multiple keys are missing" do
24
+ json = '{"id": 1}'
25
+ expect {
26
+ expect(json).to have_json_keys(:id, :name, :email)
27
+ }.to fail_with(/Missing: \["name", "email"\]/)
28
+ end
29
+ end
30
+
31
+ context "with Hash" do
32
+ it "passes when all keys are present" do
33
+ data = { id: 1, name: "John", email: "john@example.com" }
34
+ expect(data).to have_json_keys(:id, :name, :email)
35
+ end
36
+ end
37
+
38
+ context "with at_path" do
39
+ it "checks keys at specified path" do
40
+ json = '{"user": {"id": 1, "name": "John"}}'
41
+ expect(json).to have_json_keys(:id, :name).at_path("user")
42
+ end
43
+
44
+ it "fails when path does not exist" do
45
+ json = '{"data": {"id": 1}}'
46
+ expect {
47
+ expect(json).to have_json_keys(:id).at_path("user")
48
+ }.to fail_with(/Missing: \["id"\]/)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "actual.not_to have_json_keys" do
54
+ it "passes when not all keys are present" do
55
+ json = '{"id": 1, "name": "John"}'
56
+ expect(json).not_to have_json_keys(:id, :name, :email)
57
+ end
58
+
59
+ it "fails when all keys are present" do
60
+ json = '{"id": 1, "name": "John"}'
61
+ expect {
62
+ expect(json).not_to have_json_keys(:id, :name)
63
+ }.to fail_with(/expected JSON NOT to have keys/)
64
+ end
65
+ end
66
+
67
+ describe "with configuration" do
68
+ before do
69
+ APIMatchers.setup { |config| config.response_body_method = :body }
70
+ end
71
+
72
+ after do
73
+ APIMatchers.setup { |config| config.response_body_method = nil }
74
+ end
75
+
76
+ it "extracts body from response object" do
77
+ response = OpenStruct.new(body: '{"id": 1, "name": "John"}')
78
+ expect(response).to have_json_keys(:id, :name)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe APIMatchers::JsonStructure::HaveJsonType do
4
+ describe "actual.to have_json_type" do
5
+ context "Integer type" do
6
+ it "passes for integer value" do
7
+ json = '{"id": 42}'
8
+ expect(json).to have_json_type(Integer).at_path("id")
9
+ end
10
+
11
+ it "fails for non-integer value" do
12
+ json = '{"id": "42"}'
13
+ expect {
14
+ expect(json).to have_json_type(Integer).at_path("id")
15
+ }.to fail_with(/expected JSON value at 'id' to be of type Integer/)
16
+ end
17
+ end
18
+
19
+ context "String type" do
20
+ it "passes for string value" do
21
+ json = '{"name": "John"}'
22
+ expect(json).to have_json_type(String).at_path("name")
23
+ end
24
+
25
+ it "fails for non-string value" do
26
+ json = '{"name": 123}'
27
+ expect {
28
+ expect(json).to have_json_type(String).at_path("name")
29
+ }.to fail_with(/expected JSON value at 'name' to be of type String/)
30
+ end
31
+ end
32
+
33
+ context "Boolean type" do
34
+ it "passes for true value" do
35
+ json = '{"active": true}'
36
+ expect(json).to have_json_type(:boolean).at_path("active")
37
+ end
38
+
39
+ it "passes for false value" do
40
+ json = '{"active": false}'
41
+ expect(json).to have_json_type(:boolean).at_path("active")
42
+ end
43
+
44
+ it "fails for non-boolean value" do
45
+ json = '{"active": "true"}'
46
+ expect {
47
+ expect(json).to have_json_type(:boolean).at_path("active")
48
+ }.to fail_with(/expected JSON value at 'active' to be of type Boolean/)
49
+ end
50
+ end
51
+
52
+ context "Array type" do
53
+ it "passes for array value" do
54
+ json = '{"items": [1, 2, 3]}'
55
+ expect(json).to have_json_type(Array).at_path("items")
56
+ end
57
+
58
+ it "fails for non-array value" do
59
+ json = '{"items": "not an array"}'
60
+ expect {
61
+ expect(json).to have_json_type(Array).at_path("items")
62
+ }.to fail_with(/expected JSON value at 'items' to be of type Array/)
63
+ end
64
+ end
65
+
66
+ context "Hash type" do
67
+ it "passes for hash/object value" do
68
+ json = '{"user": {"name": "John"}}'
69
+ expect(json).to have_json_type(Hash).at_path("user")
70
+ end
71
+
72
+ it "fails for non-hash value" do
73
+ json = '{"user": "not a hash"}'
74
+ expect {
75
+ expect(json).to have_json_type(Hash).at_path("user")
76
+ }.to fail_with(/expected JSON value at 'user' to be of type Hash/)
77
+ end
78
+ end
79
+
80
+ context "NilClass type" do
81
+ it "passes for null value" do
82
+ json = '{"value": null}'
83
+ expect(json).to have_json_type(NilClass).at_path("value")
84
+ end
85
+
86
+ it "fails for non-null value" do
87
+ json = '{"value": "something"}'
88
+ expect {
89
+ expect(json).to have_json_type(NilClass).at_path("value")
90
+ }.to fail_with(/expected JSON value at 'value' to be of type NilClass/)
91
+ end
92
+ end
93
+
94
+ context "Numeric type" do
95
+ it "passes for integer value" do
96
+ json = '{"count": 42}'
97
+ expect(json).to have_json_type(Numeric).at_path("count")
98
+ end
99
+
100
+ it "passes for float value" do
101
+ json = '{"price": 19.99}'
102
+ expect(json).to have_json_type(Numeric).at_path("price")
103
+ end
104
+ end
105
+
106
+ context "nested path" do
107
+ it "navigates nested structure" do
108
+ json = '{"user": {"profile": {"age": 30}}}'
109
+ expect(json).to have_json_type(Integer).at_path("user.profile.age")
110
+ end
111
+ end
112
+
113
+ context "at root" do
114
+ it "checks type at root level without at_path" do
115
+ json = '[1, 2, 3]'
116
+ expect(json).to have_json_type(Array)
117
+ end
118
+ end
119
+ end
120
+
121
+ describe "actual.not_to have_json_type" do
122
+ it "passes when type does not match" do
123
+ json = '{"id": "42"}'
124
+ expect(json).not_to have_json_type(Integer).at_path("id")
125
+ end
126
+
127
+ it "fails when type matches" do
128
+ json = '{"id": 42}'
129
+ expect {
130
+ expect(json).not_to have_json_type(Integer).at_path("id")
131
+ }.to fail_with(/expected JSON value at 'id' NOT to be of type Integer/)
132
+ end
133
+ end
134
+ end