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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +82 -54
- data/lib/openapi_first/error_responses/default.rb +1 -0
- data/lib/openapi_first/error_responses/jsonapi.rb +1 -0
- data/lib/openapi_first/response_parser.rb +3 -0
- data/lib/openapi_first/test/configuration.rb +17 -3
- data/lib/openapi_first/test/coverage/plan.rb +7 -3
- data/lib/openapi_first/test/coverage.rb +2 -2
- data/lib/openapi_first/test/observe.rb +11 -2
- data/lib/openapi_first/test/observer_middleware.rb +23 -0
- data/lib/openapi_first/test.rb +2 -2
- data/lib/openapi_first/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7687c4d4ac32654b4a2fd8880eeaeb0ca98ab6fac19b8d9a336fe5d5bb94785a
|
4
|
+
data.tar.gz: 6fb04355d69916cfc3d1dce1e21d0f163082878cf1348421a9b1006b070621f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
20
|
-
|
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
|
-
- [
|
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
|
-
##
|
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
|
|
@@ -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]
|
30
|
+
(@apps[api] ||= []) << app
|
30
31
|
end
|
31
32
|
|
32
|
-
attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
|
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
|
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]
|
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
|
data/lib/openapi_first/test.rb
CHANGED
@@ -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,
|
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.
|
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.
|
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.
|
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
|