openapi_first 2.10.1 → 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: 79f9fda1c73303674f66f87ff97d6749e085fb6a01ca1f9d604e53a9f9a1abd7
4
- data.tar.gz: b15c401b9e9b83fc091e597b09765a3ec518005c79f4406af4abe4fa1a8b7735
3
+ metadata.gz: 7687c4d4ac32654b4a2fd8880eeaeb0ca98ab6fac19b8d9a336fe5d5bb94785a
4
+ data.tar.gz: 6fb04355d69916cfc3d1dce1e21d0f163082878cf1348421a9b1006b070621f3
5
5
  SHA512:
6
- metadata.gz: 84bbbc5bf681ec5f62ef7b66d68be805add8a4b013ea87d9081718677bcf617c89ffab7d61d660d1b498f99532b05106591837353d62a3355febaa739a80e631
7
- data.tar.gz: 9c5771ee2b8a55e85d2841ce5d0e0058eec81df545f5eaf168176c41cd6e66c83436fffc19b6de156f444e1f58c010e8e443b0736fe6d08940774311c1ed87a3
6
+ metadata.gz: '0988025dd93f55354b646c8aeb3fe7d0febfd54925d368fd3aa5959067c1a0a948b193158130b5fa7fb91cf51ffe774f9f8f281e76ac3feb318e67ea1b079020'
7
+ data.tar.gz: f70094cd6b531e42e06f931ed31155385bf9dd97588cad6bc977e961e7fdd497e057a971112fe10a0457d145b8f916cabdad3995c90bc29d78d31e59184211ac
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
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
+
5
9
  ## 2.10.1
6
10
 
7
11
  - Don't try to track coverage for skipped requests
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,78 +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
- ### Configure test coverage
84
-
85
- OpenapiFirst::Test raises an error when a request is not defined. You can deactivate this with:
86
-
87
- ```ruby
88
- OpenapiFirst::Test.setup do |test|
89
- # …
90
- test.ignore_unknown_requests = true
91
- end
92
- ```
93
-
94
- Exclude certain _responses_ from coverage with `skip_coverage`:
95
-
96
- ```ruby
97
- OpenapiFirst::Test.setup do |test|
98
- # …
99
- test.skip_response_coverage do |response_definition|
100
- response_definition.status == '5XX'
101
- end
102
- end
103
- ```
104
-
105
- Skip coverage for a request and all responses alltogether of a route with `skip_coverage`:
106
-
107
- ```ruby
108
- OpenapiFirst::Test.setup do |test|
109
- # …
110
- test.skip_coverage do |path, request_method|
111
- path == '/bookings/{bookingId}' && requests_method == 'DELETE'
112
- end
113
- end
114
- ```
115
-
116
47
  ## Rack Middlewares
117
48
 
118
49
  ### Request validation
@@ -222,7 +153,79 @@ use OpenapiFirst::Middlewares::ResponseValidation, 'openapi.yaml', raise_error:
222
153
 
223
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.
224
155
 
225
- ## 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
226
229
 
227
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.
228
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)
@@ -27,7 +27,7 @@ module OpenapiFirst
27
27
 
28
28
  # Observe a rack app
29
29
  def observe(app, api: :default)
30
- @apps[api] = app
30
+ (@apps[api] ||= []) << app
31
31
  end
32
32
 
33
33
  attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
@@ -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,7 +40,7 @@ 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) }
43
+ configuration.apps.each { |name, apps| apps.each { |app| observe(app, api: name) } }
44
44
  Coverage.start(skip_response: configuration.skip_response_coverage, skip_route: configuration.skip_coverage)
45
45
 
46
46
  if definitions.empty?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '2.10.1'
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.10.1
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -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