First iteration of rspec driven swagger

This commit is contained in:
domaindrivendev 2016-04-06 09:19:41 -07:00
parent d579dab7d8
commit 63861a3940
17 changed files with 312 additions and 445 deletions

View File

@ -3,8 +3,8 @@ module SwaggerRails
def index
@discovery_paths = Hash[
SwaggerRails.swagger_docs.map do |name, path|
[ name, "#{root_path}#{path}" ]
SwaggerRails.swagger_docs.map do |path, doc|
[ "#{root_path}#{path}", doc[:info][:title] ]
end
]

View File

@ -33,7 +33,7 @@
if (url && url.length > 1) {
url = decodeURIComponent(url[1]);
} else {
url = "<%= @discovery_paths.values.first %>";
url = "<%= @discovery_paths.keys.first %>";
}
// Pre load translate...
@ -110,8 +110,8 @@
<div class='input'><input placeholder="api_key" id="input_apiKey" name="apiKey" type="text"/></div>
<div class='input'>
<select id="select_version">
<% @discovery_paths.each do |name, path| %>
<option value="<%= path %>"><%= name %></option>
<% @discovery_paths.each do |path, title| %>
<option value="<%= path %>"><%= title %></option>
<% end %>
</select>
<script type="text/javascript">

View File

@ -1,8 +1,12 @@
SwaggerRails.configure do |c|
# List the names and paths (relative to config/swagger) of Swagger
# documents you'd like to expose in your swagger-ui
c.swagger_docs = {
'API V1' => 'v1/swagger.json'
}
# Define the swagger documents you'd like to expose and provide global metadata
c.swagger_doc 'v1/swagger.json' do
{
info: {
title: 'API V1',
version: 'v1'
}
}
end
end

View File

@ -7,10 +7,19 @@ module SwaggerRails
end
class << self
attr_accessor :swagger_docs
attr_accessor :doc_factories
@@doc_factories = {}
@@swagger_docs = {
'V1' => 'v1/swagger.json'
}
def swagger_doc(path, &block)
@@doc_factories[path] = block
end
def swagger_docs
Hash[
@@doc_factories.map do |path, factory|
[ path, { swagger: '2.0' }.merge(factory.call) ]
end
]
end
end
end

View File

@ -5,55 +5,56 @@ module SwaggerRails
module Adapter
def path(path_template, &block)
describe(path_template, path_template: path_template, &block)
metadata = {
path_template: path_template
}
describe(path_template, metadata, &block)
end
def operation(method, summary=nil, &block)
operation_metadata = {
method: method,
def operation(http_verb, summary=nil, &block)
metadata = {
http_verb: http_verb,
summary: summary,
parameters: []
}
describe(method, operation: operation_metadata, &block)
describe(http_verb, metadata, &block)
end
[ :get, :post, :patch, :put, :delete, :head ].each do |http_verb|
define_method(http_verb) do |summary=nil, &block|
operation(http_verb, summary, &block)
end
end
def consumes(*mime_types)
metadata[:operation][:consumes] = mime_types
metadata[:consumes] = mime_types
end
def produces(*mime_types)
metadata[:operation][:produces] = mime_types
metadata[:produces] = mime_types
end
def header(name, attributes={})
parameter(name, 'header', attributes)
def parameter(name, attributes={})
metadata[:parameters] << { name: name.to_s }.merge(attributes)
end
def body(name, attributes={})
parameter(name, 'body', attributes)
end
def parameter(name, location, attributes={})
parameter_metadata = { name: name.to_s, in: location }.merge(attributes)
metadata[:operation][:parameters] << parameter_metadata
end
def response(status, description, &block)
response_metadata = { status: status, description: description }
context(description, response: response_metadata, &block)
def response(code, description, &block)
metadata = {
response_code: code,
response: {
description: description
}
}
context(description, metadata, &block)
end
def run_test!
before do |example|
SwaggerRails::TestVisitor.instance.act!(
self, example.metadata[:path_template], example.metadata[:operation]
)
SwaggerRails::TestVisitor.instance.submit_request!(self, example.metadata)
end
it "returns a #{metadata[:response][:status]} status" do |example|
SwaggerRails::TestVisitor.instance.assert!(
self, example.metadata[:response]
)
it "returns a #{metadata[:response_code]} status" do |example|
SwaggerRails::TestVisitor.instance.assert_response!(self, example.metadata)
end
end
end

View File

@ -1,36 +1,60 @@
require 'rspec/core/formatters/base_text_formatter'
require 'rails_helper'
module SwaggerRails
module RSpec
class Formatter
::RSpec::Core::Formatters.register self,
:example_group_started,
:example_group_finished,
:stop
def initialize(output)
@output = output
@swagger_docs = {}
@group_level = 0
@swagger_docs = SwaggerRails.swagger_docs
@output.puts 'Generating Swagger Docs ...'
end
def example_group_started(notification)
@group_level += 1
group = notification.group
metadata = group.metadata
@output.puts "group_level: #{@group_level}"
@output.puts metadata.slice(:doc, :path_template, :operation, :response).inspect
end
def example_group_finished(notification)
@group_level -= 1
metadata = notification.group.metadata
return unless metadata.has_key?(:response_code)
swagger_data = swagger_data_from(metadata)
swagger_doc = @swagger_docs[metadata[:docs_path]] || @swagger_docs.values.first
swagger_doc.deep_merge!(swagger_data)
end
def stop(notification)
@swagger_docs.each do |path, doc|
file_path = File.join(Rails.root, 'config/swagger', path)
File.open(file_path, 'w') do |file|
file.write(JSON.pretty_generate(doc))
end
end
@output.puts 'Swagger Doc generated'
end
private
def swagger_data_from(metadata)
{
paths: {
metadata[:path_template] => {
metadata[:http_verb] => operation_from(metadata)
}
}
}
end
def operation_from(metadata)
metadata.slice(:summary, :consumes, :produces, :parameters).tap do |operation|
operation[:responses] = {
metadata[:response_code] => metadata[:response]
}
end
end
end
end

View File

@ -5,18 +5,18 @@ module SwaggerRails
include Singleton
def act!(test, path_template, operation)
params_data = params_data_for(test, operation[:parameters])
def submit_request!(test, metadata)
params_data = params_data_for(test, metadata[:parameters])
path = build_path(path_template, params_data)
path = build_path(metadata[:path_template], params_data)
body_or_params = build_body_or_params(params_data)
headers = build_headers(params_data, operation[:consumes], operation[:produces])
headers = build_headers(params_data, metadata[:consumes], metadata[:produces])
test.send(operation[:method], path, body_or_params, headers)
test.send(metadata[:http_verb], path, body_or_params, headers)
end
def assert!(test, expected_response)
test.assert_response(expected_response[:status].to_i)
def assert_response!(test, metadata)
test.assert_response(metadata[:response_code].to_i)
end
private
@ -30,57 +30,29 @@ module SwaggerRails
end
def build_path(path_template, params_data)
path = path_template.dup
params_data.each do |param_data|
path.sub!("\{#{param_data[:name]}\}", param_data[:value].to_s)
path_template.dup.tap do |path|
params_data.each do |param_data|
path.sub!("\{#{param_data[:name]}\}", param_data[:value].to_s)
end
end
return path
end
def build_body_or_params(params_data)
body_params_data = params_data.select { |p| p[:in] == 'body' }
body_params_data = params_data.select { |p| p[:in] == :body }
return body_params_data.first[:value].to_json if body_params_data.any?
query_params_data = params_data.select { |p| p[:in] == 'query' }
query_params_data = params_data.select { |p| p[:in] == :query }
Hash[query_params_data.map { |p| [ p[:name], p[:value] ] }]
end
def build_headers(params_data, consumes, produces)
header_params_data = params_data.select { |p| p[:in] == 'header' }
header_params_data = params_data.select { |p| p[:in] == :header }
headers = Hash[header_params_data.map { |p| [ p[:name].underscore.upcase, p[:value] ] }]
headers['ACCEPT'] = consumes.join(';') if consumes.present?
headers['CONTENT_TYPE'] = produces.join(';') if produces.present?
headers['ACCEPT'] = produces.join(';') if produces.present?
headers['CONTENT_TYPE'] = consumes.join(';') if consumes.present?
return headers
end
end
end
#require 'swagger_rails/testing/test_case_builder'
#
#module SwaggerRails
#
# class TestVisitor
#
# def initialize(swagger)
# @swagger = swagger
# end
#
# def run_test(path_template, http_method, test, &block)
# builder = TestCaseBuilder.new(path_template, http_method, @swagger)
# builder.instance_exec(&block) if block_given?
# test_data = builder.test_data
#
# test.send(http_method,
# test_data[:path],
# test_data[:params],
# test_data[:headers]
# )
#
# test.assert_response(test_data[:expected_response][:status])
# test.assert_equal(test_data[:expected_response][:body], JSON.parse(test.response.body))
# end
# end
#end
#

View File

@ -8,5 +8,5 @@ require 'rspec/core/rake_task'
desc 'Generate Swagger JSON files from integration specs'
RSpec::Core::RakeTask.new('swagger_rails:gen') do |t|
t.pattern = 'spec/integration/**/*_spec.rb'
t.rspec_opts = [ '--format SwaggerRails::RSpec::SwaggerFormatter' ]
t.rspec_opts = [ '--format SwaggerRails::RSpec::Formatter', '--dry-run' ]
end

View File

@ -5,6 +5,7 @@ class Blog < ActiveRecord::Base
def as_json(options)
{
id: id,
title: title,
content: content
}

View File

@ -1,8 +1,12 @@
SwaggerRails.configure do |c|
# List the names and paths (relative to config/swagger) of Swagger
# documents you'd like to expose in your swagger-ui
c.swagger_docs = {
'API V1' => 'v1/swagger.json'
}
# Define the swagger documents you'd like to expose and provide global metadata
c.swagger_doc 'v1/swagger.json' do
{
info: {
title: 'API V1',
version: 'v1'
}
}
end
end

View File

@ -1,122 +1,79 @@
{
"swagger": "2.0",
"info": {
"version": "0.0.0",
"title": "Dummy app for testing swagger_rails"
},
"paths": {
"/blogs": {
"post": {
"description": "Creates a new Blog",
"parameters": [
{
"name": "X-Forwarded-For",
"in": "header",
"type": "string",
"default": "client1"
},
{
"name": "blog",
"in": "body",
"schema": {
"$ref": "#/definitions/Blog"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/Blog"
}
},
"400": {
"description": "Invalid Request",
"schema": {
"$ref": "#/definitions/RequestError"
}
}
}
},
"get": {
"description": "Searches Bloggs",
"parameters": [
{
"name": "published",
"in": "query",
"type": "boolean",
"default": "true"
},
{
"name": "keywords",
"in": "query",
"type": "string",
"default": "Ruby on Rails"
}
],
"responses": {
"200": {
"description": "Ok",
"schema": {
"type": "array",
"item": {
"$ref": "#/definitions/Blog"
}
}
}
}
}
},
"/blogs/{id}": {
"get": {
"description": "Retrieves a specific Blog by unique ID",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"default": "123"
}
],
"responses": {
"200": {
"description": "Ok",
"schema": {
"$ref": "#/definitions/Blog"
}
}
}
}
}
},
"definitions": {
"Blog": {
"properties": {
"swagger": "2.0",
"info": {
"title": "API V1",
"version": "v1"
},
"paths": {
"/blogs": {
"post": {
"summary": "creates a new blog",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"name": "blog",
"in": "body",
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
"type": "string"
},
"content": {
"type": "string",
"format": "date-time"
"type": "string"
}
},
"example": {
"title": "Test Blog",
"content": "Hello World"
}
},
"RequestError": {
"type": "object",
"additionalProperties": {
"type": "array",
"item": {
"type": "string"
}
},
"example": {
"title": [ "is required" ]
}
}
],
"responses": {
"201": {
"description": "valid request"
},
"422": {
"description": "invalid request"
}
}
},
"get": {
"summary": "searches existing blogs",
"produces": [
"application/json"
],
"parameters": [
],
"responses": {
"200": {
"description": "valid request"
}
}
}
},
"/blogs/{id}": {
"get": {
"summary": "retreives a specific blog",
"produces": [
"application/json"
],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string"
}
],
"responses": {
"200": {
"description": "blog found"
}
}
}
}
}
}
}

View File

@ -1,13 +1,19 @@
require 'rails_helper'
describe 'Blogs API', doc: 'blogs/v1' do
describe 'Blogs API', docs_path: 'blogs/v1/swagger.json' do
path '/blogs' do
operation 'post', 'creates a new blog' do
post 'creates a new blog' do
consumes 'application/json'
produces 'application/json'
body :blog
parameter :blog, :in => :body, schema: {
:type => :object,
:properties => {
title: { type: 'string' },
content: { type: 'string' }
}
}
let(:blog) { { title: 'foo', content: 'bar' } }
@ -21,7 +27,7 @@ describe 'Blogs API', doc: 'blogs/v1' do
end
end
operation 'get', 'searches existing blogs' do
get 'searches existing blogs' do
produces 'application/json'
response '200', 'valid request' do
@ -31,9 +37,9 @@ describe 'Blogs API', doc: 'blogs/v1' do
end
path '/blogs/{id}' do
operation 'get', 'retreives a specific blog' do
get 'retreives a specific blog' do
produces 'application/json'
parameter :id, 'path'
parameter :id, :in => :path, :type => :string
response '200', 'blog found' do
let(:blog) { Blog.create(title: 'foo', content: 'bar') }

View File

@ -50,4 +50,7 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
require 'swagger_rails/rspec/adapter'
config.extend SwaggerRails::RSpec::Adapter
end

View File

@ -89,7 +89,4 @@ RSpec.configure do |config|
# as the one that triggered the failure.
Kernel.srand config.seed
=end
require 'swagger_rails/rspec/adapter'
config.extend SwaggerRails::RSpec::Adapter
end

107
spec/test_visitor_spec.rb Normal file
View File

@ -0,0 +1,107 @@
require 'rails_helper'
require 'swagger_rails/test_visitor'
module SwaggerRails
describe TestVisitor do
let(:test) { spy('test') }
subject { described_class.instance }
describe '#submit_request!' do
before { subject.submit_request!(test, metadata) }
context 'always' do
let(:metadata) do
{
path_template: '/resource',
http_verb: :get,
parameters: []
}
end
it 'dispatches the request to the provided test object' do
expect(test).to have_received(:get)
end
end
context 'given path parameters' do
let(:metadata) do
allow(test).to receive(:id).and_return(1)
return {
path_template: '/resource/{id}',
http_verb: :get,
parameters: [ { name: 'id', :in => :path, type: 'string' } ]
}
end
it 'builds the path from values on the test object' do
expect(test).to have_received(:get).with('/resource/1', {}, {})
end
end
context 'given body parameters' do
let(:metadata) do
allow(test).to receive(:resource).and_return({ foo: 'bar' })
return {
path_template: '/resource',
http_verb: :post,
consumes: [ 'application/json' ],
parameters: [ { name: 'resource', :in => :body, schema: { type: 'object' } } ]
}
end
it 'builds a body from value on the test object' do
expect(test).to have_received(:post).with(
'/resource',
"{\"foo\":\"bar\"}",
{ 'CONTENT_TYPE' => 'application/json' }
)
end
end
context 'given query parameters' do
let(:metadata) do
allow(test).to receive(:type).and_return('foo')
return {
path_template: '/resource',
http_verb: :get,
parameters: [ { name: 'type', :in => :query, type: 'string' } ]
}
end
it 'builds query params from values on the test object' do
expect(test).to have_received(:get).with('/resource', { 'type' => 'foo' }, {})
end
end
context 'given header parameters' do
let(:metadata) do
allow(test).to receive(:date).and_return('2000-01-01')
return {
path_template: '/resource',
http_verb: :get,
produces: [ 'application/json' ],
parameters: [ { name: 'Date', :in => :header, type: 'string' } ]
}
end
it 'builds request headers from values on the test object' do
expect(test).to have_received(:get).with(
'/resource',
{},
{ 'DATE' => '2000-01-01', 'ACCEPT' => 'application/json' }
)
end
end
end
describe '#assert_response' do
before { subject.assert_response!(test, metadata) }
let(:metadata) { { response_code: '200' } }
it 'dispatches the assert to the provided test object' do
expect(test).to have_received(:assert_response).with(200)
end
end
end
end

View File

@ -1,139 +0,0 @@
require 'rails_helper'
require 'swagger_rails/testing/test_case_builder'
module SwaggerRails
describe TestCaseBuilder do
subject { described_class.new(path, method, swagger) }
let(:swagger) do
file_path = File.join(Rails.root, 'config/swagger', 'v1/swagger.json')
JSON.parse(File.read(file_path))
end
describe '#test_data' do
let(:test_data) { subject.test_data }
context 'swagger includes basePath' do
before { swagger['basePath'] = '/foobar' }
let(:path) { '/blogs' }
let(:method) { 'post' }
it 'includes a path prefixed with basePath' do
expect(test_data[:path]).to eq('/foobar/blogs')
end
end
context 'operation has path params' do
let(:path) { '/blogs/{id}' }
let(:method) { 'get' }
context 'by default' do
it "includes a path built from 'default' values" do
expect(test_data[:path]).to eq('/blogs/123')
end
end
context 'values explicitly set' do
before { subject.set id: '456' }
it 'includes a path built from set values' do
expect(test_data[:path]).to eq('/blogs/456')
end
end
end
context 'operation has query params' do
let(:path) { '/blogs' }
let(:method) { 'get' }
context 'by default' do
it "includes params built from 'default' values" do
expect(test_data[:params]).to eq({ 'published' => 'true', 'keywords' => 'Ruby on Rails' })
end
end
context 'values explicitly set' do
before { subject.set keywords: 'Java' }
it 'includes params build from set values' do
expect(test_data[:params]).to eq({ 'published' => 'true', 'keywords' => 'Java' })
end
end
end
context 'operation has body param' do
let(:path) { '/blogs' }
let(:method) { 'post' }
context 'by default' do
it "includes params string based on schema 'example'" do
expect(test_data[:params]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json)
end
end
context 'values explicitly set' do
before { subject.set blog: { 'title' => 'foobar' } }
it 'includes params string based on set value' do
expect(test_data[:params]).to eq({ 'title' => 'foobar' }.to_json)
end
end
end
context 'operation has header params' do
let(:path) { '/blogs' }
let(:method) { 'post' }
context 'by default' do
it "includes headers built from 'default' values" do
expect(test_data[:headers]).to eq({
'X-Forwarded-For' => 'client1',
'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json'
})
end
end
context 'values explicitly params' do
before { subject.set 'X-Forwarded-For' => '192.168.1.1' }
it 'includes headers built from set values' do
expect(test_data[:headers]).to eq({
'X-Forwarded-For' => '192.168.1.1',
'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json'
})
end
end
end
context 'operation returns an object' do
let(:path) { '/blogs' }
let(:method) { 'post' }
context 'by default' do
it "includes expected_response based on spec'd 2xx status" do
expect(test_data[:expected_response][:status]).to eq(201)
expect(test_data[:expected_response][:body]).to eq({ 'title' => 'Test Blog', 'content' => 'Hello World' })
end
end
context 'expected status explicitly set' do
before { subject.expect 400 }
it "includes expected_response based on set status" do
expect(test_data[:expected_response][:status]).to eq(400)
expect(test_data[:expected_response][:body]).to eq({ 'title' => [ 'is required' ] })
end
end
end
context 'operation returns an array' do
let(:path) { '/blogs' }
let(:method) { 'get' }
context 'by default' do
it "includes expected_response based on spec'd 2xx status" do
expect(test_data[:expected_response][:status]).to eq(200)
expect(test_data[:expected_response][:body]).to eq([ { 'title' => 'Test Blog', 'content' => 'Hello World' } ])
end
end
end
end
end
end

View File

@ -1,79 +0,0 @@
require 'rails_helper'
require 'swagger_rails/testing/test_visitor'
module SwaggerRails
describe TestVisitor do
subject { described_class.new(swagger) }
let(:swagger) do
file_path = File.join(Rails.root, 'config/swagger', 'v1/swagger.json')
JSON.parse(File.read(file_path))
end
let(:test) { spy('test') }
describe '#run_test' do
before do
allow(test).to receive(:response).and_return(OpenStruct.new(body: "{}"))
end
context 'by default' do
before { subject.run_test('/blogs', 'post', test) }
it "submits request based on 'default' and 'example' param values" do
expect(test).to have_received(:post).with(
'/blogs',
{ 'title' => 'Test Blog', 'content' => 'Hello World' }.to_json,
{ 'X-Forwarded-For' => 'client1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
)
end
it "asserts response matches spec'd 2xx status" do
expect(test).to have_received(:assert_response).with(201)
end
it "asserts response body matches schema 'example' for 2xx status" do
expect(test).to have_received(:assert_equal).with(
{ 'title' => 'Test Blog', 'content' => 'Hello World' },
{}
)
end
end
context 'param values explicitly provided' do
before do
subject.run_test('/blogs', 'post', test) do
set blog: { 'title' => 'foobar' }
set 'X-Forwarded-For' => '192.168.1.1'
end
end
it 'submits a request based on provided param values' do
expect(test).to have_received(:post).with(
'/blogs',
{ 'title' => 'foobar' }.to_json,
{ 'X-Forwarded-For' => '192.168.1.1', 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
)
end
end
context 'expected status explicitly set' do
before do
subject.run_test('/blogs', 'post', test) do
expect 400
end
end
it "asserts response matches set status" do
expect(test).to have_received(:assert_response).with(400)
end
it "asserts response body matches schema 'example' for set status" do
expect(test).to have_received(:assert_equal).with(
{ 'title' => [ 'is required' ] },
{}
)
end
end
end
end
end