committee 1.15.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +5 -5
  2. data/bin/committee-stub +11 -38
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +17 -0
  22. data/lib/committee/middleware/base.rb +46 -29
  23. data/lib/committee/middleware/request_validation.rb +31 -49
  24. data/lib/committee/middleware/response_validation.rb +48 -25
  25. data/lib/committee/middleware/stub.rb +62 -37
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +58 -50
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +68 -38
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +5 -2
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +31 -18
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +96 -7
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +423 -32
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +125 -23
  64. data/test/middleware/stub_test.rb +81 -20
  65. data/test/request_unpacker_test.rb +126 -52
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/{response_validator_test.rb → schema_validator/hyper_schema/response_validator_test.rb} +43 -6
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +334 -27
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +108 -1
  79. data/test/validation_error_test.rb +3 -1
  80. metadata +190 -27
  81. data/lib/committee/query_params_coercer.rb +0 -45
  82. data/lib/committee/request_validator.rb +0 -44
  83. data/lib/committee/response_generator.rb +0 -35
  84. data/lib/committee/response_validator.rb +0 -59
  85. data/lib/committee/router.rb +0 -62
  86. data/test/query_params_coercer_test.rb +0 -70
  87. data/test/request_validator_test.rb +0 -103
  88. data/test/response_generator_test.rb +0 -61
  89. data/test/router_test.rb +0 -38
@@ -1,6 +1,6 @@
1
- require_relative "test_helper"
1
+ # frozen_string_literal: true
2
2
 
3
- require "stringio"
3
+ require "test_helper"
4
4
 
5
5
  describe Committee::RequestUnpacker do
6
6
  it "unpacks JSON on Content-Type: application/json" do
@@ -9,47 +9,73 @@ describe Committee::RequestUnpacker do
9
9
  "rack.input" => StringIO.new('{"x":"y"}'),
10
10
  }
11
11
  request = Rack::Request.new(env)
12
- params = Committee::RequestUnpacker.new(request).call
13
- assert_equal({ "x" => "y" }, params)
12
+ unpacker = Committee::RequestUnpacker.new
13
+ assert_equal([{ "x" => "y" }, false], unpacker.unpack_request_params(request))
14
14
  end
15
15
 
16
- it "unpacks JSON on no Content-Type" do
16
+ it "unpacks JSON on Content-Type: application/vnd.api+json" do
17
17
  env = {
18
+ "CONTENT_TYPE" => "application/vnd.api+json",
18
19
  "rack.input" => StringIO.new('{"x":"y"}'),
19
20
  }
20
21
  request = Rack::Request.new(env)
21
- params = Committee::RequestUnpacker.new(request).call
22
- assert_equal({ "x" => "y" }, params)
22
+ unpacker = Committee::RequestUnpacker.new
23
+ assert_equal([{ "x" => "y" }, false], unpacker.unpack_request_params(request))
23
24
  end
24
25
 
25
- it "doesn't unpack JSON under other Content-Types" do
26
+ it "unpacks JSON on no Content-Type" do
26
27
  env = {
27
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
28
28
  "rack.input" => StringIO.new('{"x":"y"}'),
29
29
  }
30
30
  request = Rack::Request.new(env)
31
- params = Committee::RequestUnpacker.new(request).call
32
- assert_equal({}, params)
31
+ unpacker = Committee::RequestUnpacker.new
32
+ assert_equal([{ "x" => "y" }, false], unpacker.unpack_request_params(request))
33
33
  end
34
34
 
35
- it "unpacks JSON under other Content-Types with optimistic_json" do
35
+ it "doesn't unpack JSON on application/x-ndjson" do
36
36
  env = {
37
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
38
- "rack.input" => StringIO.new('{"x":"y"}'),
37
+ "CONTENT_TYPE" => "application/x-ndjson",
38
+ "rack.input" => StringIO.new('{"x":"y"}\n{"a":"b"}'),
39
39
  }
40
40
  request = Rack::Request.new(env)
41
- params = Committee::RequestUnpacker.new(request, optimistic_json: true).call
42
- assert_equal({ "x" => "y" }, params)
41
+ unpacker = Committee::RequestUnpacker.new
42
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
43
+ end
44
+
45
+ it "doesn't unpack JSON under other Content-Types" do
46
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
47
+ env = {
48
+ "CONTENT_TYPE" => content_type,
49
+ "rack.input" => StringIO.new('{"x":"y"}'),
50
+ }
51
+ request = Rack::Request.new(env)
52
+ unpacker = Committee::RequestUnpacker.new
53
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
54
+ end
55
+ end
56
+
57
+ it "unpacks JSON under other Content-Types with optimistic_json" do
58
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
59
+ env = {
60
+ "CONTENT_TYPE" => content_type,
61
+ "rack.input" => StringIO.new('{"x":"y"}'),
62
+ }
63
+ request = Rack::Request.new(env)
64
+ unpacker = Committee::RequestUnpacker.new(optimistic_json: true)
65
+ assert_equal([{ "x" => "y" }, false], unpacker.unpack_request_params(request))
66
+ end
43
67
  end
44
68
 
45
69
  it "returns {} when unpacking non-JSON with optimistic_json" do
46
- env = {
47
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
48
- "rack.input" => StringIO.new('x=y&foo=42'),
49
- }
50
- request = Rack::Request.new(env)
51
- params = Committee::RequestUnpacker.new(request, optimistic_json: true).call
52
- assert_equal({}, params)
70
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
71
+ env = {
72
+ "CONTENT_TYPE" => content_type,
73
+ "rack.input" => StringIO.new('x=y&foo=42'),
74
+ }
75
+ request = Rack::Request.new(env)
76
+ unpacker = Committee::RequestUnpacker.new(optimistic_json: true)
77
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
78
+ end
53
79
  end
54
80
 
55
81
  it "unpacks an empty hash on an empty request body" do
@@ -58,39 +84,45 @@ describe Committee::RequestUnpacker do
58
84
  "rack.input" => StringIO.new(""),
59
85
  }
60
86
  request = Rack::Request.new(env)
61
- params = Committee::RequestUnpacker.new(request).call
62
- assert_equal({}, params)
87
+ unpacker = Committee::RequestUnpacker.new
88
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
63
89
  end
64
90
 
65
91
  it "doesn't unpack form params" do
66
- env = {
67
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
68
- "rack.input" => StringIO.new("x=y"),
69
- }
70
- request = Rack::Request.new(env)
71
- params = Committee::RequestUnpacker.new(request).call
72
- assert_equal({}, params)
92
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
93
+ env = {
94
+ "CONTENT_TYPE" => content_type,
95
+ "rack.input" => StringIO.new("x=y"),
96
+ }
97
+ request = Rack::Request.new(env)
98
+ unpacker = Committee::RequestUnpacker.new
99
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
100
+ end
73
101
  end
74
102
 
75
103
  it "unpacks form params with allow_form_params" do
76
- env = {
77
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
78
- "rack.input" => StringIO.new("x=y"),
79
- }
80
- request = Rack::Request.new(env)
81
- params = Committee::RequestUnpacker.new(request, allow_form_params: true).call
82
- assert_equal({ "x" => "y" }, params)
104
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
105
+ env = {
106
+ "CONTENT_TYPE" => content_type,
107
+ "rack.input" => StringIO.new("x=y"),
108
+ }
109
+ request = Rack::Request.new(env)
110
+ unpacker = Committee::RequestUnpacker.new(allow_form_params: true)
111
+ assert_equal([{ "x" => "y" }, true], unpacker.unpack_request_params(request))
112
+ end
83
113
  end
84
114
 
85
115
  it "unpacks form & query params with allow_form_params and allow_query_params" do
86
- env = {
87
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
88
- "rack.input" => StringIO.new("x=y"),
89
- "QUERY_STRING" => "a=b"
90
- }
91
- request = Rack::Request.new(env)
92
- params = Committee::RequestUnpacker.new(request, allow_form_params: true, allow_query_params: true).call
93
- assert_equal({ "x" => "y", "a" => "b" }, params)
116
+ %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
117
+ env = {
118
+ "CONTENT_TYPE" => content_type,
119
+ "rack.input" => StringIO.new("x=y"),
120
+ "QUERY_STRING" => "a=b"
121
+ }
122
+ request = Rack::Request.new(env)
123
+ unpacker = Committee::RequestUnpacker.new(allow_form_params: true, allow_query_params: true)
124
+ assert_equal([ { "x" => "y"}, true], unpacker.unpack_request_params(request))
125
+ end
94
126
  end
95
127
 
96
128
  it "unpacks query params with allow_query_params" do
@@ -99,8 +131,8 @@ describe Committee::RequestUnpacker do
99
131
  "QUERY_STRING" => "a=b"
100
132
  }
101
133
  request = Rack::Request.new(env)
102
- params = Committee::RequestUnpacker.new(request, allow_query_params: true).call
103
- assert_equal({ "a" => "b" }, params)
134
+ unpacker = Committee::RequestUnpacker.new(allow_query_params: true)
135
+ assert_equal({ "a" => "b" }, unpacker.unpack_query_params(request))
104
136
  end
105
137
 
106
138
  it "errors if JSON is not an object" do
@@ -110,7 +142,7 @@ describe Committee::RequestUnpacker do
110
142
  }
111
143
  request = Rack::Request.new(env)
112
144
  assert_raises(Committee::BadRequest) do
113
- Committee::RequestUnpacker.new(request).call
145
+ Committee::RequestUnpacker.new.unpack_request_params(request)
114
146
  end
115
147
  end
116
148
 
@@ -120,7 +152,49 @@ describe Committee::RequestUnpacker do
120
152
  "rack.input" => StringIO.new('{"x":"y"}'),
121
153
  }
122
154
  request = Rack::Request.new(env)
123
- params = Committee::RequestUnpacker.new(request).call
124
- assert_equal({}, params)
155
+ unpacker = Committee::RequestUnpacker.new
156
+ assert_equal([{}, false], unpacker.unpack_request_params(request))
157
+ end
158
+
159
+ # this is mostly here for line coverage
160
+ it "unpacks JSON containing an array" do
161
+ env = {
162
+ "rack.input" => StringIO.new('{"x":[]}'),
163
+ }
164
+ request = Rack::Request.new(env)
165
+ unpacker = Committee::RequestUnpacker.new
166
+ assert_equal([{ "x" => [] }, false], unpacker.unpack_request_params(request))
167
+ end
168
+
169
+ it "unpacks http header" do
170
+ env = {
171
+ "HTTP_FOO_BAR" => "some header value",
172
+ "rack.input" => StringIO.new(""),
173
+ }
174
+ request = Rack::Request.new(env)
175
+ unpacker = Committee::RequestUnpacker.new({ allow_header_params: true })
176
+ assert_equal({ "FOO-BAR" => "some header value" }, unpacker.unpack_headers(request))
177
+ end
178
+
179
+ it "includes request body when`use_get_body` is true" do
180
+ env = {
181
+ "rack.input" => StringIO.new('{"x":1, "y":2}'),
182
+ "REQUEST_METHOD" => "GET",
183
+ "QUERY_STRING"=>"data=value&x=aaa",
184
+ }
185
+ request = Rack::Request.new(env)
186
+ unpacker = Committee::RequestUnpacker.new({ allow_query_params: true, allow_get_body: true })
187
+ assert_equal([{ 'x' => 1, 'y' => 2 }, false], unpacker.unpack_request_params(request))
188
+ end
189
+
190
+ it "doesn't include request body when `use_get_body` is false" do
191
+ env = {
192
+ "rack.input" => StringIO.new('{"x":1, "y":2}'),
193
+ "REQUEST_METHOD" => "GET",
194
+ "QUERY_STRING"=>"data=value&x=aaa",
195
+ }
196
+ request = Rack::Request.new(env)
197
+ unpacker = Committee::RequestUnpacker.new({ allow_query_params: true, use_get_body: false })
198
+ assert_equal({ 'data' => 'value', 'x' => 'aaa' }, unpacker.unpack_query_params(request))
125
199
  end
126
200
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::SchemaValidator::HyperSchema::ParameterCoercer do
6
+ before do
7
+ @schema = JsonSchema.parse!(hyper_schema_data)
8
+ @schema.expand_references!
9
+ # POST /apps/:id
10
+ @link = @schema.properties["app"].links[0]
11
+ end
12
+
13
+ it "pass datetime string" do
14
+ params = { "update_time" => "2016-04-01T16:00:00.000+09:00"}
15
+ call(params, coerce_date_times: true)
16
+
17
+ assert_kind_of DateTime, params["update_time"]
18
+ end
19
+
20
+ it "don't change object type" do
21
+ params = HashLikeObject.new
22
+ params["update_time"] = "2016-04-01T16:00:00.000+09:00"
23
+ call(params, coerce_date_times: true)
24
+
25
+ assert_kind_of DateTime, params["update_time"]
26
+ assert_kind_of HashLikeObject, params
27
+ end
28
+
29
+ it "pass invalid datetime string, not convert" do
30
+ invalid_datetime = "llmfllmf"
31
+ params = { "update_time" => invalid_datetime}
32
+ call(params, coerce_date_times: true)
33
+
34
+ assert_equal invalid_datetime, params["update_time"]
35
+ end
36
+
37
+ it "pass array property" do
38
+ params = {
39
+ "array_property" => [
40
+ {
41
+ "update_time" => "2016-04-01T16:00:00.000+09:00",
42
+ "per_page" => 1,
43
+ "nested_coercer_object" => {
44
+ "update_time" => "2016-04-01T16:00:00.000+09:00",
45
+ "threshold" => 1.5
46
+ },
47
+ "nested_no_coercer_object" => {
48
+ "per_page" => 1,
49
+ "threshold" => 1.5
50
+ },
51
+ "nested_coercer_array" => [
52
+ {
53
+ "update_time" => "2016-04-01T16:00:00.000+09:00",
54
+ "threshold" => 1.5
55
+ }
56
+ ],
57
+ "nested_no_coercer_array" => [
58
+ {
59
+ "per_page" => 1,
60
+ "threshold" => 1.5
61
+ }
62
+ ]
63
+ },
64
+ {
65
+ "update_time" => "2016-04-01T16:00:00.000+09:00",
66
+ "per_page" => 1,
67
+ "threshold" => 1.5
68
+ },
69
+ {
70
+ "threshold" => 1.5,
71
+ "per_page" => 1
72
+ }
73
+ ],
74
+ }
75
+ call(params, coerce_date_times: true, coerce_recursive: true)
76
+
77
+ first_data = params["array_property"][0]
78
+ assert_kind_of DateTime, first_data["update_time"]
79
+ assert_kind_of Integer, first_data["per_page"]
80
+
81
+ second_data = params["array_property"][1]
82
+ assert_kind_of DateTime, second_data["update_time"]
83
+ assert_kind_of Integer, second_data["per_page"]
84
+ assert_kind_of Float, second_data["threshold"]
85
+
86
+ third_data = params["array_property"][1]
87
+ assert_kind_of Integer, third_data["per_page"]
88
+ assert_kind_of Float, third_data["threshold"]
89
+
90
+ assert_kind_of DateTime, first_data["nested_coercer_object"]["update_time"]
91
+ assert_kind_of Float, first_data["nested_coercer_object"]["threshold"]
92
+
93
+ assert_kind_of Integer, first_data["nested_no_coercer_object"]["per_page"]
94
+ assert_kind_of Float, first_data["nested_no_coercer_object"]["threshold"]
95
+
96
+ assert_kind_of DateTime, first_data["nested_coercer_array"].first["update_time"]
97
+ assert_kind_of Float, first_data["nested_coercer_array"].first["threshold"]
98
+
99
+ assert_kind_of Integer, first_data["nested_no_coercer_array"].first["per_page"]
100
+ assert_kind_of Float, first_data["nested_no_coercer_array"].first["threshold"]
101
+ end
102
+
103
+ private
104
+
105
+ class HashLikeObject < Hash; end
106
+
107
+ def call(params, options={})
108
+ link = Committee::Drivers::HyperSchema::Link.new(@link)
109
+ Committee::SchemaValidator::HyperSchema::ParameterCoercer.new(params, link.schema, options).call!
110
+ end
111
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ require "stringio"
6
+
7
+ describe Committee::SchemaValidator::HyperSchema::RequestValidator do
8
+ describe 'HyperSchema' do
9
+ before do
10
+ @schema = JsonSchema.parse!(hyper_schema_data)
11
+ @schema.expand_references!
12
+ # POST /apps/:id
13
+ @link = @schema.properties["app"].links[0]
14
+ @request = Rack::Request.new({
15
+ "CONTENT_TYPE" => "application/json",
16
+ "rack.input" => StringIO.new("{}"),
17
+ "REQUEST_METHOD" => "POST"
18
+ })
19
+ end
20
+
21
+ it "passes through a valid request with Content-Type options" do
22
+ @headers = { "Content-Type" => "application/json; charset=utf-8" }
23
+ call({})
24
+ end
25
+
26
+ it "passes through a valid request" do
27
+ data = {
28
+ "name" => "heroku-api",
29
+ }
30
+ call(data)
31
+ end
32
+
33
+ it "passes through a valid request with Content-Type options" do
34
+ @request =
35
+ Rack::Request.new({
36
+ "CONTENT_TYPE" => "application/json; charset=utf-8",
37
+ "rack.input" => StringIO.new("{}"),
38
+ })
39
+ data = {
40
+ "name" => "heroku-api",
41
+ }
42
+ call(data)
43
+ end
44
+
45
+ it "detects an invalid request Content-Type" do
46
+ e = assert_raises(Committee::InvalidRequest) {
47
+ @request =
48
+ Rack::Request.new({
49
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
50
+ "rack.input" => StringIO.new("{}"),
51
+ })
52
+ call({})
53
+ }
54
+ message =
55
+ %{"Content-Type" request header must be set to "application/json".}
56
+ assert_equal message, e.message
57
+ end
58
+
59
+ it "allows skipping content_type check" do
60
+ @request =
61
+ Rack::Request.new({
62
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
63
+ "rack.input" => StringIO.new("{}"),
64
+ })
65
+ call({}, {}, check_content_type: false)
66
+ end
67
+
68
+ it "detects an missing parameter in GET requests" do
69
+ # GET /apps/search?query=...
70
+ @link = @schema.properties["app"].links[5]
71
+ @request = Rack::Request.new({})
72
+ e = assert_raises(Committee::InvalidRequest) do
73
+ call({})
74
+ end
75
+ message =
76
+ %{Invalid request.\n\n#: failed schema #/definitions/app/links/5/schema: "query" wasn't supplied.}
77
+ assert_equal message, e.message
78
+ end
79
+
80
+ it "allows an invalid Content-Type with an empty body" do
81
+ @request =
82
+ Rack::Request.new({
83
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
84
+ "rack.input" => StringIO.new(""),
85
+ })
86
+ call({})
87
+ end
88
+
89
+ it "detects a parameter of the wrong pattern" do
90
+ data = {
91
+ "name" => "%@!"
92
+ }
93
+ e = assert_raises(Committee::InvalidRequest) do
94
+ call(data)
95
+ end
96
+ message = %{Invalid request.\n\n#/name: failed schema #/definitions/app/links/0/schema/properties/name: %@! does not match /^[a-z][a-z0-9-]{3,30}$/.}
97
+ assert_equal message, e.message
98
+ end
99
+
100
+ private
101
+
102
+ def call(data, headers={}, options={})
103
+ # hyper-schema link should be dropped into driver wrapper before it's used
104
+ link = Committee::Drivers::HyperSchema::Link.new(@link)
105
+ Committee::SchemaValidator::HyperSchema::RequestValidator.new(link, options).call(@request, data, headers)
106
+ end
107
+ end
108
+
109
+ describe 'OpenAPI 2' do
110
+ before do
111
+ @schema = open_api_2_schema
112
+ @link = @schema.routes['GET'][0][1]
113
+ @request = Rack::Request.new({
114
+ "CONTENT_TYPE" => "application/json",
115
+ "rack.input" => StringIO.new("{}"),
116
+ "REQUEST_METHOD" => "POST"
117
+ })
118
+ end
119
+
120
+
121
+ it "passes through a valid request" do
122
+ headers = {
123
+ "AUTH-TOKEN" => "xxx"
124
+ }
125
+ call({}, headers)
126
+ end
127
+
128
+ it "detects an missing header parameter in GET requests" do
129
+ @link = @schema.routes['GET'][0][1]
130
+ @request = Rack::Request.new({})
131
+ e = assert_raises(Committee::InvalidRequest) do
132
+ call({}, {})
133
+ end
134
+ message = %{Invalid request.\n\n#: failed schema : "AUTH-TOKEN" wasn't supplied.}
135
+ assert_equal message, e.message
136
+ end
137
+
138
+ it "allows skipping header schema check" do
139
+ @link = @schema.routes['GET'][0][1]
140
+ @request = Rack::Request.new({})
141
+ call({}, {}, { check_header: false })
142
+ end
143
+
144
+ private
145
+
146
+ def call(data, headers={}, options={})
147
+ # hyper-schema link should be dropped into driver wrapper before it's used
148
+ Committee::SchemaValidator::HyperSchema::RequestValidator.new(@link, options).call(@request, data, headers)
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::SchemaValidator::HyperSchema::ResponseGenerator do
6
+ before do
7
+ @schema = JsonSchema.parse!(hyper_schema_data)
8
+ @schema.expand_references!
9
+ # GET /apps/:id
10
+ @get_link = @link = @schema.properties["app"].links[2]
11
+ # GET /apps
12
+ @list_link = @schema.properties["app"].links[3]
13
+ end
14
+
15
+ it "returns the schema used to match" do
16
+ _data, schema = call
17
+ assert_equal @get_link.target_schema, schema
18
+ end
19
+
20
+ it "generates string properties" do
21
+ data, _schema = call
22
+ assert data["name"].is_a?(String)
23
+ end
24
+
25
+ it "generates non-string properties" do
26
+ data, _schema = call
27
+ assert_includes [FalseClass, TrueClass], data["maintenance"].class
28
+ end
29
+
30
+ it "wraps list data in an array" do
31
+ @link = @list_link
32
+
33
+ @link.rel = nil
34
+
35
+ data, _schema = call
36
+ assert data.is_a?(Array)
37
+ end
38
+
39
+ it "wraps list data tagged with rel 'instances' in an array" do
40
+ @link = @list_link
41
+
42
+ # forces the link to use `parent`
43
+ @link.target_schema = nil
44
+
45
+ data, _schema = call
46
+
47
+ # We're testing for legacy behavior here: even without a `targetSchema` as
48
+ # long as `rel` is set to `instances` we still wrap the result in an
49
+ # array.
50
+ assert_equal "instances", @list_link.rel
51
+
52
+ assert data.is_a?(Array)
53
+ end
54
+
55
+ it "errors with no known route for generation" do
56
+ @link = @get_link
57
+
58
+ # tweak the schema so that it can't be generated
59
+ property = @schema.properties["app"].properties["maintenance"]
60
+ property.data["example"] = nil
61
+ property.type = []
62
+
63
+ e = assert_raises(RuntimeError) do
64
+ call
65
+ end
66
+
67
+ expected = <<-eos.gsub(/\n +/, "").strip
68
+ At "get /apps/{(%23%2Fdefinitions%2Fapp%2Fdefinitions%2Fname)}"
69
+ "#/definitions/app/properties/maintenance": no "example" attribute and
70
+ "null" is not allowed; don't know how to generate property.
71
+ eos
72
+ assert_equal expected, e.message
73
+ end
74
+
75
+ it "generates first enum value for a schema with enum" do
76
+ link = Committee::Drivers::OpenAPI2::Link.new
77
+ link.target_schema = JsonSchema::Schema.new
78
+ link.target_schema.enum = ["foo"]
79
+ link.target_schema.type = ["string"]
80
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
81
+ assert_equal("foo", data)
82
+ end
83
+
84
+ it "generates basic types" do
85
+ link = Committee::Drivers::OpenAPI2::Link.new
86
+ link.target_schema = JsonSchema::Schema.new
87
+
88
+ link.target_schema.type = ["integer"]
89
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
90
+ assert_equal 0, data
91
+
92
+ link.target_schema.type = ["null"]
93
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
94
+ assert_nil data
95
+
96
+ link.target_schema.type = ["string"]
97
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
98
+ assert_equal "", data
99
+ end
100
+
101
+ it "generates an empty array for an array type" do
102
+ link = Committee::Drivers::OpenAPI2::Link.new
103
+ link.target_schema = JsonSchema::Schema.new
104
+ link.target_schema.type = ["array"]
105
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
106
+ assert_equal([], data)
107
+ end
108
+
109
+ it "generates an empty object for an object with no fields" do
110
+ link = Committee::Drivers::OpenAPI2::Link.new
111
+ link.target_schema = JsonSchema::Schema.new
112
+ link.target_schema.type = ["object"]
113
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
114
+ assert_equal({}, data)
115
+ end
116
+
117
+ it "prefers an example to a built-in value" do
118
+ link = Committee::Drivers::OpenAPI2::Link.new
119
+ link.target_schema = JsonSchema::Schema.new
120
+
121
+ link.target_schema.data = { "example" => 123 }
122
+ link.target_schema.type = ["integer"]
123
+
124
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
125
+ assert_equal 123, data
126
+ end
127
+
128
+ it "prefers non-null types to null types" do
129
+ link = Committee::Drivers::OpenAPI2::Link.new
130
+ link.target_schema = JsonSchema::Schema.new
131
+
132
+ link.target_schema.type = ["null", "integer"]
133
+ data, _schema = Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
134
+ assert_equal 0, data
135
+ end
136
+
137
+ def call
138
+ # hyper-schema link should be dropped into driver wrapper before it's used
139
+ link = Committee::Drivers::HyperSchema::Link.new(@link)
140
+ Committee::SchemaValidator::HyperSchema::ResponseGenerator.new.call(link)
141
+ end
142
+ end