diff --git a/Gemfile b/Gemfile index d4c5bc7..670cece 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gemspec # To use a debugger # gem 'debugger', group: [:development, :test] +gem 'sqlite3' gem 'pry' gem 'rspec-rails' gem 'generator_spec' diff --git a/Gemfile.lock b/Gemfile.lock index 9863a5b..a957712 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,6 +105,7 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + sqlite3 (1.3.11) thor (0.19.1) tilt (1.4.1) treetop (1.4.15) @@ -119,4 +120,5 @@ DEPENDENCIES generator_spec pry rspec-rails + sqlite3 swagger_rails! diff --git a/lib/swagger_rails/testing/test_case_builder.rb b/lib/swagger_rails/testing/test_case_builder.rb new file mode 100644 index 0000000..775fd5a --- /dev/null +++ b/lib/swagger_rails/testing/test_case_builder.rb @@ -0,0 +1,115 @@ +module SwaggerRails + + class TestCaseBuilder + + def initialize(path_template, http_method, swagger) + @path_template = path_template + @http_method = http_method + @swagger = swagger + @param_values = {} + end + + def set(param_values) + @param_values.merge!(param_values.stringify_keys) + end + + def expect(status) + @expected_status = status.to_s + end + + def test_data + operation = find_operation! + parameters = operation['parameters'] || [] + responses = operation['responses'] + { + path: build_path(parameters), + params: build_params(parameters), + headers: build_headers(parameters), + expected_response: build_expected_response(responses) + } + end + + private + + def find_operation! + keys = [ 'paths', @path_template, @http_method ] + operation = find_hash_item!(@swagger, keys) + operation || (raise MetadataError.new(keys)) + end + + def find_hash_item!(hash, keys) + item = hash[keys[0]] || (return nil) + keys.length == 1 ? item : find_hash_item!(item, keys.drop(1)) + end + + def build_path(parameters) + param_values = param_values_for(parameters, 'path') + @path_template.dup.tap do |template| + template.prepend(@swagger['basePath'].presence || '') + param_values.each { |name, value| template.sub!("\{#{name}\}", value.to_s) } + end + end + + def build_params(parameters) + body_param_values = param_values_for(parameters, 'body') + return body_param_values.values.first.to_json if body_param_values.any? + param_values_for(parameters, 'query') + end + + def build_headers(parameters) + param_values_for(parameters, 'header') + .merge({ + 'CONTENT_TYPE' => 'application/json', + 'ACCEPT' => 'application/json' + }) + end + + def build_expected_response(responses) + status = @expected_status || responses.keys.find { |k| k.start_with?('2') } + response = responses[status] || (raise MetadataError.new('paths', @path_template, @http_method, 'responses', status)) + { + status: status.to_i, + body: response_body_for(response) + } + end + + def param_values_for(parameters, location) + applicable_parameters = parameters.select { |p| p['in'] == location } + Hash[applicable_parameters.map { |p| [ p['name'], param_value_for(p) ] }] + end + + def param_value_for(parameter) + return @param_values[parameter['name']] if @param_values.has_key?(parameter['name']) + return parameter['default'] unless parameter['in'] == 'body' + schema = schema_for(parameter['schema']) + schema_example_for(schema) + end + + def response_body_for(response) + return nil if response['schema'].nil? + schema = schema_for(response['schema']) + schema_example_for(schema) + end + + def schema_for(schema_or_ref) + return schema_or_ref if schema_or_ref['$ref'].nil? + @swagger['definitions'][schema_or_ref['$ref'].sub('#/definitions/', '')] + end + + def schema_example_for(schema) + return schema['example'] if schema['example'].present? + # If an array, try construct from the item example + if schema['type'] == 'array' && schema['item'].present? + item_schema = schema_for(schema['item']) + return [ schema_example_for(item_schema) ] + end + end + end + + class MetadataError < StandardError + def initialize(*path_keys) + path = path_keys.map { |key| "['#{key}']" }.join('') + super("Swagger document is missing expected metadata at #{path}") + end + end +end diff --git a/lib/swagger_rails/testing/test_helpers.rb b/lib/swagger_rails/testing/test_helpers.rb new file mode 100644 index 0000000..086c852 --- /dev/null +++ b/lib/swagger_rails/testing/test_helpers.rb @@ -0,0 +1,34 @@ +require 'swagger_rails/testing/test_visitor' + +module SwaggerRails + module TestHelpers + + def self.included(klass) + klass.extend(ClassMethods) + end + + module ClassMethods + attr_reader :test_visitor + + def swagger_doc(swagger_doc) + file_path = File.join(Rails.root, 'config/swagger', swagger_doc) + @swagger = JSON.parse(File.read(file_path)) + @test_visitor = SwaggerRails::TestVisitor.new(@swagger) + end + + def swagger_test_all + @swagger['paths'].each do |path, path_item| + path_item.keys.each do |method| + test "#{path} #{method}" do + swagger_test path, method + end + end + end + end + end + + def swagger_test(path, method, &block) + self.class.test_visitor.run_test(path, method, self, &block) + end + end +end diff --git a/lib/swagger_rails/testing/test_visitor.rb b/lib/swagger_rails/testing/test_visitor.rb new file mode 100644 index 0000000..9603e1c --- /dev/null +++ b/lib/swagger_rails/testing/test_visitor.rb @@ -0,0 +1,27 @@ +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 + diff --git a/spec/dummy/.rspec b/spec/dummy/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/spec/dummy/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/spec/dummy/app/controllers/application_controller.rb b/spec/dummy/app/controllers/application_controller.rb index d83690e..a4506fc 100644 --- a/spec/dummy/app/controllers/application_controller.rb +++ b/spec/dummy/app/controllers/application_controller.rb @@ -2,4 +2,6 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + + wrap_parameters format: [ :json ] end diff --git a/spec/dummy/app/controllers/blogs_controller.rb b/spec/dummy/app/controllers/blogs_controller.rb new file mode 100644 index 0000000..a36145a --- /dev/null +++ b/spec/dummy/app/controllers/blogs_controller.rb @@ -0,0 +1,22 @@ +class BlogsController < ApplicationController + wrap_parameters Blog + respond_to :json + + # POST /blogs + def create + @blog = Blog.create(params[:blog]) + respond_with @blog + end + + # GET /blogs + def index + @blogs = Blog.all + respond_with @blogs + end + + # GET /blogs/1 + def show + @blog = Blog.find(params[:id]) + respond_with @blog + end +end diff --git a/spec/dummy/app/models/blog.rb b/spec/dummy/app/models/blog.rb new file mode 100644 index 0000000..2f4b8f8 --- /dev/null +++ b/spec/dummy/app/models/blog.rb @@ -0,0 +1,10 @@ +class Blog < ActiveRecord::Base + attr_accessible :content, :title + + def as_json(options) + { + title: title, + content: content + } + end +end diff --git a/spec/dummy/app/views/layouts/application.html.erb b/spec/dummy/app/views/layouts/application.html.erb deleted file mode 100644 index 593a778..0000000 --- a/spec/dummy/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - - Dummy - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> - <%= csrf_meta_tags %> - - - -<%= yield %> - - - diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 56a55cb..11d35dc 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -1,12 +1,12 @@ require File.expand_path('../boot', __FILE__) # Pick the frameworks you want: -# require "active_record/railtie" +require "active_record/railtie" require "action_controller/railtie" require "action_mailer/railtie" require "action_view/railtie" require "sprockets/railtie" -# require "rails/test_unit/railtie" +require "rails/test_unit/railtie" Bundler.require(*Rails.groups) require "swagger_rails" diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml new file mode 100644 index 0000000..dc954e3 --- /dev/null +++ b/spec/dummy/config/database.yml @@ -0,0 +1,25 @@ +#SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: sqlite3 + database: db/test.sqlite3 + pool: 5 + timeout: 5000 + +production: + adapter: sqlite3 + database: db/production.sqlite3 + pool: 5 + timeout: 5000 diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 8264b95..99850bd 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do - mount SwaggerRails::Engine => '/api-docs' + resources :blogs, defaults: { :format => :json } + mount SwaggerRails::Engine => '/api-docs' end diff --git a/spec/dummy/config/swagger/v1/swagger.json b/spec/dummy/config/swagger/v1/swagger.json index e18a96f..8b859d9 100644 --- a/spec/dummy/config/swagger/v1/swagger.json +++ b/spec/dummy/config/swagger/v1/swagger.json @@ -2,43 +2,120 @@ "swagger": "2.0", "info": { "version": "0.0.0", - "title": "[Enter a description for your API here]", - "description": "The docs below are powered by the default swagger.json that gets installed with swagger_rails. You'll need to update it to describe your API. See here for the complete swagger spec - https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md" + "title": "Dummy app for testing swagger_rails" }, "paths": { - "/a/sample/resource": { + "/blogs": { "post": { - "tags": [ - "a/sample/resource" - ], - "description": "Create a new sample resource", + "description": "Creates a new Blog", "parameters": [ { - "name": "body", + "name": "X-Forwarded-For", + "in": "header", + "type": "string", + "default": "client1" + }, + { + "name": "blog", "in": "body", "schema": { - "$ref": "#/definitions/CreateSampleResource" + "$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" + "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": { - "CreateSampleResource": { + "Blog": { "properties": { - "name": { + "title": { "type": "string" }, - "date_time": { + "content": { "type": "string", "format": "date-time" } + }, + "example": { + "title": "Test Blog", + "content": "Hello World" + } + }, + "RequestError": { + "type": "object", + "additionalProperties": { + "type": "array", + "item": { + "type": "string" + } + }, + "example": { + "title": [ "is required" ] } } } diff --git a/spec/dummy/db/migrate/20160218212104_create_blogs.rb b/spec/dummy/db/migrate/20160218212104_create_blogs.rb new file mode 100644 index 0000000..81d9813 --- /dev/null +++ b/spec/dummy/db/migrate/20160218212104_create_blogs.rb @@ -0,0 +1,10 @@ +class CreateBlogs < ActiveRecord::Migration + def change + create_table :blogs do |t| + t.string :title + t.text :content + + t.timestamps + end + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb new file mode 100644 index 0000000..082bb8d --- /dev/null +++ b/spec/dummy/db/schema.rb @@ -0,0 +1,23 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 20160218212104) do + + create_table "blogs", :force => true do |t| + t.string "title" + t.text "content" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + +end diff --git a/spec/dummy/spec/integration/v1_contract_spec.rb b/spec/dummy/spec/integration/v1_contract_spec.rb new file mode 100644 index 0000000..2d05f30 --- /dev/null +++ b/spec/dummy/spec/integration/v1_contract_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' +require 'swagger_rails/testing/test_helpers' + +describe 'V1 Contract' do + include SwaggerRails::TestHelpers + swagger_doc 'v1/swagger.json' + + # TODO: improve DSL + + it 'exposes an API for managing blogs' do + swagger_test '/blogs', 'post' + + swagger_test '/blogs', 'get' + + swagger_test '/blogs/{id}', 'get' do + set id: Blog.last!.id + end + end +end diff --git a/spec/dummy/spec/rails_helper.rb b/spec/dummy/spec/rails_helper.rb new file mode 100644 index 0000000..97096a2 --- /dev/null +++ b/spec/dummy/spec/rails_helper.rb @@ -0,0 +1,53 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/dummy/spec/spec_helper.rb b/spec/dummy/spec/spec_helper.rb new file mode 100644 index 0000000..61e2738 --- /dev/null +++ b/spec/dummy/spec/spec_helper.rb @@ -0,0 +1,92 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/dummy/test/integration/v1_contract_test.rb b/spec/dummy/test/integration/v1_contract_test.rb new file mode 100644 index 0000000..23602cc --- /dev/null +++ b/spec/dummy/test/integration/v1_contract_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' +require 'swagger_rails/testing/test_helpers' + +class V1ContractTest < ActionDispatch::IntegrationTest + include SwaggerRails::TestHelpers + swagger_doc 'v1/swagger.json' + + # TODO: improve DSL + + test 'Blogs API' do + swagger_test '/blogs', 'post' + + swagger_test '/blogs', 'get' + + swagger_test '/blogs/{id}', 'get' do + set id: Blog.last!.id + end + end +end diff --git a/spec/dummy/test/test_helper.rb b/spec/dummy/test/test_helper.rb new file mode 100644 index 0000000..8298517 --- /dev/null +++ b/spec/dummy/test/test_helper.rb @@ -0,0 +1,7 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path('../../config/environment', __FILE__) +require 'rails/test_help' + +class ActiveSupport::TestCase + # Add more helper methods to be used by all tests here... +end diff --git a/spec/testing/test_case_builder_spec.rb b/spec/testing/test_case_builder_spec.rb new file mode 100644 index 0000000..d5f7404 --- /dev/null +++ b/spec/testing/test_case_builder_spec.rb @@ -0,0 +1,139 @@ +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 diff --git a/spec/testing/test_visitor_spec.rb b/spec/testing/test_visitor_spec.rb new file mode 100644 index 0000000..7409fe9 --- /dev/null +++ b/spec/testing/test_visitor_spec.rb @@ -0,0 +1,79 @@ +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