diff --git a/lib/rspec/openapi/extractors/hanami.rb b/lib/rspec/openapi/extractors/hanami.rb index ef3a9fd5..8eab32f6 100644 --- a/lib/rspec/openapi/extractors/hanami.rb +++ b/lib/rspec/openapi/extractors/hanami.rb @@ -52,6 +52,10 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new # @param [RSpec::Core::Example] example # @return Array def request_attributes(request, example) + route = Hanami.app.router.recognize(request.path, method: request.method) + + return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable? + metadata = example.metadata[:openapi] || {} summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) @@ -62,8 +66,6 @@ def request_attributes(request, example) deprecated = metadata[:deprecated] path = request.path - route = Hanami.app.router.recognize(request.path, method: request.method) - raw_path_params = route.params.filter { |_key, value| number_or_nil(value) } result = InspectorAnalyzer.call(request.method, add_id(path, route)) diff --git a/lib/rspec/openapi/extractors/rails.rb b/lib/rspec/openapi/extractors/rails.rb index ef269fbe..063f00af 100644 --- a/lib/rspec/openapi/extractors/rails.rb +++ b/lib/rspec/openapi/extractors/rails.rb @@ -6,6 +6,16 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new # @param [RSpec::Core::Example] example # @return Array def request_attributes(request, example) + # Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41 + fixed_request = request.dup + fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present? + + route, path = find_rails_route(fixed_request) + + raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil? + + return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless path + metadata = example.metadata[:openapi] || {} summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) @@ -16,14 +26,6 @@ def request_attributes(request, example) deprecated = metadata[:deprecated] raw_path_params = request.path_parameters - # Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41 - fixed_request = request.dup - fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present? - - route, path = find_rails_route(fixed_request) - raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil? - - path = path.delete_suffix('(.:format)') summary ||= route.requirements[:action] tags ||= [route.requirements[:controller]&.classify].compact # :controller and :action always exist. :format is added when routes is configured as such. @@ -42,12 +44,17 @@ def request_response(context) # @param [ActionDispatch::Request] request def find_rails_route(request, app: Rails.application, path_prefix: '') - app.routes.router.recognize(request) do |route| - path = route.path.spec.to_s + app.routes.router.recognize(request) do |route, parameters| + path = route.path.spec.to_s.delete_suffix('(.:format)') + if route.app.matches?(request) if route.app.engine? route, path = find_rails_route(request, app: route.app.app, path_prefix: path) next if route.nil? + elsif path_prefix + path == add_id(request.path, parameters) + return [route, path_prefix + path] + else + return [route, nil] end return [route, path_prefix + path] end @@ -55,4 +62,20 @@ def find_rails_route(request, app: Rails.application, path_prefix: '') nil end + + def add_id(path, parameters) + parameters.each_pair do |key, value| + next unless number_or_nil(value) + + path = path.sub("/#{value}", "/:#{key}") + end + + path + end + + def number_or_nil(string) + Integer(string || '') + rescue ArgumentError + nil + end end diff --git a/spec/apps/hanami/config/routes.rb b/spec/apps/hanami/config/routes.rb index 6d6caf52..1e281448 100644 --- a/spec/apps/hanami/config/routes.rb +++ b/spec/apps/hanami/config/routes.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'rack_test/app' module HanamiTest class Routes < Hanami::Routes @@ -35,5 +36,7 @@ class Routes < Hanami::Routes post '/extensions', to: 'extensions.create' end end + + use RackTest::App end end diff --git a/spec/apps/hanami/doc/openapi.json b/spec/apps/hanami/doc/openapi.json index cf4e3269..10ef5fbf 100644 --- a/spec/apps/hanami/doc/openapi.json +++ b/spec/apps/hanami/doc/openapi.json @@ -341,6 +341,42 @@ } } }, + "/rack/bar": { + "get": { + "summary": "GET /rack/bar", + "responses": { + "200": { + "description": "returns some content bar", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "A RACK BAR" + } + } + } + } + } + }, + "/rack/foo": { + "get": { + "summary": "GET /rack/foo", + "responses": { + "200": { + "description": "returns some content foo", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "A RACK FOO" + } + } + } + } + } + }, "/secret_items": { "get": { "summary": "index", diff --git a/spec/apps/hanami/doc/openapi.yaml b/spec/apps/hanami/doc/openapi.yaml index 8042346e..b64e6c1b 100644 --- a/spec/apps/hanami/doc/openapi.yaml +++ b/spec/apps/hanami/doc/openapi.yaml @@ -212,6 +212,28 @@ paths: schema: type: string example: ANOTHER TEST + "/rack/bar": + get: + summary: GET /rack/bar + responses: + '200': + description: returns some content bar + content: + text/plain: + schema: + type: string + example: A RACK BAR + "/rack/foo": + get: + summary: GET /rack/foo + responses: + '200': + description: returns some content foo + content: + text/plain: + schema: + type: string + example: A RACK FOO "/secret_items": get: summary: index diff --git a/spec/apps/hanami/lib/rack_test/app.rb b/spec/apps/hanami/lib/rack_test/app.rb new file mode 100644 index 00000000..7d2cea7f --- /dev/null +++ b/spec/apps/hanami/lib/rack_test/app.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RackTest + class App + def initialize(app) + @app = app + end + + def call(env) + req = Rack::Request.new(env) + path = req.path_info + + case path + when "/rack/foo" + [200, { 'Content-Type' => 'text/plain' }, ['A RACK FOO']] + when "/rack/bar" + [200, { 'Content-Type' => 'text/plain' }, ['A RACK BAR']] + else + return @app.call(env) + end + end + end +end diff --git a/spec/apps/rails/config/initializers/rack_test.rb b/spec/apps/rails/config/initializers/rack_test.rb new file mode 100644 index 00000000..b539c24c --- /dev/null +++ b/spec/apps/rails/config/initializers/rack_test.rb @@ -0,0 +1 @@ +require Rails.root.join('lib/rack_test/app') diff --git a/spec/apps/rails/config/routes.rb b/spec/apps/rails/config/routes.rb index 889043dd..1c6ff973 100644 --- a/spec/apps/rails/config/routes.rb +++ b/spec/apps/rails/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do mount ::MyEngine::Engine => '/my_engine' + mount ::RackTest::App.new, at: '/rack' get '/my_engine/test' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['ANOTHER TEST']] } diff --git a/spec/apps/rails/doc/openapi.json b/spec/apps/rails/doc/openapi.json index e048b2bf..8f7e3e8d 100644 --- a/spec/apps/rails/doc/openapi.json +++ b/spec/apps/rails/doc/openapi.json @@ -384,6 +384,42 @@ } } }, + "/rack/bar": { + "get": { + "summary": "GET /rack/bar", + "responses": { + "200": { + "description": "returns some content bar", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "A RACK BAR" + } + } + } + } + } + }, + "/rack/foo": { + "get": { + "summary": "GET /rack/foo", + "responses": { + "200": { + "description": "returns some content foo", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "A RACK FOO" + } + } + } + } + } + }, "/secret_items": { "get": { "summary": "index", diff --git a/spec/apps/rails/doc/openapi.yaml b/spec/apps/rails/doc/openapi.yaml index dbafbec8..f18c52e2 100644 --- a/spec/apps/rails/doc/openapi.yaml +++ b/spec/apps/rails/doc/openapi.yaml @@ -239,6 +239,28 @@ paths: schema: type: string example: ANOTHER TEST + "/rack/bar": + get: + summary: GET /rack/bar + responses: + '200': + description: returns some content bar + content: + text/plain: + schema: + type: string + example: A RACK BAR + "/rack/foo": + get: + summary: GET /rack/foo + responses: + '200': + description: returns some content foo + content: + text/plain: + schema: + type: string + example: A RACK FOO "/secret_items": get: summary: index diff --git a/spec/apps/rails/lib/rack_test/app.rb b/spec/apps/rails/lib/rack_test/app.rb new file mode 100644 index 00000000..886688df --- /dev/null +++ b/spec/apps/rails/lib/rack_test/app.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RackTest + class App + def call(env) + req = Rack::Request.new(env) + path = req.path_info + + case path + when "/foo" + [200, { 'Content-Type' => 'text/plain' }, ['A RACK FOO']] + when "/bar" + [200, { 'Content-Type' => 'text/plain' }, ['A RACK BAR']] + end + end + end +end diff --git a/spec/integration_tests/rails_test.rb b/spec/integration_tests/rails_test.rb index 27793de3..ef65946f 100644 --- a/spec/integration_tests/rails_test.rb +++ b/spec/integration_tests/rails_test.rb @@ -273,3 +273,18 @@ class NamespaceTest < ActionDispatch::IntegrationTest assert_response 200 end end + +class RackAppTest < ActionDispatch::IntegrationTest + i_suck_and_my_tests_are_order_dependent! + openapi! + + test 'returns some content foo' do + get '/rack/foo/' + assert_response 200 + end + + test 'returns some content bar' do + get '/rack/bar' + assert_response 200 + end +end diff --git a/spec/requests/hanami_spec.rb b/spec/requests/hanami_spec.rb index 144332c0..a1951186 100644 --- a/spec/requests/hanami_spec.rb +++ b/spec/requests/hanami_spec.rb @@ -281,3 +281,19 @@ end end end + +RSpec.describe 'Rack app test', type: :request do + describe '/rack/foo' do + it 'returns some content foo' do + get '/rack/foo' + expect(last_response.status).to eq(200) + end + end + + describe '/rack/bar' do + it 'returns some content bar' do + get '/rack/bar' + expect(last_response.status).to eq(200) + end + end +end diff --git a/spec/requests/rails_spec.rb b/spec/requests/rails_spec.rb index c4e9ca5e..b41c93f3 100644 --- a/spec/requests/rails_spec.rb +++ b/spec/requests/rails_spec.rb @@ -270,3 +270,19 @@ end end end + +RSpec.describe 'Rack app test', type: :request do + describe '/rack/foo' do + it 'returns some content foo' do + get '/rack/foo' + expect(response.status).to eq(200) + end + end + + describe '/rack/bar' do + it 'returns some content bar' do + get '/rack/bar' + expect(response.status).to eq(200) + end + end +end