Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,34 @@ Some examples' attributes can be overwritten via RSpec metadata options. Example

**NOTE**: `description` key will override also the one provided by `RSpec::OpenAPI.description_builder` method.

## Experimental minitest support

Even if you are not using `rspec` this gem might help you with its experimental support for `minitest`.

Example:

```rb
class TablesTest < ActionDispatch::IntegrationTest
openapi!

test "GET /index returns a list of tables" do
get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
assert_response :success
end

test "GET /index does not return tables if unauthorized" do
get '/tables'
assert_response :unauthorized
end

# ...
end
```

It should work with both classes inheriting from `ActionDispatch::IntegrationTest` and with classes using `Rack::Test` directly, as long as you call `openapi!` in your test class.

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.

## Links

Existing RSpec plugins which have OpenAPI integration:
Expand Down
18 changes: 16 additions & 2 deletions lib/rspec/openapi.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
require 'rspec/openapi/version'
require 'rspec/openapi/hooks' if ENV['OPENAPI']
require 'rspec/openapi/components_updater'
require 'rspec/openapi/default_schema'
require 'rspec/openapi/record_builder'
require 'rspec/openapi/result_recorder'
require 'rspec/openapi/schema_builder'
require 'rspec/openapi/schema_file'
require 'rspec/openapi/schema_merger'
require 'rspec/openapi/schema_cleaner'

if ENV['OPENAPI']
require 'rspec/openapi/minitest_hooks'
require 'rspec/openapi/rspec_hooks'
end

module RSpec::OpenAPI
@path = 'doc/openapi.yaml'
Expand All @@ -13,6 +25,7 @@ module RSpec::OpenAPI
@security_schemes = []
@example_types = %i[request]
@response_headers = []
@path_records = Hash.new { |h, k| h[k] = [] }

class << self
attr_accessor :path,
Expand All @@ -25,6 +38,7 @@ class << self
:servers,
:security_schemes,
:example_types,
:response_headers
:response_headers,
:path_records
end
end
51 changes: 0 additions & 51 deletions lib/rspec/openapi/hooks.rb

This file was deleted.

47 changes: 47 additions & 0 deletions lib/rspec/openapi/minitest_hooks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'minitest'

module RSpec
module OpenAPI
module Minitest
class Example < Struct.new(:context, :description, :metadata) ; end

module TestPatch
def self.prepended(base)
base.extend(ClassMethods)
end

def run(*args)
result = super
if ENV['OPENAPI'] && self.class.openapi?
path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
human_name = name.sub(/^test_/, "").gsub(/_/, " ")
example = Example.new(self, human_name, {})
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
RSpec::OpenAPI.path_records[path] << record if record
end
result
end

module ClassMethods
def openapi?
@openapi
end

def openapi!
@openapi = true
end
end
end
end
end
end

Minitest::Test.prepend RSpec::OpenAPI::Minitest::TestPatch

Minitest.after_run do
if ENV['OPENAPI']
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
result_recorder.record_results!
puts result_record.error_message if result_recorder.errors?
end
end
2 changes: 1 addition & 1 deletion lib/rspec/openapi/record_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def build(context, example:)
private

def rails?
defined?(Rails) && Rails.application
defined?(Rails) && Rails.respond_to?(:application) && Rails.application
end

def rack_test?(context)
Expand Down
42 changes: 42 additions & 0 deletions lib/rspec/openapi/result_recorder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class RSpec::OpenAPI::ResultRecorder
def initialize(path_records)
@path_records = path_records
@error_records = {}
end

def record_results!
title = File.basename(Dir.pwd)
@path_records.each do |path, records|
RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
schema = RSpec::OpenAPI::DefaultSchema.build(title)
schema[:info].merge!(RSpec::OpenAPI.info)
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)
RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
@error_records[e] = record # Avoid failing the build
end
end
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
end
end
end

def errors?
@error_records.any?
end

def error_message
<<~EOS
RSpec::OpenAPI got errors building #{@error_records.size} requests

#{@error_records.map {|e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
EOS
end
end
19 changes: 19 additions & 0 deletions lib/rspec/openapi/rspec_hooks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'rspec'

RSpec.configuration.after(:each) do |example|
if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
path = RSpec::OpenAPI.path.yield_self { |p| p.is_a?(Proc) ? p.call(example) : p }
record = RSpec::OpenAPI::RecordBuilder.build(self, example: example)
RSpec::OpenAPI.path_records[path] << record if record
end
end

RSpec.configuration.after(:suite) do
result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
result_recorder.record_results!
if result_recorder.errors?
error_message = result_recorder.error_message
colorizer = ::RSpec::Core::Formatters::ConsoleCodes
RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
end
end
155 changes: 155 additions & 0 deletions spec/integration_tests/rails_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
ENV['TZ'] ||= 'UTC'
ENV['RAILS_ENV'] ||= 'test'
ENV['OPENAPI_OUTPUT'] ||= 'yaml'

require File.expand_path('../rails/config/environment', __dir__)

require 'minitest/autorun'

# Patch minitest's ordering of examples to match RSpec's
# in order to get comparable results
class MiniTest::Test
def self.runnable_methods
methods_matching(/^test_/)
end
end

RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
RSpec::OpenAPI.response_headers = %w[X-Cursor]
RSpec::OpenAPI.path = File.expand_path("../rails/doc/openapi.#{ENV['OPENAPI_OUTPUT']}", __dir__)
RSpec::OpenAPI.comment = <<~COMMENT
This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi

When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
update this file automatically. You can also manually edit this file.
COMMENT
RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
RSpec::OpenAPI.info = {
description: 'My beautiful API',
license: {
'name': 'Apache 2.0',
'url': 'https://www.apache.org/licenses/LICENSE-2.0.html',
},
}

class TablesIndexTest < ActionDispatch::IntegrationTest
openapi!

def test_with_flat_query_parameters
get '/tables', params: { page: '1', per: '10' },
headers: { authorization: 'k0kubun', "X-Authorization-Token": 'token' }
assert_response 200
end

def test_with_deep_query_parameters
get '/tables', params: { filter: { "name" => "Example Table" } }, headers: { authorization: 'k0kubun' }
assert_response 200
end

def test_with_different_deep_query_parameters
get '/tables', params: { filter: { "price" => 0 } }, headers: { authorization: 'k0kubun' }
assert_response 200
end

def test_has_a_request_spec_which_does_not_make_any_request
assert true
end

def test_does_not_return_tables_if_unauthorized
get '/tables'
assert_response 401
end
end

class TablesShowTest < ActionDispatch::IntegrationTest
openapi!

def test_does_not_return_a_table_if_unauthorized
get '/tables/1'
assert_response 401
end

def test_does_not_return_a_table_if_not_found
get '/tables/2', headers: { authorization: 'k0kubun' }
assert_response 404
end

def test_returns_a_table
get '/tables/1', headers: { authorization: 'k0kubun' }
assert_response 200
end
end

class TablesCreateTest < ActionDispatch::IntegrationTest
openapi!

test 'returns a table' do
post '/tables', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {
name: 'k0kubun',
description: 'description',
database_id: 2,
}.to_json
assert_response 201
end
end

class TablesUpdateTest < ActionDispatch::IntegrationTest
openapi!

test 'returns a table' do
png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP
QBoAAAAASUVORK5CYII='.unpack('m').first
IO.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' },
}
assert_response 200
end
end

class TablesDestroyTest < ActionDispatch::IntegrationTest
openapi!

test 'returns a table' do
delete '/tables/1', headers: { authorization: 'k0kubun' }
assert_response 200
end

test 'returns no content if specified' do
delete '/tables/1', headers: { authorization: 'k0kubun' }, params: { no_content: true }
assert_response 202
end
end

class ImageTest < ActionDispatch::IntegrationTest
openapi!

test 'returns a image payload' do
get '/images/1'
assert_response 200
end

test 'can return an object with an attribute of empty array' do
get '/images'
assert_response 200
end
end

class ExtraRoutesTest < ActionDispatch::IntegrationTest
openapi!

test 'returns the block content' do
get '/test_block'
assert_response 200
end
end

class EngineTest < ActionDispatch::IntegrationTest
openapi!

test 'returns some content from the engine' do
get '/my_engine/eng_route'
assert_response 200
end
end
Loading