From 82171f7f3adb6b11bef714575359fd389249dfb9 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Fri, 12 May 2023 13:21:44 +0400 Subject: [PATCH 01/20] Add required request params to metadata --- README.md | 1 + lib/rspec/openapi/record.rb | 1 + lib/rspec/openapi/record_builder.rb | 6 ++++-- lib/rspec/openapi/schema_builder.rb | 1 + spec/rails/doc/openapi.json | 4 ++++ spec/rails/doc/openapi.yaml | 14 +++++++++----- spec/rails/doc/smart/expected.yaml | 3 +++ 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0999ebbb..be4d7d3f 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example summary: 'list all posts', description: 'list all posts ordered by pub_date', tags: %w[v1 posts], + required_request_params: %w[limit], security: [{"MyToken" => []}], } do # ... diff --git a/lib/rspec/openapi/record.rb b/lib/rspec/openapi/record.rb index f86ea4dd..cbba7be3 100644 --- a/lib/rspec/openapi/record.rb +++ b/lib/rspec/openapi/record.rb @@ -6,6 +6,7 @@ :path_params, # @param [Hash] - {:controller=>"v1/statuses", :action=>"create", :id=>"1"} :query_params, # @param [Hash] - {:query=>"string"} :request_params, # @param [Hash] - {:request=>"body"} + :required_request_params, # @param [Array] - ["param1", "param2"] :request_content_type, # @param [String] - "application/json" :request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]] :summary, # @param [String] - "v1/statuses #show" diff --git a/lib/rspec/openapi/record_builder.rb b/lib/rspec/openapi/record_builder.rb index 26e02b57..2d9d0e99 100644 --- a/lib/rspec/openapi/record_builder.rb +++ b/lib/rspec/openapi/record_builder.rb @@ -11,7 +11,7 @@ def build(context, example:) request, response = extract_request_response(context) return if request.nil? - path, summary, tags, raw_path_params, description, security = extract_request_attributes(request, example) + path, summary, tags, required_request_params, raw_path_params, description, security = extract_request_attributes(request, example) request_headers, response_headers = extract_headers(request, response) @@ -21,6 +21,7 @@ def build(context, example:) path_params: raw_path_params, query_params: request.query_parameters, request_params: raw_request_params(request), + required_request_params: required_request_params, request_content_type: request.media_type, request_headers: request_headers, summary: summary, @@ -61,6 +62,7 @@ def extract_request_attributes(request, example) metadata = example.metadata[:openapi] || {} summary = metadata[:summary] tags = metadata[:tags] + required_request_params = metadata[:required_request_params] || [], security = metadata[:security] description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) raw_path_params = request.path_parameters @@ -75,7 +77,7 @@ def extract_request_attributes(request, example) raw_path_params = raw_path_params.slice(*(raw_path_params.keys - %i[controller action format])) end summary ||= "#{request.method} #{path}" - [path, summary, tags, raw_path_params, description, security] + [path, summary, tags, required_request_params, raw_path_params, description, security] end def extract_request_response(context) diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 5a65cb0f..4d2bb2b7 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -73,6 +73,7 @@ def build_parameters(record) parameters << { name: build_parameter_name(key, value), in: 'query', + required: record.required_request_params.include?(key), schema: build_property(try_cast(value)), example: (try_cast(value) if example_enabled?), }.compact diff --git a/spec/rails/doc/openapi.json b/spec/rails/doc/openapi.json index 9f87dab8..9b0a9559 100644 --- a/spec/rails/doc/openapi.json +++ b/spec/rails/doc/openapi.json @@ -141,6 +141,7 @@ { "name": "filter[name]", "in": "query", + "required": false, "schema": { "type": "object", "properties": { @@ -159,6 +160,7 @@ { "name": "filter[price]", "in": "query", + "required": false, "schema": { "type": "object", "properties": { @@ -177,6 +179,7 @@ { "name": "page", "in": "query", + "required": false, "schema": { "type": "integer" }, @@ -185,6 +188,7 @@ { "name": "per", "in": "query", + "required": false, "schema": { "type": "integer" }, diff --git a/spec/rails/doc/openapi.yaml b/spec/rails/doc/openapi.yaml index 0139283f..b78290b5 100644 --- a/spec/rails/doc/openapi.yaml +++ b/spec/rails/doc/openapi.yaml @@ -28,6 +28,7 @@ paths: example: token - name: filter[name] in: query + required: false schema: type: object properties: @@ -39,6 +40,7 @@ paths: name: Example Table - name: filter[price] in: query + required: false schema: type: object properties: @@ -50,11 +52,13 @@ paths: price: '0' - name: page in: query + required: false schema: type: integer example: 1 - name: per in: query + required: false schema: type: integer example: 10 @@ -109,7 +113,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -203,7 +207,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -268,7 +272,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -381,7 +385,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -445,7 +449,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' diff --git a/spec/rails/doc/smart/expected.yaml b/spec/rails/doc/smart/expected.yaml index e32f02fb..6303668e 100644 --- a/spec/rails/doc/smart/expected.yaml +++ b/spec/rails/doc/smart/expected.yaml @@ -28,16 +28,19 @@ paths: example: token - name: page in: query + required: false schema: type: integer example: 42 - name: per in: query + required: false schema: type: integer example: 10 - name: show_columns in: query + required: false schema: type: string example: "true" From df045002cf9d4bcd786afa2915e6c04bd4815d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Sat, 13 May 2023 14:33:19 +0900 Subject: [PATCH 02/20] Enable coverage report (#106) * Enable coverage report * Collect coverage across multiple processes --- .github/workflows/test.yml | 8 ++++++++ .simplecov_spawn.rb | 9 +++++++++ Gemfile | 2 ++ spec/spec_helper.rb | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .simplecov_spawn.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b579c412..a89032c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,11 +26,19 @@ jobs: rails: 6.1.6 - ruby: ruby:3.1 rails: 7.0.3 + coverage: coverage env: RAILS_VERSION: ${{ matrix.rails == '' && '6.1.6' || matrix.rails }} + COVERAGE: ${{ matrix.coverage || '' }} steps: - uses: actions/checkout@v2 - name: bundle install run: bundle install -j$(nproc) --retry 3 - run: bundle exec rspec timeout-minutes: 1 + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + if: matrix.coverage == 'coverage' + with: + fail_ci_if_error: true + files: ./coverage/coverage.xml diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb new file mode 100644 index 00000000..69dbb79e --- /dev/null +++ b/.simplecov_spawn.rb @@ -0,0 +1,9 @@ +unless ENV["COVERAGE"].empty? + require 'simplecov' + require 'simplecov-cobertura' + + SimpleCov.command_name 'spawn' + SimpleCov.at_fork.call(Process.pid) + SimpleCov.formatter SimpleCov::Formatter::CoberturaFormatter + SimpleCov.start +end \ No newline at end of file diff --git a/Gemfile b/Gemfile index 09f8e80a..cc9124e7 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,8 @@ gem 'rspec-rails' group :test do gem 'super_diff' + gem 'simplecov' + gem 'simplecov-cobertura' end group :development do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f2f3dd1b..c22dd54d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,7 +17,7 @@ def run_tests(*args, command:, openapi: false, output: :yaml) env = { 'OPENAPI' => ('1' if openapi), 'OPENAPI_OUTPUT' => output.to_s }.compact Bundler.public_send(Bundler.respond_to?(:with_unbundled_env) ? :with_unbundled_env : :with_clean_env) do Dir.chdir(repo_root) do - assert_run env, 'bundle', 'exec', command, *args + assert_run env, 'bundle', 'exec', command, '-r./.simplecov_spawn', *args end end end From 8120184e238b85683bb3f104f77697e7ef1d6a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Sat, 13 May 2023 15:16:37 +0900 Subject: [PATCH 03/20] Add coverage badge (#115) * Add coverage badge * Change repo owner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0999ebbb..a42c10b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rspec-openapi ![test](https://github.com/k0kubun/rspec-openapi/workflows/test/badge.svg) +# rspec-openapi ![test](https://github.com/exoego/rspec-openapi/workflows/test/badge.svg) [![codecov](https://codecov.io/gh/exoego/rspec-openapi/branch/master/graph/badge.svg?token=egYm6AlxkD)](https://codecov.io/gh/exoego/rspec-openapi) Generate OpenAPI schema from RSpec request specs. From dbbd5b2b95b3f7a1616a1c572c1812c08ddcc7bc Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Sat, 13 May 2023 12:01:41 +0400 Subject: [PATCH 04/20] add required_request_params example to specs --- lib/rspec/openapi/record_builder.rb | 2 +- spec/rails/doc/smart/expected.yaml | 2 +- spec/requests/rails_smart_merge_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rspec/openapi/record_builder.rb b/lib/rspec/openapi/record_builder.rb index 2d9d0e99..5f816508 100644 --- a/lib/rspec/openapi/record_builder.rb +++ b/lib/rspec/openapi/record_builder.rb @@ -62,7 +62,7 @@ def extract_request_attributes(request, example) metadata = example.metadata[:openapi] || {} summary = metadata[:summary] tags = metadata[:tags] - required_request_params = metadata[:required_request_params] || [], + required_request_params = metadata[:required_request_params] || [] security = metadata[:security] description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) raw_path_params = request.path_parameters diff --git a/spec/rails/doc/smart/expected.yaml b/spec/rails/doc/smart/expected.yaml index 6303668e..6e5536d4 100644 --- a/spec/rails/doc/smart/expected.yaml +++ b/spec/rails/doc/smart/expected.yaml @@ -40,7 +40,7 @@ paths: example: 10 - name: show_columns in: query - required: false + required: true schema: type: string example: "true" diff --git a/spec/requests/rails_smart_merge_spec.rb b/spec/requests/rails_smart_merge_spec.rb index 847ce028..72002191 100644 --- a/spec/requests/rails_smart_merge_spec.rb +++ b/spec/requests/rails_smart_merge_spec.rb @@ -36,7 +36,7 @@ # Small subset of `rails_spec.rb` with slight changes RSpec.describe 'Tables', type: :request do - describe '#index' do + describe '#index', openapi: { required_request_params: 'show_columns' } do context it 'returns a list of tables' do it 'with flat query parameters' do # These new params replace them in old spec From bcfd2de95a6225d8fd2bb1ea2919ab31d86c193a Mon Sep 17 00:00:00 2001 From: Bijan Rahnema Date: Mon, 15 May 2023 22:31:58 +0200 Subject: [PATCH 05/20] Add instructions for minitest generation. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a42c10b8..a05fc777 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,12 @@ It should work with both classes inheriting from `ActionDispatch::IntegrationTes Please note that not all features present in the rspec integration work with minitest (yet). For example, custom per test case metadata is not supported. A custom `description_builder` will not work either. +Run minitest with OPENAPI=1 to generate `doc/openapi.yaml` for your request specs. + +```bash +$ OPENAPI=1 bundle exec rails t +``` + ## Links Existing RSpec plugins which have OpenAPI integration: From ad2913a098a8c9216fc1a1fd97b98079d99f5fc2 Mon Sep 17 00:00:00 2001 From: t0yohei Date: Sun, 21 May 2023 21:17:17 +0900 Subject: [PATCH 06/20] fix multiple uploaded file examples --- lib/rspec/openapi/schema_builder.rb | 5 +++++ spec/integration_tests/rails_test.rb | 6 ++---- spec/rails/doc/openapi.json | 19 +++++++++++++------ spec/rails/doc/openapi.yaml | 15 ++++++++++----- spec/requests/rails_spec.rb | 7 +++---- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 5a65cb0f..23bd8cd4 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -195,6 +195,11 @@ def adjust_params(value) value[key] = v.original_filename elsif v.is_a?(Hash) adjust_params(v) + elsif v.is_a?(Array) + result = v.map do |item| + item.is_a?(ActionDispatch::Http::UploadedFile) ? item.original_filename : item + end + value[key] = result end end end diff --git a/spec/integration_tests/rails_test.rb b/spec/integration_tests/rails_test.rb index da0a723d..42f40c03 100644 --- a/spec/integration_tests/rails_test.rb +++ b/spec/integration_tests/rails_test.rb @@ -103,14 +103,12 @@ class TablesCreateTest < ActionDispatch::IntegrationTest class TablesUpdateTest < ActionDispatch::IntegrationTest openapi! - test 'returns a table' do + test 'returns a table with array' do png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP QBoAAAAASUVORK5CYII='.unpack1('m') File.binwrite('test.png', png) image = Rack::Test::UploadedFile.new('test.png', 'image/png') - patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { - nested: { image: image, caption: 'Some caption' }, - } + patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { images: [image, image] } assert_response 200 end end diff --git a/spec/rails/doc/openapi.json b/spec/rails/doc/openapi.json index 9f87dab8..a12acd76 100644 --- a/spec/rails/doc/openapi.json +++ b/spec/rails/doc/openapi.json @@ -474,24 +474,31 @@ "image", "caption" ] + }, + "images": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } } }, "required": [ - "nested" + "images" ] }, "example": { - "nested": { - "image": "test.png", - "caption": "Some caption" - } + "images": [ + "test.png", + "test.png" + ] } } } }, "responses": { "200": { - "description": "returns a table", + "description": "returns a table with array", "content": { "application/json": { "schema": { diff --git a/spec/rails/doc/openapi.yaml b/spec/rails/doc/openapi.yaml index 0139283f..f9488a6a 100644 --- a/spec/rails/doc/openapi.yaml +++ b/spec/rails/doc/openapi.yaml @@ -326,15 +326,20 @@ paths: required: - image - caption + images: + type: array + items: + type: string + format: binary required: - - nested + - images example: - nested: - image: test.png - caption: Some caption + images: + - test.png + - test.png responses: '200': - description: returns a table + description: returns a table with array content: application/json: schema: diff --git a/spec/requests/rails_spec.rb b/spec/requests/rails_spec.rb index febdce15..0256a245 100644 --- a/spec/requests/rails_spec.rb +++ b/spec/requests/rails_spec.rb @@ -90,10 +90,9 @@ File.binwrite('test.png', png) end let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } - it 'returns a table' do - patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { - nested: { image: image, caption: 'Some caption' }, - } + + it 'returns a table with array' do + patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { images: [image, image] } expect(response.status).to eq(200) end end From 7e30bac3b2f02b1eecdc37af222b0e17b94931fc Mon Sep 17 00:00:00 2001 From: t0yohei Date: Mon, 22 May 2023 00:57:59 +0900 Subject: [PATCH 07/20] separate image upload tests --- spec/integration_tests/rails_test.rb | 35 +++- .../app/controllers/images_controller.rb | 24 ++- spec/rails/config/routes.rb | 8 +- spec/rails/doc/openapi.json | 182 +++++++++++++++--- spec/rails/doc/openapi.yaml | 120 +++++++++--- spec/requests/rails_spec.rb | 53 ++++- 6 files changed, 352 insertions(+), 70 deletions(-) diff --git a/spec/integration_tests/rails_test.rb b/spec/integration_tests/rails_test.rb index 42f40c03..4fb48930 100644 --- a/spec/integration_tests/rails_test.rb +++ b/spec/integration_tests/rails_test.rb @@ -103,12 +103,8 @@ class TablesCreateTest < ActionDispatch::IntegrationTest class TablesUpdateTest < ActionDispatch::IntegrationTest openapi! - test 'returns a table with array' do - png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP - QBoAAAAASUVORK5CYII='.unpack1('m') - File.binwrite('test.png', png) - image = Rack::Test::UploadedFile.new('test.png', 'image/png') - patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { images: [image, image] } + test 'returns a table' do + patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { name: 'test' } assert_response 200 end end @@ -139,6 +135,33 @@ class ImageTest < ActionDispatch::IntegrationTest get '/images' assert_response 200 end + + test 'returns a image payload with upload' do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + image = Rack::Test::UploadedFile.new('test.png', 'image/png') + post '/images/upload', params: { image: image } + assert_response 200 + end + + test 'returns a image payload with upload nested' do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + image = Rack::Test::UploadedFile.new('test.png', 'image/png') + post '/images/upload_nested', params: { nested_image: { image: image, caption: 'Some caption' } } + assert_response 200 + end + + test 'returns a image payload with upload multiple' do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + image = Rack::Test::UploadedFile.new('test.png', 'image/png') + post '/images/upload_multiple', params: { images: [image, image] } + assert_response 200 + end end class ExtraRoutesTest < ActionDispatch::IntegrationTest diff --git a/spec/rails/app/controllers/images_controller.rb b/spec/rails/app/controllers/images_controller.rb index a750581c..bcb49eb7 100644 --- a/spec/rails/app/controllers/images_controller.rb +++ b/spec/rails/app/controllers/images_controller.rb @@ -1,8 +1,6 @@ class ImagesController < ApplicationController def show - png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP - QBoAAAAASUVORK5CYII='.unpack('m').first - send_data png, type: 'image/png', disposition: 'inline' + send_image end def index @@ -14,4 +12,24 @@ def index ] render json: list end + + def upload + send_image + end + + def upload_nested + send_image + end + + def upload_multiple + send_image + end + + private + + def send_image + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack('m').first + send_data png, type: 'image/png', disposition: 'inline' + end end diff --git a/spec/rails/config/routes.rb b/spec/rails/config/routes.rb index 5d76adf4..ad083baf 100644 --- a/spec/rails/config/routes.rb +++ b/spec/rails/config/routes.rb @@ -3,7 +3,13 @@ defaults format: 'json' do resources :tables, only: [:index, :show, :create, :update, :destroy] - resources :images, only: [:index, :show] + resources :images, only: [:index, :show] do + collection do + post 'upload' + post 'upload_nested' + post 'upload_multiple' + end + end resources :users, only: [:show, :create] get '/test_block' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['A TEST']] } diff --git a/spec/rails/doc/openapi.json b/spec/rails/doc/openapi.json index a12acd76..9cccb728 100644 --- a/spec/rails/doc/openapi.json +++ b/spec/rails/doc/openapi.json @@ -455,50 +455,27 @@ ], "requestBody": { "content": { - "multipart/form-data": { + "application/x-www-form-urlencoded": { "schema": { "type": "object", "properties": { - "nested": { - "type": "object", - "properties": { - "image": { - "type": "string", - "format": "binary" - }, - "caption": { - "type": "string" - } - }, - "required": [ - "image", - "caption" - ] - }, - "images": { - "type": "array", - "items": { - "type": "string", - "format": "binary" - } + "name": { + "type": "string" } }, "required": [ - "images" + "name" ] }, "example": { - "images": [ - "test.png", - "test.png" - ] + "name": "test" } } } }, "responses": { "200": { - "description": "returns a table with array", + "description": "returns a table", "content": { "application/json": { "schema": { @@ -804,6 +781,153 @@ } } } + }, + "/images/upload_multiple": { + "post": { + "summary": "upload_multiple", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "images": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + } + }, + "required": [ + "images" + ] + }, + "example": { + "images": [ + "test.png", + "test.png" + ] + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload multiple", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload_nested": { + "post": { + "summary": "upload_nested", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "nested_image": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + }, + "caption": { + "type": "string" + } + }, + "required": [ + "image", + "caption" + ] + } + }, + "required": [ + "nested_image" + ] + }, + "example": { + "nested_image": { + "image": "test.png", + "caption": "Some caption" + } + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload nested", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload": { + "post": { + "summary": "upload", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "image" + ] + }, + "example": { + "image": "test.png" + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/spec/rails/doc/openapi.yaml b/spec/rails/doc/openapi.yaml index f9488a6a..d37600f5 100644 --- a/spec/rails/doc/openapi.yaml +++ b/spec/rails/doc/openapi.yaml @@ -311,35 +311,19 @@ paths: example: 1 requestBody: content: - multipart/form-data: + application/x-www-form-urlencoded: schema: type: object properties: - nested: - type: object - properties: - image: - type: string - format: binary - caption: - type: string - required: - - image - - caption - images: - type: array - items: - type: string - format: binary + name: + type: string required: - - images + - name example: - images: - - test.png - - test.png + name: test responses: '200': - description: returns a table with array + description: returns a table content: application/json: schema: @@ -538,3 +522,95 @@ paths: example: - name: file.png tags: [] + "/images/upload_multiple": + post: + summary: upload_multiple + tags: + - Image + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + images: + type: array + items: + type: string + format: binary + required: + - images + example: + images: + - test.png + - test.png + responses: + '200': + description: returns a image payload with upload multiple + content: + image/png: + schema: + type: string + format: binary + "/images/upload": + post: + summary: upload + tags: + - Image + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + image: + type: string + format: binary + required: + - image + example: + image: test.png + responses: + '200': + description: returns a image payload with upload + content: + image/png: + schema: + type: string + format: binary + "/images/upload_nested": + post: + summary: upload_nested + tags: + - Image + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + nested_image: + type: object + properties: + image: + type: string + format: binary + caption: + type: string + required: + - image + - caption + required: + - nested_image + example: + nested_image: + image: test.png + caption: Some caption + responses: + '200': + description: returns a image payload with upload nested + content: + image/png: + schema: + type: string + format: binary diff --git a/spec/requests/rails_spec.rb b/spec/requests/rails_spec.rb index 0256a245..d97e1512 100644 --- a/spec/requests/rails_spec.rb +++ b/spec/requests/rails_spec.rb @@ -84,15 +84,8 @@ end describe '#update' do - before do - png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP - QBoAAAAASUVORK5CYII='.unpack1('m') - File.binwrite('test.png', png) - end - let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } - - it 'returns a table with array' do - patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { images: [image, image] } + it 'returns a table' do + patch '/tables/1', headers: { authorization: 'k0kubun' }, params: { name: 'test' } expect(response.status).to eq(200) end end @@ -124,6 +117,48 @@ expect(response.status).to eq(200) end end + + describe '#upload' do + before do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + end + let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } + + it 'returns a image payload with upload' do + post '/images/upload', params: { image: image } + expect(response.status).to eq(200) + end + end + + describe '#upload_nested' do + before do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + end + let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } + + it 'returns a image payload with upload nested' do + post '/images/upload_nested', params: { nested_image: { image: image, caption: 'Some caption' } } + expect(response.status).to eq(200) + end + end + + describe '#upload_multiple' do + before do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + end + let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } + + it 'returns a image payload with upload multiple' do + post '/images/upload_multiple', params: { images: [image, image] } + expect(response.status).to eq(200) + end + end end RSpec.describe 'Extra routes', type: :request do From d5cfe61a39aede186f1f127a701b36921db3bf9c Mon Sep 17 00:00:00 2001 From: Alexey Matskevich <45947106+AlexeyMatskevich@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:50:53 +0600 Subject: [PATCH 08/20] Update schema_cleaner.rb undefined method `empty?' for nil:NilClass --- lib/rspec/openapi/schema_cleaner.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/openapi/schema_cleaner.rb b/lib/rspec/openapi/schema_cleaner.rb index 89e287ed..28171bc2 100644 --- a/lib/rspec/openapi/schema_cleaner.rb +++ b/lib/rspec/openapi/schema_cleaner.rb @@ -47,7 +47,7 @@ def cleanup_empty_required_array!(base) paths_to_objects.each do |path| parent = base.dig(*path.take(path.length - 1)) # "required" array must not be present if empty - parent.delete('required') if parent['required'].empty? + parent.delete('required') if parent['required']&.empty? end end From f2d78d05f628b98ba2f17cd8c904a6580cdff6b3 Mon Sep 17 00:00:00 2001 From: Alexey Matskevich <45947106+AlexeyMatskevich@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:59:39 +0600 Subject: [PATCH 09/20] Update schema_cleaner.rb Apply rubocop offenses --- lib/rspec/openapi/schema_cleaner.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/openapi/schema_cleaner.rb b/lib/rspec/openapi/schema_cleaner.rb index 28171bc2..f6cf5b2e 100644 --- a/lib/rspec/openapi/schema_cleaner.rb +++ b/lib/rspec/openapi/schema_cleaner.rb @@ -47,7 +47,7 @@ def cleanup_empty_required_array!(base) paths_to_objects.each do |path| parent = base.dig(*path.take(path.length - 1)) # "required" array must not be present if empty - parent.delete('required') if parent['required']&.empty? + parent.delete('required') if parent['required'] && parent['required'].empty? end end From 5f2c0cf812d5fd90538d67814bbe62766027c465 Mon Sep 17 00:00:00 2001 From: exoego Date: Tue, 27 Jun 2023 14:50:52 +0900 Subject: [PATCH 10/20] chore: handle nil --- .simplecov_spawn.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb index 69dbb79e..511ce732 100644 --- a/.simplecov_spawn.rb +++ b/.simplecov_spawn.rb @@ -1,4 +1,4 @@ -unless ENV["COVERAGE"].empty? +unless ENV["COVERAGE"] && ENV["COVERAGE"].empty? require 'simplecov' require 'simplecov-cobertura' From 8297fd37c639f1dd431011d985ef60890856f8c2 Mon Sep 17 00:00:00 2001 From: exoego Date: Tue, 27 Jun 2023 14:55:44 +0900 Subject: [PATCH 11/20] fix minor issues --- .simplecov_spawn.rb | 6 ++++-- Gemfile | 2 +- lib/rspec/openapi/record_builder.rb | 3 ++- lib/rspec/openapi/schema_builder.rb | 7 ++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb index 511ce732..4243ef7e 100644 --- a/.simplecov_spawn.rb +++ b/.simplecov_spawn.rb @@ -1,4 +1,6 @@ -unless ENV["COVERAGE"] && ENV["COVERAGE"].empty? +# frozen_string_literal: true + +unless ENV['COVERAGE'] && ENV['COVERAGE'].empty? require 'simplecov' require 'simplecov-cobertura' @@ -6,4 +8,4 @@ SimpleCov.at_fork.call(Process.pid) SimpleCov.formatter SimpleCov::Formatter::CoberturaFormatter SimpleCov.start -end \ No newline at end of file +end diff --git a/Gemfile b/Gemfile index cc9124e7..897b4735 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,9 @@ gem 'roda' gem 'rspec-rails' group :test do - gem 'super_diff' gem 'simplecov' gem 'simplecov-cobertura' + gem 'super_diff' end group :development do diff --git a/lib/rspec/openapi/record_builder.rb b/lib/rspec/openapi/record_builder.rb index 5f816508..54f5fd93 100644 --- a/lib/rspec/openapi/record_builder.rb +++ b/lib/rspec/openapi/record_builder.rb @@ -11,7 +11,8 @@ def build(context, example:) request, response = extract_request_response(context) return if request.nil? - path, summary, tags, required_request_params, raw_path_params, description, security = extract_request_attributes(request, example) + path, summary, tags, required_request_params, raw_path_params, description, security = + extract_request_attributes(request, example) request_headers, response_headers = extract_headers(request, response) diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 83770af7..92999075 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -192,11 +192,12 @@ def build_example(value) def adjust_params(value) value.each do |key, v| - if v.is_a?(ActionDispatch::Http::UploadedFile) + case v + when ActionDispatch::Http::UploadedFile value[key] = v.original_filename - elsif v.is_a?(Hash) + when Hash adjust_params(v) - elsif v.is_a?(Array) + when Array result = v.map do |item| item.is_a?(ActionDispatch::Http::UploadedFile) ? item.original_filename : item end From d83c10f297f2a47283e7b4dbe898691b9982a27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Wed, 28 Jun 2023 16:36:34 +0900 Subject: [PATCH 12/20] ci: Fix coverage collection due to SimpleCov load timing (#122) --- .simplecov_spawn.rb | 8 ++++-- scripts/rspec | 11 ++++++++ scripts/rspec_with_simplecov | 50 ++++++++++++++++++++++++++++++++++++ spec/rails/doc/openapi.yaml | 10 ++++---- spec/spec_helper.rb | 7 +++-- 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100755 scripts/rspec create mode 100755 scripts/rspec_with_simplecov diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb index 4243ef7e..4afee1b3 100644 --- a/.simplecov_spawn.rb +++ b/.simplecov_spawn.rb @@ -6,6 +6,10 @@ SimpleCov.command_name 'spawn' SimpleCov.at_fork.call(Process.pid) - SimpleCov.formatter SimpleCov::Formatter::CoberturaFormatter - SimpleCov.start + SimpleCov.formatter SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::CoberturaFormatter, + ]) + SimpleCov.start do + add_filter '/spec/' + end end diff --git a/scripts/rspec b/scripts/rspec new file mode 100755 index 00000000..9adf4c3c --- /dev/null +++ b/scripts/rspec @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# (The MIT License) +# Copyright (c) 2012 Chad Humphries, David Chelimsky, Myron Marston +# Copyright (c) 2009 Chad Humphries, David Chelimsky +# Copyright (c) 2006 David Chelimsky, The RSpec Development Team +# Copyright (c) 2005 Steven Baker + +require 'rspec/core' +RSpec::Core::Runner.invoke diff --git a/scripts/rspec_with_simplecov b/scripts/rspec_with_simplecov new file mode 100755 index 00000000..c8927bb4 --- /dev/null +++ b/scripts/rspec_with_simplecov @@ -0,0 +1,50 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# (The MIT License) +# Copyright (c) 2012 Chad Humphries, David Chelimsky, Myron Marston +# Copyright (c) 2009 Chad Humphries, David Chelimsky +# Copyright (c) 2006 David Chelimsky, The RSpec Development Team +# Copyright (c) 2005 Steven Baker + +# Turn on verbose to make sure we not generating any ruby warning +$VERBOSE = true + +# So our "did they run the rspec command?" detection logic thinks +# that we run `rspec`. +$0 = 'rspec' + +# This is necessary for when `--standalone` is being used. +$:.unshift File.expand_path '../bundle', __dir__ + +# For the travis build we put the bundle directory up a directory +# so it can be shared among the repos for faster bundle installs. +$:.unshift File.expand_path '../../bundle', __dir__ + +require 'bundler/setup' + +# To use simplecov while running rspec-core's test suite, we must +# load simplecov _before_ loading any of rspec-core's files. +# So, this executable exists purely as a wrapper script that +# first loads simplecov, and then loads rspec. +begin + # Simplecov emits some ruby warnings when loaded, so silence them. + old_verbose = $VERBOSE + $VERBOSE = false + + unless ENV['COVERAGE'] && ENV['COVERAGE'].empty? || RUBY_VERSION < '1.9.3' + require 'simplecov' + + SimpleCov.start do + add_filter './bundle/' + add_filter './tmp/' + add_filter './spec/' + minimum_coverage(RUBY_PLATFORM == 'java' ? 94 : 97) + end + end +rescue LoadError +ensure + $VERBOSE = old_verbose +end + +load File.expand_path('rspec', __dir__) diff --git a/spec/rails/doc/openapi.yaml b/spec/rails/doc/openapi.yaml index f165de05..c9def75f 100644 --- a/spec/rails/doc/openapi.yaml +++ b/spec/rails/doc/openapi.yaml @@ -113,7 +113,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -207,7 +207,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -272,7 +272,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -374,7 +374,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -438,7 +438,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c22dd54d..c6423e9a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,10 @@ def assert_run(*args) end def run_tests(*args, command:, openapi: false, output: :yaml) - env = { 'OPENAPI' => ('1' if openapi), 'OPENAPI_OUTPUT' => output.to_s }.compact + env = { + 'OPENAPI' => ('1' if openapi), + 'OPENAPI_OUTPUT' => output.to_s, + }.compact Bundler.public_send(Bundler.respond_to?(:with_unbundled_env) ? :with_unbundled_env : :with_clean_env) do Dir.chdir(repo_root) do assert_run env, 'bundle', 'exec', command, '-r./.simplecov_spawn', *args @@ -23,7 +26,7 @@ def run_tests(*args, command:, openapi: false, output: :yaml) end def rspec(*args, openapi: false, output: :yaml) - run_tests(*args, command: 'rspec', openapi: openapi, output: output) + run_tests(*args, command: 'scripts/rspec_with_simplecov', openapi: openapi, output: output) end def minitest(*args, openapi: false, output: :yaml) From fd3494056920cd4e41dda2da849f74efa5a40c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Wed, 28 Jun 2023 16:45:52 +0900 Subject: [PATCH 13/20] chore: minor rubocop fix (#123) --- .rubocop.yml | 2 ++ .simplecov_spawn.rb | 4 ++-- scripts/rspec_with_simplecov | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 82f64f13..193faee4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,8 @@ Style/ClassAndModuleChildren: EnforcedStyle: compact Exclude: - 'lib/rspec/openapi/version.rb' +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent Metrics/BlockLength: Exclude: - 'spec/**/*' diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb index 4afee1b3..0f27d808 100644 --- a/.simplecov_spawn.rb +++ b/.simplecov_spawn.rb @@ -7,8 +7,8 @@ SimpleCov.command_name 'spawn' SimpleCov.at_fork.call(Process.pid) SimpleCov.formatter SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::CoberturaFormatter, - ]) + SimpleCov::Formatter::CoberturaFormatter, + ]) SimpleCov.start do add_filter '/spec/' end diff --git a/scripts/rspec_with_simplecov b/scripts/rspec_with_simplecov index c8927bb4..a3f73ac6 100755 --- a/scripts/rspec_with_simplecov +++ b/scripts/rspec_with_simplecov @@ -15,11 +15,11 @@ $VERBOSE = true $0 = 'rspec' # This is necessary for when `--standalone` is being used. -$:.unshift File.expand_path '../bundle', __dir__ +$LOAD_PATH.unshift File.expand_path '../bundle', __dir__ # For the travis build we put the bundle directory up a directory # so it can be shared among the repos for faster bundle installs. -$:.unshift File.expand_path '../../bundle', __dir__ +$LOAD_PATH.unshift File.expand_path '../../bundle', __dir__ require 'bundler/setup' @@ -32,7 +32,7 @@ begin old_verbose = $VERBOSE $VERBOSE = false - unless ENV['COVERAGE'] && ENV['COVERAGE'].empty? || RUBY_VERSION < '1.9.3' + unless (ENV.fetch('COVERAGE', nil) && ENV['COVERAGE'].empty?) || RUBY_VERSION < '1.9.3' require 'simplecov' SimpleCov.start do @@ -43,6 +43,7 @@ begin end end rescue LoadError + # simplecov is not available ensure $VERBOSE = old_verbose end From 0c92c205ef09a5ab23b1a50135eed8e3bde57d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Wed, 28 Jun 2023 22:34:52 +0900 Subject: [PATCH 14/20] ci: coverage config (#124) --- .simplecov_spawn.rb | 2 +- scripts/rspec_with_simplecov | 6 ++---- .../rails/app/controllers/users_controller.rb | 4 ++++ spec/rails/config/routes.rb | 4 +++- spec/rails/doc/smart/expected.yaml | 20 +++++++++++++++++++ spec/requests/rails_smart_merge_spec.rb | 7 +++++++ spec/requests/rails_spec.rb | 5 +++++ 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.simplecov_spawn.rb b/.simplecov_spawn.rb index 0f27d808..040e7669 100644 --- a/.simplecov_spawn.rb +++ b/.simplecov_spawn.rb @@ -4,12 +4,12 @@ require 'simplecov' require 'simplecov-cobertura' - SimpleCov.command_name 'spawn' SimpleCov.at_fork.call(Process.pid) SimpleCov.formatter SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::CoberturaFormatter, ]) SimpleCov.start do add_filter '/spec/' + add_filter '/scripts/' end end diff --git a/scripts/rspec_with_simplecov b/scripts/rspec_with_simplecov index a3f73ac6..b6fa6544 100755 --- a/scripts/rspec_with_simplecov +++ b/scripts/rspec_with_simplecov @@ -36,10 +36,8 @@ begin require 'simplecov' SimpleCov.start do - add_filter './bundle/' - add_filter './tmp/' - add_filter './spec/' - minimum_coverage(RUBY_PLATFORM == 'java' ? 94 : 97) + # Flaky :( + # enable_coverage :branch end end rescue LoadError diff --git a/spec/rails/app/controllers/users_controller.rb b/spec/rails/app/controllers/users_controller.rb index e15eead5..8acafe6b 100644 --- a/spec/rails/app/controllers/users_controller.rb +++ b/spec/rails/app/controllers/users_controller.rb @@ -16,6 +16,10 @@ def show render json: find_user(params[:id]) end + def active + render json: find_user(params[:id]).present? + end + private def find_user(id = nil) diff --git a/spec/rails/config/routes.rb b/spec/rails/config/routes.rb index ad083baf..a0737865 100644 --- a/spec/rails/config/routes.rb +++ b/spec/rails/config/routes.rb @@ -10,7 +10,9 @@ post 'upload_multiple' end end - resources :users, only: [:show, :create] + resources :users, only: [:show, :create] do + get 'active' + end get '/test_block' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['A TEST']] } end diff --git a/spec/rails/doc/smart/expected.yaml b/spec/rails/doc/smart/expected.yaml index 6e5536d4..aebfa2cf 100644 --- a/spec/rails/doc/smart/expected.yaml +++ b/spec/rails/doc/smart/expected.yaml @@ -207,6 +207,26 @@ paths: schema: type: integer example: 2 + "/users/{user_id}/active": + get: + responses: + "200": + description: "returns a boolean" + content: + "application/json": + schema: + type: boolean + example: true + summary: active + tags: + - User + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + example: 1 components: securitySchemes: Scheme1: diff --git a/spec/requests/rails_smart_merge_spec.rb b/spec/requests/rails_smart_merge_spec.rb index 72002191..eaa5c96a 100644 --- a/spec/requests/rails_smart_merge_spec.rb +++ b/spec/requests/rails_smart_merge_spec.rb @@ -102,4 +102,11 @@ expect(response.status).to eq(200) end end + + describe '#active' do + it 'returns a boolean' do + get '/users/1/active' + expect(response.status).to eq(200) + end + end end diff --git a/spec/requests/rails_spec.rb b/spec/requests/rails_spec.rb index d97e1512..186174a1 100644 --- a/spec/requests/rails_spec.rb +++ b/spec/requests/rails_spec.rb @@ -70,6 +70,11 @@ get '/tables/2', headers: { authorization: 'k0kubun' } expect(response.status).to eq(404) end + + it 'does not return a table if not found (openapi: false)', openapi: false do + get '/tables/3', headers: { authorization: 'k0kubun' } + expect(response.status).to eq(404) + end end describe '#create' do From df08a26955967558c9f3d51d398bcf25dc8c9967 Mon Sep 17 00:00:00 2001 From: Junya Ishihara Date: Thu, 6 Jul 2023 16:21:47 +0900 Subject: [PATCH 15/20] Fix arrayed uploaded file examples --- lib/rspec/openapi/schema_builder.rb | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 92999075..6e6cc3ea 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -5,7 +5,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new # @return [Hash] def build(record) response = { - description: record.description, + description: record.description } response_headers = build_response_headers(record) @@ -16,8 +16,8 @@ def build(record) response[:content] = { normalize_content_type(record.response_content_type) => { schema: build_property(record.response_body, disposition: disposition), - example: response_example(record, disposition: disposition), - }.compact, + example: response_example(record, disposition: disposition) + }.compact } end @@ -31,11 +31,11 @@ def build(record) parameters: build_parameters(record), requestBody: build_request_body(record), responses: { - record.status.to_s => response, - }, - }.compact, - }, - }, + record.status.to_s => response + } + }.compact + } + } } end @@ -65,7 +65,7 @@ def build_parameters(record) in: 'path', required: true, schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?), + example: (try_cast(value) if example_enabled?) }.compact end @@ -75,7 +75,7 @@ def build_parameters(record) in: 'query', required: record.required_request_params.include?(key), schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?), + example: (try_cast(value) if example_enabled?) }.compact end @@ -85,7 +85,7 @@ def build_parameters(record) in: 'header', required: true, schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?), + example: (try_cast(value) if example_enabled?) }.compact end @@ -99,7 +99,7 @@ def build_response_headers(record) record.response_headers.each do |key, value| headers[key] = { - schema: build_property(try_cast(value)), + schema: build_property(try_cast(value)) }.compact end @@ -124,9 +124,9 @@ def build_request_body(record) content: { normalize_content_type(record.request_content_type) => { schema: build_property(record.request_params), - example: (build_example(record.request_params) if example_enabled?), - }.compact, - }, + example: (build_example(record.request_params) if example_enabled?) + }.compact + } } end @@ -199,7 +199,7 @@ def adjust_params(value) adjust_params(v) when Array result = v.map do |item| - item.is_a?(ActionDispatch::Http::UploadedFile) ? item.original_filename : item + adjust_params(item) end value[key] = result end From 24beca9d3ceec64e858c4fa1de693683aee0ca0f Mon Sep 17 00:00:00 2001 From: Junya Ishihara Date: Thu, 6 Jul 2023 16:49:46 +0900 Subject: [PATCH 16/20] Add tests --- lib/rspec/openapi/schema_builder.rb | 39 +++-- spec/integration_tests/rails_test.rb | 9 ++ .../app/controllers/images_controller.rb | 4 + spec/rails/config/routes.rb | 1 + spec/rails/doc/openapi.json | 146 ++++++++++++------ spec/rails/doc/openapi.yaml | 95 ++++++++---- spec/requests/rails_spec.rb | 14 ++ 7 files changed, 219 insertions(+), 89 deletions(-) diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 6e6cc3ea..641c27b4 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -5,7 +5,7 @@ class << RSpec::OpenAPI::SchemaBuilder = Object.new # @return [Hash] def build(record) response = { - description: record.description + description: record.description, } response_headers = build_response_headers(record) @@ -16,8 +16,8 @@ def build(record) response[:content] = { normalize_content_type(record.response_content_type) => { schema: build_property(record.response_body, disposition: disposition), - example: response_example(record, disposition: disposition) - }.compact + example: response_example(record, disposition: disposition), + }.compact, } end @@ -31,11 +31,11 @@ def build(record) parameters: build_parameters(record), requestBody: build_request_body(record), responses: { - record.status.to_s => response - } - }.compact - } - } + record.status.to_s => response, + }, + }.compact, + }, + }, } end @@ -65,7 +65,7 @@ def build_parameters(record) in: 'path', required: true, schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?) + example: (try_cast(value) if example_enabled?), }.compact end @@ -75,7 +75,7 @@ def build_parameters(record) in: 'query', required: record.required_request_params.include?(key), schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?) + example: (try_cast(value) if example_enabled?), }.compact end @@ -85,7 +85,7 @@ def build_parameters(record) in: 'header', required: true, schema: build_property(try_cast(value)), - example: (try_cast(value) if example_enabled?) + example: (try_cast(value) if example_enabled?), }.compact end @@ -99,7 +99,7 @@ def build_response_headers(record) record.response_headers.each do |key, value| headers[key] = { - schema: build_property(try_cast(value)) + schema: build_property(try_cast(value)), }.compact end @@ -124,9 +124,9 @@ def build_request_body(record) content: { normalize_content_type(record.request_content_type) => { schema: build_property(record.request_params), - example: (build_example(record.request_params) if example_enabled?) - }.compact - } + example: (build_example(record.request_params) if example_enabled?), + }.compact, + }, } end @@ -199,7 +199,14 @@ def adjust_params(value) adjust_params(v) when Array result = v.map do |item| - adjust_params(item) + case item + when ActionDispatch::Http::UploadedFile + item.original_filename + when Hash + adjust_params(item) + else + item + end end value[key] = result end diff --git a/spec/integration_tests/rails_test.rb b/spec/integration_tests/rails_test.rb index 4fb48930..b252a87e 100644 --- a/spec/integration_tests/rails_test.rb +++ b/spec/integration_tests/rails_test.rb @@ -162,6 +162,15 @@ class ImageTest < ActionDispatch::IntegrationTest post '/images/upload_multiple', params: { images: [image, image] } assert_response 200 end + + test 'returns a image payload with upload multiple nested' do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + image = Rack::Test::UploadedFile.new('test.png', 'image/png') + post '/images/upload_multiple_nested', params: { images: [{ image: image }, { image: image }] } + assert_response 200 + end end class ExtraRoutesTest < ActionDispatch::IntegrationTest diff --git a/spec/rails/app/controllers/images_controller.rb b/spec/rails/app/controllers/images_controller.rb index bcb49eb7..b2aab36e 100644 --- a/spec/rails/app/controllers/images_controller.rb +++ b/spec/rails/app/controllers/images_controller.rb @@ -25,6 +25,10 @@ def upload_multiple send_image end + def upload_multiple_nested + send_image + end + private def send_image diff --git a/spec/rails/config/routes.rb b/spec/rails/config/routes.rb index a0737865..4acd108b 100644 --- a/spec/rails/config/routes.rb +++ b/spec/rails/config/routes.rb @@ -8,6 +8,7 @@ post 'upload' post 'upload_nested' post 'upload_multiple' + post 'upload_multiple_nested' end end resources :users, only: [:show, :create] do diff --git a/spec/rails/doc/openapi.json b/spec/rails/doc/openapi.json index 0353c573..e6518ace 100644 --- a/spec/rails/doc/openapi.json +++ b/spec/rails/doc/openapi.json @@ -786,9 +786,9 @@ } } }, - "/images/upload_multiple": { + "/images/upload_nested": { "post": { - "summary": "upload_multiple", + "summary": "upload_nested", "tags": [ "Image" ], @@ -798,30 +798,81 @@ "schema": { "type": "object", "properties": { - "images": { - "type": "array", - "items": { - "type": "string", - "format": "binary" - } + "nested_image": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + }, + "caption": { + "type": "string" + } + }, + "required": [ + "image", + "caption" + ] } }, "required": [ - "images" + "nested_image" ] }, "example": { - "images": [ - "test.png", - "test.png" + "nested_image": { + "image": "test.png", + "caption": "Some caption" + } + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload nested", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload": { + "post": { + "summary": "upload", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "image" ] + }, + "example": { + "image": "test.png" } } } }, "responses": { "200": { - "description": "returns a image payload with upload multiple", + "description": "returns a image payload with upload", "content": { "image/png": { "schema": { @@ -834,9 +885,9 @@ } } }, - "/images/upload_nested": { + "/images/upload_multiple_nested": { "post": { - "summary": "upload_nested", + "summary": "upload_multiple_nested", "tags": [ "Image" ], @@ -846,39 +897,42 @@ "schema": { "type": "object", "properties": { - "nested_image": { - "type": "object", - "properties": { - "image": { - "type": "string", - "format": "binary" + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } }, - "caption": { - "type": "string" - } - }, - "required": [ - "image", - "caption" - ] + "required": [ + "image" + ] + } } }, "required": [ - "nested_image" + "images" ] }, "example": { - "nested_image": { - "image": "test.png", - "caption": "Some caption" - } + "images": [ + { + "image": "test.png" + }, + { + "image": "test.png" + } + ] } } } }, "responses": { "200": { - "description": "returns a image payload with upload nested", + "description": "returns a image payload with upload multiple nested", "content": { "image/png": { "schema": { @@ -891,9 +945,9 @@ } } }, - "/images/upload": { + "/images/upload_multiple": { "post": { - "summary": "upload", + "summary": "upload_multiple", "tags": [ "Image" ], @@ -903,24 +957,30 @@ "schema": { "type": "object", "properties": { - "image": { - "type": "string", - "format": "binary" + "images": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } } }, "required": [ - "image" + "images" ] }, "example": { - "image": "test.png" + "images": [ + "test.png", + "test.png" + ] } } } }, "responses": { "200": { - "description": "returns a image payload with upload", + "description": "returns a image payload with upload multiple", "content": { "image/png": { "schema": { diff --git a/spec/rails/doc/openapi.yaml b/spec/rails/doc/openapi.yaml index c9def75f..7e7eb55d 100644 --- a/spec/rails/doc/openapi.yaml +++ b/spec/rails/doc/openapi.yaml @@ -526,36 +526,6 @@ paths: example: - name: file.png tags: [] - "/images/upload_multiple": - post: - summary: upload_multiple - tags: - - Image - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - images: - type: array - items: - type: string - format: binary - required: - - images - example: - images: - - test.png - - test.png - responses: - '200': - description: returns a image payload with upload multiple - content: - image/png: - schema: - type: string - format: binary "/images/upload": post: summary: upload @@ -618,3 +588,68 @@ paths: schema: type: string format: binary + "/images/upload_multiple_nested": + post: + summary: upload_multiple_nested + tags: + - Image + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + images: + type: array + items: + type: object + properties: + image: + type: string + format: binary + required: + - image + required: + - images + example: + images: + - image: test.png + - image: test.png + responses: + '200': + description: returns a image payload with upload multiple nested + content: + image/png: + schema: + type: string + format: binary + "/images/upload_multiple": + post: + summary: upload_multiple + tags: + - Image + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + images: + type: array + items: + type: string + format: binary + required: + - images + example: + images: + - test.png + - test.png + responses: + '200': + description: returns a image payload with upload multiple + content: + image/png: + schema: + type: string + format: binary diff --git a/spec/requests/rails_spec.rb b/spec/requests/rails_spec.rb index 186174a1..d5febe6c 100644 --- a/spec/requests/rails_spec.rb +++ b/spec/requests/rails_spec.rb @@ -164,6 +164,20 @@ expect(response.status).to eq(200) end end + + describe '#upload_multiple_nested' do + before do + png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP + QBoAAAAASUVORK5CYII='.unpack1('m') + File.binwrite('test.png', png) + end + let(:image) { Rack::Test::UploadedFile.new('test.png', 'image/png') } + + it 'returns a image payload with upload multiple nested' do + post '/images/upload_multiple_nested', params: { images: [{ image: image }, { image: image }] } + expect(response.status).to eq(200) + end + end end RSpec.describe 'Extra routes', type: :request do From 5baea9253bd03b25c833f7e76844f4d56cb809c5 Mon Sep 17 00:00:00 2001 From: Andy Pfister Date: Wed, 26 Jul 2023 12:57:07 +0200 Subject: [PATCH 17/20] Don't dump records into temporary file I browsed the source code the other day and found this statement. As it is referenced nowhere else in the code, I assume it was used for debugging purposes when implementing the Minitest support and is therefore no longer necessary. --- lib/rspec/openapi/result_recorder.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rspec/openapi/result_recorder.rb b/lib/rspec/openapi/result_recorder.rb index 9a2ec890..1ff6b560 100644 --- a/lib/rspec/openapi/result_recorder.rb +++ b/lib/rspec/openapi/result_recorder.rb @@ -15,7 +15,6 @@ def record_results! RSpec::OpenAPI::SchemaMerger.merge!(spec, schema) new_from_zero = {} records.each do |record| - File.open('/tmp/records', 'a') { |f| f.puts record.to_yaml } begin record_schema = RSpec::OpenAPI::SchemaBuilder.build(record) RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema) From f34f3f126140b5719948a4a11155ffe5f880867a Mon Sep 17 00:00:00 2001 From: Andy Pfister Date: Wed, 26 Jul 2023 15:01:24 +0200 Subject: [PATCH 18/20] Add failing tests --- spec/minitest/rack_test_spec.rb | 6 ++++++ spec/minitest/rails_spec.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/spec/minitest/rack_test_spec.rb b/spec/minitest/rack_test_spec.rb index 49d8b190..20c8d424 100644 --- a/spec/minitest/rack_test_spec.rb +++ b/spec/minitest/rack_test_spec.rb @@ -32,4 +32,10 @@ expect(new_yaml).to eq org_yaml end end + + describe 'with disabled OpenAPI generation' do + it 'can run tests' do + minitest 'spec/integration_tests/roda_test.rb' + end + end end diff --git a/spec/minitest/rails_spec.rb b/spec/minitest/rails_spec.rb index a5d71c5e..d67a3cab 100644 --- a/spec/minitest/rails_spec.rb +++ b/spec/minitest/rails_spec.rb @@ -33,4 +33,10 @@ expect(new_yaml).to eq org_yaml end end + + describe 'with disabled OpenAPI generation' do + it 'can run tests' do + minitest 'spec/integration_tests/rails_test.rb' + end + end end From ed1f83ffb485cfdf5552e92ad635620c6d4506b4 Mon Sep 17 00:00:00 2001 From: Andy Pfister Date: Wed, 26 Jul 2023 14:25:49 +0200 Subject: [PATCH 19/20] Prepend `openapi!` in any case If you run tests with Minitest at themoment, but without the environment variable `OPENAPI`, you are greeted with an error message. `undefined method `openapi!' for Api::V0::SomeControllerTest:Class (NoMethodError)` This is because the current code adds the `openapi!` method only if `OPENAPI` environment variable is set. This commit modifies the loading logic, always requiring the `minitest` hooks class to be loaded if `Minitest` constant is defined, to avoid issues with RSpec. Then, the `openapi?` and `openapi!` methods are always prepended to `Minitest::Test`, while the modified `run` method and the `after_run` hook are only activated when `OPENAPI` environment variable is set. --- lib/rspec/openapi.rb | 6 ++---- lib/rspec/openapi/minitest_hooks.rb | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/rspec/openapi.rb b/lib/rspec/openapi.rb index a6c4a9c6..c91b054e 100644 --- a/lib/rspec/openapi.rb +++ b/lib/rspec/openapi.rb @@ -10,10 +10,8 @@ require 'rspec/openapi/schema_merger' require 'rspec/openapi/schema_cleaner' -if ENV['OPENAPI'] - require 'rspec/openapi/minitest_hooks' - require 'rspec/openapi/rspec_hooks' -end +require 'rspec/openapi/minitest_hooks' if Object.const_defined?('Minitest') +require 'rspec/openapi/rspec_hooks' if ENV['OPENAPI'] && Object.const_defined?('RSpec') module RSpec::OpenAPI @path = 'doc/openapi.yaml' diff --git a/lib/rspec/openapi/minitest_hooks.rb b/lib/rspec/openapi/minitest_hooks.rb index 30ce3aa0..73dda985 100644 --- a/lib/rspec/openapi/minitest_hooks.rb +++ b/lib/rspec/openapi/minitest_hooks.rb @@ -5,11 +5,7 @@ module RSpec::OpenAPI::Minitest Example = Struct.new(:context, :description, :metadata, :file_path) - module TestPatch - def self.prepended(base) - base.extend(ClassMethods) - end - + module RunPatch def run(*args) result = super if ENV['OPENAPI'] && self.class.openapi? @@ -22,6 +18,12 @@ def run(*args) end result end + end + + module ActivateOpenApiClassMethods + def self.prepended(base) + base.extend(ClassMethods) + end module ClassMethods def openapi? @@ -35,10 +37,12 @@ def openapi! end end -Minitest::Test.prepend RSpec::OpenAPI::Minitest::TestPatch +Minitest::Test.prepend RSpec::OpenAPI::Minitest::ActivateOpenApiClassMethods + +if ENV['OPENAPI'] + Minitest::Test.prepend RSpec::OpenAPI::Minitest::RunPatch -Minitest.after_run do - if ENV['OPENAPI'] + Minitest.after_run do result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records) result_recorder.record_results! puts result_record.error_message if result_recorder.errors? From a37d3ec81e635251fe5e397f838daf5a4b9a7444 Mon Sep 17 00:00:00 2001 From: Andy Pfister Date: Wed, 26 Jul 2023 14:54:39 +0200 Subject: [PATCH 20/20] Ensure Minitest is loaded before RSpec OpenAPI The alternative to this approach would be to remove the check for `Object.const_defined?('Minitest')`, but I assume in environments where you only have RSpec, not Minitest (like when you use another framework than Rails), this would not work. --- spec/integration_tests/rails_test.rb | 3 +-- spec/integration_tests/roda_test.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/integration_tests/rails_test.rb b/spec/integration_tests/rails_test.rb index 4fb48930..64d97a6a 100644 --- a/spec/integration_tests/rails_test.rb +++ b/spec/integration_tests/rails_test.rb @@ -4,9 +4,8 @@ ENV['RAILS_ENV'] ||= 'test' ENV['OPENAPI_OUTPUT'] ||= 'yaml' -require File.expand_path('../rails/config/environment', __dir__) - require 'minitest/autorun' +require File.expand_path('../rails/config/environment', __dir__) RSpec::OpenAPI.request_headers = %w[X-Authorization-Token] RSpec::OpenAPI.response_headers = %w[X-Cursor] diff --git a/spec/integration_tests/roda_test.rb b/spec/integration_tests/roda_test.rb index 5bcc6b98..366133e4 100644 --- a/spec/integration_tests/roda_test.rb +++ b/spec/integration_tests/roda_test.rb @@ -3,8 +3,8 @@ require_relative '../roda/roda_app' require 'json' require 'rack/test' -require 'rspec/openapi' require 'minitest/autorun' +require 'rspec/openapi' ENV['OPENAPI_OUTPUT'] ||= 'yaml'