mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-25 07:16:40 +00:00
rename to rswag plus major refactor - almost a rewrite
This commit is contained in:
2
rswag-specs/.rspec
Normal file
2
rswag-specs/.rspec
Normal file
@@ -0,0 +1,2 @@
|
||||
--color
|
||||
--require spec_helper
|
||||
20
rswag-specs/MIT-LICENSE
Normal file
20
rswag-specs/MIT-LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright 2015 domaindrivendev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
27
rswag-specs/Rakefile
Normal file
27
rswag-specs/Rakefile
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env rake
|
||||
begin
|
||||
require 'bundler/setup'
|
||||
rescue LoadError
|
||||
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
||||
end
|
||||
begin
|
||||
require 'rdoc/task'
|
||||
rescue LoadError
|
||||
require 'rdoc/rdoc'
|
||||
require 'rake/rdoctask'
|
||||
RDoc::Task = Rake::RDocTask
|
||||
end
|
||||
|
||||
RDoc::Task.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'rswag-specs'
|
||||
rdoc.options << '--line-numbers'
|
||||
rdoc.rdoc_files.include('README.rdoc')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
Bundler::GemHelper.install_tasks
|
||||
|
||||
8
rswag-specs/lib/generators/rswag/specs/install/USAGE
Normal file
8
rswag-specs/lib/generators/rswag/specs/install/USAGE
Normal file
@@ -0,0 +1,8 @@
|
||||
Description:
|
||||
Adds swagger_helper to enable Swagger DSL in integration specs
|
||||
|
||||
Example:
|
||||
rails generate rswag:specs:install
|
||||
|
||||
This will create:
|
||||
spec/swagger_helper.rb
|
||||
@@ -0,0 +1,14 @@
|
||||
require 'rails/generators'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
class InstallGenerator < Rails::Generators::Base
|
||||
source_root File.expand_path('../templates', __FILE__)
|
||||
|
||||
def add_swagger_helper
|
||||
template('swagger_helper.rb', 'spec/swagger_helper.rb')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Specify a root folder where Swagger JSON files are generated
|
||||
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
||||
# to ensure that it's confiugred to server Swagger from the same folder
|
||||
config.swagger_root = Rails.root.to_s + '/swagger'
|
||||
|
||||
# Define one or more Swagger documents and provide global metadata for each one
|
||||
# When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will
|
||||
# be generated at the provided relative path under swagger_root
|
||||
# By default, the operations defined in spec files are added to the first
|
||||
# document below. You can override this behavior by adding a swagger_doc tag to the
|
||||
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
|
||||
config.swagger_docs = {
|
||||
'v1/swagger.json' => {
|
||||
swagger: '2.0',
|
||||
info: {
|
||||
title: 'API V1',
|
||||
version: 'v1'
|
||||
},
|
||||
paths: {}
|
||||
}
|
||||
}
|
||||
end
|
||||
22
rswag-specs/lib/rswag/specs.rb
Normal file
22
rswag-specs/lib/rswag/specs.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'rspec/core'
|
||||
require 'rswag/specs/version'
|
||||
require 'rswag/specs/example_group_helpers'
|
||||
require 'rswag/specs/example_helpers'
|
||||
require 'rswag/specs/railtie' if defined?(Rails::Railtie)
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
# Extend RSpec with a swagger-based DSL
|
||||
::RSpec.configure do |c|
|
||||
c.add_setting :swagger_root
|
||||
c.add_setting :swagger_docs
|
||||
c.extend ExampleGroupHelpers, type: :request
|
||||
c.include ExampleHelpers, type: :request
|
||||
end
|
||||
|
||||
# Support Rails 3+ and RSpec 2+ (sigh!)
|
||||
RAILS_VERSION = Rails::VERSION::MAJOR
|
||||
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
|
||||
end
|
||||
end
|
||||
80
rswag-specs/lib/rswag/specs/example_group_helpers.rb
Normal file
80
rswag-specs/lib/rswag/specs/example_group_helpers.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
module Rswag
|
||||
module Specs
|
||||
module ExampleGroupHelpers
|
||||
|
||||
def path(path, &block)
|
||||
api_metadata = { path: path}
|
||||
describe(path, api_metadata, &block)
|
||||
end
|
||||
|
||||
[ :get, :post, :patch, :put, :delete, :head ].each do |verb|
|
||||
define_method(verb) do |summary, &block|
|
||||
api_metadata = { operation: { verb: verb, summary: summary } }
|
||||
describe(verb, api_metadata, &block)
|
||||
end
|
||||
end
|
||||
|
||||
[ :operationId, :deprecated, :security ].each do |attr_name|
|
||||
define_method(attr_name) do |value|
|
||||
metadata[:operation][attr_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE: 'description' requires special treatment because ExampleGroup already
|
||||
# defines a method with that name. Provide an override that supports the existing
|
||||
# functionality while also setting the appropriate metadata if applicable
|
||||
def description(value=nil)
|
||||
return super() if value.nil?
|
||||
metadata[:operation][:description] = value
|
||||
end
|
||||
|
||||
# These are array properties - note the splat operator
|
||||
[ :tags, :consumes, :produces, :schemes ].each do |attr_name|
|
||||
define_method(attr_name) do |*value|
|
||||
metadata[:operation][attr_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
def parameter(attributes)
|
||||
attributes[:required] = true if attributes[:in].to_sym == :path
|
||||
metadata[:operation][:parameters] ||= []
|
||||
metadata[:operation][:parameters] << attributes
|
||||
end
|
||||
|
||||
def response(code, description, &block)
|
||||
api_metadata = { response: { code: code, description: description } }
|
||||
context(description, api_metadata, &block)
|
||||
end
|
||||
|
||||
def schema(value)
|
||||
metadata[:response][:schema] = value
|
||||
end
|
||||
|
||||
def header(name, attributes)
|
||||
metadata[:response][:headers] ||= {}
|
||||
metadata[:response][:headers][name] = attributes
|
||||
end
|
||||
|
||||
def run_test!
|
||||
# NOTE: rspec 2.x support
|
||||
if RSPEC_VERSION < 3
|
||||
before do
|
||||
submit_request(example.metadata)
|
||||
end
|
||||
|
||||
it "returns a #{metadata[:response][:code]} response" do
|
||||
assert_response_matches_metadata(example.metadata)
|
||||
end
|
||||
else
|
||||
before do |example|
|
||||
submit_request(example.metadata)
|
||||
end
|
||||
|
||||
it "returns a #{metadata[:response][:code]} response" do |example|
|
||||
assert_response_matches_metadata(example.metadata)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
rswag-specs/lib/rswag/specs/example_helpers.rb
Normal file
43
rswag-specs/lib/rswag/specs/example_helpers.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
require 'rswag/specs/request_factory'
|
||||
require 'rswag/specs/response_validator'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
module ExampleHelpers
|
||||
|
||||
def submit_request(api_metadata)
|
||||
factory = RequestFactory.new(api_metadata, global_metadata(api_metadata[:swagger_doc]))
|
||||
|
||||
if RAILS_VERSION < 5
|
||||
send(
|
||||
api_metadata[:operation][:verb],
|
||||
factory.build_fullpath(self),
|
||||
factory.build_body(self),
|
||||
factory.build_headers(self)
|
||||
)
|
||||
else
|
||||
send(
|
||||
api_metadata[:operation][:verb],
|
||||
factory.build_fullpath(self),
|
||||
{
|
||||
params: factory.build_body(self),
|
||||
headers: factory.build_headers(self)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_response_matches_metadata(api_metadata)
|
||||
validator = ResponseValidator.new(api_metadata, global_metadata(api_metadata[:swagger_doc]))
|
||||
validator.validate!(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def global_metadata(swagger_doc)
|
||||
swagger_docs = ::RSpec.configuration.swagger_docs
|
||||
swagger_doc.nil? ? swagger_docs.values.first : swagger_docs[swagger_doc]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
10
rswag-specs/lib/rswag/specs/railtie.rb
Normal file
10
rswag-specs/lib/rswag/specs/railtie.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
module Rswag
|
||||
module Specs
|
||||
class Railtie < ::Rails::Railtie
|
||||
|
||||
rake_tasks do
|
||||
load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
70
rswag-specs/lib/rswag/specs/request_factory.rb
Normal file
70
rswag-specs/lib/rswag/specs/request_factory.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
require 'json'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
class RequestFactory
|
||||
|
||||
def initialize(api_metadata, global_metadata)
|
||||
@api_metadata = api_metadata
|
||||
@global_metadata = global_metadata
|
||||
end
|
||||
|
||||
def build_fullpath(example)
|
||||
@api_metadata[:path].dup.tap do |t|
|
||||
parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
|
||||
t.concat(build_query(example))
|
||||
t.prepend(@global_metadata[:basePath] || '')
|
||||
end
|
||||
end
|
||||
|
||||
def build_query(example)
|
||||
query_string = parameters_in(:query)
|
||||
.map { |p| "#{p[:name]}=#{example.send(p[:name])}" }
|
||||
.join('&')
|
||||
|
||||
query_string.empty? ? '' : "?#{query_string}"
|
||||
end
|
||||
|
||||
def build_body(example)
|
||||
body_parameter = parameters_in(:body).first
|
||||
body_parameter.nil? ? '' : example.send(body_parameter[:name]).to_json
|
||||
end
|
||||
|
||||
def build_headers(example)
|
||||
headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ]
|
||||
headers.tap do |h|
|
||||
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
|
||||
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
|
||||
h['ACCEPT'] = produces.join(';') unless produces.nil?
|
||||
h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parameters_in(location)
|
||||
(@api_metadata[:operation][:parameters] || [])
|
||||
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
|
||||
.concat(resolve_api_key_parameters)
|
||||
.select { |p| p[:in] == location }
|
||||
end
|
||||
|
||||
def resolve_parameter(ref)
|
||||
defined_params = @global_metadata[:parameters]
|
||||
key = ref.sub('#/parameters/', '')
|
||||
raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key]
|
||||
defined_params[key]
|
||||
end
|
||||
|
||||
def resolve_api_key_parameters
|
||||
@api_key_params ||= begin
|
||||
global_requirements = (@global_metadata[:security] || {})
|
||||
requirements = global_requirements.merge(@api_metadata[:operation][:security] || {})
|
||||
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements.keys)
|
||||
definitions.values.select { |d| d[:type] == :apiKey }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
rswag-specs/lib/rswag/specs/response_validator.rb
Normal file
38
rswag-specs/lib/rswag/specs/response_validator.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require 'json-schema'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
class ResponseValidator
|
||||
|
||||
def initialize(api_metadata, global_metadata)
|
||||
@api_metadata = api_metadata
|
||||
@global_metadata = global_metadata
|
||||
end
|
||||
|
||||
def validate!(response)
|
||||
validate_code!(response.code)
|
||||
validate_body!(response.body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_code!(code)
|
||||
if code.to_s != @api_metadata[:response][:code].to_s
|
||||
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{@api_metadata[:response][:code]}'"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_body!(body)
|
||||
schema = @api_metadata[:response][:schema]
|
||||
return if schema.nil?
|
||||
begin
|
||||
JSON::Validator.validate!(schema.merge(@global_metadata), body)
|
||||
rescue JSON::Schema::ValidationError => ex
|
||||
raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class UnexpectedResponse < StandardError; end
|
||||
end
|
||||
end
|
||||
79
rswag-specs/lib/rswag/specs/swagger_formatter.rb
Normal file
79
rswag-specs/lib/rswag/specs/swagger_formatter.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
require 'active_support/core_ext/hash/deep_merge'
|
||||
require 'rspec/core/formatters/base_text_formatter'
|
||||
require 'swagger_helper'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
class SwaggerFormatter
|
||||
|
||||
# NOTE: rspec 2.x support
|
||||
if RSPEC_VERSION > 2
|
||||
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
|
||||
end
|
||||
|
||||
def initialize(output)
|
||||
@output = output
|
||||
@swagger_root = ::RSpec.configuration.swagger_root
|
||||
raise ConfigurationError, 'Missing swagger_root. See swagger_helper.rb' if @swagger_root.nil?
|
||||
@swagger_docs = ::RSpec.configuration.swagger_docs || []
|
||||
raise ConfigurationError, 'Missing swagger_docs. See swagger_helper.rb' if @swagger_docs.empty?
|
||||
|
||||
@output.puts 'Generating Swagger docs ...'
|
||||
end
|
||||
|
||||
def example_group_finished(notification)
|
||||
# NOTE: rspec 2.x support
|
||||
if RSPEC_VERSION > 2
|
||||
metadata = notification.group.metadata
|
||||
else
|
||||
metadata = notification.metadata
|
||||
end
|
||||
|
||||
return unless metadata.has_key?(:response)
|
||||
swagger_doc = get_swagger_doc(metadata[:swagger_doc])
|
||||
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
|
||||
end
|
||||
|
||||
def stop(notification=nil)
|
||||
@swagger_docs.each do |url_path, doc|
|
||||
file_path = File.join(@swagger_root, url_path)
|
||||
dirname = File.dirname(file_path)
|
||||
FileUtils.mkdir_p dirname unless File.exists?(dirname)
|
||||
|
||||
File.open(file_path, 'w') do |file|
|
||||
file.write(JSON.pretty_generate(doc))
|
||||
end
|
||||
|
||||
@output.puts "Swagger doc generated at #{file_path}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_swagger_doc(tag)
|
||||
return @swagger_docs.values.first if tag.nil?
|
||||
raise ConfigurationError, "Unknown swagger_doc '#{tag}'" unless @swagger_docs.has_key?(tag)
|
||||
@swagger_docs[tag]
|
||||
end
|
||||
|
||||
def metadata_to_swagger(metadata)
|
||||
response_code = metadata[:response][:code]
|
||||
response = metadata[:response].reject { |k,v| k == :code }
|
||||
verb = metadata[:operation][:verb]
|
||||
operation = metadata[:operation]
|
||||
.reject { |k,v| k == :verb }
|
||||
.merge(responses: { response_code => response })
|
||||
|
||||
{
|
||||
paths: {
|
||||
metadata[:path] => {
|
||||
verb => operation
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class ConfigurationError < StandardError; end
|
||||
end
|
||||
end
|
||||
5
rswag-specs/lib/rswag/specs/version.rb
Normal file
5
rswag-specs/lib/rswag/specs/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Rswag
|
||||
module Specs
|
||||
VERSION = '1.0.0'
|
||||
end
|
||||
end
|
||||
18
rswag-specs/lib/tasks/rswag-specs_tasks.rake
Normal file
18
rswag-specs/lib/tasks/rswag-specs_tasks.rake
Normal file
@@ -0,0 +1,18 @@
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
namespace :rswag do
|
||||
namespace :specs do
|
||||
|
||||
desc 'Generate Swagger JSON files from integration specs'
|
||||
RSpec::Core::RakeTask.new('swaggerize') do |t|
|
||||
t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
|
||||
|
||||
# NOTE: rspec 2.x support
|
||||
if Rswag::Specs::RSPEC_VERSION > 2
|
||||
t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined' ]
|
||||
else
|
||||
t.rspec_opts = [ '--format Rswag::Specs::SwaggerFormatter', '--order defined' ]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
rswag-specs/rswag-specs.gemspec
Normal file
20
rswag-specs/rswag-specs.gemspec
Normal file
@@ -0,0 +1,20 @@
|
||||
$:.push File.expand_path("../lib", __FILE__)
|
||||
|
||||
# Maintain your gem's version:
|
||||
require 'rswag/specs/version'
|
||||
|
||||
# Describe your gem and declare its dependencies:
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "rswag-specs"
|
||||
s.version = Rswag::Specs::VERSION
|
||||
s.authors = ["Richie Morris"]
|
||||
s.email = ["domaindrivendev@gmail.com"]
|
||||
s.homepage = "https://github.com/domaindrivendev/rswag"
|
||||
s.summary = "A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files"
|
||||
|
||||
s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ]
|
||||
|
||||
s.add_dependency "rails", ">= 3.1", "< 5.1"
|
||||
s.add_dependency 'json-schema'
|
||||
s.add_development_dependency 'rspec-rails'
|
||||
end
|
||||
0
rswag-specs/spec/generators/rswag/specs/fixtures/spec/.gitkeep
vendored
Normal file
0
rswag-specs/spec/generators/rswag/specs/fixtures/spec/.gitkeep
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
require 'generator_spec'
|
||||
require 'generators/rswag/specs/install/install_generator'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe InstallGenerator do
|
||||
include GeneratorSpec::TestCase
|
||||
destination File.expand_path('../tmp', __FILE__)
|
||||
|
||||
before(:all) do
|
||||
prepare_destination
|
||||
fixtures_dir = File.expand_path('../fixtures', __FILE__)
|
||||
FileUtils.cp_r("#{fixtures_dir}/spec", destination_root)
|
||||
|
||||
run_generator
|
||||
end
|
||||
|
||||
it 'installs the swagger_helper for rspec' do
|
||||
assert_file('spec/swagger_helper.rb')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Specify a root folder where Swagger JSON files are generated
|
||||
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
||||
# to ensure that it's confiugred to server Swagger from the same folder
|
||||
config.swagger_root = Rails.root.to_s + '/swagger'
|
||||
|
||||
# Define one or more Swagger documents and provide global metadata for each one
|
||||
# When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will
|
||||
# be generated at the provided relative path under swagger_root
|
||||
# By default, the operations defined in spec files are added to the first
|
||||
# document below. You can override this behavior by adding a swagger_doc tag to the
|
||||
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
|
||||
config.swagger_docs = {
|
||||
'v1/swagger.json' => {
|
||||
swagger: '2.0',
|
||||
info: {
|
||||
title: 'API V1',
|
||||
version: 'v1'
|
||||
},
|
||||
paths: {}
|
||||
}
|
||||
}
|
||||
end
|
||||
143
rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb
Normal file
143
rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
require 'rswag/specs/example_group_helpers'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe ExampleGroupHelpers do
|
||||
subject { double('example_group') }
|
||||
let(:api_metadata) { {} }
|
||||
before do
|
||||
subject.extend ExampleGroupHelpers
|
||||
allow(subject).to receive(:describe)
|
||||
allow(subject).to receive(:context)
|
||||
allow(subject).to receive(:metadata).and_return(api_metadata)
|
||||
end
|
||||
|
||||
describe '#path(path)' do
|
||||
before { subject.path('/blogs') }
|
||||
|
||||
it "delegates to 'describe' with 'path' metadata" do
|
||||
expect(subject).to have_received(:describe).with(
|
||||
'/blogs', path: '/blogs'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get|post|patch|put|delete|head(verb, summary)' do
|
||||
before { subject.post('Creates a blog') }
|
||||
|
||||
it "delegates to 'describe' with 'operation' metadata" do
|
||||
expect(subject).to have_received(:describe).with(
|
||||
:post, operation: { verb: :post, summary: 'Creates a blog' }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do
|
||||
let(:api_metadata) { { operation: {} } }
|
||||
before do
|
||||
subject.tags('Blogs', 'Admin')
|
||||
subject.description('Some description')
|
||||
subject.operationId('createBlog')
|
||||
subject.consumes('application/json', 'application/xml')
|
||||
subject.produces('application/json', 'application/xml')
|
||||
subject.schemes('http', 'https')
|
||||
subject.deprecated(true)
|
||||
end
|
||||
|
||||
it "adds to the 'operation' metadata" do
|
||||
expect(api_metadata[:operation]).to match(
|
||||
tags: [ 'Blogs', 'Admin' ],
|
||||
description: 'Some description',
|
||||
operationId: 'createBlog',
|
||||
consumes: [ 'application/json', 'application/xml' ],
|
||||
produces: [ 'application/json', 'application/xml' ],
|
||||
schemes: [ 'http', 'https' ],
|
||||
deprecated: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do
|
||||
let(:api_metadata) { { operation: {} } }
|
||||
before do
|
||||
subject.tags('Blogs', 'Admin')
|
||||
subject.description('Some description')
|
||||
subject.operationId('createBlog')
|
||||
subject.consumes('application/json', 'application/xml')
|
||||
subject.produces('application/json', 'application/xml')
|
||||
subject.schemes('http', 'https')
|
||||
subject.deprecated(true)
|
||||
subject.security(api_key: [])
|
||||
end
|
||||
|
||||
it "adds to the 'operation' metadata" do
|
||||
expect(api_metadata[:operation]).to match(
|
||||
tags: [ 'Blogs', 'Admin' ],
|
||||
description: 'Some description',
|
||||
operationId: 'createBlog',
|
||||
consumes: [ 'application/json', 'application/xml' ],
|
||||
produces: [ 'application/json', 'application/xml' ],
|
||||
schemes: [ 'http', 'https' ],
|
||||
deprecated: true,
|
||||
security: { api_key: [] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameter(attributes)' do
|
||||
let(:api_metadata) { { operation: {} } }
|
||||
|
||||
context 'always' do
|
||||
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
|
||||
|
||||
it "adds to the 'operation parameters' metadata" do
|
||||
expect(api_metadata[:operation][:parameters]).to match(
|
||||
[ name: :blog, in: :body, schema: { type: 'object' } ]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "'path' parameter" do
|
||||
before { subject.parameter(name: :id, in: :path) }
|
||||
|
||||
it "automatically sets the 'required' flag" do
|
||||
expect(api_metadata[:operation][:parameters]).to match(
|
||||
[ name: :id, in: :path, required: true ]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#response(code, description)' do
|
||||
before { subject.response('201', 'success') }
|
||||
|
||||
it "delegates to 'context' with 'response' metadata" do
|
||||
expect(subject).to have_received(:context).with(
|
||||
'success', response: { code: '201', description: 'success' }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#schema(value)' do
|
||||
let(:api_metadata) { { response: {} } }
|
||||
before { subject.schema(type: 'object') }
|
||||
|
||||
it "adds to the 'response' metadata" do
|
||||
expect(api_metadata[:response][:schema]).to match(type: 'object')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#header(name, attributes)' do
|
||||
let(:api_metadata) { { response: {} } }
|
||||
before { subject.header('Date', type: 'string') }
|
||||
|
||||
it "adds to the 'response headers' metadata" do
|
||||
expect(api_metadata[:response][:headers]).to match(
|
||||
'Date' => { type: 'string' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
66
rswag-specs/spec/rswag/specs/example_helpers_spec.rb
Normal file
66
rswag-specs/spec/rswag/specs/example_helpers_spec.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
require 'rswag/specs/example_helpers'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe ExampleHelpers do
|
||||
let(:api_metadata) do
|
||||
{
|
||||
path: '/blogs/{blog_id}/comments/{id}',
|
||||
operation: {
|
||||
verb: :put,
|
||||
summary: 'Updates a blog',
|
||||
parameters: [
|
||||
{ name: :blog_id, in: :path, type: 'integer' },
|
||||
{ name: 'id', in: :path, type: 'integer' },
|
||||
{ name: 'q1', in: :query, type: 'string' },
|
||||
{ name: :blog, in: :body, schema: { type: 'object' } }
|
||||
],
|
||||
security: {
|
||||
api_key: []
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:global_metadata) do
|
||||
{
|
||||
securityDefinitions: {
|
||||
api_key: {
|
||||
type: :apiKey,
|
||||
name: 'api_key',
|
||||
in: :query
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
subject { double('example') }
|
||||
|
||||
before do
|
||||
subject.extend ExampleHelpers
|
||||
allow(subject).to receive(:blog_id).and_return(1)
|
||||
allow(subject).to receive(:id).and_return(2)
|
||||
allow(subject).to receive(:q1).and_return('foo')
|
||||
allow(subject).to receive(:api_key).and_return('fookey')
|
||||
allow(subject).to receive(:blog).and_return(text: 'Some comment')
|
||||
allow(subject).to receive(:global_metadata).and_return(global_metadata)
|
||||
allow(subject).to receive(:put)
|
||||
end
|
||||
|
||||
describe '#submit_request(api_metadata)' do
|
||||
before do
|
||||
stub_const('Rails::VERSION::MAJOR', 3)
|
||||
subject.submit_request(api_metadata)
|
||||
end
|
||||
|
||||
it "submits a request built from metadata and 'let' values" do
|
||||
expect(subject).to have_received(:put).with(
|
||||
'/blogs/1/comments/2?q1=foo&api_key=fookey',
|
||||
"{\"text\":\"Some comment\"}",
|
||||
{}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
171
rswag-specs/spec/rswag/specs/request_factory_spec.rb
Normal file
171
rswag-specs/spec/rswag/specs/request_factory_spec.rb
Normal file
@@ -0,0 +1,171 @@
|
||||
require 'rswag/specs/request_factory'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe RequestFactory do
|
||||
let(:api_metadata) do
|
||||
{
|
||||
path: '/blogs/{blog_id}/comments/{id}',
|
||||
operation: {
|
||||
verb: :put,
|
||||
summary: 'Updates a blog',
|
||||
parameters: [
|
||||
{ name: :blog_id, in: :path, type: 'integer' },
|
||||
{ name: 'id', in: :path, type: 'integer' }
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:global_metadata) { {} }
|
||||
|
||||
subject { RequestFactory.new(api_metadata, global_metadata) }
|
||||
|
||||
let(:example) { double('example') }
|
||||
before do
|
||||
allow(example).to receive(:blog_id).and_return(1)
|
||||
allow(example).to receive(:id).and_return('2')
|
||||
end
|
||||
|
||||
describe '#build_fullpath(example)' do
|
||||
let(:path) { subject.build_fullpath(example) }
|
||||
|
||||
context 'always' do
|
||||
it "builds a path using metadata and example values" do
|
||||
expect(path).to eq('/blogs/1/comments/2')
|
||||
end
|
||||
end
|
||||
|
||||
context "'query' parameters" do
|
||||
before do
|
||||
api_metadata[:operation][:parameters] << { name: 'q1', in: :query, type: 'string' }
|
||||
api_metadata[:operation][:parameters] << { name: 'q2', in: :query, type: 'string' }
|
||||
allow(example).to receive(:q1).and_return('foo')
|
||||
allow(example).to receive(:q2).and_return('bar')
|
||||
end
|
||||
|
||||
it "appends a query string using metadata and example values" do
|
||||
expect(path).to eq('/blogs/1/comments/2?q1=foo&q2=bar')
|
||||
end
|
||||
end
|
||||
|
||||
context "global definition for 'api_key in query'" do
|
||||
before do
|
||||
global_metadata[:securityDefinitions] = { api_key: { type: :apiKey, name: 'api_key', in: :query } }
|
||||
allow(example).to receive(:api_key).and_return('fookey')
|
||||
end
|
||||
|
||||
context 'global requirement' do
|
||||
before { global_metadata[:security] = { api_key: [] } }
|
||||
|
||||
it "appends the api_key using metadata and example value" do
|
||||
expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
|
||||
end
|
||||
end
|
||||
|
||||
context 'operation-specific requirement' do
|
||||
before { api_metadata[:operation][:security] = { api_key: [] } }
|
||||
|
||||
it "appends the api_key using metadata and example value" do
|
||||
expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'global basePath' do
|
||||
before { global_metadata[:basePath] = '/foobar' }
|
||||
|
||||
it 'prepends the basePath' do
|
||||
expect(path).to eq('/foobar/blogs/1/comments/2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_body(example)' do
|
||||
let(:body) { subject.build_body(example) }
|
||||
|
||||
context "no 'body' parameter" do
|
||||
it "returns ''" do
|
||||
expect(body).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context "'body' parameter" do
|
||||
before do
|
||||
api_metadata[:operation][:parameters] << { name: 'comment', in: :body, schema: { type: 'object' } }
|
||||
allow(example).to receive(:comment).and_return(text: 'Some comment')
|
||||
end
|
||||
|
||||
it 'returns the example value as a json string' do
|
||||
expect(body).to eq("{\"text\":\"Some comment\"}")
|
||||
end
|
||||
end
|
||||
|
||||
context "referenced 'body' parameter" do
|
||||
before do
|
||||
api_metadata[:operation][:parameters] << { '$ref' => '#/parameters/comment' }
|
||||
global_metadata[:parameters] = {
|
||||
'comment' => { name: 'comment', in: :body, schema: { type: 'object' } }
|
||||
}
|
||||
allow(example).to receive(:comment).and_return(text: 'Some comment')
|
||||
end
|
||||
|
||||
it 'returns the example value as a json string' do
|
||||
expect(body).to eq("{\"text\":\"Some comment\"}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_headers' do
|
||||
let(:headers) { subject.build_headers(example) }
|
||||
|
||||
context "no 'header' params" do
|
||||
it 'returns an empty hash' do
|
||||
expect(headers).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context "'header' params" do
|
||||
before do
|
||||
api_metadata[:operation][:parameters] << { name: 'Api-Key', in: :header, type: 'string' }
|
||||
allow(example).to receive(:'Api-Key').and_return('foobar')
|
||||
end
|
||||
|
||||
it 'returns a hash of names with example values' do
|
||||
expect(headers).to eq({ 'Api-Key' => 'foobar' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'consumes & produces' do
|
||||
before do
|
||||
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
|
||||
api_metadata[:operation][:produces] = [ 'application/json', 'application/xml' ]
|
||||
end
|
||||
|
||||
it "includes corresponding 'Accept' & 'Content-Type' headers" do
|
||||
expect(headers).to match(
|
||||
'ACCEPT' => 'application/json;application/xml',
|
||||
'CONTENT_TYPE' => 'application/json;application/xml'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'global consumes & produces' do
|
||||
let(:global_metadata) do
|
||||
{
|
||||
consumes: [ 'application/json', 'application/xml' ],
|
||||
produces: [ 'application/json', 'application/xml' ]
|
||||
}
|
||||
end
|
||||
|
||||
it "includes corresponding 'Accept' & 'Content-Type' headers" do
|
||||
expect(headers).to match(
|
||||
'ACCEPT' => 'application/json;application/xml',
|
||||
'CONTENT_TYPE' => 'application/json;application/xml'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
72
rswag-specs/spec/rswag/specs/response_validator_spec.rb
Normal file
72
rswag-specs/spec/rswag/specs/response_validator_spec.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
require 'rswag/specs/response_validator'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe ResponseValidator do
|
||||
let(:api_metadata) { { response: { code: 200 } } }
|
||||
let(:global_metadata) { {} }
|
||||
|
||||
subject { ResponseValidator.new(api_metadata, global_metadata) }
|
||||
|
||||
describe '#validate!(response)' do
|
||||
let(:call) { subject.validate!(response) }
|
||||
|
||||
context "no 'schema' provided" do
|
||||
context 'response code matches' do
|
||||
let(:response) { OpenStruct.new(code: 200, body: '') }
|
||||
it { expect { call }.to_not raise_error }
|
||||
end
|
||||
|
||||
context 'response code does not match' do
|
||||
let(:response) { OpenStruct.new(code: 201, body: '') }
|
||||
it { expect { call }.to raise_error UnexpectedResponse }
|
||||
end
|
||||
end
|
||||
|
||||
context "'schema' provided" do
|
||||
before do
|
||||
api_metadata[:response][:schema] = {
|
||||
type: 'object',
|
||||
properties: { text: { type: 'string' } },
|
||||
required: [ 'text' ]
|
||||
}
|
||||
end
|
||||
|
||||
context 'response code & body matches' do
|
||||
let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some comment\"}") }
|
||||
it { expect { call }.to_not raise_error }
|
||||
end
|
||||
|
||||
context 'response code matches & body does not' do
|
||||
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some comment\"}") }
|
||||
it { expect { call }.to raise_error UnexpectedResponse }
|
||||
end
|
||||
end
|
||||
|
||||
context "referenced 'schema' provided" do
|
||||
before do
|
||||
api_metadata[:response][:schema] = { '$ref' => '#/definitions/author' }
|
||||
global_metadata[:definitions] = {
|
||||
author: {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: [ 'name' ]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'response code & body matches' do
|
||||
let(:response) { OpenStruct.new(code: 200, body: "{\"name\":\"Some name\"}") }
|
||||
it { expect { call }.to_not raise_error }
|
||||
end
|
||||
|
||||
context 'response code matches & body does not' do
|
||||
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some name\"}") }
|
||||
it { expect { call }.to raise_error UnexpectedResponse }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
125
rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb
Normal file
125
rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb
Normal file
@@ -0,0 +1,125 @@
|
||||
require 'rswag/specs/swagger_formatter'
|
||||
require 'ostruct'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
|
||||
describe SwaggerFormatter do
|
||||
# Mock infrastructure - output, RSpec.configuration etc.
|
||||
let(:output) { double('output').as_null_object }
|
||||
let(:swagger_root) { File.expand_path('../tmp', __FILE__) }
|
||||
let(:swagger_docs) do
|
||||
{
|
||||
'v1/swagger.json' => { info: { version: 'v1' } }
|
||||
}
|
||||
end
|
||||
let(:config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) }
|
||||
before { allow(RSpec).to receive(:configuration).and_return(config) }
|
||||
|
||||
subject { described_class.new(output) }
|
||||
|
||||
describe '::new(output)' do
|
||||
context 'swagger_root not configured' do
|
||||
let(:swagger_root) { nil }
|
||||
it { expect { subject }.to raise_error ConfigurationError }
|
||||
end
|
||||
|
||||
context 'swagger_docs not configured' do
|
||||
let(:swagger_docs) { nil }
|
||||
it { expect { subject }.to raise_error ConfigurationError }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#example_group_finished(notification)' do
|
||||
# Mock notification parameter
|
||||
let(:api_metadata) do
|
||||
{
|
||||
path: '/blogs',
|
||||
operation: { verb: :post, summary: 'Creates a blog' },
|
||||
response: { code: '201', description: 'blog created' }
|
||||
}
|
||||
end
|
||||
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
|
||||
|
||||
let(:call) { subject.example_group_finished(notification) }
|
||||
|
||||
context 'single swagger_doc' do
|
||||
before { call }
|
||||
|
||||
it 'converts metadata to swagger and merges into the doc' do
|
||||
expect(swagger_docs.values.first).to match(
|
||||
info: { version: 'v1' },
|
||||
paths: {
|
||||
'/blogs' => {
|
||||
post: {
|
||||
summary: 'Creates a blog',
|
||||
responses: {
|
||||
'201' => { description: 'blog created' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple swagger_docs' do
|
||||
let(:swagger_docs) do
|
||||
{
|
||||
'v1/swagger.json' => {},
|
||||
'v2/swagger.json' => {}
|
||||
}
|
||||
end
|
||||
|
||||
context "no 'swagger_doc' tag" do
|
||||
before { call }
|
||||
|
||||
it 'merges into the first doc' do
|
||||
expect(swagger_docs.values.first).to have_key(:paths)
|
||||
end
|
||||
end
|
||||
|
||||
context "matching 'swagger_doc' tag" do
|
||||
before do
|
||||
api_metadata[:swagger_doc] = 'v2/swagger.json'
|
||||
call
|
||||
end
|
||||
|
||||
it 'merges into the matched doc' do
|
||||
expect(swagger_docs.values.last).to have_key(:paths)
|
||||
end
|
||||
end
|
||||
|
||||
context "non matching 'swagger_doc' tag" do
|
||||
before { api_metadata[:swagger_doc] = 'foobar' }
|
||||
it { expect { call }.to raise_error ConfigurationError }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#stop' do
|
||||
let(:notification) { double('notification') }
|
||||
let(:swagger_docs) do
|
||||
{
|
||||
'v1/swagger.json' => { info: { version: 'v1' } },
|
||||
'v2/swagger.json' => { info: { version: 'v2' } },
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root)
|
||||
subject.stop(notification)
|
||||
end
|
||||
|
||||
it 'writes the swagger_doc(s) to file' do
|
||||
expect(File).to exist("#{swagger_root}/v1/swagger.json")
|
||||
expect(File).to exist("#{swagger_root}/v2/swagger.json")
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_r(swagger_root) if File.exists?(swagger_root)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
7
rswag-specs/spec/spec_helper.rb
Normal file
7
rswag-specs/spec/spec_helper.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module Rails
|
||||
module VERSION
|
||||
MAJOR = 3
|
||||
end
|
||||
end
|
||||
|
||||
require 'rswag/specs'
|
||||
2
rswag-specs/spec/swagger_helper.rb
Normal file
2
rswag-specs/spec/swagger_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
# NOTE: For the specs in this gem, all configuration is completely mocked out
|
||||
# The file just needs to be present because it gets required by the swagger_formatter
|
||||
Reference in New Issue
Block a user