diff --git a/rswag-api/open_api-rswag-api.gemspec b/rswag-api/open_api-rswag-api.gemspec index 80255b9..fce54c7 100644 --- a/rswag-api/open_api-rswag-api.gemspec +++ b/rswag-api/open_api-rswag-api.gemspec @@ -6,7 +6,7 @@ Gem::Specification.new do |s| s.version = ENV['TRAVIS_TAG'] || '0.0.0' s.authors = ["Richie Morris", "Jay Danielian"] s.email = ["domaindrivendev@gmail.com"] - s.homepage = "https://github.com/domaindrivendev/rswag" + s.homepage = "https://github.com/jdanielian/rswag" s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints" s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints" s.license = "MIT" diff --git a/rswag-specs/lib/open_api/rswag/specs.rb b/rswag-specs/lib/open_api/rswag/specs.rb new file mode 100644 index 0000000..8ede89a --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs.rb @@ -0,0 +1,29 @@ +require 'rspec/core' +require 'open_api/rswag/specs/example_group_helpers' +require 'open_api/rswag/specs/example_helpers' +require 'open_api/rswag/specs/configuration' +require 'open_api/rswag/specs/railtie' if defined?(Rails::Railtie) + +module OpenApi + 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.add_setting :swagger_dry_run + c.extend ExampleGroupHelpers, type: :request + c.include ExampleHelpers, type: :request + end + + def self.config + @config ||= Configuration.new(RSpec.configuration) + 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 +end diff --git a/rswag-specs/lib/open_api/rswag/specs/configuration.rb b/rswag-specs/lib/open_api/rswag/specs/configuration.rb new file mode 100644 index 0000000..b552f28 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/configuration.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module OpenApi + module Rswag + module Specs + class Configuration + def initialize(rspec_config) + @rspec_config = rspec_config + end + + def swagger_root + @swagger_root ||= begin + if @rspec_config.swagger_root.nil? + raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' + end + + @rspec_config.swagger_root + end + end + + def swagger_docs + @swagger_docs ||= begin + if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? + raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' + end + + @rspec_config.swagger_docs + end + end + + def swagger_dry_run + @swagger_dry_run ||= begin + @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run + end + end + + def get_swagger_doc(name) + return swagger_docs.values.first if name.nil? + raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] + + swagger_docs[name] + end + end + + class ConfigurationError < StandardError; end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb new file mode 100644 index 0000000..9c356c5 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/example_group_helpers.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true +require 'hashie' + +module OpenApi + module Rswag + module Specs + module ExampleGroupHelpers + def path(template, metadata = {}, &block) + metadata[:path_item] = { template: template } + describe(template, metadata, &block) + end + + %i[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 + + %i[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 + %i[tags consumes produces schemes].each do |attr_name| + define_method(attr_name) do |*value| + metadata[:operation][attr_name] = value + end + end + + # NICE TO HAVE + # TODO: update generator templates to include 3.0 syntax + # TODO: setup travis CI? + + # MUST HAVES + # TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response + # can we handle all of them? + # Then can look at handling different request_body things like $ref, etc + # TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0 + # TODO: look at adding examples in content request_body + # https://swagger.io/docs/specification/describing-request-body/ + # need to make sure we output requestBody in the swagger generator .json + # also need to make sure that it can handle content: , required: true/false, schema: ref + + def request_body(attributes) + # can make this generic, and accept any incoming hash (like parameter method) + attributes.compact! + + if metadata[:operation][:requestBody].blank? + metadata[:operation][:requestBody] = attributes + elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content] + # merge in + content_hash = metadata[:operation][:requestBody][:content] + incoming_content_hash = attributes[:content] + content_hash.merge!(incoming_content_hash) if incoming_content_hash + end + end + + def request_body_json(schema:, required: true, description: nil, examples: nil) + passed_examples = Array(examples) + content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + if passed_examples.any? + # the request_factory is going to have to resolve the different ways that the example can be given + # it can contain a 'value' key which is a direct hash (easiest) + # it can contain a 'external_value' key which makes an external call to load the json + # it can contain a '$ref' key. Which points to #/components/examples/blog + passed_examples.each do |passed_example| + if passed_example.is_a?(Symbol) + example_key_name = passed_example + # TODO: write more tests around this adding to the parameter + # if symbol try and use save_request_example + param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema } + parameter(param_attributes) + elsif passed_example.is_a?(Hash) && passed_example[:externalValue] + param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema } + parameter(param_attributes) + elsif passed_example.is_a?(Hash) && passed_example['$ref'] + param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema } + parameter(param_attributes) + end + end + end + end + + def request_body_text_plain(required: false, description: nil, examples: nil) + content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + end + + # TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type + def request_body_xml(schema:,required: false, description: nil, examples: nil) + passed_examples = Array(examples) + content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} } + request_body(description: description, required: required, content: content_hash) + end + + def request_body_multipart(schema:, description: nil) + content_hash = { 'multipart/form-data' => { schema: schema }} + request_body(description: description, content: content_hash) + + schema.extend(Hashie::Extensions::DeepLocate) + file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary } + + hash_locator = [] + + file_properties.each do |match| + hash_match = schema.deep_locate -> (_k, v, _obj) { v == match } + hash_locator.concat(hash_match) unless hash_match.empty? + end + + property_hashes = hash_locator.flat_map do |locator| + locator.select { |_k,v| file_properties.include?(v) } + end + + property_hashes.each do |property_hash| + file_name = property_hash.keys.first + parameter name: file_name, in: :formData, type: :file, required: true + end + end + + def parameter(attributes) + if attributes[:in] && attributes[:in].to_sym == :path + attributes[:required] = true + end + + if attributes[:type] && attributes[:schema].nil? + attributes[:schema] = {type: attributes[:type]} + end + + if metadata.key?(:operation) + metadata[:operation][:parameters] ||= [] + metadata[:operation][:parameters] << attributes + else + metadata[:path_item][:parameters] ||= [] + metadata[:path_item][:parameters] << attributes + end + end + + def response(code, description, metadata = {}, &block) + metadata[:response] = { code: code, description: description } + context(description, metadata, &block) + end + + def schema(value, content_type: 'application/json') + content_hash = {content_type => {schema: value}} + metadata[:response][:content] = content_hash + end + + def header(name, attributes) + metadata[:response][:headers] ||= {} + + if attributes[:type] && attributes[:schema].nil? + attributes[:schema] = {type: attributes[:type]} + attributes.delete(:type) + end + + metadata[:response][:headers][name] = attributes + end + + # NOTE: Similar to 'description', 'examples' need to handle the case when + # being invoked with no params to avoid overriding 'examples' method of + # rspec-core ExampleGroup + def examples(example = nil) + return super() if example.nil? + + metadata[:response][:examples] = example + end + + # checks the examples in the parameters should be able to add $ref and externalValue examples. + # This syntax would look something like this in the integration _spec.rb file + # + # request_body_json schema: { '$ref' => '#/components/schemas/blog' }, + # examples: [:blog, {name: :external_blog, + # externalValue: 'http://api.sample.org/myjson_example'}, + # {name: :another_example, + # '$ref' => '#/components/examples/flexible_blog_example'}] + # The first value :blog, points to a let param of the same name, and is used to make the request in the + # integration test (it is used to build the request payload) + # + # The second item in the array shows how to add an externalValue for the examples in the requestBody section + # The third item shows how to add a $ref item that points to the components/examples section of the swagger spec. + # + # NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui + # will not show it yet + def merge_other_examples!(example_metadata) + # example.metadata[:operation][:requestBody][:content]['application/json'][:examples] + content_node = example_metadata[:operation][:requestBody][:content]['application/json'] + return unless content_node + + external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {} + ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {} + examples_node = content_node[:examples] ||= {} + + nodes_to_add = [] + nodes_to_add << external_example unless external_example.empty? + nodes_to_add << ref_example unless ref_example.empty? + + nodes_to_add.each do |node| + json_request_examples = examples_node ||= {} + other_name = node[:name][:name] + other_key = node[:name][:externalValue] ? :externalValue : '$ref' + if other_name + json_request_examples.merge!(other_name => {other_key => node[:param_value]}) + end + end + end + + def run_test!(&block) + # 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(metadata) + block.call(response) if block_given? + 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, &block) + example.instance_exec(response, &block) if block_given? + end + + after do |example| + body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] } + + if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json'] + # save response examples by default + example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty? + + # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test + if response.code.to_s =~ /^2\d{2}$/ + example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples] + json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples] + json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) } + + example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples + end + end + + self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody] + + end + end + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb b/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb new file mode 100644 index 0000000..ddfd27c --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/example_helpers.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'open_api/rswag/specs/request_factory' +require 'open_api/rswag/specs/response_validator' + +module OpenApi + module Rswag + module Specs + module ExampleHelpers + def submit_request(metadata) + request = RequestFactory.new.build_request(metadata, self) + + if RAILS_VERSION < 5 + send( + request[:verb], + request[:path], + request[:payload], + request[:headers] + ) + else + send( + request[:verb], + request[:path], + params: request[:payload], + headers: request[:headers] + ) + end + end + + def assert_response_matches_metadata(metadata) + ResponseValidator.new.validate!(metadata, response) + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb b/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb new file mode 100644 index 0000000..17e46b9 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/extended_schema.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'json-schema' + +module OpenApi + module Rswag + module Specs + class ExtendedSchema < JSON::Schema::Draft4 + def initialize + super + @attributes['type'] = ExtendedTypeAttribute + @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema') + @names = ['http://tempuri.org/rswag/specs/extended_schema'] + end + end + + class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute + def self.validate(current_schema, data, fragments, processor, validator, options = {}) + return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true) + + super + end + end + + JSON::Validator.register_validator(ExtendedSchema.new) + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/railtie.rb b/rswag-specs/lib/open_api/rswag/specs/railtie.rb new file mode 100644 index 0000000..e3e419e --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/railtie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module OpenApi + module Rswag + module Specs + class Railtie < ::Rails::Railtie + rake_tasks do + load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __dir__) + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/request_factory.rb b/rswag-specs/lib/open_api/rswag/specs/request_factory.rb new file mode 100644 index 0000000..adf9f58 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/request_factory.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/conversions' +require 'json' + +module OpenApi + module Rswag + module Specs + class RequestFactory + def initialize(config = ::OpenApi::Rswag::Specs.config) + @config = config + end + + def build_request(metadata, example) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + parameters = expand_parameters(metadata, swagger_doc, example) + + {}.tap do |request| + add_verb(request, metadata) + add_path(request, metadata, swagger_doc, parameters, example) + add_headers(request, metadata, swagger_doc, parameters, example) + add_payload(request, parameters, example) + end + end + + private + + def expand_parameters(metadata, swagger_doc, example) + operation_params = metadata[:operation][:parameters] || [] + path_item_params = metadata[:path_item][:parameters] || [] + security_params = derive_security_params(metadata, swagger_doc) + + # NOTE: Use of + instead of concat to avoid mutation of the metadata object + (operation_params + path_item_params + security_params) + .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } + .uniq { |p| p[:name] } + .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } + end + + def derive_security_params(metadata, swagger_doc) + requirements = metadata[:operation][:security] || swagger_doc[:security] || [] + scheme_names = requirements.flat_map(&:keys) + components = swagger_doc[:components] || {} + schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values + + schemes.map do |scheme| + param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } + param.merge(type: :string, required: requirements.one?) + end + end + + def resolve_parameter(ref, swagger_doc) + key = ref.sub('#/parameters/', '').to_sym + definitions = swagger_doc[:parameters] + raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] + + definitions[key] + end + + def add_verb(request, metadata) + request[:verb] = metadata[:operation][:verb] + end + + def add_path(request, metadata, swagger_doc, parameters, example) + template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] + + request[:path] = template.tap do |template| + parameters.select { |p| p[:in] == :path }.each do |p| + template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) + end + + parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| + template.concat(i == 0 ? '?' : '&') + template.concat(build_query_string_part(p, example.send(p[:name]))) + end + end + end + + def build_query_string_part(param, value) + name = param[:name] + return "#{name}=#{value}" unless param[:type].to_sym == :array + + case param[:collectionFormat] + when :ssv + "#{name}=#{value.join(' ')}" + when :tsv + "#{name}=#{value.join('\t')}" + when :pipes + "#{name}=#{value.join('|')}" + when :multi + value.map { |v| "#{name}=#{v}" }.join('&') + else + "#{name}=#{value.join(',')}" # csv is default + end + end + + def add_headers(request, metadata, swagger_doc, parameters, example) + tuples = parameters + .select { |p| p[:in] == :header } + .map { |p| [p[:name], example.send(p[:name]).to_s] } + + # Accept header + produces = metadata[:operation][:produces] || swagger_doc[:produces] + if produces + accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first + tuples << ['Accept', accept] + end + + # Content-Type header + consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] + if consumes + content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first + tuples << ['Content-Type', content_type] + end + + # Rails test infrastructure requires rackified headers + rackified_tuples = tuples.map do |pair| + [ + case pair[0] + when 'Accept' then 'HTTP_ACCEPT' + when 'Content-Type' then 'CONTENT_TYPE' + when 'Authorization' then 'HTTP_AUTHORIZATION' + else pair[0] + end, + pair[1] + ] + end + + request[:headers] = Hash[rackified_tuples] + end + + def add_payload(request, parameters, example) + content_type = request[:headers]['CONTENT_TYPE'] + return if content_type.nil? + + if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type) + request[:payload] = build_form_payload(parameters, example) + else + request[:payload] = build_json_payload(parameters, example) + end + end + + def build_form_payload(parameters, example) + # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/ + # Rather that serializing with the appropriate encoding (e.g. multipart/form-data), + # Rails test infrastructure allows us to send the values directly as a hash + # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test + tuples = parameters + .select { |p| p[:in] == :formData } + .map { |p| [p[:name], example.send(p[:name])] } + Hash[tuples] + end + + def build_json_payload(parameters, example) + body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first + return nil unless body_param + + source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name]) + source_body_param ||= body_param[:param_value] + source_body_param ? source_body_param.to_json : nil + end + end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/response_validator.rb b/rswag-specs/lib/open_api/rswag/specs/response_validator.rb new file mode 100644 index 0000000..85da237 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/response_validator.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/slice' +require 'json-schema' +require 'json' +require 'open_api/rswag/specs/extended_schema' + +module OpenApi + module Rswag + module Specs + class ResponseValidator + def initialize(config = ::OpenApi::Rswag::Specs.config) + @config = config + end + + def validate!(metadata, response) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + + validate_code!(metadata, response) + validate_headers!(metadata, response.headers) + validate_body!(metadata, swagger_doc, response.body) + end + + private + + def validate_code!(metadata, response) + expected = metadata[:response][:code].to_s + if response.code != expected + raise UnexpectedResponse, + "Expected response code '#{response.code}' to match '#{expected}'\n" \ + "Response body: #{response.body}" + end + end + + def validate_headers!(metadata, headers) + expected = (metadata[:response][:headers] || {}).keys + expected.each do |name| + raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil? + end + end + + def validate_body!(metadata, swagger_doc, body) + test_schemas = extract_schemas(metadata) + return if test_schemas.nil? + + components = swagger_doc[:components] || {} + components_schemas = { components: { schemas: components[:schemas] } } + + validation_schema = test_schemas[:schema] # response_schema + .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') + .merge(components_schemas) + + errors = JSON::Validator.fully_validate(validation_schema, body) + raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? + end + + def extract_schemas(metadata) + metadata[:operation] = {produces: []} if metadata[:operation].nil? + produces = Array(metadata[:operation][:produces]) + + producer_content = produces.first || 'application/json' + response_content = metadata[:response][:content] || {producer_content => {}} + response_content[producer_content] + end + end + + class UnexpectedResponse < StandardError; end + end + end +end diff --git a/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb new file mode 100644 index 0000000..8d83ef9 --- /dev/null +++ b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/deep_merge' +require 'swagger_helper' + +module OpenApi + 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, config = ::OpenApi::Rswag::Specs.config) + @output = output + @config = config + + @output.puts 'Generating Swagger docs ...' + end + + def example_group_finished(notification) + # NOTE: rspec 2.x support + metadata = if RSPEC_VERSION > 2 + notification.group.metadata + else + notification.metadata + end + + return unless metadata.key?(:response) + + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + swagger_doc.deep_merge!(metadata_to_swagger(metadata)) + end + + def stop(_notification = nil) + @config.swagger_docs.each do |url_path, doc| + # remove 2.0 parameters + doc[:paths]&.each_pair do |_k, v| + v.each_pair do |_verb, value| + is_hash = value.is_a?(Hash) + if is_hash && value.dig(:parameters) + schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] } + if value && schema_param && value&.dig(:requestBody, :content, 'application/json') + value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema]) + end + + value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData } + value[:parameters].each { |p| p.delete(:type) } + value[:headers].each { |p| p.delete(:type)} if value[:headers] + end + + value.delete(:consumes) if is_hash && value.dig(:consumes) + value.delete(:produces) if is_hash && value.dig(:produces) + end + end + + file_path = File.join(@config.swagger_root, url_path) + dirname = File.dirname(file_path) + FileUtils.mkdir_p dirname unless File.exist?(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 metadata_to_swagger(metadata) + response_code = metadata[:response][:code] + response = metadata[:response].reject { |k, _v| k == :code } + + # need to merge in to response + if response[:examples]&.dig('application/json') + example = response[:examples].dig('application/json').dup + schema = response.dig(:content, 'application/json', :schema) + new_hash = {example: example} + new_hash[:schema] = schema if schema + response.merge!(content: { 'application/json' => new_hash }) + response.delete(:examples) + end + + + verb = metadata[:operation][:verb] + operation = metadata[:operation] + .reject { |k, _v| k == :verb } + .merge(responses: { response_code => response }) + + path_template = metadata[:path_item][:template] + path_item = metadata[:path_item] + .reject { |k, _v| k == :template } + .merge(verb => operation) + + { paths: { path_template => path_item } } + end + end + end + end +end diff --git a/rswag-specs/lib/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb deleted file mode 100644 index de29ce9..0000000 --- a/rswag-specs/lib/rswag/specs.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'rspec/core' -require 'rswag/specs/example_group_helpers' -require 'rswag/specs/example_helpers' -require 'rswag/specs/configuration' -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.add_setting :swagger_dry_run - c.extend ExampleGroupHelpers, type: :request - c.include ExampleHelpers, type: :request - end - - def self.config - @config ||= Configuration.new(RSpec.configuration) - 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 diff --git a/rswag-specs/lib/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb deleted file mode 100644 index 6cc2767..0000000 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Rswag - module Specs - class Configuration - def initialize(rspec_config) - @rspec_config = rspec_config - end - - def swagger_root - @swagger_root ||= begin - if @rspec_config.swagger_root.nil? - raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' - end - - @rspec_config.swagger_root - end - end - - def swagger_docs - @swagger_docs ||= begin - if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? - raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' - end - - @rspec_config.swagger_docs - end - end - - def swagger_dry_run - @swagger_dry_run ||= begin - @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run - end - end - - def get_swagger_doc(name) - return swagger_docs.values.first if name.nil? - raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] - - swagger_docs[name] - end - end - - class ConfigurationError < StandardError; end - end -end diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb deleted file mode 100644 index b79d76f..0000000 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ /dev/null @@ -1,264 +0,0 @@ -# frozen_string_literal: true -require 'hashie' - -module Rswag - module Specs - module ExampleGroupHelpers - def path(template, metadata = {}, &block) - metadata[:path_item] = { template: template } - describe(template, metadata, &block) - end - - %i[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 - - %i[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 - %i[tags consumes produces schemes].each do |attr_name| - define_method(attr_name) do |*value| - metadata[:operation][attr_name] = value - end - end - - # NICE TO HAVE - # TODO: update generator templates to include 3.0 syntax - # TODO: setup travis CI? - - # MUST HAVES - # TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response - # can we handle all of them? - # Then can look at handling different request_body things like $ref, etc - # TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0 - # TODO: look at adding examples in content request_body - # https://swagger.io/docs/specification/describing-request-body/ - # need to make sure we output requestBody in the swagger generator .json - # also need to make sure that it can handle content: , required: true/false, schema: ref - - def request_body(attributes) - # can make this generic, and accept any incoming hash (like parameter method) - attributes.compact! - - if metadata[:operation][:requestBody].blank? - metadata[:operation][:requestBody] = attributes - elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content] - # merge in - content_hash = metadata[:operation][:requestBody][:content] - incoming_content_hash = attributes[:content] - content_hash.merge!(incoming_content_hash) if incoming_content_hash - end - end - - def request_body_json(schema:, required: true, description: nil, examples: nil) - passed_examples = Array(examples) - content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} } - request_body(description: description, required: required, content: content_hash) - if passed_examples.any? - # the request_factory is going to have to resolve the different ways that the example can be given - # it can contain a 'value' key which is a direct hash (easiest) - # it can contain a 'external_value' key which makes an external call to load the json - # it can contain a '$ref' key. Which points to #/components/examples/blog - passed_examples.each do |passed_example| - if passed_example.is_a?(Symbol) - example_key_name = passed_example - # TODO: write more tests around this adding to the parameter - # if symbol try and use save_request_example - param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema } - parameter(param_attributes) - elsif passed_example.is_a?(Hash) && passed_example[:externalValue] - param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema } - parameter(param_attributes) - elsif passed_example.is_a?(Hash) && passed_example['$ref'] - param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema } - parameter(param_attributes) - end - end - end - end - - def request_body_text_plain(required: false, description: nil, examples: nil) - content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} } - request_body(description: description, required: required, content: content_hash) - end - - # TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type - def request_body_xml(schema:,required: false, description: nil, examples: nil) - passed_examples = Array(examples) - content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} } - request_body(description: description, required: required, content: content_hash) - end - - def request_body_multipart(schema:, description: nil) - content_hash = { 'multipart/form-data' => { schema: schema }} - request_body(description: description, content: content_hash) - - schema.extend(Hashie::Extensions::DeepLocate) - file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary } - - hash_locator = [] - - file_properties.each do |match| - hash_match = schema.deep_locate -> (_k, v, _obj) { v == match } - hash_locator.concat(hash_match) unless hash_match.empty? - end - - property_hashes = hash_locator.flat_map do |locator| - locator.select { |_k,v| file_properties.include?(v) } - end - - property_hashes.each do |property_hash| - file_name = property_hash.keys.first - parameter name: file_name, in: :formData, type: :file, required: true - end - end - - def parameter(attributes) - if attributes[:in] && attributes[:in].to_sym == :path - attributes[:required] = true - end - - if attributes[:type] && attributes[:schema].nil? - attributes[:schema] = {type: attributes[:type]} - end - - if metadata.key?(:operation) - metadata[:operation][:parameters] ||= [] - metadata[:operation][:parameters] << attributes - else - metadata[:path_item][:parameters] ||= [] - metadata[:path_item][:parameters] << attributes - end - end - - def response(code, description, metadata = {}, &block) - metadata[:response] = { code: code, description: description } - context(description, metadata, &block) - end - - def schema(value, content_type: 'application/json') - content_hash = {content_type => {schema: value}} - metadata[:response][:content] = content_hash - end - - def header(name, attributes) - metadata[:response][:headers] ||= {} - - if attributes[:type] && attributes[:schema].nil? - attributes[:schema] = {type: attributes[:type]} - attributes.delete(:type) - end - - metadata[:response][:headers][name] = attributes - end - - # NOTE: Similar to 'description', 'examples' need to handle the case when - # being invoked with no params to avoid overriding 'examples' method of - # rspec-core ExampleGroup - def examples(example = nil) - return super() if example.nil? - - metadata[:response][:examples] = example - end - - # checks the examples in the parameters should be able to add $ref and externalValue examples. - # This syntax would look something like this in the integration _spec.rb file - # - # request_body_json schema: { '$ref' => '#/components/schemas/blog' }, - # examples: [:blog, {name: :external_blog, - # externalValue: 'http://api.sample.org/myjson_example'}, - # {name: :another_example, - # '$ref' => '#/components/examples/flexible_blog_example'}] - # The first value :blog, points to a let param of the same name, and is used to make the request in the - # integration test (it is used to build the request payload) - # - # The second item in the array shows how to add an externalValue for the examples in the requestBody section - # The third item shows how to add a $ref item that points to the components/examples section of the swagger spec. - # - # NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui - # will not show it yet - def merge_other_examples!(example_metadata) - # example.metadata[:operation][:requestBody][:content]['application/json'][:examples] - content_node = example_metadata[:operation][:requestBody][:content]['application/json'] - return unless content_node - - external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {} - ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {} - examples_node = content_node[:examples] ||= {} - - nodes_to_add = [] - nodes_to_add << external_example unless external_example.empty? - nodes_to_add << ref_example unless ref_example.empty? - - nodes_to_add.each do |node| - json_request_examples = examples_node ||= {} - other_name = node[:name][:name] - other_key = node[:name][:externalValue] ? :externalValue : '$ref' - if other_name - json_request_examples.merge!(other_name => {other_key => node[:param_value]}) - end - end - end - - def run_test!(&block) - # 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(metadata) - block.call(response) if block_given? - 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, &block) - example.instance_exec(response, &block) if block_given? - end - - after do |example| - body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] } - - if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json'] - # save response examples by default - example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty? - - # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test - if response.code.to_s =~ /^2\d{2}$/ - example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples] - json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples] - json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) } - - example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples - end - end - - self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody] - - end - end - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/example_helpers.rb b/rswag-specs/lib/rswag/specs/example_helpers.rb deleted file mode 100644 index c447742..0000000 --- a/rswag-specs/lib/rswag/specs/example_helpers.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rswag/specs/request_factory' -require 'rswag/specs/response_validator' - -module Rswag - module Specs - module ExampleHelpers - def submit_request(metadata) - request = RequestFactory.new.build_request(metadata, self) - - if RAILS_VERSION < 5 - send( - request[:verb], - request[:path], - request[:payload], - request[:headers] - ) - else - send( - request[:verb], - request[:path], - params: request[:payload], - headers: request[:headers] - ) - end - end - - def assert_response_matches_metadata(metadata) - ResponseValidator.new.validate!(metadata, response) - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/extended_schema.rb b/rswag-specs/lib/rswag/specs/extended_schema.rb deleted file mode 100644 index 3af8efc..0000000 --- a/rswag-specs/lib/rswag/specs/extended_schema.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'json-schema' - -module Rswag - module Specs - class ExtendedSchema < JSON::Schema::Draft4 - def initialize - super - @attributes['type'] = ExtendedTypeAttribute - @uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema') - @names = ['http://tempuri.org/rswag/specs/extended_schema'] - end - end - - class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute - def self.validate(current_schema, data, fragments, processor, validator, options = {}) - return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true) - - super - end - end - - JSON::Validator.register_validator(ExtendedSchema.new) - end -end diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb deleted file mode 100644 index 4e6b095..0000000 --- a/rswag-specs/lib/rswag/specs/railtie.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Rswag - module Specs - class Railtie < ::Rails::Railtie - rake_tasks do - load File.expand_path('../../tasks/rswag-specs_tasks.rake', __dir__) - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb deleted file mode 100644 index 9fc20b3..0000000 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/conversions' -require 'json' - -module Rswag - module Specs - class RequestFactory - def initialize(config = ::Rswag::Specs.config) - @config = config - end - - def build_request(metadata, example) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - parameters = expand_parameters(metadata, swagger_doc, example) - - {}.tap do |request| - add_verb(request, metadata) - add_path(request, metadata, swagger_doc, parameters, example) - add_headers(request, metadata, swagger_doc, parameters, example) - add_payload(request, parameters, example) - end - end - - private - - def expand_parameters(metadata, swagger_doc, example) - operation_params = metadata[:operation][:parameters] || [] - path_item_params = metadata[:path_item][:parameters] || [] - security_params = derive_security_params(metadata, swagger_doc) - - # NOTE: Use of + instead of concat to avoid mutation of the metadata object - (operation_params + path_item_params + security_params) - .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } - .uniq { |p| p[:name] } - .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } - end - - def derive_security_params(metadata, swagger_doc) - requirements = metadata[:operation][:security] || swagger_doc[:security] || [] - scheme_names = requirements.flat_map(&:keys) - components = swagger_doc[:components] || {} - schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values - - schemes.map do |scheme| - param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } - param.merge(type: :string, required: requirements.one?) - end - end - - def resolve_parameter(ref, swagger_doc) - key = ref.sub('#/parameters/', '').to_sym - definitions = swagger_doc[:parameters] - raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] - - definitions[key] - end - - def add_verb(request, metadata) - request[:verb] = metadata[:operation][:verb] - end - - def add_path(request, metadata, swagger_doc, parameters, example) - template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] - - request[:path] = template.tap do |template| - parameters.select { |p| p[:in] == :path }.each do |p| - template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) - end - - parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| - template.concat(i == 0 ? '?' : '&') - template.concat(build_query_string_part(p, example.send(p[:name]))) - end - end - end - - def build_query_string_part(param, value) - name = param[:name] - return "#{name}=#{value}" unless param[:type].to_sym == :array - - case param[:collectionFormat] - when :ssv - "#{name}=#{value.join(' ')}" - when :tsv - "#{name}=#{value.join('\t')}" - when :pipes - "#{name}=#{value.join('|')}" - when :multi - value.map { |v| "#{name}=#{v}" }.join('&') - else - "#{name}=#{value.join(',')}" # csv is default - end - end - - def add_headers(request, metadata, swagger_doc, parameters, example) - tuples = parameters - .select { |p| p[:in] == :header } - .map { |p| [p[:name], example.send(p[:name]).to_s] } - - # Accept header - produces = metadata[:operation][:produces] || swagger_doc[:produces] - if produces - accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first - tuples << ['Accept', accept] - end - - # Content-Type header - consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] - if consumes - content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first - tuples << ['Content-Type', content_type] - end - - # Rails test infrastructure requires rackified headers - rackified_tuples = tuples.map do |pair| - [ - case pair[0] - when 'Accept' then 'HTTP_ACCEPT' - when 'Content-Type' then 'CONTENT_TYPE' - when 'Authorization' then 'HTTP_AUTHORIZATION' - else pair[0] - end, - pair[1] - ] - end - - request[:headers] = Hash[rackified_tuples] - end - - def add_payload(request, parameters, example) - content_type = request[:headers]['CONTENT_TYPE'] - return if content_type.nil? - - if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type) - request[:payload] = build_form_payload(parameters, example) - else - request[:payload] = build_json_payload(parameters, example) - end - end - - def build_form_payload(parameters, example) - # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/ - # Rather that serializing with the appropriate encoding (e.g. multipart/form-data), - # Rails test infrastructure allows us to send the values directly as a hash - # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test - tuples = parameters - .select { |p| p[:in] == :formData } - .map { |p| [p[:name], example.send(p[:name])] } - Hash[tuples] - end - - def build_json_payload(parameters, example) - body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first - return nil unless body_param - - source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name]) - source_body_param ||= body_param[:param_value] - source_body_param ? source_body_param.to_json : nil - end - end - end -end diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb deleted file mode 100644 index 38d4dbf..0000000 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require 'active_support/core_ext/hash/slice' -require 'json-schema' -require 'json' -require 'rswag/specs/extended_schema' - -module Rswag - module Specs - class ResponseValidator - def initialize(config = ::Rswag::Specs.config) - @config = config - end - - def validate!(metadata, response) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - - validate_code!(metadata, response) - validate_headers!(metadata, response.headers) - validate_body!(metadata, swagger_doc, response.body) - end - - private - - def validate_code!(metadata, response) - expected = metadata[:response][:code].to_s - if response.code != expected - raise UnexpectedResponse, - "Expected response code '#{response.code}' to match '#{expected}'\n" \ - "Response body: #{response.body}" - end - end - - def validate_headers!(metadata, headers) - expected = (metadata[:response][:headers] || {}).keys - expected.each do |name| - raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil? - end - end - - def validate_body!(metadata, swagger_doc, body) - test_schemas = extract_schemas(metadata) - return if test_schemas.nil? - - components = swagger_doc[:components] || {} - components_schemas = { components: { schemas: components[:schemas] } } - - validation_schema = test_schemas[:schema] # response_schema - .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') - .merge(components_schemas) - errors = JSON::Validator.fully_validate(validation_schema, body) - raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any? - end - - def extract_schemas(metadata) - produces = Array(metadata[:operation][:produces]) - response_content = metadata[:response][:content] || {} - producer_content = produces.first || 'application/json' - response_content[producer_content] - end - end - - class UnexpectedResponse < StandardError; end - end -end diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb deleted file mode 100644 index d0d447f..0000000 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'active_support/core_ext/hash/deep_merge' -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, config = Rswag::Specs.config) - @output = output - @config = config - - @output.puts 'Generating Swagger docs ...' - end - - def example_group_finished(notification) - # NOTE: rspec 2.x support - metadata = if RSPEC_VERSION > 2 - notification.group.metadata - else - notification.metadata - end - - return unless metadata.key?(:response) - - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - swagger_doc.deep_merge!(metadata_to_swagger(metadata)) - end - - def stop(_notification = nil) - @config.swagger_docs.each do |url_path, doc| - # remove 2.0 parameters - doc[:paths]&.each_pair do |_k, v| - v.each_pair do |_verb, value| - is_hash = value.is_a?(Hash) - if is_hash && value.dig(:parameters) - schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] } - if value && schema_param && value&.dig(:requestBody, :content, 'application/json') - value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema]) - end - - value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData } - value[:parameters].each { |p| p.delete(:type) } - value[:headers].each { |p| p.delete(:type)} if value[:headers] - end - - value.delete(:consumes) if is_hash && value.dig(:consumes) - value.delete(:produces) if is_hash && value.dig(:produces) - end - end - - file_path = File.join(@config.swagger_root, url_path) - dirname = File.dirname(file_path) - FileUtils.mkdir_p dirname unless File.exist?(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 metadata_to_swagger(metadata) - response_code = metadata[:response][:code] - response = metadata[:response].reject { |k, _v| k == :code } - - # need to merge in to response - if response[:examples]&.dig('application/json') - example = response[:examples].dig('application/json').dup - schema = response.dig(:content, 'application/json', :schema) - new_hash = {example: example} - new_hash[:schema] = schema if schema - response.merge!(content: { 'application/json' => new_hash }) - response.delete(:examples) - end - - - verb = metadata[:operation][:verb] - operation = metadata[:operation] - .reject { |k, _v| k == :verb } - .merge(responses: { response_code => response }) - - path_template = metadata[:path_item][:template] - path_item = metadata[:path_item] - .reject { |k, _v| k == :template } - .merge(verb => operation) - - { paths: { path_template => path_item } } - end - end - end -end diff --git a/rswag-specs/rswag-specs.gemspec b/rswag-specs/open_api-rswag-specs.gemspec similarity index 88% rename from rswag-specs/rswag-specs.gemspec rename to rswag-specs/open_api-rswag-specs.gemspec index f402cdb..424fcfa 100644 --- a/rswag-specs/rswag-specs.gemspec +++ b/rswag-specs/open_api-rswag-specs.gemspec @@ -6,9 +6,9 @@ $LOAD_PATH.push File.expand_path('lib', __dir__) Gem::Specification.new do |s| s.name = 'rswag-specs' s.version = ENV['TRAVIS_TAG'] || '0.0.0' - s.authors = ['Richie Morris'] + s.authors = ['Richie Morris', 'Jay Danielian'] s.email = ['domaindrivendev@gmail.com'] - s.homepage = 'https://github.com/domaindrivendev/rswag' + s.homepage = 'https://github.com/jdanielian/rswag' s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files' s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs' s.license = 'MIT' diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index d265fce..5791baf 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -1,75 +1,77 @@ # frozen_string_literal: true -require 'rswag/specs/configuration' +require 'open_api/rswag/specs/configuration' -module Rswag - module Specs - describe Configuration do - subject { described_class.new(rspec_config) } +module OpenApi + module Rswag + module Specs + describe Configuration do + subject { described_class.new(rspec_config) } - let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } - let(:swagger_root) { 'foobar' } - let(:swagger_docs) do - { - 'v1/swagger.json' => { info: { title: 'v1' } }, - 'v2/swagger.json' => { info: { title: 'v2' } } - } - end - - describe '#swagger_root' do - let(:response) { subject.swagger_root } - - context 'provided in rspec config' do - it { expect(response).to eq('foobar') } + let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } + let(:swagger_root) { 'foobar' } + let(:swagger_docs) do + { + 'v1/swagger.json' => { info: { title: 'v1' } }, + 'v2/swagger.json' => { info: { title: 'v2' } } + } end - context 'not provided' do - let(:swagger_root) { nil } - it { expect { response }.to raise_error ConfigurationError } - end - end + describe '#swagger_root' do + let(:response) { subject.swagger_root } - describe '#swagger_docs' do - let(:response) { subject.swagger_docs } + context 'provided in rspec config' do + it { expect(response).to eq('foobar') } + end - context 'provided in rspec config' do - it { expect(response).to be_an_instance_of(Hash) } - end - - context 'not provided' do - let(:swagger_docs) { nil } - it { expect { response }.to raise_error ConfigurationError } - end - - context 'provided but empty' do - let(:swagger_docs) { {} } - it { expect { response }.to raise_error ConfigurationError } - end - end - - describe '#get_swagger_doc(tag=nil)' do - let(:swagger_doc) { subject.get_swagger_doc(tag) } - - context 'no tag provided' do - let(:tag) { nil } - - it 'returns the first doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v1' }) + context 'not provided' do + let(:swagger_root) { nil } + it { expect { response }.to raise_error ConfigurationError } end end - context 'tag provided' do - context 'matching doc' do - let(:tag) { 'v2/swagger.json' } + describe '#swagger_docs' do + let(:response) { subject.swagger_docs } - it 'returns the matching doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v2' }) + context 'provided in rspec config' do + it { expect(response).to be_an_instance_of(Hash) } + end + + context 'not provided' do + let(:swagger_docs) { nil } + it { expect { response }.to raise_error ConfigurationError } + end + + context 'provided but empty' do + let(:swagger_docs) { {} } + it { expect { response }.to raise_error ConfigurationError } + end + end + + describe '#get_swagger_doc(tag=nil)' do + let(:swagger_doc) { subject.get_swagger_doc(tag) } + + context 'no tag provided' do + let(:tag) { nil } + + it 'returns the first doc in rspec config' do + expect(swagger_doc).to eq(info: { title: 'v1' }) end end - context 'no matching doc' do - let(:tag) { 'foobar' } - it { expect { swagger_doc }.to raise_error ConfigurationError } + context 'tag provided' do + context 'matching doc' do + let(:tag) { 'v2/swagger.json' } + + it 'returns the matching doc in rspec config' do + expect(swagger_doc).to eq(info: { title: 'v2' }) + end + end + + context 'no matching doc' do + let(:tag) { 'foobar' } + it { expect { swagger_doc }.to raise_error ConfigurationError } + end end end end diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb index e2be0aa..430c059 100644 --- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb @@ -1,215 +1,217 @@ # frozen_string_literal: true -require 'rswag/specs/example_group_helpers' +require 'open_api/rswag/specs/example_group_helpers' -module Rswag - module Specs - describe ExampleGroupHelpers do - subject { double('example_group') } +module OpenApi + module Rswag + module Specs + describe ExampleGroupHelpers do + subject { double('example_group') } - 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 - let(:api_metadata) { {} } - - 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_item: { template: '/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 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.extend ExampleGroupHelpers + allow(subject).to receive(:describe) + allow(subject).to receive(:context) + allow(subject).to receive(:metadata).and_return(api_metadata) end - let(:api_metadata) { { operation: {} } } + let(:api_metadata) { {} } - it "adds to the 'operation' metadata" do - expect(api_metadata[:operation]).to match( - tags: %w[Blogs Admin], - description: 'Some description', - operationId: 'createBlog', - consumes: ['application/json', 'application/xml'], - produces: ['application/json', 'application/xml'], - schemes: %w[http https], - deprecated: true - ) - end - end + describe '#path(path)' do + before { subject.path('/blogs') } - describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do - 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 - let(:api_metadata) { { operation: {} } } - - it "adds to the 'operation' metadata" do - expect(api_metadata[:operation]).to match( - tags: %w[Blogs Admin], - description: 'Some description', - operationId: 'createBlog', - consumes: ['application/json', 'application/xml'], - produces: ['application/json', 'application/xml'], - schemes: %w[http https], - deprecated: true, - security: { api_key: [] } - ) - end - end - - describe '#request_body_json(schema)' do - let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined - context 'when required is not supplied' do - before { subject.request_body_json(schema: { type: 'object' }) } - - it 'adds required true by default' do - expect(api_metadata[:operation][:requestBody]).to match( - required: true, content: { 'application/json' => { schema: { type: 'object' } } } + it "delegates to 'describe' with 'path' metadata" do + expect(subject).to have_received(:describe).with( + '/blogs', path_item: { template: '/blogs' } ) end end - context 'when required is supplied' do - before { subject.request_body_json(schema: { type: 'object' }, required: false) } + describe '#get|post|patch|put|delete|head(verb, summary)' do + before { subject.post('Creates a blog') } - it 'adds required false' do - expect(api_metadata[:operation][:requestBody]).to match( - required: false, content: { 'application/json' => { schema: { type: 'object' } } } + 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 - context 'when required is supplied' do - before { subject.request_body_json(schema: { type: 'object' }, description: 'my description') } - - it 'adds description' do - expect(api_metadata[:operation][:requestBody]).to match( - description: 'my description', required: true, content: { 'application/json' => { schema: { type: 'object' } } } - ) + describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do + 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 - end - end + let(:api_metadata) { { operation: {} } } - describe '#parameter(attributes)' do - context "when called at the 'path' level" do - before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } - let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet - - it "adds to the 'path_item parameters' metadata" do - expect(api_metadata[:path_item][:parameters]).to match( - [name: :blog, in: :body, schema: { type: 'object' }] - ) + it "adds to the 'operation' metadata" do + expect(api_metadata[:operation]).to match( + tags: %w[Blogs Admin], + description: 'Some description', + operationId: 'createBlog', + consumes: ['application/json', 'application/xml'], + produces: ['application/json', 'application/xml'], + schemes: %w[http https], + deprecated: true + ) end end - context "when called at the 'operation' level" do - before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do + 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 + let(:api_metadata) { { operation: {} } } + + it "adds to the 'operation' metadata" do + expect(api_metadata[:operation]).to match( + tags: %w[Blogs Admin], + description: 'Some description', + operationId: 'createBlog', + consumes: ['application/json', 'application/xml'], + produces: ['application/json', 'application/xml'], + schemes: %w[http https], + deprecated: true, + security: { api_key: [] } + ) + end + end + + describe '#request_body_json(schema)' do let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined + context 'when required is not supplied' do + before { subject.request_body_json(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' }] + it 'adds required true by default' do + expect(api_metadata[:operation][:requestBody]).to match( + required: true, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + + context 'when required is supplied' do + before { subject.request_body_json(schema: { type: 'object' }, required: false) } + + it 'adds required false' do + expect(api_metadata[:operation][:requestBody]).to match( + required: false, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + + context 'when required is supplied' do + before { subject.request_body_json(schema: { type: 'object' }, description: 'my description') } + + it 'adds description' do + expect(api_metadata[:operation][:requestBody]).to match( + description: 'my description', required: true, content: { 'application/json' => { schema: { type: 'object' } } } + ) + end + end + end + + describe '#parameter(attributes)' do + context "when called at the 'path' level" do + before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet + + it "adds to the 'path_item parameters' metadata" do + expect(api_metadata[:path_item][:parameters]).to match( + [name: :blog, in: :body, schema: { type: 'object' }] + ) + end + end + + context "when called at the 'operation' level" do + before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) } + let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined + + 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) } + let(:api_metadata) { { operation: {} } } + + it "automatically sets the 'required' flag" do + expect(api_metadata[:operation][:parameters]).to match( + [name: :id, in: :path, required: true] + ) + end + end + + context "when 'in' parameter key is not defined" do + before { subject.parameter(name: :id) } + let(:api_metadata) { { operation: {} } } + + it "does not require the 'in' parameter key" do + expect(api_metadata[:operation][:parameters]).to match([name: :id]) + 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 - context "'path' parameter" do - before { subject.parameter(name: :id, in: :path) } - let(:api_metadata) { { operation: {} } } + describe '#schema(value)' do + before { subject.schema(type: 'object') } + let(:api_metadata) { { response: {} } } - it "automatically sets the 'required' flag" do - expect(api_metadata[:operation][:parameters]).to match( - [name: :id, in: :path, required: true] - ) + it "adds to the 'response' metadata" do + expect(api_metadata[:response][:content]['application/json'][:schema]).to match(type: 'object') end end - context "when 'in' parameter key is not defined" do - before { subject.parameter(name: :id) } - let(:api_metadata) { { operation: {} } } + describe '#header(name, attributes)' do + before { subject.header('Date', type: 'string') } + let(:api_metadata) { { response: {} } } - it "does not require the 'in' parameter key" do - expect(api_metadata[:operation][:parameters]).to match([name: :id]) + it "adds to the 'response headers' metadata" do + expect(api_metadata[:response][:headers]).to match( + 'Date' => {schema: { type: 'string' }} + ) 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 - before { subject.schema(type: 'object') } - let(:api_metadata) { { response: {} } } - - it "adds to the 'response' metadata" do - expect(api_metadata[:response][:content]['application/json'][:schema]).to match(type: 'object') - end - end - - describe '#header(name, attributes)' do - before { subject.header('Date', type: 'string') } - let(:api_metadata) { { response: {} } } - - it "adds to the 'response headers' metadata" do - expect(api_metadata[:response][:headers]).to match( - 'Date' => {schema: { type: 'string' }} - ) - end - end - - describe '#examples(example)' do - let(:json_example) do - { - 'application/json' => { - foo: 'bar' + describe '#examples(example)' do + let(:json_example) do + { + 'application/json' => { + foo: 'bar' + } } - } - end - let(:api_metadata) { { response: {} } } + end + let(:api_metadata) { { response: {} } } - before do - subject.examples(json_example) - end + before do + subject.examples(json_example) + end - it "adds to the 'response examples' metadata" do - expect(api_metadata[:response][:examples]).to eq(json_example) + it "adds to the 'response examples' metadata" do + expect(api_metadata[:response][:examples]).to eq(json_example) + end end end end diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index 3c51633..672a546 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -1,69 +1,71 @@ # frozen_string_literal: true -require 'rswag/specs/example_helpers' +require 'open_api/rswag/specs/example_helpers' -module Rswag - module Specs - describe ExampleHelpers do - subject { double('example') } +module OpenApi + module Rswag + module Specs + describe ExampleHelpers do + subject { double('example') } - before do - subject.extend(ExampleHelpers) - allow(Rswag::Specs).to receive(:config).and_return(config) - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - stub_const('Rswag::Specs::RAILS_VERSION', 3) - end - let(:config) { double('config') } - let(:swagger_doc) do - { - components: { - securitySchemes: { - api_key: { - type: :apiKey, - name: 'api_key', - in: :query - } - } - } - } - end - let(:metadata) do - { - path_item: { template: '/blogs/{blog_id}/comments/{id}' }, - operation: { - verb: :put, - summary: 'Updates a blog', - consumes: ['application/json'], - 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 - - describe '#submit_request(metadata)' do before do - 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(:put) - subject.submit_request(metadata) + subject.extend(ExampleHelpers) + allow(OpenApi::Rswag::Specs).to receive(:config).and_return(config) + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + stub_const('Rswag::Specs::RAILS_VERSION', 3) + end + let(:config) { double('config') } + let(:swagger_doc) do + { + components: { + securitySchemes: { + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } + } + } + } + end + let(:metadata) do + { + path_item: { template: '/blogs/{blog_id}/comments/{id}' }, + operation: { + verb: :put, + summary: 'Updates a blog', + consumes: ['application/json'], + 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 - 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"}', - 'CONTENT_TYPE' => 'application/json' - ) + describe '#submit_request(metadata)' do + before do + 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(:put) + subject.submit_request(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"}', + 'CONTENT_TYPE' => 'application/json' + ) + end end end end diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index c2cabb9..a43bfd4 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -1,351 +1,353 @@ # frozen_string_literal: true -require 'rswag/specs/request_factory' +require 'open_api/rswag/specs/request_factory' -module Rswag - module Specs - describe RequestFactory do - subject { RequestFactory.new(config) } +module OpenApi + module Rswag + module Specs + describe RequestFactory do + subject { RequestFactory.new(config) } - before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - end - let(:config) { double('config') } - let(:swagger_doc) { {} } - let(:example) { double('example') } - let(:metadata) do - { - path_item: { template: '/blogs' }, - operation: { verb: :get } - } - end - - describe '#build_request(metadata, example)' do - let(:request) { subject.build_request(metadata, example) } - - it 'builds request hash for given example' do - expect(request[:verb]).to eq(:get) - expect(request[:path]).to eq('/blogs') + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + end + let(:config) { double('config') } + let(:swagger_doc) { {} } + let(:example) { double('example') } + let(:metadata) do + { + path_item: { template: '/blogs' }, + operation: { verb: :get } + } end - context "'path' parameters" do - before do - metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}' - metadata[:operation][:parameters] = [ - { name: 'blog_id', in: :path, type: :number }, - { name: 'id', in: :path, type: :number } - ] - allow(example).to receive(:blog_id).and_return(1) - allow(example).to receive(:id).and_return(2) - end + describe '#build_request(metadata, example)' do + let(:request) { subject.build_request(metadata, example) } - it 'builds the path from example values' do - expect(request[:path]).to eq('/blogs/1/comments/2') - end - end - - context "'query' parameters" do - before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string }, - { name: 'q2', in: :query, type: :string } - ] - allow(example).to receive(:q1).and_return('foo') - allow(example).to receive(:q2).and_return('bar') - end - - it 'builds the query string from example values' do - expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') - end - end - - context "'query' parameters of type 'array'" do - before do - metadata[:operation][:parameters] = [ - { name: 'things', in: :query, type: :array, collectionFormat: collection_format } - ] - allow(example).to receive(:things).and_return(%w[foo bar]) - end - - context 'collectionFormat = csv' do - let(:collection_format) { :csv } - it 'formats as comma separated values' do - expect(request[:path]).to eq('/blogs?things=foo,bar') - end - end - - context 'collectionFormat = ssv' do - let(:collection_format) { :ssv } - it 'formats as space separated values' do - expect(request[:path]).to eq('/blogs?things=foo bar') - end - end - - context 'collectionFormat = tsv' do - let(:collection_format) { :tsv } - it 'formats as tab separated values' do - expect(request[:path]).to eq('/blogs?things=foo\tbar') - end - end - - context 'collectionFormat = pipes' do - let(:collection_format) { :pipes } - it 'formats as pipe separated values' do - expect(request[:path]).to eq('/blogs?things=foo|bar') - end - end - - context 'collectionFormat = multi' do - let(:collection_format) { :multi } - it 'formats as multiple parameter instances' do - expect(request[:path]).to eq('/blogs?things=foo&things=bar') - end - end - end - - context "'header' parameters" do - before do - metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }] - allow(example).to receive(:'Api-Key').and_return('foobar') - end - - it 'adds names and example values to headers' do - expect(request[:headers]).to eq('Api-Key' => 'foobar') - end - end - - context 'optional parameters not provided' do - before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string, required: false }, - { name: 'Api-Key', in: :header, type: :string, required: false } - ] - end - - it 'builds request hash without them' do + it 'builds request hash for given example' do + expect(request[:verb]).to eq(:get) expect(request[:path]).to eq('/blogs') - expect(request[:headers]).to eq({}) - end - end - - context 'consumes content' do - before do - metadata[:operation][:consumes] = ['application/json', 'application/xml'] end - context "no 'Content-Type' provided" do - it "sets 'CONTENT_TYPE' header to first in list" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json') - end - end - - context "explicit 'Content-Type' provided" do + context "'path' parameters" do before do - allow(example).to receive(:'Content-Type').and_return('application/xml') - end - - it "sets 'CONTENT_TYPE' header to example value" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') - end - end - - context 'JSON payload' do - before do - metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }] - allow(example).to receive(:comment).and_return(text: 'Some comment') - end - - it "serializes first 'body' parameter to JSON string" do - expect(request[:payload]).to eq('{"text":"Some comment"}') - end - end - - context 'form payload' do - before do - metadata[:operation][:consumes] = ['multipart/form-data'] + metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}' metadata[:operation][:parameters] = [ - { name: 'f1', in: :formData, type: :string }, - { name: 'f2', in: :formData, type: :string } + { name: 'blog_id', in: :path, type: :number }, + { name: 'id', in: :path, type: :number } ] - allow(example).to receive(:f1).and_return('foo blah') - allow(example).to receive(:f2).and_return('bar blah') + allow(example).to receive(:blog_id).and_return(1) + allow(example).to receive(:id).and_return(2) end - it 'sets payload to hash of names and example values' do - expect(request[:payload]).to eq( - 'f1' => 'foo blah', - 'f2' => 'bar blah' - ) - end - end - end - - context 'produces content' do - before do - metadata[:operation][:produces] = ['application/json', 'application/xml'] - end - - context "no 'Accept' value provided" do - it "sets 'HTTP_ACCEPT' header to first in list" do - expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json') + it 'builds the path from example values' do + expect(request[:path]).to eq('/blogs/1/comments/2') end end - context "explicit 'Accept' value provided" do + context "'query' parameters" do before do - allow(example).to receive(:Accept).and_return('application/xml') + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string }, + { name: 'q2', in: :query, type: :string } + ] + allow(example).to receive(:q1).and_return('foo') + allow(example).to receive(:q2).and_return('bar') end - it "sets 'HTTP_ACCEPT' header to example value" do - expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml') + it 'builds the query string from example values' do + expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end - end - context 'basic auth' do - before do - swagger_doc[:components] = { securitySchemes: { - basic: { type: :basic } + context "'query' parameters of type 'array'" do + before do + metadata[:operation][:parameters] = [ + { name: 'things', in: :query, type: :array, collectionFormat: collection_format } + ] + allow(example).to receive(:things).and_return(%w[foo bar]) + end + + context 'collectionFormat = csv' do + let(:collection_format) { :csv } + it 'formats as comma separated values' do + expect(request[:path]).to eq('/blogs?things=foo,bar') + end + end + + context 'collectionFormat = ssv' do + let(:collection_format) { :ssv } + it 'formats as space separated values' do + expect(request[:path]).to eq('/blogs?things=foo bar') + end + end + + context 'collectionFormat = tsv' do + let(:collection_format) { :tsv } + it 'formats as tab separated values' do + expect(request[:path]).to eq('/blogs?things=foo\tbar') + end + end + + context 'collectionFormat = pipes' do + let(:collection_format) { :pipes } + it 'formats as pipe separated values' do + expect(request[:path]).to eq('/blogs?things=foo|bar') + end + end + + context 'collectionFormat = multi' do + let(:collection_format) { :multi } + it 'formats as multiple parameter instances' do + expect(request[:path]).to eq('/blogs?things=foo&things=bar') + end + end + end + + context "'header' parameters" do + before do + metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }] + allow(example).to receive(:'Api-Key').and_return('foobar') + end + + it 'adds names and example values to headers' do + expect(request[:headers]).to eq('Api-Key' => 'foobar') + end + end + + context 'optional parameters not provided' do + before do + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string, required: false }, + { name: 'Api-Key', in: :header, type: :string, required: false } + ] + end + + it 'builds request hash without them' do + expect(request[:path]).to eq('/blogs') + expect(request[:headers]).to eq({}) + end + end + + context 'consumes content' do + before do + metadata[:operation][:consumes] = ['application/json', 'application/xml'] + end + + context "no 'Content-Type' provided" do + it "sets 'CONTENT_TYPE' header to first in list" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json') + end + end + + context "explicit 'Content-Type' provided" do + before do + allow(example).to receive(:'Content-Type').and_return('application/xml') + end + + it "sets 'CONTENT_TYPE' header to example value" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') + end + end + + context 'JSON payload' do + before do + metadata[:operation][:parameters] = [{ name: :comment, in: :body, schema: { type: 'object' } }] + allow(example).to receive(:comment).and_return(text: 'Some comment') + end + + it "serializes first 'body' parameter to JSON string" do + expect(request[:payload]).to eq('{"text":"Some comment"}') + end + end + + context 'form payload' do + before do + metadata[:operation][:consumes] = ['multipart/form-data'] + metadata[:operation][:parameters] = [ + { name: 'f1', in: :formData, type: :string }, + { name: 'f2', in: :formData, type: :string } + ] + allow(example).to receive(:f1).and_return('foo blah') + allow(example).to receive(:f2).and_return('bar blah') + end + + it 'sets payload to hash of names and example values' do + expect(request[:payload]).to eq( + 'f1' => 'foo blah', + 'f2' => 'bar blah' + ) + end + end + end + + context 'produces content' do + before do + metadata[:operation][:produces] = ['application/json', 'application/xml'] + end + + context "no 'Accept' value provided" do + it "sets 'HTTP_ACCEPT' header to first in list" do + expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json') + end + end + + context "explicit 'Accept' value provided" do + before do + allow(example).to receive(:Accept).and_return('application/xml') + end + + it "sets 'HTTP_ACCEPT' header to example value" do + expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml') + end + end + end + + context 'basic auth' do + before do + swagger_doc[:components] = { securitySchemes: { + basic: { type: :basic } } - } - metadata[:operation][:security] = [basic: []] - allow(example).to receive(:Authorization).and_return('Basic foobar') - end - - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - end - end - - context 'apiKey' do - before do - swagger_doc[:components] = { securitySchemes: { - apiKey: { type: :apiKey, name: 'api_key', in: key_location } } - } - metadata[:operation][:security] = [apiKey: []] - allow(example).to receive(:api_key).and_return('foobar') + metadata[:operation][:security] = [basic: []] + allow(example).to receive(:Authorization).and_return('Basic foobar') + end + + it "sets 'HTTP_AUTHORIZATION' header to example value" do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') + end end - context 'in query' do - let(:key_location) { :query } + context 'apiKey' do + before do + swagger_doc[:components] = { securitySchemes: { + apiKey: { type: :apiKey, name: 'api_key', in: key_location } + } + } + metadata[:operation][:security] = [apiKey: []] + allow(example).to receive(:api_key).and_return('foobar') + end - it 'adds name and example value to the query string' do + context 'in query' do + let(:key_location) { :query } + + it 'adds name and example value to the query string' do + expect(request[:path]).to eq('/blogs?api_key=foobar') + end + end + + context 'in header' do + let(:key_location) { :header } + + it 'adds name and example value to the headers' do + expect(request[:headers]).to eq('api_key' => 'foobar') + end + end + + context 'in header with auth param already added' do + let(:key_location) { :header } + before do + metadata[:operation][:parameters] = [ + { name: 'q1', in: :query, type: :string }, + { name: 'api_key', in: :header, type: :string } + ] + allow(example).to receive(:q1).and_return('foo') + allow(example).to receive(:api_key).and_return('foobar') + end + + it 'adds authorization parameter only once' do + expect(request[:headers]).to eq('api_key' => 'foobar') + expect(metadata[:operation][:parameters].size).to eq 2 + end + end + end + + context 'oauth2' do + before do + swagger_doc[:components] = { securitySchemes: { + oauth2: { type: :oauth2, scopes: ['read:blogs'] } + } + } + metadata[:operation][:security] = [oauth2: ['read:blogs']] + allow(example).to receive(:Authorization).and_return('Bearer foobar') + end + + it "sets 'HTTP_AUTHORIZATION' header to example value" do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar') + end + end + + context 'paired security requirements' do + before do + swagger_doc[:components] = { securitySchemes: { + basic: { type: :basic }, + api_key: { type: :apiKey, name: 'api_key', in: :query } + } + } + metadata[:operation][:security] = [{ basic: [], api_key: [] }] + allow(example).to receive(:Authorization).and_return('Basic foobar') + allow(example).to receive(:api_key).and_return('foobar') + end + + it 'sets both params to example values' do + expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') expect(request[:path]).to eq('/blogs?api_key=foobar') end end - context 'in header' do - let(:key_location) { :header } + context 'path-level parameters' do + before do + metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }] + metadata[:path_item][: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 'adds name and example value to the headers' do - expect(request[:headers]).to eq('api_key' => 'foobar') + it 'populates operation and path level parameters ' do + expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end - context 'in header with auth param already added' do - let(:key_location) { :header } + context 'referenced parameters' do before do - metadata[:operation][:parameters] = [ - { name: 'q1', in: :query, type: :string }, - { name: 'api_key', in: :header, type: :string } - ] + swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } + metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] allow(example).to receive(:q1).and_return('foo') + end + + it 'uses the referenced metadata to build the request' do + expect(request[:path]).to eq('/blogs?q1=foo') + end + end + + context 'global basePath' do + before { swagger_doc[:basePath] = '/api' } + + it 'prepends to the path' do + expect(request[:path]).to eq('/api/blogs') + end + end + + context 'global consumes' do + before { swagger_doc[:consumes] = ['application/xml'] } + + it "defaults 'CONTENT_TYPE' to global value(s)" do + expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') + end + end + + context 'global security requirements' do + before do + swagger_doc[:components] = {securitySchemes: { apiKey: { type: :apiKey, name: 'api_key', in: :query } }} + swagger_doc[:security] = [apiKey: []] allow(example).to receive(:api_key).and_return('foobar') end - it 'adds authorization parameter only once' do - expect(request[:headers]).to eq('api_key' => 'foobar') - expect(metadata[:operation][:parameters].size).to eq 2 + it 'applieds the scheme by default' do + expect(request[:path]).to eq('/blogs?api_key=foobar') end end end - - context 'oauth2' do - before do - swagger_doc[:components] = { securitySchemes: { - oauth2: { type: :oauth2, scopes: ['read:blogs'] } - } - } - metadata[:operation][:security] = [oauth2: ['read:blogs']] - allow(example).to receive(:Authorization).and_return('Bearer foobar') - end - - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar') - end - end - - context 'paired security requirements' do - before do - swagger_doc[:components] = { securitySchemes: { - basic: { type: :basic }, - api_key: { type: :apiKey, name: 'api_key', in: :query } - } - } - metadata[:operation][:security] = [{ basic: [], api_key: [] }] - allow(example).to receive(:Authorization).and_return('Basic foobar') - allow(example).to receive(:api_key).and_return('foobar') - end - - it 'sets both params to example values' do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - expect(request[:path]).to eq('/blogs?api_key=foobar') - end - end - - context 'path-level parameters' do - before do - metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }] - metadata[:path_item][: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 'populates operation and path level parameters ' do - expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') - end - end - - context 'referenced parameters' do - before do - swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } - metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] - allow(example).to receive(:q1).and_return('foo') - end - - it 'uses the referenced metadata to build the request' do - expect(request[:path]).to eq('/blogs?q1=foo') - end - end - - context 'global basePath' do - before { swagger_doc[:basePath] = '/api' } - - it 'prepends to the path' do - expect(request[:path]).to eq('/api/blogs') - end - end - - context 'global consumes' do - before { swagger_doc[:consumes] = ['application/xml'] } - - it "defaults 'CONTENT_TYPE' to global value(s)" do - expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') - end - end - - context 'global security requirements' do - before do - swagger_doc[:components] = {securitySchemes: { apiKey: { type: :apiKey, name: 'api_key', in: :query } }} - swagger_doc[:security] = [apiKey: []] - allow(example).to receive(:api_key).and_return('foobar') - end - - it 'applieds the scheme by default' do - expect(request[:path]).to eq('/blogs?api_key=foobar') - end - end end end end diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index 1e952e0..9241b36 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -1,76 +1,82 @@ # frozen_string_literal: true -require 'rswag/specs/response_validator' +require 'open_api/rswag/specs/response_validator' -module Rswag - module Specs - describe ResponseValidator do - subject { ResponseValidator.new(config) } +module OpenApi + module Rswag + module Specs + describe ResponseValidator do + subject { ResponseValidator.new(config) } - before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - end - let(:config) { double('config') } - let(:swagger_doc) {{}} - let(:example) { double('example') } - let(:metadata) do - { - response: { - code: 200, - headers: { 'X-Rate-Limit-Limit' => { type: :integer } }, - schema: { - type: :object, - properties: { text: { type: :string } }, - required: ['text'] - } + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + end + let(:config) { double('config') } + let(:swagger_doc) {{}} + let(:example) { double('example') } + let(:metadata) do + { + response: { + code: 200, + headers: { 'X-Rate-Limit-Limit' => { type: :integer } }, + content: + {'application/json' => { + schema: { + type: :object, + properties: { text: { type: :string } }, + required: ['text'] + } + } + } + } } - } - end - - describe '#validate!(metadata, response)' do - let(:call) { subject.validate!(metadata, response) } - let(:response) do - OpenStruct.new( - code: '200', - headers: { 'X-Rate-Limit-Limit' => '10' }, - body: '{"text":"Some comment"}' - ) end - context 'response matches metadata' do - it { expect { call }.to_not raise_error } - end - - context 'response code differs from metadata' do - before { response.code = '400' } - it { expect { call }.to raise_error /Expected response code/ } - end - - context 'response headers differ from metadata' do - before { response.headers = {} } - it { expect { call }.to raise_error /Expected response header/ } - end - - context 'response body differs from metadata' do - before { response.body = '{"foo":"Some comment"}' } - it { expect { call }.to raise_error /Expected response body/ } - end - - context 'referenced schemas' do - before do - swagger_doc[:components] = {} - swagger_doc[:components][:schemas] = { - 'blog' => { - type: :object, - properties: { foo: { type: :string } }, - required: ['foo'] - } - } - metadata[:response][:schema] = { '$ref' => '#/components/schemas/blog' } + describe '#validate!(metadata, response)' do + let(:call) { subject.validate!(metadata, response) } + let(:response) do + OpenStruct.new( + code: '200', + headers: { 'X-Rate-Limit-Limit' => '10' }, + body: '{"text":"Some comment"}' + ) end - it 'uses the referenced schema to validate the response body' do - expect { call }.to raise_error /Expected response body/ + context 'response matches metadata' do + it { expect { call }.to_not raise_error } + end + + context 'response code differs from metadata' do + before { response.code = '400' } + it { expect { call }.to raise_error /Expected response code/ } + end + + context 'response headers differ from metadata' do + before { response.headers = {} } + it { expect { call }.to raise_error /Expected response header/ } + end + + context 'response body differs from metadata' do + before { response.body = '{"foo":"Some comment"}' } + it { expect { call }.to raise_error /Expected response body/ } + end + + context 'referenced schemas' do + before do + swagger_doc[:components] = {} + swagger_doc[:components][:schemas] = { + 'blog' => { + type: :object, + properties: { foo: { type: :string } }, + required: ['foo'] + } + } + metadata[:response][:content]['application/json'][:schema] = { '$ref' => '#/components/schemas/blog' } + end + + it 'uses the referenced schema to validate the response body' do + expect { call }.to raise_error /Expected response body/ + end end end end diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index 302d062..ece4dc3 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -1,71 +1,73 @@ # frozen_string_literal: true -require 'rswag/specs/swagger_formatter' +require 'open_api/rswag/specs/swagger_formatter' require 'ostruct' -module Rswag - module Specs - describe SwaggerFormatter do - subject { described_class.new(output, config) } +module OpenApi + module Rswag + module Specs + describe SwaggerFormatter do + subject { described_class.new(output, config) } - # Mock out some infrastructure - before do - allow(config).to receive(:swagger_root).and_return(swagger_root) - end - let(:config) { double('config') } - let(:output) { double('output').as_null_object } - let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) } - - describe '#example_group_finished(notification)' do + # Mock out some infrastructure before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - subject.example_group_finished(notification) - end - let(:swagger_doc) { {} } - let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } - let(:api_metadata) do - { - path_item: { template: '/blogs' }, - operation: { verb: :post, summary: 'Creates a blog' }, - response: { code: '201', description: 'blog created' } - } + allow(config).to receive(:swagger_root).and_return(swagger_root) end + let(:config) { double('config') } + let(:output) { double('output').as_null_object } + let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) } - it 'converts to swagger and merges into the corresponding swagger doc' do - expect(swagger_doc).to match( - paths: { - '/blogs' => { - post: { - summary: 'Creates a blog', - responses: { - '201' => { description: 'blog created' } - } - } - } + describe '#example_group_finished(notification)' do + before do + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + subject.example_group_finished(notification) + end + let(:swagger_doc) { {} } + let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } + let(:api_metadata) do + { + path_item: { template: '/blogs' }, + operation: { verb: :post, summary: 'Creates a blog' }, + response: { code: '201', description: 'blog created' } } - ) - end - end + end - describe '#stop' do - before do - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) - allow(config).to receive(:swagger_docs).and_return( - 'v1/swagger.json' => { info: { version: 'v1' } }, - 'v2/swagger.json' => { info: { version: 'v2' } } - ) - subject.stop(notification) + it 'converts to swagger and merges into the corresponding swagger doc' do + expect(swagger_doc).to match( + paths: { + '/blogs' => { + post: { + summary: 'Creates a blog', + responses: { + '201' => { description: 'blog created' } + } + } + } + } + ) + end end - let(:notification) { double('notification') } + describe '#stop' do + before do + FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) + allow(config).to receive(:swagger_docs).and_return( + 'v1/swagger.json' => { info: { version: 'v1' } }, + 'v2/swagger.json' => { info: { version: 'v2' } } + ) + 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 + let(:notification) { double('notification') } - after do - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) + 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.exist?(swagger_root) + end end end end diff --git a/rswag-specs/spec/spec_helper.rb b/rswag-specs/spec/spec_helper.rb index 63504e1..54e3c9c 100644 --- a/rswag-specs/spec/spec_helper.rb +++ b/rswag-specs/spec/spec_helper.rb @@ -4,4 +4,4 @@ module Rails end end -require 'rswag/specs' +require 'open_api/rswag/specs'