diff --git a/examples/openapi.yaml b/examples/openapi.yaml index d49713d4..89ccdf89 100644 --- a/examples/openapi.yaml +++ b/examples/openapi.yaml @@ -12,7 +12,7 @@ tags: paths: /: get: - operationId: things#index + operationId: example#root summary: Get metadata from the root of the API tags: ["Metadata"] responses: diff --git a/examples/rack_handler.rb b/examples/rack_handler.rb index 3e196545..9a23756f 100644 --- a/examples/rack_handler.rb +++ b/examples/rack_handler.rb @@ -10,7 +10,7 @@ not_found = ->(_request) { [404, {}, []] } handlers = { - 'things#index' => lambda do |_request| + 'example#root' => lambda do |_request| [200, { Rack::CONTENT_TYPE => 'application/json' }, ['{"hello": "world"}']] end } diff --git a/lib/openapi_first/test.rb b/lib/openapi_first/test.rb index 0515e2b2..ec61d2a2 100644 --- a/lib/openapi_first/test.rb +++ b/lib/openapi_first/test.rb @@ -22,8 +22,8 @@ def initialize yield self end - def register(*) - Test.register(*) + def register(oad, as: :default) + Test.register(oad, as:) end attr_accessor :minimum_coverage, :coverage_formatter_options, :coverage_formatter @@ -47,7 +47,7 @@ def handle_exit end return unless minimum_coverage > coverage - puts "API Coverage fails with exit 2, because API coverage of #{coverage}%" \ + puts "API Coverage fails with exit 2, because API coverage of #{coverage}% " \ "is below minimum of #{minimum_coverage}%!" exit 2 # :nocov: diff --git a/lib/openapi_first/test/coverage.rb b/lib/openapi_first/test/coverage.rb index 2c35d631..ad593b73 100644 --- a/lib/openapi_first/test/coverage.rb +++ b/lib/openapi_first/test/coverage.rb @@ -53,11 +53,11 @@ def reset end def track_request(request, oad) - current_run[oad.key].track_request(request) + current_run[oad.key]&.track_request(request) end def track_response(response, _request, oad) - current_run[oad.key].track_response(response) + current_run[oad.key]&.track_response(response) end def result diff --git a/lib/openapi_first/test/methods.rb b/lib/openapi_first/test/methods.rb index 02138540..52f5ba5f 100644 --- a/lib/openapi_first/test/methods.rb +++ b/lib/openapi_first/test/methods.rb @@ -7,21 +7,54 @@ module OpenapiFirst module Test # Methods to use in integration tests module Methods - def self.[](*) + def self.included(base) + base.include(DefaultApiMethod) + base.include(AssertionMethod) + end + + def self.[](application_under_test = nil, api: nil) mod = Module.new do def self.included(base) - OpenapiFirst::Test::Methods.included(base) + base.include OpenapiFirst::Test::Methods::AssertionMethod end end - mod.define_method(:app) { OpenapiFirst::Test.app(*) } + + if api + mod.define_method(:openapi_first_default_api) { api } + else + mod.include(DefaultApiMethod) + end + + if application_under_test + mod.define_method(:app) { OpenapiFirst::Test.app(application_under_test, api: openapi_first_default_api) } + end + mod end - def self.included(base) - if Test.minitest?(base) - base.include(OpenapiFirst::Test::MinitestHelpers) - else - base.include(OpenapiFirst::Test::PlainHelpers) + # Default methods + module DefaultApiMethod + # This is the default api that is used by assert_api_conform + # :default is the default name that is used if you don't pass an `api:` option to `OpenapiFirst::Test.register` + # This is overwritten if you pass an `api:` option to `include OpenapiFirst::Test::Methods[…]` + def openapi_first_default_api + klass = self.class + if klass.respond_to?(:metadata) && klass.metadata[:api] + klass.metadata[:api] + else + :default + end + end + end + + # @visibility private + module AssertionMethod + def self.included(base) + if Test.minitest?(base) + base.include(OpenapiFirst::Test::MinitestHelpers) + else + base.include(OpenapiFirst::Test::PlainHelpers) + end end end end diff --git a/lib/openapi_first/test/minitest_helpers.rb b/lib/openapi_first/test/minitest_helpers.rb index 30bebe4c..a8f73281 100644 --- a/lib/openapi_first/test/minitest_helpers.rb +++ b/lib/openapi_first/test/minitest_helpers.rb @@ -5,7 +5,7 @@ module Test # Assertion methods for Minitest module MinitestHelpers # :nocov: - def assert_api_conform(status: nil, api: :default) + def assert_api_conform(status: nil, api: openapi_first_default_api) api = OpenapiFirst::Test[api] request = respond_to?(:last_request) ? last_request : @request response = respond_to?(:last_response) ? last_response : @response diff --git a/lib/openapi_first/test/plain_helpers.rb b/lib/openapi_first/test/plain_helpers.rb index 73170417..573153b7 100644 --- a/lib/openapi_first/test/plain_helpers.rb +++ b/lib/openapi_first/test/plain_helpers.rb @@ -5,7 +5,7 @@ module Test # Assertion methods to use when no known test framework was found # These methods just raise an exception if an error was found module PlainHelpers - def assert_api_conform(status: nil, api: :default) + def assert_api_conform(status: nil, api: openapi_first_default_api) api = OpenapiFirst::Test[api] # :nocov: request = respond_to?(:last_request) ? last_request : @request diff --git a/spec/middlewares/request_validation/request_body_validation_spec.rb b/spec/middlewares/request_validation/request_body_validation_spec.rb index ae6cbfe4..fd073cfe 100644 --- a/spec/middlewares/request_validation/request_body_validation_spec.rb +++ b/spec/middlewares/request_validation/request_body_validation_spec.rb @@ -212,12 +212,35 @@ def fixture_path(name) end it 'accepts an empty request body' do + header Rack::CONTENT_TYPE, 'application/json' + post path + + expect(last_response.status).to be(200), last_response.body + expect(last_request.env[OpenapiFirst::REQUEST].parsed_body).to eq nil + end + + it 'accepts an empty request body without content-type' do + post path + + expect(last_response.status).to be(200), last_response.body + expect(last_request.env[OpenapiFirst::REQUEST].parsed_body).to eq nil + end + + it 'accepts an unknown content-type and an empty request body' do + header Rack::CONTENT_TYPE, 'foo/bar' post path expect(last_response.status).to be(200), last_response.body expect(last_request.env[OpenapiFirst::REQUEST].parsed_body).to eq nil end + it 'returns 400 if content-type is unknown and request body is invalid' do + header Rack::CONTENT_TYPE, 'foo/bar' + post path, JSON.generate({ say: 'no ' }) + + expect(last_response.status).to be(400), last_response.body + end + it 'returns 400 if request body is invalid' do header Rack::CONTENT_TYPE, 'application/json' post path, JSON.generate({ say: 'no ' }) diff --git a/spec/test/coverage_spec.rb b/spec/test/coverage_spec.rb index 80e88b5f..43bce6b7 100644 --- a/spec/test/coverage_spec.rb +++ b/spec/test/coverage_spec.rb @@ -65,4 +65,18 @@ specify { expect(result.coverage).to eq(50) } end end + + describe '.track_request' do + it 'ignores unregistered OADs' do + oad = double(key: 'unknown') + described_class.track_request(double, oad) + end + end + + describe '.track_response' do + it 'ignores unregistered OADs' do + oad = double(key: 'unknown') + described_class.track_response(double(:response), double(:request), oad) + end + end end diff --git a/spec/test/methods_spec.rb b/spec/test/methods_spec.rb index 63e2eccf..95b47f03 100644 --- a/spec/test/methods_spec.rb +++ b/spec/test/methods_spec.rb @@ -3,35 +3,16 @@ require 'minitest' RSpec.describe OpenapiFirst::Test::Methods do - it 'can be included' do - minitest_class = Class.new(Minitest::Test) do + it 'includes PlainHelpers when included' do + test_class = Class.new do include OpenapiFirst::Test::Methods end - expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) - - other_class = Class.new do - include OpenapiFirst::Test::Methods - end - expect(other_class.included_modules).to include(OpenapiFirst::Test::PlainHelpers) + expect(test_class.included_modules).to include(OpenapiFirst::Test::PlainHelpers) end - it 'adds an app method that wraps the app' do + it 'raises OpenapiFirst::Error when assertion fails' do OpenapiFirst::Test.register('./examples/openapi.yaml') - myapp = ->(_env) { Rack::Response.new('hello').finish } - minitest_class = Class.new(Minitest::Test) do - include OpenapiFirst::Test::Methods[myapp] - end - expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) - - test_app = minitest_class.new(1).app - env = Rack::MockRequest.env_for('/') - expect(test_app.call(env)).to eq(Rack::Response.new('hello').finish) - expect(env[OpenapiFirst::REQUEST]).to be_valid - end - - it 'detects wrong response status for Minitest' do - OpenapiFirst::Test.register('./examples/openapi.yaml') - minitest_class = Class.new(Minitest::Test) do + test_class = Class.new do include OpenapiFirst::Test::Methods def last_request = Rack::Request.new(Rack::MockRequest.env_for('/')) @@ -39,21 +20,153 @@ def last_response = Rack::Response.new end expect do - minitest_class.new('hey').assert_api_conform(status: 444) - end.to raise_error(Minitest::Assertion) + test_class.new.assert_api_conform(status: 444) + end.to raise_error(OpenapiFirst::Error) end - it 'detects wrong response status for non Minitest' do - OpenapiFirst::Test.register('./examples/openapi.yaml') - minitest_class = Class.new do + context 'with RSpec' do + let(:app) do + lambda do |_| + res = Rack::Response.new(JSON.generate(hello: 'there')) + res.content_type = 'application/json' + res.finish + end + end + + context 'with metadata', api: :v1 do include OpenapiFirst::Test::Methods + include Rack::Test::Methods - def last_request = Rack::Request.new(Rack::MockRequest.env_for('/')) - def last_response = Rack::Response.new + it 'targets that api when calling assert_api_conform' do + expect do + assert_api_conform(status: 200) + end.to raise_error(OpenapiFirst::Test::NotRegisteredError) do |ex| + expect(ex.message).to start_with("API description ':v1' not found.") + end + end end - expect do - minitest_class.new.assert_api_conform(status: 444) - end.to raise_error(OpenapiFirst::Error) + context 'with an [api:] option', api: :v2 do + include OpenapiFirst::Test::Methods[api: :v1] + + it 'targets the api from the argument when calling assert_api_conform' do + expect do + assert_api_conform(status: 200) + end.to raise_error(OpenapiFirst::Test::NotRegisteredError) do |ex| + expect(ex.message).to start_with("API description ':v1' not found.") + end + end + end + + context 'with an [Application] argument and metadata', api: :v2 do + include OpenapiFirst::Test::Methods[->(_) { Rack::Response.new('hey').finish }] + include Rack::Test::Methods + + it 'targets that api when calling the app' do + OpenapiFirst::Test.register('./examples/openapi.yaml', as: :v2) + + get('/') + + expect(last_request.env[OpenapiFirst::REQUEST].operation_id).to eq('example#root') + end + end + end + + context 'with Minitest' do + it 'includes MinitestHelpers when included' do + minitest_class = Class.new(Minitest::Test) do + include OpenapiFirst::Test::Methods + end + expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) + end + + it 'raises Minitest::Assertion when assertion fails' do + OpenapiFirst::Test.register('./examples/openapi.yaml') + minitest_class = Class.new(Minitest::Test) do + include OpenapiFirst::Test::Methods + + def last_request = Rack::Request.new(Rack::MockRequest.env_for('/')) + def last_response = Rack::Response.new + end + + expect do + minitest_class.new('hey').assert_api_conform(status: 444) + end.to raise_error(Minitest::Assertion) + end + end + + context 'with [arguments]' do + it 'adds an app method that wraps the default API' do + OpenapiFirst::Test.register('./examples/openapi.yaml') + myapp = ->(_env) { Rack::Response.new('hello').finish } + + minitest_class = Class.new(Minitest::Test) do + include OpenapiFirst::Test::Methods[myapp] + end + + expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) + test_app = minitest_class.new(1).app + env = Rack::MockRequest.env_for('/') + expect(test_app.call(env)).to eq(Rack::Response.new('hello').finish) + expect(env[OpenapiFirst::REQUEST]).to be_valid + end + + it 'adds an app method that wraps the app for a specific API' do + OpenapiFirst::Test.register('./examples/openapi.yaml', as: :v1) + myapp = ->(_env) { Rack::Response.new('hello').finish } + + minitest_class = Class.new(Minitest::Test) do + include OpenapiFirst::Test::Methods[myapp, api: :v1] + end + + expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) + test_app = minitest_class.new(1).app + env = Rack::MockRequest.env_for('/') + expect(test_app.call(env)).to eq(Rack::Response.new('hello').finish) + expect(env[OpenapiFirst::REQUEST]).to be_valid + end + + it 'adds an assert_api_conform method that targets the specified API' do + OpenapiFirst::Test.register('./examples/openapi.yaml', as: :v1) + test_class = Class.new do + include OpenapiFirst::Test::Methods[api: :v1] + + def last_request = Rack::Request.new(Rack::MockRequest.env_for('/')) + def last_response = Rack::Response.new + end + + expect(test_class.new.openapi_first_default_api).to eq(:v1) + + expect do + test_class.new.assert_api_conform(status: 444) + end.to raise_error(OpenapiFirst::Error) + end + + it 'adds an assert_api_conform method that still can target another API' do + OpenapiFirst::Test.register('./examples/openapi.yaml', as: :v1) + test_class = Class.new do + include OpenapiFirst::Test::Methods[api: :v1] + + def last_request = Rack::Request.new(Rack::MockRequest.env_for('/')) + def last_response = Rack::Response.new + end + + expect do + test_class.new.assert_api_conform(status: 444, api: :other) + end.to raise_error(OpenapiFirst::Test::NotRegisteredError) do |ex| + expect(ex.message).to start_with("API description ':other' not found.") + end + end + + it 'does not add an app method if app is nil' do + OpenapiFirst::Test.register('./examples/openapi.yaml', as: :v1) + + minitest_class = Class.new(Minitest::Test) do + include OpenapiFirst::Test::Methods[api: :v1] + end + + expect(minitest_class.included_modules).to include(OpenapiFirst::Test::MinitestHelpers) + expect(minitest_class.new(1).respond_to?(:app)).to eq(false) + end end end diff --git a/spec/test_spec.rb b/spec/test_spec.rb index 218848f7..61f4a226 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -24,8 +24,10 @@ expect(described_class[:default].filepath).to eq('./examples/openapi.yaml') end - it 'can register an OAD with a custom name' do + it 'can register multiple OADs' do + described_class.register('./spec/data/dice.yaml') described_class.register('./examples/openapi.yaml', as: :mine) + expect(described_class[:default].filepath).to eq('./spec/data/dice.yaml') expect(described_class[:mine].filepath).to eq('./examples/openapi.yaml') end