Skip to content

Commit 7daefb3

Browse files
committed
Added Test.setup { it.observe } and low-level Test::Callable[]
Added `Test.setup { it.observe }` and low-level `Test::Callable[]` to inject request/response validation in rack app as an alternative to overwrite the `app` method in a test
1 parent 0837b26 commit 7daefb3

File tree

5 files changed

+111
-7
lines changed

5 files changed

+111
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## Unreleased
44

55
OpenapiFirst::Test is stricter and more configurable:
6-
- Added `OpenapiFirst::Test::Configuration#ignored_unknown_status` to configure response status(es) that do not have to be descriped in the API description.
6+
- Added `Test.setup { it.observe }` and low level `Test::Callable[]` to inject request/response validation in rack app as an alternative to overwrite the `app` method in a test
7+
- Added `Test.observe(App, api: :my_api)`
8+
- Added Test::Configuration#ignored_unknown_status` to configure response status(es) that do not have to be descriped in the API description.
79
- 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 `report_coverage = false` or `report_coverage = :warn`
810
- Added `OpenapiFirst::Test::Configuration#report_coverage=` to configure the behavior if not all requests/responses of the API under test have been tested.
911
- Deprecated `OpenapiFirst::Test::Configuration#minimum_coverage=` use "#report_coverage=true/false/:info" instead to modify the behavior

lib/openapi_first/test.rb

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,21 @@ module OpenapiFirst
88
module Test
99
autoload :Coverage, 'openapi_first/test/coverage'
1010
autoload :Methods, 'openapi_first/test/methods'
11+
autoload :Callable, 'openapi_first/test/callable'
1112
extend Registry
1213

1314
class CoverageError < Error; end
1415

16+
# @visible private
17+
module Observed; end
18+
19+
# Inject request/response validation in a rack app class
20+
def self.observe(app, api: :default)
21+
mod = Callable[api:]
22+
app.prepend(mod) unless app.include?(Observed)
23+
app.include(Observed)
24+
end
25+
1526
def self.minitest?(base)
1627
base.include?(::Minitest::Assertions)
1728
rescue NameError
@@ -31,9 +42,9 @@ def self.setup
3142

3243
install
3344
yield configuration
34-
configuration.registry.each do |name, oad|
35-
register(oad, as: name)
36-
end
45+
46+
configuration.registry.each { |name, oad| register(oad, as: name) }
47+
configuration.apps.each { |name, app| observe(app, api: name) }
3748
Coverage.start(skip_response: configuration.skip_response_coverage)
3849

3950
if definitions.empty?
@@ -61,9 +72,10 @@ def self.handle_exit
6172
)
6273
return unless configuration.report_coverage == true
6374

64-
return if Coverage.result.coverage >= configuration.minimum_coverage
75+
coverage = Coverage.result.coverage
76+
return if coverage >= configuration.minimum_coverage
6577

66-
puts 'API Coverage fails with exit 2, because not all described requests and responses have been tested.'
78+
puts "API Coverage fails with exit 2, because not all described requests and responses have been tested (#{coverage.round(4)}% covered)." # rubocop:disable Layout/LineLength
6779

6880
exit 2
6981
end

lib/openapi_first/test/callable.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module OpenapiFirst
4+
module Test
5+
# Return a Module with a call method that wrapps silent request/response validation to monitor a Rack app
6+
# This is used by Openapi::Test.observe
7+
module Callable
8+
# Returns a Module with a `call(env)` method that wraps super inside silent request/response validation
9+
# You can use this like `Application.prepend(OpenapiFirst::Test.app_module)` to monitor your app during testing.
10+
def self.[](api: :default)
11+
definition = OpenapiFirst::Test[api]
12+
Module.new.tap do |mod|
13+
mod.define_method(:call) do |env|
14+
request = Rack::Request.new(env)
15+
definition.validate_request(request, raise_error: false)
16+
response = super(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
24+
end
25+
end

lib/openapi_first/test/configuration.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@ def initialize
1313
@ignored_unknown_status = [404]
1414
@report_coverage = true
1515
@registry = {}
16+
@apps = {}
1617
end
1718

1819
# Register OADs, but don't load them just yet
20+
# @param [OpenapiFirst::OAD] oad The OAD to register
21+
# @param [Symbol] as The name to register the OAD under
1922
def register(oad, as: :default)
2023
@registry[as] = oad
2124
end
2225

26+
# Observe a rack app
27+
def observe(app, api: :default)
28+
@apps[api] = app
29+
end
30+
2331
attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error
24-
attr_reader :registry, :minimum_coverage, :report_coverage, :ignored_unknown_status
32+
attr_reader :registry, :apps, :minimum_coverage, :report_coverage, :ignored_unknown_status
2533

2634
def minimum_coverage=(value)
2735
warn 'Setting OpenapiFirst::Test::Configuration#minimum_coverage= is deprecated ' \
@@ -30,6 +38,8 @@ def minimum_coverage=(value)
3038
@minimum_coverage = value
3139
end
3240

41+
# Configure report coverage
42+
# @param [Boolean, :warn] value Whether to report coverage or just warn.
3343
def report_coverage=(value)
3444
allowed_values = [true, false, :warn]
3545
unless allowed_values.include?(value)

spec/test_spec.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,51 @@
33
require 'minitest'
44

55
RSpec.describe OpenapiFirst::Test do
6+
let(:definition) { OpenapiFirst.load('./examples/openapi.yaml') }
7+
8+
let(:app) do
9+
Class.new do
10+
def call(_env)
11+
Rack::Response.new.finish
12+
end
13+
end
14+
end
15+
16+
describe 'Callable[]' do
17+
it 'returns a Module that can call the api' do
18+
described_class.register(definition, as: :some)
19+
mod = described_class::Callable[api: :some]
20+
app.prepend(mod)
21+
22+
expect(definition).to receive(:validate_request)
23+
expect(definition).to receive(:validate_response)
24+
25+
app.new.call({})
26+
end
27+
end
28+
29+
describe '.observe' do
30+
it 'injects request/response validation in the app' do
31+
described_class.register(definition, as: :some)
32+
described_class.observe(app, api: :some)
33+
34+
expect(definition).to receive(:validate_request)
35+
expect(definition).to receive(:validate_response)
36+
37+
app.new.call({})
38+
end
39+
40+
it 'injects request/response validation just once' do
41+
described_class.register(definition, as: :some)
42+
2.times { described_class.observe(app, api: :some) }
43+
44+
expect(definition).to receive(:validate_request).once
45+
expect(definition).to receive(:validate_response).once
46+
47+
app.new.call({})
48+
end
49+
end
50+
651
describe '.minitest?' do
752
it 'detects minitest' do
853
test_case = Class.new(Minitest::Test)
@@ -81,6 +126,16 @@
81126
expect(described_class.definitions[:default].filepath).to eq(OpenapiFirst.load('./examples/openapi.yaml').filepath)
82127
end
83128

129+
it 'observes an app' do
130+
described_class.setup do |test|
131+
test.register(definition, as: :some)
132+
test.observe(app, api: :some)
133+
end
134+
135+
expect(definition).to receive(:validate_request)
136+
app.new.call({})
137+
end
138+
84139
it 'sets up minimum_coverage' do
85140
described_class.setup do |test|
86141
test.register('./examples/openapi.yaml')

0 commit comments

Comments
 (0)