Skip to content

Commit 65c86cc

Browse files
authored
Merge pull request #371 from ahx/plausible-test
Make OpenapiFirst::Test more strict and more configurable
2 parents 5c37b23 + bb8008d commit 65c86cc

23 files changed

+820
-188
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
## Unreleased
44

5+
## 2.8.0
6+
7+
### OpenapiFirst::Test is now stricter and more configurable
8+
9+
Changes:
10+
- Changed OpenapiFirst::Test to raises an "invalid response" error if it sees an invalid response (https://github.com/ahx/openapi_first/issues/366).
11+
You can change this back to the old behavior by setting `OpenapiFirst::Test::Configuration#response_raise_error = false` (but you shouldn't).
12+
- Added `Test.setup { it.observe(MyApp) }`, `Test.observe(App, api: :my_api)` and internal `Test::Callable[]` to inject request/response validation in rack app as an alternative to overwrite the `app` method in a test
13+
- Added `Test::Configuration#ignored_unknown_status` to configure response status(es) that do not have to be descriped in the API description. 404 statuses are ignored by default.
14+
- Changed `OpenapiFirst::Test` to make tests fail if API description is not covered by tests. You can adapt this behavior via `OpenapiFirst::Test.setup` / `skip_response_coverage` or deactivate coverage with `OpenapiFirst::Test::Configuration#report_coverage = false` or `report_coverage = :warn`
15+
516
## 2.7.4
617

718
- Return 400 if Rack cannot parse query string instead of raising an exception. Fixes https://github.com/ahx/openapi_first/issues/372

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ group :test, :development do
1212
gem 'bundler'
1313
gem 'minitest'
1414
gem 'rack-test'
15+
gem 'rails'
1516
gem 'rake'
1617
gem 'rspec'
1718
gem 'rubocop'
1819
gem 'rubocop-performance'
1920
gem 'simplecov'
21+
gem 'sinatra'
2022
end

Gemfile.lock

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
openapi_first (2.7.4)
4+
openapi_first (2.8.0)
55
hana (~> 1.3)
66
json_schemer (>= 2.1, < 3.0)
77
openapi_parameters (>= 0.5.1, < 2.0)
@@ -10,6 +10,26 @@ PATH
1010
GEM
1111
remote: https://rubygems.org/
1212
specs:
13+
actioncable (8.0.2)
14+
actionpack (= 8.0.2)
15+
activesupport (= 8.0.2)
16+
nio4r (~> 2.0)
17+
websocket-driver (>= 0.6.1)
18+
zeitwerk (~> 2.6)
19+
actionmailbox (8.0.2)
20+
actionpack (= 8.0.2)
21+
activejob (= 8.0.2)
22+
activerecord (= 8.0.2)
23+
activestorage (= 8.0.2)
24+
activesupport (= 8.0.2)
25+
mail (>= 2.8.0)
26+
actionmailer (8.0.2)
27+
actionpack (= 8.0.2)
28+
actionview (= 8.0.2)
29+
activejob (= 8.0.2)
30+
activesupport (= 8.0.2)
31+
mail (>= 2.8.0)
32+
rails-dom-testing (~> 2.2)
1333
actionpack (8.0.2)
1434
actionview (= 8.0.2)
1535
activesupport (= 8.0.2)
@@ -20,12 +40,34 @@ GEM
2040
rails-dom-testing (~> 2.2)
2141
rails-html-sanitizer (~> 1.6)
2242
useragent (~> 0.16)
43+
actiontext (8.0.2)
44+
actionpack (= 8.0.2)
45+
activerecord (= 8.0.2)
46+
activestorage (= 8.0.2)
47+
activesupport (= 8.0.2)
48+
globalid (>= 0.6.0)
49+
nokogiri (>= 1.8.5)
2350
actionview (8.0.2)
2451
activesupport (= 8.0.2)
2552
builder (~> 3.1)
2653
erubi (~> 1.11)
2754
rails-dom-testing (~> 2.2)
2855
rails-html-sanitizer (~> 1.6)
56+
activejob (8.0.2)
57+
activesupport (= 8.0.2)
58+
globalid (>= 0.3.6)
59+
activemodel (8.0.2)
60+
activesupport (= 8.0.2)
61+
activerecord (8.0.2)
62+
activemodel (= 8.0.2)
63+
activesupport (= 8.0.2)
64+
timeout (>= 0.4.0)
65+
activestorage (8.0.2)
66+
actionpack (= 8.0.2)
67+
activejob (= 8.0.2)
68+
activerecord (= 8.0.2)
69+
activesupport (= 8.0.2)
70+
marcel (~> 1.0)
2971
activesupport (8.0.2)
3072
base64
3173
benchmark (>= 0.3)
@@ -47,13 +89,22 @@ GEM
4789
concurrent-ruby (1.3.5)
4890
connection_pool (2.5.3)
4991
crass (1.0.6)
92+
date (3.4.1)
5093
diff-lcs (1.6.2)
5194
docile (1.4.1)
5295
drb (2.2.3)
96+
erb (5.0.1)
5397
erubi (1.13.1)
98+
globalid (1.2.1)
99+
activesupport (>= 6.1)
54100
hana (1.3.7)
55101
i18n (1.14.7)
56102
concurrent-ruby (~> 1.0)
103+
io-console (0.8.0)
104+
irb (1.15.2)
105+
pp (>= 0.6.0)
106+
rdoc (>= 4.0.0)
107+
reline (>= 0.4.2)
57108
json (2.12.2)
58109
json_schemer (2.4.0)
59110
bigdecimal
@@ -66,7 +117,26 @@ GEM
66117
loofah (2.24.1)
67118
crass (~> 1.0.2)
68119
nokogiri (>= 1.12.0)
120+
mail (2.8.1)
121+
mini_mime (>= 0.1.1)
122+
net-imap
123+
net-pop
124+
net-smtp
125+
marcel (1.0.4)
126+
mini_mime (1.1.5)
69127
minitest (5.25.5)
128+
mustermann (3.0.3)
129+
ruby2_keywords (~> 0.0.1)
130+
net-imap (0.5.9)
131+
date
132+
net-protocol
133+
net-pop (0.1.2)
134+
net-protocol
135+
net-protocol (0.2.2)
136+
timeout
137+
net-smtp (0.5.1)
138+
net-protocol
139+
nio4r (2.7.4)
70140
nokogiri (1.18.8-arm64-darwin)
71141
racc (~> 1.4)
72142
nokogiri (1.18.8-x86_64-linux-gnu)
@@ -77,26 +147,63 @@ GEM
77147
parser (3.3.8.0)
78148
ast (~> 2.4.1)
79149
racc
150+
pp (0.6.2)
151+
prettyprint
152+
prettyprint (0.2.0)
80153
prism (1.4.0)
154+
psych (5.2.6)
155+
date
156+
stringio
81157
racc (1.8.1)
82158
rack (3.1.16)
159+
rack-protection (4.1.1)
160+
base64 (>= 0.1.0)
161+
logger (>= 1.6.0)
162+
rack (>= 3.0.0, < 4)
83163
rack-session (2.1.1)
84164
base64 (>= 0.1.0)
85165
rack (>= 3.0.0)
86166
rack-test (2.2.0)
87167
rack (>= 1.3)
88168
rackup (2.2.1)
89169
rack (>= 3)
170+
rails (8.0.2)
171+
actioncable (= 8.0.2)
172+
actionmailbox (= 8.0.2)
173+
actionmailer (= 8.0.2)
174+
actionpack (= 8.0.2)
175+
actiontext (= 8.0.2)
176+
actionview (= 8.0.2)
177+
activejob (= 8.0.2)
178+
activemodel (= 8.0.2)
179+
activerecord (= 8.0.2)
180+
activestorage (= 8.0.2)
181+
activesupport (= 8.0.2)
182+
bundler (>= 1.15.0)
183+
railties (= 8.0.2)
90184
rails-dom-testing (2.3.0)
91185
activesupport (>= 5.0.0)
92186
minitest
93187
nokogiri (>= 1.6)
94188
rails-html-sanitizer (1.6.2)
95189
loofah (~> 2.21)
96190
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
191+
railties (8.0.2)
192+
actionpack (= 8.0.2)
193+
activesupport (= 8.0.2)
194+
irb (~> 1.13)
195+
rackup (>= 1.0.0)
196+
rake (>= 12.2)
197+
thor (~> 1.0, >= 1.2.2)
198+
zeitwerk (~> 2.6)
97199
rainbow (3.1.1)
98200
rake (13.3.0)
201+
rdoc (6.14.1)
202+
erb
203+
psych (>= 4.0.0)
99204
regexp_parser (2.10.0)
205+
reline (0.6.1)
206+
io-console (~> 0.5)
100207
rspec (3.13.1)
101208
rspec-core (~> 3.13.0)
102209
rspec-expectations (~> 3.13.0)
@@ -129,6 +236,7 @@ GEM
129236
rubocop (>= 1.75.0, < 2.0)
130237
rubocop-ast (>= 1.38.0, < 2.0)
131238
ruby-progressbar (1.13.0)
239+
ruby2_keywords (0.0.5)
132240
securerandom (0.4.1)
133241
simplecov (0.22.0)
134242
docile (~> 1.1)
@@ -137,13 +245,29 @@ GEM
137245
simplecov-html (0.13.1)
138246
simplecov_json_formatter (0.1.4)
139247
simpleidn (0.2.3)
248+
sinatra (4.1.1)
249+
logger (>= 1.6.0)
250+
mustermann (~> 3.0)
251+
rack (>= 3.0.0, < 4)
252+
rack-protection (= 4.1.1)
253+
rack-session (>= 2.0.0, < 3)
254+
tilt (~> 2.0)
255+
stringio (3.1.7)
256+
thor (1.3.2)
257+
tilt (2.6.0)
258+
timeout (0.4.3)
140259
tzinfo (2.0.6)
141260
concurrent-ruby (~> 1.0)
142261
unicode-display_width (3.1.4)
143262
unicode-emoji (~> 4.0, >= 4.0.4)
144263
unicode-emoji (4.0.4)
145264
uri (1.0.3)
146265
useragent (0.16.11)
266+
websocket-driver (0.8.0)
267+
base64
268+
websocket-extensions (>= 0.1.0)
269+
websocket-extensions (0.1.5)
270+
zeitwerk (2.7.3)
147271

148272
PLATFORMS
149273
arm64-darwin-21
@@ -159,11 +283,13 @@ DEPENDENCIES
159283
rack (>= 3.0.0)
160284
rack-test
161285
rackup
286+
rails
162287
rake
163288
rspec
164289
rubocop
165290
rubocop-performance
166291
simplecov
292+
sinatra
167293

168294
BUNDLED WITH
169295
2.3.10

README.md

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
# openapi_first
22

3-
openapi_first is a Ruby gem for request / response validation and contract-testing against an [OpenAPI](https://www.openapis.org/) 3.0 or 3.1 API description. It makes an APIFirst workflow easy and reliable.
3+
openapi_first is a Ruby gem for request / response validation and contract-testing against an [OpenAPI](https://www.openapis.org/) 3.0 or 3.1 Openapi API description (OAD). It makes an APIFirst workflow easy and reliable.
44

5-
You can use openapi_first on production for [request validation](#request-validation) and in your [tests](#contract-testing) to avoid API drift with it's request/response validation and coverage features.
5+
## Usage
6+
7+
Use an OAD to validate incoming requests in production:
8+
```ruby
9+
use OpenapiFirst::Middlewares::RequestValidation, 'openapi/openapi.yaml'
10+
```
11+
12+
Turn your request tests into contract tests against an OAD:
13+
```ruby
14+
# spec_helper.rb
15+
require 'openapi_first'
16+
OpenapiFirst::Test.setup do |config|
17+
config.register('openapi/openapi.yaml')
18+
end
19+
require 'application' # Load Application code
20+
OpenapiFirst::Test.observe(Application)
21+
```
622

723
## Contents
824

@@ -35,27 +51,28 @@ Here is how to set it up:
3551
This should go at the top of your test helper file before loading your application code.
3652
```ruby
3753
require 'openapi_first'
38-
OpenapiFirst::Test.setup do |test|
39-
test.register('openapi/openapi.yaml')
40-
# Optional: Make tests fail if coverage is below minimum
41-
test.minimum_coverage = 100
42-
# Optional: Skip certain responses, which are described in your API description, but need no test coverage
43-
test.skip_response_coverage { it.status == '500' } #
44-
end
45-
```
46-
2. Add an `app` method to your tests by including a Module. This `app` method wraps your application with silent request / response validation. This validates all requests/responses in your test run. (✷1)
47-
```ruby
48-
RSpec.configure do |config|
49-
config.include OpenapiFirst::Test::Methods[MyApp], type: :request
50-
end
51-
```
52-
Or add the `app` method yourself:
53-
54-
```ruby
55-
def app
56-
OpenapiFirst::Test.app(MyApp)
54+
OpenapiFirst::Test.setup do |config|
55+
config.register('openapi/openapi.yaml')
5756
end
5857
```
58+
2. Observe your application. You can do this in one of two ways:
59+
- Inject a Module to wrap (prepend) the `call` method of your Rack app Class.
60+
```ruby
61+
OpenapiFirst::Test.observe(MyApplication)
62+
```
63+
- Or add an `app` method to your tests, which wraps your application with silent request / response validation. (✷1)
64+
```ruby
65+
RSpec.configure do |config|
66+
config.include OpenapiFirst::Test::Methods[MyApp], type: :request
67+
end
68+
```
69+
Or add the `app` method yourself:
70+
71+
```ruby
72+
def app
73+
OpenapiFirst::Test.app(MyApp)
74+
end
75+
```
5976
3. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses.
6077

6178
(✷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.

benchmarks/Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: ..
33
specs:
4-
openapi_first (2.7.4)
4+
openapi_first (2.8.0)
55
hana (~> 1.3)
66
json_schemer (>= 2.1, < 3.0)
77
openapi_parameters (>= 0.5.1, < 2.0)
@@ -15,7 +15,7 @@ GEM
1515
benchmark-ips (2.14.0)
1616
benchmark-memory (0.2.0)
1717
memory_profiler (~> 1)
18-
bigdecimal (3.2.1)
18+
bigdecimal (3.2.2)
1919
committee (5.5.4)
2020
json_schema (~> 0.14, >= 0.14.3)
2121
openapi_parser (~> 2.0)

lib/openapi_first/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def clone
3030
HOOKS.each do |hook|
3131
define_method(hook) do |&block|
3232
hooks[hook] << block
33+
block
3334
end
3435
end
3536

lib/openapi_first/definition.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@ def validate_response(rack_request, rack_response, raise_error: false)
8686

8787
response_match = route.match_response(status: rack_response.status, content_type: rack_response.content_type)
8888
error = response_match.error
89-
if error
90-
ValidatedResponse.new(rack_response, error:)
91-
else
92-
response_match.response.validate(rack_response)
93-
end.tap do |validated|
94-
@config.hooks[:after_response_validation]&.each { |hook| hook.call(validated, rack_request, self) }
95-
raise validated.error.exception(validated) if raise_error && validated.invalid?
96-
end
89+
validated = if error
90+
ValidatedResponse.new(rack_response, error:)
91+
else
92+
response_match.response.validate(rack_response)
93+
end
94+
@config.hooks[:after_response_validation]&.each { |hook| hook.call(validated, rack_request, self) }
95+
raise validated.error.exception(validated) if raise_error && validated.invalid?
96+
97+
validated
9798
end
9899

99100
private

0 commit comments

Comments
 (0)