openapi_first 2.9.3 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef8ea95833884daf36e8a33742ce4a6eddfb9343b67de0b5e787ebc377c0691d
4
- data.tar.gz: b63916918c1d1116d99c2e071c3e8e752b1cce66cd431dceb5076a3e70688c10
3
+ metadata.gz: 7687c4d4ac32654b4a2fd8880eeaeb0ca98ab6fac19b8d9a336fe5d5bb94785a
4
+ data.tar.gz: 6fb04355d69916cfc3d1dce1e21d0f163082878cf1348421a9b1006b070621f3
5
5
  SHA512:
6
- metadata.gz: 3cbf77e0a4a51b213a90458c1181bd29474f378195210cf76466c24ef832195b32e4cbd8aec7ba97ece0bd7d9a97b6022e02b5288600d8879b82401c398d1ddc
7
- data.tar.gz: 8789c3dadcff66b6cbb831dfae948da5adcbcd2788ff40765f39d4a1a0ba238cef1d7df609b4dd57e8ba71653103e21f9fb6a7e1f0a212e45c02d4f8c5e41e39
6
+ metadata.gz: '0988025dd93f55354b646c8aeb3fe7d0febfd54925d368fd3aa5959067c1a0a948b193158130b5fa7fb91cf51ffe774f9f8f281e76ac3feb318e67ea1b079020'
7
+ data.tar.gz: f70094cd6b531e42e06f931ed31155385bf9dd97588cad6bc977e961e7fdd497e057a971112fe10a0457d145b8f916cabdad3995c90bc29d78d31e59184211ac
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.11.0
6
+
7
+ - OpenapiFirst::Test.observe now works with `Rack::URLMap` (returned by `Rack::Builder.app`) and probably all objects that respond to `.call`
8
+
9
+ ## 2.10.1
10
+
11
+ - Don't try to track coverage for skipped requests
12
+ - Add Test::Configuration#skip_coverage to skip test coverage for specific paths + request methods and all responses
13
+ - Deprecate setting minimum_coverage value. Use skip_response_coverage, ignored_unknown_status to configure coverage instead.
14
+ - Update openapi_parameters to make parsing array query parameters more consistent.
15
+ Now parsing empty array query parameter like `ids=&` or `ids&` both result in an empty array value (`[]`) instead of `nil` or `""`.
16
+ - Fix Test::Coverage.result returning < 100 even if plan is fully covered
17
+
18
+ ## 2.10.0 (yanked)
19
+
5
20
  ## 2.9.3
6
21
 
7
22
  - Fix OpenapiFirst.load when MultiJson is configured to return symbol keys
data/README.md CHANGED
@@ -4,31 +4,34 @@ openapi_first is a Ruby gem for request / response validation and contract-testi
4
4
 
5
5
  ## Usage
6
6
 
7
- Use an OAD to validate incoming requests in production:
7
+ Use an OAD to validate incoming requests:
8
8
  ```ruby
9
9
  use OpenapiFirst::Middlewares::RequestValidation, 'openapi/openapi.yaml'
10
10
  ```
11
11
 
12
- Turn your request tests into contract tests against an OAD:
12
+ Turn your request tests into [contract tests](#contract-testing) against an OAD:
13
13
  ```ruby
14
14
  # spec_helper.rb
15
15
  require 'openapi_first'
16
16
  OpenapiFirst::Test.setup do |config|
17
17
  config.register('openapi/openapi.yaml')
18
18
  end
19
- require 'application' # Load Application code
20
- OpenapiFirst::Test.observe(Application)
19
+
20
+ require 'my_app'
21
+ RSpec.configure do |config|
22
+ config.include OpenapiFirst::Test::Methods[MyApp], type: :request
23
+ end
21
24
  ```
22
25
 
23
26
  ## Contents
24
27
 
25
28
  <!-- TOC -->
26
29
 
27
- - [Contract testing](#contract-testing)
28
30
  - [Rack Middlewares](#rack-middlewares)
29
31
  - [Request validation](#request-validation)
30
32
  - [Response validation](#response-validation)
31
- - [Test assertions](#test-assertions)
33
+ - [Contract testing](#contract-testing)
34
+ - [Test assertions](#test-assertions)
32
35
  - [Manual use](#manual-use)
33
36
  - [Framework integration](#framework-integration)
34
37
  - [Configuration](#configuration)
@@ -41,53 +44,6 @@ OpenapiFirst::Test.observe(Application)
41
44
 
42
45
  <!-- /TOC -->
43
46
 
44
- ## Contract Testing
45
-
46
- You can see your OpenAPI API description as a contract that your clients can rely on as how your API behaves. There are two aspects of contract testing: Validation and Coverage. By validating requests and responses, you can avoid that your API implementation processes requests or returns responses that don't match your API description. To make sure your _whole_ API description is implemented, openapi_first can check that all of your API description is covered when you test your API with [rack-test](https://github.com/rack/rack-test).
47
-
48
- Here is how to set it up:
49
-
50
- 1. Register all OpenAPI documents to track coverage for.
51
- This should go at the top of your test helper file before loading your application code.
52
- ```ruby
53
- require 'openapi_first'
54
- OpenapiFirst::Test.setup do |config|
55
- config.register('openapi/openapi.yaml')
56
- end
57
- ```
58
- 2. Observe your application. You can do this in one of two ways:
59
- - Add an `app` method to your tests, which wraps your application with silent request / response validation. (✷1)
60
- ```ruby
61
- RSpec.configure do |config|
62
- config.include OpenapiFirst::Test::Methods[MyApp], type: :request
63
- end
64
- ```
65
- Or add the `app` method yourself:
66
-
67
- ```ruby
68
- def app
69
- OpenapiFirst::Test.app(MyApp)
70
- end
71
- ```
72
- - Or inject a Module to wrap (prepend) the `call` method of your Rack app Class.
73
-
74
- NOTE: This is still work in progress. It works with basic Sinatra apps, but does not work with Hanami or Rails out of the box, yet. PRs welcome 🤗
75
-
76
- ```ruby
77
- OpenapiFirst::Test.observe(MyApplication)
78
- ```
79
- 3. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses.
80
-
81
- (✷1): It does not matter what method of openapi_first you use to validate requests/responses. Instead of using `OpenapiFirstTest.app` to wrap your application, you could also use the [middlewares](#rack-middlewares) or [test assertion method](#test-assertions), but you would have to do that for all requests/responses defined in your API description to make coverage work.
82
-
83
- OpenapiFirst' request validation raises an error when a request is not defined. You can deactivate this during testing:
84
-
85
- ```ruby
86
- OpenapiFirst::Test.setup do |test|
87
- test.ignore_unknown_requests = true
88
- end
89
- ```
90
-
91
47
  ## Rack Middlewares
92
48
 
93
49
  ### Request validation
@@ -197,7 +153,79 @@ use OpenapiFirst::Middlewares::ResponseValidation, 'openapi.yaml', raise_error:
197
153
 
198
154
  If you are adopting OpenAPI you can use these options together with [hooks](#hooks) to get notified about requests/responses that do match your API description.
199
155
 
200
- ## Test assertions
156
+ ## Contract Testing
157
+
158
+ You can see your OpenAPI API description as a contract that your clients can rely on as how your API behaves. There are two aspects of contract testing: Validation and Coverage. By validating requests and responses, you can avoid that your API implementation processes requests or returns responses that don't match your API description. To make sure your _whole_ API description is implemented, openapi_first can check that all of your API description is covered when you test your API with [rack-test](https://github.com/rack/rack-test).
159
+
160
+ Here is how to set it up:
161
+
162
+ 1. Register all OpenAPI documents to track coverage for.
163
+ This should go at the top of your test helper file before loading your application code.
164
+ ```ruby
165
+ require 'openapi_first'
166
+ OpenapiFirst::Test.setup do |config|
167
+ config.register('openapi/openapi.yaml')
168
+ end
169
+ ```
170
+ 2. Observe your application. You can do this in multiple ways:
171
+ - Add an `app` method to your tests, which wraps your application with silent request / response validation. (✷1)
172
+ ```ruby
173
+ module RequestSpecHelpers
174
+ def app
175
+ OpenapiFirst::Test.app(MyApp)
176
+ end
177
+ end
178
+
179
+ RSpec.configure do |config|
180
+ config.include RequestSpecHelpers, type: :request
181
+ end
182
+ ```
183
+
184
+ Or do this by creating a Module and including it to add an "app" method.
185
+
186
+ ```ruby
187
+ RSpec.configure do |config|
188
+ config.include OpenapiFirst::Test::Methods[MyApp], type: :request
189
+ end
190
+ ```
191
+ 4. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses.
192
+
193
+ (✷1): It does not matter what method of openapi_first you use to validate requests/responses. Instead of using `OpenapiFirstTest.app` to wrap your application, you could also use the [middlewares](#rack-middlewares) or [test assertion method](#test-assertions), but you would have to do that for all requests/responses defined in your API description to make coverage work.
194
+
195
+ ### Configure test coverage
196
+
197
+ OpenapiFirst::Test raises an error when a request is not defined. You can deactivate this with:
198
+
199
+ ```ruby
200
+ OpenapiFirst::Test.setup do |test|
201
+ # …
202
+ test.ignore_unknown_requests = true
203
+ end
204
+ ```
205
+
206
+ Exclude certain _responses_ from coverage with `skip_coverage`:
207
+
208
+ ```ruby
209
+ OpenapiFirst::Test.setup do |test|
210
+ # …
211
+ test.skip_response_coverage do |response_definition|
212
+ response_definition.status == '5XX'
213
+ end
214
+ end
215
+ ```
216
+
217
+ Skip coverage for a request and all responses alltogether of a route with `skip_coverage`:
218
+
219
+ ```ruby
220
+ OpenapiFirst::Test.setup do |test|
221
+ # …
222
+ test.skip_coverage do |path, request_method|
223
+ path == '/bookings/{bookingId}' && requests_method == 'DELETE'
224
+ end
225
+ end
226
+ ```
227
+
228
+ ### Test assertions
201
229
 
202
230
  openapi_first ships with a simple but powerful Test method to run request and response validation in your tests without using the middlewares. This is designed to be used with rack-test or Ruby on Rails integration tests or request specs.
203
231
 
@@ -6,6 +6,7 @@ module OpenapiFirst
6
6
  # See also https://www.rfc-editor.org/rfc/rfc9457.html
7
7
  class Default
8
8
  include OpenapiFirst::ErrorResponse
9
+
9
10
  OpenapiFirst.register_error_response(:default, self)
10
11
 
11
12
  TITLES = {
@@ -5,6 +5,7 @@ module OpenapiFirst
5
5
  # A JSON:API conform error response. See https://jsonapi.org/.
6
6
  class Jsonapi
7
7
  include OpenapiFirst::ErrorResponse
8
+
8
9
  OpenapiFirst.register_error_response(:jsonapi, self)
9
10
 
10
11
  def body
@@ -23,11 +23,14 @@ module OpenapiFirst
23
23
 
24
24
  def read_body(rack_response)
25
25
  buffered_body = +''
26
+
26
27
  if rack_response.body.respond_to?(:each)
27
28
  rack_response.body.each { |chunk| buffered_body.to_s << chunk }
28
29
  return buffered_body
29
30
  end
30
31
  rack_response.body
32
+ rescue TypeError
33
+ raise Error, "Cannot not read response body. Response is not string-like, but is a #{rack_response.body.class}."
31
34
  end
32
35
 
33
36
  def build_headers_parser(headers)
@@ -9,6 +9,7 @@ module OpenapiFirst
9
9
  @coverage_formatter = Coverage::TerminalFormatter
10
10
  @coverage_formatter_options = {}
11
11
  @skip_response_coverage = nil
12
+ @skip_coverage = nil
12
13
  @response_raise_error = true
13
14
  @ignored_unknown_status = [404]
14
15
  @report_coverage = true
@@ -26,12 +27,12 @@ module OpenapiFirst
26
27
 
27
28
  # Observe a rack app
28
29
  def observe(app, api: :default)
29
- @apps[api] = app
30
+ (@apps[api] ||= []) << app
30
31
  end
31
32
 
32
- attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error, :minimum_coverage,
33
+ attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
33
34
  :ignore_unknown_requests
34
- attr_reader :registry, :apps, :report_coverage, :ignored_unknown_status
35
+ attr_reader :registry, :apps, :report_coverage, :ignored_unknown_status, :minimum_coverage
35
36
 
36
37
  # Configure report coverage
37
38
  # @param [Boolean, :warn] value Whether to report coverage or just warn.
@@ -44,11 +45,24 @@ module OpenapiFirst
44
45
  @report_coverage = value
45
46
  end
46
47
 
48
+ # @deprecated Use skip_response_coverage, ignored_unknown_status or skip_coverage to configure coverage
49
+ def minimum_coverage=(value)
50
+ warn 'OpenapiFirst::Test::Configuration#minimum_coverage= is deprecated. ' \
51
+ 'Use skip_response_coverage, ignored_unknown_status to configure coverage instead.'
52
+ @minimum_coverage = value
53
+ end
54
+
47
55
  def skip_response_coverage(&block)
48
56
  return @skip_response_coverage unless block_given?
49
57
 
50
58
  @skip_response_coverage = block
51
59
  end
60
+
61
+ def skip_coverage(&block)
62
+ return @skip_coverage unless block_given?
63
+
64
+ @skip_coverage = block
65
+ end
52
66
  end
53
67
  end
54
68
  end
@@ -12,9 +12,11 @@ module OpenapiFirst
12
12
  class Plan
13
13
  class UnknownRequestError < StandardError; end
14
14
 
15
- def self.for(oad, skip_response: nil)
15
+ def self.for(oad, skip_response: nil, skip_route: nil)
16
16
  plan = new(definition_key: oad.key, filepath: oad.filepath)
17
- oad.routes.each do |route|
17
+ routes = oad.routes
18
+ routes = routes.reject { |route| skip_route[route.path, route.request_method] } if skip_route
19
+ routes.each do |route|
18
20
  responses = skip_response ? route.responses.reject(&skip_response) : route.responses
19
21
  plan.add_route request_method: route.request_method,
20
22
  path: route.path,
@@ -35,7 +37,7 @@ module OpenapiFirst
35
37
  private attr_reader :index
36
38
 
37
39
  def track_request(validated_request)
38
- index[validated_request.request_definition.key].track(validated_request) if validated_request.known?
40
+ index[validated_request.request_definition.key]&.track(validated_request) if validated_request.known?
39
41
  end
40
42
 
41
43
  def track_response(validated_response)
@@ -51,6 +53,8 @@ module OpenapiFirst
51
53
  return 0 if done.zero?
52
54
 
53
55
  all = tasks.count
56
+ return 100 if done == all
57
+
54
58
  (done / (all.to_f / 100))
55
59
  end
56
60
 
@@ -19,9 +19,9 @@ module OpenapiFirst
19
19
 
20
20
  def install = Test.install
21
21
 
22
- def start(skip_response: nil)
22
+ def start(skip_response: nil, skip_route: nil)
23
23
  @current_run = Test.definitions.values.to_h do |oad|
24
- plan = Plan.for(oad, skip_response:)
24
+ plan = Plan.for(oad, skip_response:, skip_route:)
25
25
  [oad.key, plan]
26
26
  end
27
27
  end
@@ -10,14 +10,23 @@ module OpenapiFirst
10
10
  # Inject silent request/response validation to observe rack apps during testing
11
11
  module Observe
12
12
  def self.observe(app, api: :default)
13
+ definition = OpenapiFirst::Test[api]
14
+ mod = OpenapiFirst::Test::Callable[definition]
15
+
16
+ if app.respond_to?(:call)
17
+ return if app.singleton_class.include?(Observed)
18
+
19
+ app.singleton_class.prepend(mod)
20
+ app.singleton_class.include(Observed)
21
+ return
22
+ end
23
+
13
24
  unless app.instance_methods.include?(:call)
14
25
  raise ObserveError, "Don't know how to observe #{app}, because it has no call instance method."
15
26
  end
16
27
 
17
28
  return if app.include?(Observed)
18
29
 
19
- definition = OpenapiFirst::Test[api]
20
- mod = OpenapiFirst::Test::Callable[definition]
21
30
  app.prepend(mod)
22
31
  app.include(Observed)
23
32
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Test
5
+ # Middleware that observes requests and responses. This is used to trigger hooks added by OpenapiFirst::Tests.
6
+ class ObserverMiddleware
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @definition = OpenapiFirst::Test[options.fetch(:api, :default)]
10
+ end
11
+
12
+ def call(env)
13
+ request = Rack::Request.new(env)
14
+
15
+ @definition.validate_request(request, raise_error: false)
16
+ response = @app.call(env)
17
+ status, headers, body = response
18
+ @definition.validate_response(request, Rack::Response[status, headers, body], raise_error: false)
19
+ response
20
+ end
21
+ end
22
+ end
23
+ end
@@ -40,8 +40,8 @@ module OpenapiFirst
40
40
  yield configuration
41
41
 
42
42
  configuration.registry.each { |name, oad| register(oad, as: name) }
43
- configuration.apps.each { |name, app| observe(app, api: name) }
44
- Coverage.start(skip_response: configuration.skip_response_coverage)
43
+ configuration.apps.each { |name, apps| apps.each { |app| observe(app, api: name) } }
44
+ Coverage.start(skip_response: configuration.skip_response_coverage, skip_route: configuration.skip_coverage)
45
45
 
46
46
  if definitions.empty?
47
47
  raise NotRegisteredError,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '2.9.3'
4
+ VERSION = '2.11.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.3
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -49,7 +49,7 @@ dependencies:
49
49
  requirements:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 0.5.1
52
+ version: 0.6.1
53
53
  - - "<"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '2.0'
@@ -59,7 +59,7 @@ dependencies:
59
59
  requirements:
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 0.5.1
62
+ version: 0.6.1
63
63
  - - "<"
64
64
  - !ruby/object:Gem::Version
65
65
  version: '2.0'
@@ -134,6 +134,7 @@ files:
134
134
  - lib/openapi_first/test/methods.rb
135
135
  - lib/openapi_first/test/minitest_helpers.rb
136
136
  - lib/openapi_first/test/observe.rb
137
+ - lib/openapi_first/test/observer_middleware.rb
137
138
  - lib/openapi_first/test/plain_helpers.rb
138
139
  - lib/openapi_first/test/registry.rb
139
140
  - lib/openapi_first/validated_request.rb