openapi_first 3.1.1 → 3.2.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 +33 -0
- data/README.md +18 -0
- data/lib/openapi_first/builder.rb +4 -1
- data/lib/openapi_first/configuration.rb +1 -0
- data/lib/openapi_first/definition.rb +15 -11
- data/lib/openapi_first/test/app.rb +10 -2
- data/lib/openapi_first/test/configuration.rb +32 -4
- data/lib/openapi_first/test/methods.rb +8 -2
- data/lib/openapi_first/test/observe.rb +1 -1
- data/lib/openapi_first/test.rb +9 -10
- data/lib/openapi_first/validated_request.rb +3 -0
- data/lib/openapi_first/validated_response.rb +3 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +7 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d95e7c0085b398b3ec034554d30cf4bd6e9dc488c5cc19d7777ec88aa5ed9ec
|
|
4
|
+
data.tar.gz: 323d16c0d405b4233ebe4377cb1a9960db84d19033193d3ccf9badb052a00f24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d80c5803dcb0746cdf9def8e1b470394b424926a2a0235da90bc47e3f748f74fdbf278cd8e7640b157aabe279e2028e80581f4a7ebcde4b4d1238e9ee0040618
|
|
7
|
+
data.tar.gz: 1022635f98bd37c776d137315fd61f9ba82dd72831ca16ef2e834f45bcba0456e90614eebc85df1fd08b7e03183daa16beaa098575d4161f74b54e3d1ef998b6
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.2.0
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Changed OpenapiFirst::Test to track the request _after_ the app has handled the request. See [PR #434](https://github.com/ahx/openapi_first/pull/434). You can restore the old behavior with
|
|
9
|
+
```ruby
|
|
10
|
+
include OpenapiFirst::Test::Methods[MyApp, validate_request_before_handling: true]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `OpenapiFirst::ValidatedRequest#unknown?` and `OpenapiFirst::ValidatedResponse#unknown?`
|
|
16
|
+
- Added new hook: `after_response_body_property_validation`
|
|
17
|
+
- Added support for a static `path_prefix` value to be set on the creation of a Definition. See [PR #432](https://github.com/ahx/openapi_first/pull/432):
|
|
18
|
+
```ruby
|
|
19
|
+
OpenapiFirst.configure do |config|
|
|
20
|
+
config.register('openapi/openapi.yaml' path_prefix: '/weather')
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
- Added `OpenapiFirst::Test::Configuration#ignore_response_error` and `#ignore_request_error` to configure which request/response errors should not raise an error during testing:
|
|
24
|
+
```ruby
|
|
25
|
+
OpenapiFirst::Test.setup do |test|
|
|
26
|
+
test.ignore_request_error do |validated_request|
|
|
27
|
+
# Ignore unknown requests on certain paths
|
|
28
|
+
validated_request.path.start_with?('/api/v1') && validated_request.unknown?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test.ignore_response_error do |validated_response, rack_request|
|
|
32
|
+
# Ignore invalid response bodies on certain paths
|
|
33
|
+
validated_request.path.start_with?('/api/legacy/stuff') && validated_request.error.type == :invalid_body
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
5
38
|
## 3.1.1
|
|
6
39
|
|
|
7
40
|
- Changed: Return uniqe errors in default error responses
|
data/README.md
CHANGED
|
@@ -231,6 +231,23 @@ Here is how to set it up:
|
|
|
231
231
|
|
|
232
232
|
### Configure test coverage
|
|
233
233
|
|
|
234
|
+
You can ignore errors for certain requests/responses. This will stop `OpenapiFirst::Test` from raising an exception if the block returns true.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
OpenapiFirst::Test.setup do |test|
|
|
238
|
+
test.ignore_request_error do |validated_request|
|
|
239
|
+
# Ignore unknown requests on certain paths
|
|
240
|
+
validated_request.path.start_with?('/api/v1') && validated_request.unknown?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
test.ignore_response_error do |validated_response, rack_request|
|
|
244
|
+
# Ignore invalid response bodies on certain paths
|
|
245
|
+
validated_request.path.start_with?('/api/legacy/stuff') && validated_request.error.type == :invalid_body
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
|
|
234
251
|
OpenapiFirst::Test raises an error when a response status is not defined except for 404 and 500. You can change this:
|
|
235
252
|
|
|
236
253
|
```ruby
|
|
@@ -368,6 +385,7 @@ Available hooks:
|
|
|
368
385
|
- `after_response_validation`
|
|
369
386
|
- `after_request_parameter_property_validation`
|
|
370
387
|
- `after_request_body_property_validation`
|
|
388
|
+
- `after_response_body_property_validation`
|
|
371
389
|
|
|
372
390
|
Setup per per instance:
|
|
373
391
|
|
|
@@ -165,7 +165,10 @@ module OpenapiFirst
|
|
|
165
165
|
responses.flat_map do |status, response_object|
|
|
166
166
|
headers = build_response_headers(response_object['headers'])
|
|
167
167
|
response_object['content']&.map do |content_type, content_object|
|
|
168
|
-
content_schema = content_object['schema'].schema(
|
|
168
|
+
content_schema = content_object['schema'].schema(
|
|
169
|
+
configuration: schemer_configuration,
|
|
170
|
+
after_property_validation: config.after_response_body_property_validation
|
|
171
|
+
)
|
|
169
172
|
Response.new(status:,
|
|
170
173
|
headers:,
|
|
171
174
|
content_type:,
|
|
@@ -11,6 +11,8 @@ module OpenapiFirst
|
|
|
11
11
|
|
|
12
12
|
# @return [String,nil]
|
|
13
13
|
attr_reader :filepath
|
|
14
|
+
# @return [String,nil]
|
|
15
|
+
attr_reader :path_prefix
|
|
14
16
|
# @return [Configuration]
|
|
15
17
|
attr_reader :config
|
|
16
18
|
# @return [Enumerable[String]]
|
|
@@ -20,8 +22,10 @@ module OpenapiFirst
|
|
|
20
22
|
|
|
21
23
|
# @param contents [Hash] The OpenAPI document.
|
|
22
24
|
# @param filepath [String] The file path of the OpenAPI document.
|
|
23
|
-
|
|
25
|
+
# @param path_prefix [String,nil] An optional path prefix, that is not documented, that all requests begin with.
|
|
26
|
+
def initialize(contents, filepath = nil, path_prefix = nil)
|
|
24
27
|
@filepath = filepath
|
|
28
|
+
@path_prefix = path_prefix
|
|
25
29
|
@config = OpenapiFirst.configuration.child
|
|
26
30
|
yield @config if block_given?
|
|
27
31
|
@config.freeze
|
|
@@ -79,23 +83,22 @@ module OpenapiFirst
|
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
# Validates the response against the API description.
|
|
82
|
-
# @param
|
|
83
|
-
# @param
|
|
84
|
-
# @param raise_error [Boolean]
|
|
86
|
+
# @param request [Rack::Request] The Rack request object.
|
|
87
|
+
# @param response [Rack::Response] The Rack response object.
|
|
88
|
+
# @param raise_error [Boolean] Whether to raise an error if validation fails.
|
|
85
89
|
# @return [ValidatedResponse] The validated response object.
|
|
86
|
-
def validate_response(
|
|
87
|
-
route = @router.match(
|
|
88
|
-
content_type: rack_request.content_type)
|
|
90
|
+
def validate_response(request, response, raise_error: false)
|
|
91
|
+
route = @router.match(request.request_method, resolve_path(request), content_type: request.content_type)
|
|
89
92
|
return if route.error # Skip response validation for unknown requests
|
|
90
93
|
|
|
91
|
-
response_match = route.match_response(status:
|
|
94
|
+
response_match = route.match_response(status: response.status, content_type: response.content_type)
|
|
92
95
|
error = response_match.error
|
|
93
96
|
validated = if error
|
|
94
|
-
ValidatedResponse.new(
|
|
97
|
+
ValidatedResponse.new(response, error:)
|
|
95
98
|
else
|
|
96
|
-
response_match.response.validate(
|
|
99
|
+
response_match.response.validate(response)
|
|
97
100
|
end
|
|
98
|
-
@config.after_response_validation&.each { |hook| hook.call(validated,
|
|
101
|
+
@config.after_response_validation&.each { |hook| hook.call(validated, request, self) }
|
|
99
102
|
raise validated.error.exception(validated) if raise_error && validated.invalid?
|
|
100
103
|
|
|
101
104
|
validated
|
|
@@ -104,6 +107,7 @@ module OpenapiFirst
|
|
|
104
107
|
private
|
|
105
108
|
|
|
106
109
|
def resolve_path(rack_request)
|
|
110
|
+
return rack_request.path.delete_prefix(path_prefix) if path_prefix && rack_request.path.start_with?(path_prefix)
|
|
107
111
|
return rack_request.path unless @config.path
|
|
108
112
|
|
|
109
113
|
@config.path.call(rack_request)
|
|
@@ -10,16 +10,24 @@ module OpenapiFirst
|
|
|
10
10
|
# A wrapper of the original app
|
|
11
11
|
# with silent request/response validation to track requests/responses.
|
|
12
12
|
class App < SimpleDelegator
|
|
13
|
-
def initialize(app, api:)
|
|
13
|
+
def initialize(app, api:, validate_request_before_handling:)
|
|
14
14
|
super(app)
|
|
15
15
|
@app = app
|
|
16
16
|
@definition = Test[api]
|
|
17
|
+
@validate_request_before_handling = validate_request_before_handling
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call(env)
|
|
20
21
|
request = Rack::Request.new(env)
|
|
21
|
-
|
|
22
|
+
if @validate_request_before_handling
|
|
23
|
+
env[Test::REQUEST] = @definition.validate_request(request, raise_error: false)
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
response = @app.call(env)
|
|
27
|
+
unless @validate_request_before_handling
|
|
28
|
+
env[Test::REQUEST] = @definition.validate_request(request, raise_error: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
status, headers, body = response
|
|
24
32
|
env[Test::RESPONSE] =
|
|
25
33
|
@definition.validate_response(request, Rack::Response[status, headers, body], raise_error: false)
|
|
@@ -15,6 +15,8 @@ module OpenapiFirst
|
|
|
15
15
|
@ignore_unknown_response_status = false
|
|
16
16
|
@report_coverage = true
|
|
17
17
|
@ignore_unknown_requests = false
|
|
18
|
+
@ignore_request_error = nil
|
|
19
|
+
@ignore_response_error = nil
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Register OADs, but don't load them just yet
|
|
@@ -50,6 +52,23 @@ module OpenapiFirst
|
|
|
50
52
|
@report_coverage = value
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
# Ignore certain errors for certain requests
|
|
56
|
+
# @param block A Proc that will be called with [OpenapiFirst::ValidatedRequest]
|
|
57
|
+
def ignore_request_error(&block)
|
|
58
|
+
raise ArgumentError, 'You have to pass a block' unless block_given?
|
|
59
|
+
|
|
60
|
+
@ignore_request_error = block
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ignore certain errors for certain responses
|
|
64
|
+
# @param block A Proc that will be called with [OpenapiFirst::ValidatedResponse, Rack::Request]
|
|
65
|
+
def ignore_response_error(&block)
|
|
66
|
+
raise ArgumentError, 'You have to pass a block' unless block_given?
|
|
67
|
+
|
|
68
|
+
@ignore_response_error = block
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param block A Proc that will be called with [OpenapiFirst::ValidatedResponse, Rack::Request]
|
|
53
72
|
def skip_response_coverage(&block)
|
|
54
73
|
return @skip_response_coverage unless block_given?
|
|
55
74
|
|
|
@@ -63,13 +82,22 @@ module OpenapiFirst
|
|
|
63
82
|
end
|
|
64
83
|
|
|
65
84
|
alias ignore_unknown_response_status? ignore_unknown_response_status
|
|
85
|
+
alias ignore_unknown_requests? ignore_unknown_requests
|
|
66
86
|
|
|
67
|
-
def
|
|
68
|
-
return false if
|
|
87
|
+
def raise_request_error?(validated_request)
|
|
88
|
+
return false if @ignore_request_error&.call(validated_request)
|
|
89
|
+
return false if ignore_unknown_requests? && validated_request.unknown?
|
|
90
|
+
|
|
91
|
+
validated_request.unknown?
|
|
92
|
+
end
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
def raise_response_error?(validated_response, rack_request)
|
|
95
|
+
return false if @ignore_response_error&.call(validated_response, rack_request)
|
|
96
|
+
return false if response_raise_error == false
|
|
97
|
+
return false if ignored_unknown_status.include?(validated_response.status)
|
|
98
|
+
return false if ignore_unknown_response_status? && validated_response.error.type == :response_status_not_found
|
|
71
99
|
|
|
72
|
-
|
|
100
|
+
true
|
|
73
101
|
end
|
|
74
102
|
end
|
|
75
103
|
end
|
|
@@ -12,12 +12,13 @@ module OpenapiFirst
|
|
|
12
12
|
base.include(AssertionMethod)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.[](application_under_test = nil, api: nil)
|
|
15
|
+
def self.[](application_under_test = nil, api: nil, validate_request_before_handling: false)
|
|
16
16
|
mod = Module.new do
|
|
17
17
|
def self.included(base)
|
|
18
18
|
base.include OpenapiFirst::Test::Methods::AssertionMethod
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
|
+
mod.define_method(:openapi_first_validate_request_before_handling?) { validate_request_before_handling }
|
|
21
22
|
|
|
22
23
|
if api
|
|
23
24
|
mod.define_method(:openapi_first_default_api) { api }
|
|
@@ -26,7 +27,12 @@ module OpenapiFirst
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
if application_under_test
|
|
29
|
-
mod.define_method(:app)
|
|
30
|
+
mod.define_method(:app) do
|
|
31
|
+
OpenapiFirst::Test.app(
|
|
32
|
+
application_under_test, api: openapi_first_default_api,
|
|
33
|
+
validate_request_before_handling: openapi_first_validate_request_before_handling?
|
|
34
|
+
)
|
|
35
|
+
end
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
mod
|
data/lib/openapi_first/test.rb
CHANGED
|
@@ -40,7 +40,7 @@ module OpenapiFirst
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Sets up OpenAPI test coverage and OAD registration.
|
|
43
|
-
# @
|
|
43
|
+
# @yield [OpenapiFirst::Test::Configuration] configuration A configuration to setup test integration
|
|
44
44
|
def self.setup
|
|
45
45
|
install
|
|
46
46
|
yield configuration if block_given?
|
|
@@ -95,9 +95,9 @@ module OpenapiFirst
|
|
|
95
95
|
# Returns the Rack app wrapped with silent request, response validation
|
|
96
96
|
# You can use this if you want to track coverage via Test::Coverage, but don't want to use
|
|
97
97
|
# the middlewares or manual request, response validation.
|
|
98
|
-
def self.app(app, spec: nil, api: :default)
|
|
98
|
+
def self.app(app, spec: nil, api: :default, validate_request_before_handling: false)
|
|
99
99
|
spec ||= self[api]
|
|
100
|
-
App.new(app, api: spec)
|
|
100
|
+
App.new(app, api: spec, validate_request_before_handling:)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def self.install
|
|
@@ -115,9 +115,7 @@ module OpenapiFirst
|
|
|
115
115
|
|
|
116
116
|
@after_response_validation = config.after_response_validation do |validated_response, rack_request, oad|
|
|
117
117
|
next unless registered?(oad)
|
|
118
|
-
|
|
119
|
-
raise validated_response.error.exception
|
|
120
|
-
end
|
|
118
|
+
raise validated_response.error.exception if raise_response_error?(validated_response, rack_request)
|
|
121
119
|
|
|
122
120
|
Coverage.track_response(validated_response, rack_request, oad)
|
|
123
121
|
end
|
|
@@ -156,15 +154,16 @@ module OpenapiFirst
|
|
|
156
154
|
|
|
157
155
|
def raise_request_error?(validated_request)
|
|
158
156
|
return false if validated_request.valid?
|
|
159
|
-
return false if validated_request.known?
|
|
160
157
|
|
|
161
|
-
|
|
158
|
+
configuration.raise_request_error?(validated_request)
|
|
162
159
|
end
|
|
163
160
|
|
|
164
161
|
def many?(array) = array.length > 1
|
|
165
162
|
|
|
166
|
-
def raise_response_error?(
|
|
167
|
-
|
|
163
|
+
def raise_response_error?(validated_response, rack_request)
|
|
164
|
+
return false if validated_response.valid?
|
|
165
|
+
|
|
166
|
+
configuration.raise_response_error?(validated_response, rack_request)
|
|
168
167
|
end
|
|
169
168
|
end
|
|
170
169
|
end
|
|
@@ -69,6 +69,9 @@ module OpenapiFirst
|
|
|
69
69
|
# Returns true if the request is defined.
|
|
70
70
|
def known? = request_definition != nil
|
|
71
71
|
|
|
72
|
+
# Returns true if the request is not defined.
|
|
73
|
+
def unknown? = !known?
|
|
74
|
+
|
|
72
75
|
# Merged path, query, body parameters.
|
|
73
76
|
# Here path has the highest precedence, then query, then body.
|
|
74
77
|
# @return [Hash<String, anything>]
|
|
@@ -44,6 +44,9 @@ module OpenapiFirst
|
|
|
44
44
|
# Returns true if the response is defined.
|
|
45
45
|
def known? = response_definition != nil
|
|
46
46
|
|
|
47
|
+
# Returns true if the response is not defined.
|
|
48
|
+
def unknown? = !known?
|
|
49
|
+
|
|
47
50
|
# Checks if the response is invalid.
|
|
48
51
|
# @return [Boolean]
|
|
49
52
|
def invalid?
|
data/lib/openapi_first.rb
CHANGED
|
@@ -55,8 +55,11 @@ module OpenapiFirst
|
|
|
55
55
|
|
|
56
56
|
# Load and dereference an OpenAPI spec file or return the Definition if it's already loaded
|
|
57
57
|
# @param filepath_or_definition [String, Definition] The path to the file or a Definition object
|
|
58
|
+
# @param only [Proc, nil] An optional proc to filter paths. It is called with the path string and should return
|
|
59
|
+
# true/false
|
|
60
|
+
# @param path_prefix [String, nil] An optional path prefix, that is not documented, that all requests begin with.
|
|
58
61
|
# @return [Definition]
|
|
59
|
-
def self.load(filepath_or_definition, only: nil, &)
|
|
62
|
+
def self.load(filepath_or_definition, only: nil, path_prefix: nil, &)
|
|
60
63
|
return filepath_or_definition if filepath_or_definition.is_a?(Definition)
|
|
61
64
|
return self[filepath_or_definition] if filepath_or_definition.is_a?(Symbol)
|
|
62
65
|
|
|
@@ -64,16 +67,16 @@ module OpenapiFirst
|
|
|
64
67
|
raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
|
|
65
68
|
|
|
66
69
|
contents = FileLoader.load(filepath)
|
|
67
|
-
parse(contents, only:, filepath:, &)
|
|
70
|
+
parse(contents, only:, filepath:, path_prefix:, &)
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
# Parse a dereferenced Hash
|
|
71
74
|
# @return [Definition]
|
|
72
75
|
# TODO: This needs to work with unresolved contents as well
|
|
73
|
-
def self.parse(contents, only: nil, filepath: nil, &)
|
|
76
|
+
def self.parse(contents, only: nil, filepath: nil, path_prefix: nil, &)
|
|
74
77
|
contents = ::JSON.parse(::JSON.generate(contents)) # Deeply stringify keys, because of YAML. See https://github.com/ahx/openapi_first/issues/367
|
|
75
78
|
contents['paths'].filter!(&->(key, _) { only.call(key) }) if only
|
|
76
|
-
Definition.new(contents, filepath, &)
|
|
79
|
+
Definition.new(contents, filepath, path_prefix, &)
|
|
77
80
|
end
|
|
78
81
|
end
|
|
79
82
|
|