json_matchers 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,7 +18,7 @@ module JsonMatchers
18
18
  elsif payload.is_a?(Array) || payload.is_a?(Hash)
19
19
  payload.to_json
20
20
  else
21
- payload
21
+ payload.to_s
22
22
  end
23
23
  end
24
24
  end
@@ -1,94 +1,34 @@
1
- require "delegate"
2
1
  require "json_matchers"
3
- require "json_matchers/payload"
2
+ require "json_matchers/assertion"
4
3
 
5
4
  module JsonMatchers
6
- class RSpec < SimpleDelegator
7
- attr_reader :schema_name
8
-
9
- def initialize(schema_name, **options)
10
- @schema_name = schema_name
11
-
12
- super(JsonMatchers::Matcher.new(schema_path, options))
13
- end
14
-
15
- def failure_message(json)
16
- <<-FAIL
17
- #{validation_failure_message}
18
-
19
- ---
20
-
21
- expected
22
-
23
- #{pretty_json(json)}
24
-
25
- to match schema "#{schema_name}":
26
-
27
- #{pretty_json(schema_body)}
28
-
29
- FAIL
30
- end
31
-
32
- def failure_message_when_negated(json)
33
- <<-FAIL
34
- #{validation_failure_message}
35
-
36
- ---
37
-
38
- expected
39
-
40
- #{pretty_json(json)}
5
+ self.schema_root = File.join("spec", "support", "api", "schemas")
6
+ end
41
7
 
42
- not to match schema "#{schema_name}":
8
+ RSpec::Matchers.define :match_json_schema do |schema_name, **options|
9
+ assertion = JsonMatchers::Assertion.new(schema_name.to_s, options)
43
10
 
44
- #{pretty_json(schema_body)}
11
+ match do |json|
12
+ assertion.valid?(json)
13
+ end
45
14
 
46
- FAIL
15
+ if respond_to?(:failure_message)
16
+ failure_message do
17
+ assertion.valid_failure_message
47
18
  end
48
19
 
49
- private
50
-
51
- def pretty_json(json)
52
- payload = Payload.new(json).to_s
53
-
54
- JSON.pretty_generate(JSON.parse(payload))
20
+ failure_message_when_negated do
21
+ assertion.invalid_failure_message
55
22
  end
56
-
57
- def schema_path
58
- JsonMatchers.path_to_schema(schema_name)
23
+ else
24
+ failure_message_for_should do
25
+ assertion.valid_failure_message
59
26
  end
60
27
 
61
- def schema_body
62
- File.read(schema_path)
28
+ failure_message_for_should_not do
29
+ assertion.invalid_failure_message
63
30
  end
64
31
  end
65
32
  end
66
33
 
67
- if RSpec.respond_to?(:configure)
68
- RSpec::Matchers.define :match_json_schema do |schema_name, **options|
69
- matcher = JsonMatchers::RSpec.new(schema_name, options)
70
-
71
- match do |json|
72
- matcher.matches?(json)
73
- end
74
-
75
- if respond_to?(:failure_message)
76
- failure_message do |json|
77
- matcher.failure_message(json)
78
- end
79
-
80
- failure_message_when_negated do |json|
81
- matcher.failure_message_when_negated(json)
82
- end
83
- else
84
- failure_message_for_should do |json|
85
- matcher.failure_message(json)
86
- end
87
-
88
- failure_message_for_should_not do |json|
89
- matcher.failure_message_when_negated(json)
90
- end
91
- end
92
- end
93
- RSpec::Matchers.alias_matcher :match_response_schema, :match_json_schema
94
- end
34
+ RSpec::Matchers.alias_matcher :match_response_schema, :match_json_schema
@@ -1,11 +1,10 @@
1
1
  require "json-schema"
2
- require "json_matchers/payload"
3
2
 
4
3
  module JsonMatchers
5
4
  class Validator
6
- def initialize(options:, response:, schema_path:)
5
+ def initialize(options:, payload:, schema_path:)
7
6
  @options = options.dup
8
- @payload = Payload.new(response).to_s
7
+ @payload = payload
9
8
  @schema_path = schema_path.to_s
10
9
  end
11
10
 
@@ -1,3 +1,3 @@
1
1
  module JsonMatchers
2
- VERSION = "0.7.3"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -0,0 +1,88 @@
1
+ require "json_matchers/payload"
2
+ require_relative "./support/fake_response"
3
+ require_relative "./support/fake_schema"
4
+
5
+ FactoryBot.define do
6
+ factory :response, class: FakeResponse do
7
+ skip_create
8
+
9
+ trait :object do
10
+ body { { "id": 1 } }
11
+ end
12
+
13
+ trait :invalid_object do
14
+ body { { "id": "1" } }
15
+ end
16
+
17
+ initialize_with do
18
+ body = attributes.fetch(:body, nil)
19
+ json = attributes.except(:body)
20
+ payload = JsonMatchers::Payload.new(body || json)
21
+
22
+ FakeResponse.new(payload.to_s)
23
+ end
24
+ end
25
+
26
+ factory :schema, class: FakeSchema do
27
+ skip_create
28
+
29
+ sequence(:name) { |n| "json_schema-#{n}" }
30
+
31
+ trait :invalid do
32
+ json { "" }
33
+ end
34
+
35
+ trait :object do
36
+ json do
37
+ {
38
+ "type": "object",
39
+ "required": [
40
+ "id",
41
+ ],
42
+ "properties": {
43
+ "id": { "type": "number" },
44
+ },
45
+ "additionalProperties": false,
46
+ }
47
+ end
48
+ end
49
+ trait(:objects) { object }
50
+
51
+ trait :array_of do
52
+ initialize_with do
53
+ schema_body_as_json = attributes.fetch(:json, nil)
54
+ schema_body = attributes.except(:json, :name)
55
+
56
+ FakeSchema.new(name, {
57
+ "type": "array",
58
+ "items": schema_body_as_json || schema_body,
59
+ })
60
+ end
61
+ end
62
+
63
+ trait :referencing_objects do
64
+ association :items, factory: [:schema, :object]
65
+
66
+ initialize_with do
67
+ FakeSchema.new(name, {
68
+ "type": "array",
69
+ "items": { "$ref": "#{items.name}.json" },
70
+ })
71
+ end
72
+ end
73
+
74
+ initialize_with do
75
+ schema_body_as_json = attributes.fetch(:json, nil)
76
+ schema_body = attributes.except(:json, :name)
77
+
78
+ FakeSchema.new(name, schema_body_as_json || schema_body)
79
+ end
80
+
81
+ after :create do |schema|
82
+ path = File.join(JsonMatchers.schema_root, "#{schema.name}.json")
83
+ payload = JsonMatchers::Payload.new(schema.json)
84
+
85
+ IO.write(path, payload)
86
+ end
87
+ end
88
+ end
@@ -1,242 +1,228 @@
1
+ require "active_support/core_ext/string"
2
+
1
3
  describe JsonMatchers, "#match_json_schema" do
2
- it "fails with an invalid JSON body" do
3
- create_schema("foo", "")
4
+ it "fails with an invalid JSON schema" do
5
+ schema = create(:schema, :invalid)
6
+
7
+ json = build(:response)
4
8
 
5
9
  expect {
6
- expect(response_for("")).to match_json_schema("foo")
10
+ expect(json).to match_json_schema(schema)
7
11
  }.to raise_error(JsonMatchers::InvalidSchemaError)
8
12
  end
9
13
 
10
14
  it "does not fail with an empty JSON body" do
11
- create_schema("foo", {})
15
+ schema = create(:schema, {})
16
+
17
+ json = build(:response, {})
18
+
19
+ expect(json).to match_json_schema(schema)
20
+ end
21
+
22
+ it "supports asserting with the match_response_schema alias" do
23
+ schema = create(:schema, :object)
24
+
25
+ json = build(:response, :invalid_object)
26
+
27
+ expect(json).not_to match_response_schema(schema)
28
+ end
29
+
30
+ it "supports refuting with the match_response_schema alias" do
31
+ schema = create(:schema, :object)
32
+
33
+ json = build(:response, :invalid_object)
34
+
35
+ expect(json).not_to match_response_schema(schema)
36
+ end
37
+
38
+ it "fails when the body contains a property with the wrong type" do
39
+ schema = create(:schema, :object)
12
40
 
13
- expect(response_for({})).to match_json_schema("foo")
41
+ json = build(:response, :invalid_object)
42
+
43
+ expect(json).not_to match_json_schema(schema)
14
44
  end
15
45
 
16
46
  it "fails when the body is missing a required property" do
17
- create_schema("foo_schema", {
18
- "type": "object",
19
- "required": ["foo"],
20
- })
47
+ schema = create(:schema, :object)
48
+
49
+ json = build(:response, {})
21
50
 
22
- expect(response_for({})).not_to match_json_schema("foo_schema")
51
+ expect(json).not_to match_json_schema(schema)
23
52
  end
24
53
 
25
54
  context "when passed a Hash" do
26
- it "validates when the schema matches" do
27
- create_schema("foo_schema", {
28
- "type": "object",
29
- "required": [
30
- "id",
31
- ],
32
- "properties": {
33
- "id": { "type": "number" },
34
- },
35
- "additionalProperties": false,
36
- })
37
-
38
- expect({ "id": 1 }).to match_json_schema("foo_schema")
55
+ it "validates that the schema matches" do
56
+ schema = create(:schema, :object)
57
+
58
+ json = build(:response, :object)
59
+ json_as_hash = json.to_h
60
+
61
+ expect(json_as_hash).to match_json_schema(schema)
39
62
  end
40
63
 
41
64
  it "fails with message when negated" do
42
- create_schema("foo_schema", {
43
- "type": "object",
44
- "required": [
45
- "id",
46
- ],
47
- "properties": {
48
- "id": { "type": "number" },
49
- },
50
- "additionalProperties": false,
51
- })
65
+ schema = create(:schema, :object)
66
+
67
+ json = build(:response, :invalid_object)
68
+ json_as_hash = json.to_h
52
69
 
53
70
  expect {
54
- expect({ "id": "1" }).to match_json_schema("foo_schema")
55
- }.to raise_formatted_error(%{{ "type": "number" }})
71
+ expect(json_as_hash).to match_json_schema(schema)
72
+ }.to raise_error_containing(schema)
56
73
  end
57
74
  end
58
75
 
59
76
  context "when passed a Array" do
60
- it "validates when the schema matches" do
61
- create_schema("foo_schema", {
62
- "type": "array",
63
- "items": {
64
- "required": [
65
- "id",
66
- ],
67
- "properties": {
68
- "id": { "type": "number" },
69
- },
70
- "additionalProperties": false,
71
- },
72
- })
73
-
74
- expect([{ "id": 1 }]).to match_json_schema("foo_schema")
77
+ it "validates a root-level Array in the JSON" do
78
+ schema = create(:schema, :array_of, :objects)
79
+
80
+ json = build(:response, :object)
81
+ json_as_array = [json.to_h]
82
+
83
+ expect(json_as_array).to match_json_schema(schema)
84
+ end
85
+
86
+ it "refutes a root-level Array in the JSON" do
87
+ schema = create(:schema, :array_of, :objects)
88
+
89
+ json = build(:response, :invalid_object)
90
+ json_as_array = [json.to_h]
91
+
92
+ expect(json_as_array).not_to match_json_schema(schema)
75
93
  end
76
94
 
77
95
  it "fails with message when negated" do
78
- create_schema("foo_schema", {
79
- "type": "array",
80
- "items": {
81
- "type": "object",
82
- "required": [
83
- "id",
84
- ],
85
- "properties": {
86
- "id": { "type": "number" },
87
- },
88
- "additionalProperties": false,
89
- },
90
- })
96
+ schema = create(:schema, :array_of, :object)
97
+
98
+ json = build(:response, :invalid_object)
99
+ json_as_array = [json.to_h]
91
100
 
92
101
  expect {
93
- expect([{ "id": "1" }]).to match_json_schema("foo_schema")
94
- }.to raise_formatted_error(%{{ "type": "number" }})
102
+ expect(json_as_array).to match_json_schema(schema)
103
+ }.to raise_error_containing(schema)
95
104
  end
96
105
  end
97
106
 
98
107
  context "when JSON is a string" do
99
- before(:each) do
100
- create_schema("foo_schema", {
101
- "type": "object",
102
- "required": [
103
- "id",
104
- ],
105
- "properties": {
106
- "id": { "type": "number" },
107
- },
108
- "additionalProperties": false,
109
- })
110
- end
108
+ it "validates that the schema matches" do
109
+ schema = create(:schema, :object)
110
+
111
+ json = build(:response, :object)
112
+ json_as_string = json.to_json
111
113
 
112
- it "validates when the schema matches" do
113
- expect({ "id": 1 }.to_json).
114
- to match_json_schema("foo_schema")
114
+ expect(json_as_string).to match_json_schema(schema)
115
115
  end
116
116
 
117
117
  it "fails with message when negated" do
118
+ schema = create(:schema, :object)
119
+
120
+ json = build(:response, :invalid_object)
121
+ json_as_string = json.to_json
122
+
118
123
  expect {
119
- expect({ "id": "1" }.to_json).to match_json_schema("foo_schema")
120
- }.to raise_formatted_error(%{{ "type": "number" }})
124
+ expect(json_as_string).to match_json_schema(schema)
125
+ }.to raise_error_containing(schema)
121
126
  end
122
127
  end
123
128
 
124
129
  it "fails when the body contains a property with the wrong type" do
125
- create_schema("foo_schema", {
126
- "type": "object",
127
- "properties": {
128
- "foo": { "type": "string" },
129
- },
130
- })
131
-
132
- expect(response_for("foo": 1)).
133
- not_to match_json_schema("foo_schema")
134
- end
130
+ schema = create(:schema, :object)
135
131
 
136
- it "contains the body in the failure message" do
137
- create_schema("foo", { "type": "array" })
132
+ json = build(:response, :invalid_object)
138
133
 
139
- expect {
140
- expect(response_for("bar": 5)).to match_json_schema("foo")
141
- }.to raise_formatted_error(%{{ "bar": 5 }})
134
+ expect(json).not_to match_json_schema(schema)
142
135
  end
143
136
 
144
- it "contains the body in the failure message when negated" do
145
- create_schema("foo", { "type": "array" })
137
+ describe "the failure message" do
138
+ it "contains the body" do
139
+ schema = create(:schema, :object)
146
140
 
147
- expect {
148
- expect(response_for([])).not_to match_json_schema("foo")
149
- }.to raise_formatted_error("[ ]")
150
- end
141
+ json = build(:response, :invalid_object)
151
142
 
152
- it "contains the schema in the failure message" do
153
- schema = { "type": "array" }
154
- create_schema("foo", schema)
143
+ expect {
144
+ expect(json).to match_json_schema(schema)
145
+ }.to raise_error_containing(json)
146
+ end
155
147
 
156
- expect {
157
- expect(response_for("bar": 5)).to match_json_schema("foo")
158
- }.to raise_formatted_error(%{{ "type": "array" }})
159
- end
148
+ it "contains the schema" do
149
+ schema = create(:schema, :object)
160
150
 
161
- it "contains the schema in the failure message when negated" do
162
- schema = { "type": "array" }
163
- create_schema("foo", schema)
151
+ json = build(:response, :invalid_object)
164
152
 
165
- expect {
166
- expect(response_for([])).not_to match_json_schema("foo")
167
- }.to raise_formatted_error(%{{ "type": "array" }})
153
+ expect {
154
+ expect(json).to match_json_schema(schema)
155
+ }.to raise_error_containing(schema)
156
+ end
157
+
158
+ it "when negated, contains the body" do
159
+ schema = create(:schema, :object)
160
+
161
+ json = build(:response, :object)
162
+
163
+ expect {
164
+ expect(json).not_to match_json_schema(schema)
165
+ }.to raise_error_containing(json)
166
+ end
167
+
168
+ it "when negated, contains the schema" do
169
+ schema = create(:schema, :object)
170
+
171
+ json = build(:response, :object)
172
+
173
+ expect {
174
+ expect(json).not_to match_json_schema(schema)
175
+ }.to raise_error_containing(schema)
176
+ end
168
177
  end
169
178
 
170
- it "does not fail when the schema matches" do
171
- create_schema("array_schema", {
172
- "type": "array",
173
- "items": { "type": "string" },
174
- })
179
+ it "validates against a schema that uses $ref" do
180
+ schema = create(:schema, :referencing_objects)
181
+
182
+ json = build(:response, :object)
183
+ json_as_array = [json.to_h]
175
184
 
176
- expect(response_for(["valid"])).to match_json_schema("array_schema")
185
+ expect(json_as_array).to match_json_schema(schema)
177
186
  end
178
187
 
179
- it "supports $ref" do
180
- create_schema("single", {
181
- "type": "object",
182
- "required": ["foo"],
183
- "properties": {
184
- "foo": { "type": "string" },
185
- },
186
- })
187
- create_schema("collection", {
188
- "type": "array",
189
- "items": { "$ref": "single.json" },
190
- })
191
-
192
- valid_response = response_for([{ "foo": "is a string" }])
193
- invalid_response = response_for([{ "foo": 0 }])
194
-
195
- expect(valid_response).to match_json_schema("collection")
196
- expect(valid_response).to match_response_schema("collection")
197
- expect(invalid_response).not_to match_json_schema("collection")
198
- expect(invalid_response).not_to match_response_schema("collection")
188
+ it "fails against a schema that uses $ref" do
189
+ schema = create(:schema, :referencing_objects)
190
+
191
+ json = build(:response, :invalid_object)
192
+ json_as_array = [json.to_h]
193
+
194
+ expect(json_as_array).not_to match_json_schema(schema)
199
195
  end
200
196
 
201
197
  context "when options are passed directly to the matcher" do
202
198
  it "forwards options to the validator" do
203
- create_schema("foo_schema", {
204
- "type": "object",
205
- "properties": {
206
- "id": { "type": "number" },
207
- "title": { "type": "string" },
208
- },
209
- })
210
-
211
- expect(response_for({ "id": 1, "title": "bar" })).
212
- to match_json_schema("foo_schema", strict: true)
213
- expect(response_for({ "id": 1 })).
214
- not_to match_json_schema("foo_schema", strict: true)
199
+ schema = create(:schema, :object)
200
+
201
+ matching_json = build(:response, :object)
202
+ invalid_json = build(:response, { "id": 1, "title": "bar" })
203
+
204
+ expect(matching_json).to match_json_schema(schema, strict: true)
205
+ expect(invalid_json).not_to match_json_schema(schema, strict: true)
215
206
  end
216
207
  end
217
208
 
218
209
  context "when options are configured globally" do
219
210
  it "forwards them to the validator" do
220
211
  with_options(strict: true) do
221
- create_schema("foo_schema", {
222
- "type": "object",
223
- "properties": {
224
- "id": { "type": "number" },
225
- "title": { "type": "string" },
226
- },
227
- })
228
-
229
- expect(response_for({ "id": 1, "title": "bar" })).
230
- to match_json_schema("foo_schema")
231
- expect(response_for({ "id": 1 })).
232
- not_to match_json_schema("foo_schema")
212
+ schema = create(:schema, :object)
213
+
214
+ matching_json = build(:response, :object)
215
+ invalid_json = build(:response, { "id": 1, "title": "bar" })
216
+
217
+ expect(matching_json).to match_json_schema(schema)
218
+ expect(invalid_json).not_to match_json_schema(schema)
233
219
  end
234
220
  end
235
221
 
236
222
  context "when configured to record errors" do
237
223
  it "includes the reasons for failure in the exception's message" do
238
224
  with_options(record_errors: true) do
239
- create_schema("foo_schema", {
225
+ schema = create(:schema, {
240
226
  "type": "object",
241
227
  "properties": {
242
228
  "username": {
@@ -247,22 +233,22 @@ describe JsonMatchers, "#match_json_schema" do
247
233
  },
248
234
  },
249
235
  })
250
- invalid_payload = response_for({ "username": "foo" })
236
+
237
+ invalid_json = build(:response, { "username": "foo" })
251
238
 
252
239
  expect {
253
- expect(invalid_payload).to match_json_schema("foo_schema")
240
+ expect(invalid_json).to match_json_schema(schema)
254
241
  }.to raise_error(/minimum/)
255
242
  end
256
243
  end
257
244
  end
258
245
  end
259
246
 
260
- def raise_formatted_error(error_message)
247
+ def raise_error_containing(schema_or_body)
261
248
  raise_error do |error|
262
- sanitized_message = error.message.
263
- gsub(/\A[[:space:]]+/, "").
264
- gsub(/[[:space:]]+\z/, "").
265
- gsub(/[[:space:]]+/, " ")
249
+ sanitized_message = error.message.squish
250
+ json = JSON.pretty_generate(schema_or_body.to_h)
251
+ error_message = json.squish
266
252
 
267
253
  expect(sanitized_message).to include(error_message)
268
254
  end