rename to rswag plus major refactor - almost a rewrite

This commit is contained in:
richie
2016-10-11 18:31:12 -07:00
parent f8d993356f
commit c558098c39
453 changed files with 52410 additions and 35793 deletions

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,7 @@
module Rails
module VERSION
MAJOR = 3
end
end
require 'rswag/specs'

View 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