rspec-swag 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop_rspec_alias_config.yml +17 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +25 -0
- data/lib/rspec/swag/configuration.rb +66 -0
- data/lib/rspec/swag/example_group_helpers.rb +138 -0
- data/lib/rspec/swag/example_helpers.rb +25 -0
- data/lib/rspec/swag/extended_schema.rb +25 -0
- data/lib/rspec/swag/project_initializer.rb +41 -0
- data/lib/rspec/swag/rake_task.rb +4 -0
- data/lib/rspec/swag/request_factory.rb +308 -0
- data/lib/rspec/swag/response_validator.rb +101 -0
- data/lib/rspec/swag/swagger_formatter.rb +219 -0
- data/lib/rspec/swag/tasks/rspec_swag_tasks.rake +37 -0
- data/lib/rspec/swag/templates/spec/swagger_helper.rb +51 -0
- data/lib/rspec/swag/version.rb +7 -0
- data/lib/rspec/swag.rb +29 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 725c8f570fabf5d664224343b2241b840ca69ad177973cdb0fb1c4938318c134
|
4
|
+
data.tar.gz: 598a18e4bfe3321f8361668f9340b36607a6cd4a54678489b7e98bd974e6d343
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 64d7acf87b824fdb1419f0c12f3dd7015b789a4613b1c7459fedd9191a69bc825a5a30c86aa6b01582705e5583b3a3301bf4404ff28ba56151b3beedf2527b99
|
7
|
+
data.tar.gz: a3cded64dbf1e3e2fd87a8c908341210bca6f00d19b8741475aaad2a14a3362149563d7a77984b99ed0fe50919414111c9b8652a3e660db3bad318c04a274280
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2024 GracefulPotato
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
begin
|
6
|
+
require "bundler/setup"
|
7
|
+
rescue LoadError
|
8
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
9
|
+
end
|
10
|
+
begin
|
11
|
+
require "rdoc/task"
|
12
|
+
rescue LoadError
|
13
|
+
require "rdoc/rdoc"
|
14
|
+
require "rake/rdoctask"
|
15
|
+
RDoc::Task = Rake::RDocTask
|
16
|
+
end
|
17
|
+
|
18
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
19
|
+
rdoc.rdoc_dir = "rdoc"
|
20
|
+
rdoc.title = "rspec-swag"
|
21
|
+
rdoc.options << "--line-numbers"
|
22
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
23
|
+
end
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Swag
|
5
|
+
class Configuration
|
6
|
+
def initialize(rspec_config)
|
7
|
+
@rspec_config = rspec_config
|
8
|
+
end
|
9
|
+
|
10
|
+
def openapi_root
|
11
|
+
@openapi_root ||=
|
12
|
+
@rspec_config.openapi_root || raise(ConfigurationError, "No openapi_root provided. See swagger_helper.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
def openapi_specs
|
16
|
+
@openapi_specs ||= begin
|
17
|
+
if @rspec_config.openapi_specs.nil? || @rspec_config.openapi_specs.empty?
|
18
|
+
raise ConfigurationError, "No openapi_specs defined. See swagger_helper.rb"
|
19
|
+
end
|
20
|
+
|
21
|
+
@rspec_config.openapi_specs
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def swagger_dry_run
|
26
|
+
@swagger_dry_run ||= begin
|
27
|
+
@rspec_config.swagger_dry_run = ENV["SWAGGER_DRY_RUN"] == "1" if ENV.key?("SWAGGER_DRY_RUN")
|
28
|
+
|
29
|
+
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def openapi_format
|
34
|
+
@openapi_format ||= begin
|
35
|
+
if @rspec_config.openapi_format.nil? || @rspec_config.openapi_format.empty?
|
36
|
+
@rspec_config.openapi_format = :json
|
37
|
+
end
|
38
|
+
|
39
|
+
unless [:json, :yaml].include?(@rspec_config.openapi_format)
|
40
|
+
raise ConfigurationError, "Unknown openapi_format '#{@rspec_config.openapi_format}'"
|
41
|
+
end
|
42
|
+
|
43
|
+
@rspec_config.openapi_format
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_openapi_spec(name)
|
48
|
+
return openapi_specs.values.first if name.nil?
|
49
|
+
raise ConfigurationError, "Unknown openapi_spec '#{name}'" unless openapi_specs[name]
|
50
|
+
|
51
|
+
openapi_specs[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_openapi_spec_version(name)
|
55
|
+
doc = get_openapi_spec(name)
|
56
|
+
doc[:openapi] || doc[:swagger]
|
57
|
+
end
|
58
|
+
|
59
|
+
def openapi_strict_schema_validation
|
60
|
+
@rspec_config.openapi_strict_schema_validation || false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class ConfigurationError < StandardError; end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module Swag
|
7
|
+
module ExampleGroupHelpers
|
8
|
+
def path(template, metadata = {}, &block)
|
9
|
+
metadata[:path_item] = { template: template }
|
10
|
+
describe(template, metadata, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
[:get, :post, :patch, :put, :delete, :head, :options, :trace].each do |verb|
|
14
|
+
define_method(verb) do |summary, **metadata, &block|
|
15
|
+
api_metadata = { operation: { verb: verb, summary: summary } }.deep_merge(metadata)
|
16
|
+
describe(verb, **api_metadata, &block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
[:operationId, :deprecated, :security].each do |attr_name|
|
21
|
+
define_method(attr_name) do |value|
|
22
|
+
metadata[:operation][attr_name] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# NOTE: 'description' requires special treatment because ExampleGroup already
|
27
|
+
# defines a method with that name. Provide an override that supports the existing
|
28
|
+
# functionality while also setting the appropriate metadata if applicable
|
29
|
+
def description(value = nil)
|
30
|
+
return super() if value.nil?
|
31
|
+
|
32
|
+
metadata[:operation][:description] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# These are array properties - note the splat operator
|
36
|
+
[:tags, :consumes, :produces, :schemes].each do |attr_name|
|
37
|
+
define_method(attr_name) do |*value|
|
38
|
+
metadata[:operation][attr_name] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def parameter(attributes)
|
43
|
+
attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path
|
44
|
+
|
45
|
+
if metadata.key?(:operation)
|
46
|
+
metadata[:operation][:parameters] ||= []
|
47
|
+
metadata[:operation][:parameters] << attributes
|
48
|
+
else
|
49
|
+
metadata[:path_item][:parameters] ||= []
|
50
|
+
metadata[:path_item][:parameters] << attributes
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def request_body_example(value:, summary: nil, name: nil)
|
55
|
+
return unless metadata.key?(:operation)
|
56
|
+
|
57
|
+
metadata[:operation][:request_examples] ||= []
|
58
|
+
example = { value: value }
|
59
|
+
example[:summary] = summary if summary
|
60
|
+
# We need the examples to have a unique name for a set of examples,
|
61
|
+
# so just make the name the length if one isn't provided.
|
62
|
+
example[:name] = name || metadata[:operation][:request_examples].length
|
63
|
+
metadata[:operation][:request_examples] << example
|
64
|
+
end
|
65
|
+
|
66
|
+
def response(code, description, metadata = {}, &block)
|
67
|
+
metadata[:response] = { code: code, description: description }
|
68
|
+
context(description, metadata, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def schema(value)
|
72
|
+
metadata[:response][:schema] = value
|
73
|
+
end
|
74
|
+
|
75
|
+
def header(name, attributes)
|
76
|
+
metadata[:response][:headers] ||= {}
|
77
|
+
|
78
|
+
metadata[:response][:headers][name] = attributes
|
79
|
+
end
|
80
|
+
|
81
|
+
# NOTE: Similar to 'description', 'examples' need to handle the case when
|
82
|
+
# being invoked with no params to avoid overriding 'examples' method of
|
83
|
+
# rspec-core ExampleGroup
|
84
|
+
def examples(examples = nil)
|
85
|
+
return super() if examples.nil?
|
86
|
+
|
87
|
+
# should we add a deprecation warning?
|
88
|
+
examples.each_with_index do |(mime, example_object), index|
|
89
|
+
example(mime, "example_#{index}", example_object)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def example(mime, name, value, summary = nil, description = nil)
|
94
|
+
# Todo - move initialization of metadata somewhere else.
|
95
|
+
metadata[:response][:content] = {} if metadata[:response][:content].blank?
|
96
|
+
|
97
|
+
if metadata[:response][:content][mime].blank?
|
98
|
+
metadata[:response][:content][mime] = {}
|
99
|
+
metadata[:response][:content][mime][:examples] = {}
|
100
|
+
end
|
101
|
+
|
102
|
+
example_object = {
|
103
|
+
value: value,
|
104
|
+
summary: summary,
|
105
|
+
description: description
|
106
|
+
}.select { |_, v| v.present? }
|
107
|
+
# TODO, issue a warning if example is being overridden with the same key
|
108
|
+
metadata[:response][:content][mime][:examples].merge!(
|
109
|
+
{ name.to_sym => example_object }
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Perform request and assert response matches swagger definitions
|
115
|
+
#
|
116
|
+
# @param description [String] description of the test
|
117
|
+
# @param args [Array] arguments to pass to the `it` method
|
118
|
+
# @param options [Hash] options to pass to the `it` method
|
119
|
+
# @param &block [Proc] you can make additional assertions within that block
|
120
|
+
# @return [void]
|
121
|
+
def run_test!(description = nil, *args, **options, &block)
|
122
|
+
# swagger metadata value defaults to true
|
123
|
+
options[:swagger] = true unless options.key?(:swagger)
|
124
|
+
|
125
|
+
description ||= "returns a #{metadata[:response][:code]} response"
|
126
|
+
|
127
|
+
before do |example|
|
128
|
+
submit_request(example.metadata)
|
129
|
+
end
|
130
|
+
|
131
|
+
it description, *args, **options do |example|
|
132
|
+
assert_response_matches_metadata(example.metadata, &block)
|
133
|
+
example.instance_exec(last_response, &block) if block_given?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec/swag/request_factory"
|
4
|
+
require "rspec/swag/response_validator"
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module Swag
|
8
|
+
module ExampleHelpers
|
9
|
+
def submit_request(metadata)
|
10
|
+
request = RequestFactory.new.build_request(metadata, self)
|
11
|
+
|
12
|
+
send(
|
13
|
+
request[:verb],
|
14
|
+
request[:path],
|
15
|
+
request[:payload],
|
16
|
+
request[:headers]
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def assert_response_matches_metadata(metadata)
|
21
|
+
ResponseValidator.new.validate!(metadata, last_response)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json-schema"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module Swag
|
7
|
+
class ExtendedSchema < JSON::Schema::Draft4
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@uri = URI.parse("http://tempuri.org/rswag/specs/extended_schema")
|
11
|
+
@names = ["http://tempuri.org/rswag/specs/extended_schema"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate(current_schema, data, *)
|
15
|
+
if data.nil? && (current_schema.schema["nullable"] == true || current_schema.schema["x-nullable"] == true)
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
JSON::Validator.register_validator(ExtendedSchema.new)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
module Swag
|
7
|
+
class ProjectInitializer
|
8
|
+
attr_reader :destination, :source
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@destination = Dir.pwd
|
12
|
+
@source = File.expand_path("templates", __dir__)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
copy_template "spec/swagger_helper.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def copy_template(file)
|
22
|
+
destination_file = File.join(destination, file)
|
23
|
+
return report_exists(file) if File.exist?(destination_file)
|
24
|
+
|
25
|
+
report_creating(file)
|
26
|
+
FileUtils.mkdir_p(File.dirname(destination_file))
|
27
|
+
File.open(destination_file, "w") do |f|
|
28
|
+
f.write File.read(File.join(source, file))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def report_exists(file)
|
33
|
+
puts " exist #{file}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def report_creating(file)
|
37
|
+
puts " create #{file}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,308 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/hash/slice"
|
5
|
+
require "active_support/core_ext/hash/conversions"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module Swag
|
10
|
+
# rubocop:disable Metrics/ClassLength
|
11
|
+
class RequestFactory
|
12
|
+
def initialize(config = ::RSpec::Swag.config)
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_request(metadata, example)
|
17
|
+
swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
|
18
|
+
parameters = expand_parameters(metadata, swagger_doc, example)
|
19
|
+
|
20
|
+
{}.tap do |request|
|
21
|
+
add_verb(request, metadata)
|
22
|
+
add_path(request, metadata, swagger_doc, parameters, example)
|
23
|
+
add_headers(request, metadata, swagger_doc, parameters, example)
|
24
|
+
add_payload(request, parameters, example)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def expand_parameters(metadata, swagger_doc, example)
|
31
|
+
operation_params = metadata[:operation][:parameters] || []
|
32
|
+
path_item_params = metadata[:path_item][:parameters] || []
|
33
|
+
security_params = derive_security_params(metadata, swagger_doc)
|
34
|
+
|
35
|
+
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
|
36
|
+
(operation_params + path_item_params + security_params)
|
37
|
+
.map { |p| p["$ref"] ? resolve_parameter(p["$ref"], swagger_doc) : p }
|
38
|
+
.uniq { |p| p[:name] }
|
39
|
+
.reject { |p| p[:required] == false && !example.respond_to?(extract_getter(p)) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def derive_security_params(metadata, swagger_doc)
|
43
|
+
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
|
44
|
+
scheme_names = requirements.flat_map(&:keys)
|
45
|
+
schemes = security_version(scheme_names, swagger_doc)
|
46
|
+
|
47
|
+
schemes.map do |scheme|
|
48
|
+
param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: "Authorization", in: :header }
|
49
|
+
param.merge(type: :string, required: requirements.one?)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def security_version(scheme_names, swagger_doc)
|
54
|
+
if doc_version(swagger_doc).start_with?("2")
|
55
|
+
(swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
|
56
|
+
else # Openapi3
|
57
|
+
if swagger_doc.key?(:securityDefinitions)
|
58
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions is replaced in OpenAPI3! "\
|
59
|
+
"Rename to components/securitySchemes (in swagger_helper.rb)")
|
60
|
+
swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] }
|
61
|
+
swagger_doc.delete(:securityDefinitions)
|
62
|
+
end
|
63
|
+
components = swagger_doc[:components] || {}
|
64
|
+
(components[:securitySchemes] || {}).slice(*scheme_names).values
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def resolve_parameter(ref, swagger_doc)
|
69
|
+
key = key_version(ref, swagger_doc)
|
70
|
+
definitions = definition_version(swagger_doc)
|
71
|
+
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
|
72
|
+
|
73
|
+
definitions[key]
|
74
|
+
end
|
75
|
+
|
76
|
+
def key_version(ref, swagger_doc)
|
77
|
+
if doc_version(swagger_doc).start_with?("2")
|
78
|
+
ref.sub("#/parameters/", "").to_sym
|
79
|
+
elsif ref.start_with?("#/parameters/") # Openapi3
|
80
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: #/parameters/ refs are replaced in OpenAPI3! " \
|
81
|
+
"Rename to #/components/parameters/")
|
82
|
+
ref.sub("#/parameters/", "").to_sym
|
83
|
+
else
|
84
|
+
ref.sub("#/components/parameters/", "").to_sym
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def definition_version(swagger_doc)
|
89
|
+
if doc_version(swagger_doc).start_with?("2")
|
90
|
+
swagger_doc[:parameters]
|
91
|
+
elsif swagger_doc.key?(:parameters) # Openapi3
|
92
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: parameters is replaced in OpenAPI3! "\
|
93
|
+
"Rename to components/parameters (in swagger_helper.rb)")
|
94
|
+
swagger_doc[:parameters]
|
95
|
+
else
|
96
|
+
components = swagger_doc[:components] || {}
|
97
|
+
components[:parameters]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_verb(request, metadata)
|
102
|
+
request[:verb] = metadata[:operation][:verb]
|
103
|
+
end
|
104
|
+
|
105
|
+
def base_path_from_servers(swagger_doc, use_server = :default)
|
106
|
+
return "" if swagger_doc[:servers].nil? || swagger_doc[:servers].empty?
|
107
|
+
|
108
|
+
server = swagger_doc[:servers].first
|
109
|
+
variables = {}
|
110
|
+
server.fetch(:variables, {}).each_pair { |k, v| variables[k] = v[use_server] }
|
111
|
+
base_path = server[:url].gsub(/\{(.*?)\}/) { variables[::Regexp.last_match(1).to_sym] }
|
112
|
+
URI(base_path).path
|
113
|
+
end
|
114
|
+
|
115
|
+
# rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
|
116
|
+
def add_path(request, metadata, swagger_doc, parameters, example)
|
117
|
+
open_api_3_doc = doc_version(swagger_doc).start_with?("3")
|
118
|
+
uses_base_path = swagger_doc[:basePath].present?
|
119
|
+
|
120
|
+
if open_api_3_doc && uses_base_path
|
121
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: basePath is replaced in OpenAPI3! " \
|
122
|
+
"Update your swagger_helper.rb")
|
123
|
+
end
|
124
|
+
|
125
|
+
template = if uses_base_path
|
126
|
+
(swagger_doc[:basePath] || "") + metadata[:path_item][:template]
|
127
|
+
else # OpenAPI 3
|
128
|
+
base_path_from_servers(swagger_doc) + metadata[:path_item][:template]
|
129
|
+
end
|
130
|
+
|
131
|
+
request[:path] = template.tap do |path_template|
|
132
|
+
parameters.select { |p| p[:in] == :path }.each do |p|
|
133
|
+
unless example.respond_to?(extract_getter(p))
|
134
|
+
raise ArgumentError, "`#{p[:name]}` parameter key present, but not defined within example group" \
|
135
|
+
"(i. e `it` or `let` block)"
|
136
|
+
end
|
137
|
+
path_template.gsub!("{#{p[:name]}}", example.send(extract_getter(p)).to_s)
|
138
|
+
end
|
139
|
+
|
140
|
+
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
|
141
|
+
path_template.concat(i.zero? ? "?" : "&")
|
142
|
+
path_template.concat(build_query_string_part(p, example.send(extract_getter(p)), swagger_doc))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
# rubocop:enable all
|
147
|
+
|
148
|
+
# rubocop:disable Metrics/BlockNesting,Style/HashLikeCase,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
|
149
|
+
def build_query_string_part(param, value, swagger_doc)
|
150
|
+
name = param[:name]
|
151
|
+
escaped_name = CGI.escape(name.to_s)
|
152
|
+
|
153
|
+
# OAS 3: https://swagger.io/docs/specification/serialization/
|
154
|
+
if swagger_doc && doc_version(swagger_doc).start_with?("3") && param[:schema]
|
155
|
+
style = param[:style]&.to_sym || :form
|
156
|
+
explode = param[:explode].nil? ? true : param[:explode]
|
157
|
+
|
158
|
+
case param[:schema][:type]&.to_sym
|
159
|
+
when :object
|
160
|
+
case style
|
161
|
+
when :deepObject
|
162
|
+
return { name => value }.to_query
|
163
|
+
when :form
|
164
|
+
return value.to_query if explode
|
165
|
+
|
166
|
+
return "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(",")
|
167
|
+
|
168
|
+
end
|
169
|
+
when :array
|
170
|
+
case explode
|
171
|
+
when true
|
172
|
+
return value.to_a.flatten.map { |v| "#{escaped_name}=#{CGI.escape(v.to_s)}" }.join("&")
|
173
|
+
else
|
174
|
+
separator = case style
|
175
|
+
when :form then ","
|
176
|
+
when :spaceDelimited then "%20"
|
177
|
+
when :pipeDelimited then "|"
|
178
|
+
end
|
179
|
+
return "#{escaped_name}=" + value.to_a.flatten.map { |v| CGI.escape(v.to_s) }.join(separator)
|
180
|
+
end
|
181
|
+
else
|
182
|
+
return "#{name}=#{value}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
type = param[:type] || param.dig(:schema, :type)
|
187
|
+
return "#{escaped_name}=#{CGI.escape(value.to_s)}" unless type&.to_sym == :array
|
188
|
+
|
189
|
+
case param[:collectionFormat]
|
190
|
+
when :ssv
|
191
|
+
"#{name}=#{value.join(' ')}"
|
192
|
+
when :tsv
|
193
|
+
"#{name}=#{value.join('\t')}"
|
194
|
+
when :pipes
|
195
|
+
"#{name}=#{value.join('|')}"
|
196
|
+
when :multi
|
197
|
+
value.map { |v| "#{name}=#{v}" }.join("&")
|
198
|
+
else
|
199
|
+
"#{name}=#{value.join(',')}" # csv is default
|
200
|
+
end
|
201
|
+
end
|
202
|
+
# rubocop:enable all
|
203
|
+
|
204
|
+
# rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
205
|
+
def add_headers(request, metadata, swagger_doc, parameters, example)
|
206
|
+
tuples = parameters
|
207
|
+
.select { |p| p[:in] == :header }
|
208
|
+
.map { |p| [p[:name], example.send(extract_getter(p)).to_s] }
|
209
|
+
|
210
|
+
# Accept header
|
211
|
+
produces = metadata[:operation][:produces] || swagger_doc[:produces]
|
212
|
+
if produces
|
213
|
+
accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
|
214
|
+
tuples << ["Accept", accept]
|
215
|
+
end
|
216
|
+
|
217
|
+
# Content-Type header
|
218
|
+
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
|
219
|
+
if consumes
|
220
|
+
content_type = example.respond_to?(:"Content-Type") ? example.send(:"Content-Type") : consumes.first
|
221
|
+
tuples << ["Content-Type", content_type]
|
222
|
+
end
|
223
|
+
|
224
|
+
# Host header
|
225
|
+
host = metadata[:operation][:host] || swagger_doc[:host]
|
226
|
+
if host.present?
|
227
|
+
host = example.respond_to?(:Host) ? example.send(:Host) : host
|
228
|
+
tuples << ["Host", host]
|
229
|
+
end
|
230
|
+
|
231
|
+
# Rails test infrastructure requires rack-formatted headers
|
232
|
+
rack_formatted_tuples = tuples.map do |pair|
|
233
|
+
[
|
234
|
+
case pair[0]
|
235
|
+
when "Accept" then "HTTP_ACCEPT"
|
236
|
+
when "Content-Type" then "CONTENT_TYPE"
|
237
|
+
when "Authorization" then "HTTP_AUTHORIZATION"
|
238
|
+
when "Host" then "HTTP_HOST"
|
239
|
+
else pair[0]
|
240
|
+
end,
|
241
|
+
pair[1]
|
242
|
+
]
|
243
|
+
end
|
244
|
+
|
245
|
+
request[:headers] = Hash[rack_formatted_tuples]
|
246
|
+
end
|
247
|
+
# rubocop:enable all
|
248
|
+
|
249
|
+
def add_payload(request, parameters, example)
|
250
|
+
content_type = request[:headers]["CONTENT_TYPE"]
|
251
|
+
return if content_type.nil?
|
252
|
+
|
253
|
+
request[:payload] = if ["application/x-www-form-urlencoded", "multipart/form-data"].include?(content_type)
|
254
|
+
build_form_payload(parameters, example)
|
255
|
+
elsif content_type == "application/json"
|
256
|
+
build_json_payload(parameters, example)
|
257
|
+
else
|
258
|
+
build_raw_payload(parameters, example)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def build_form_payload(parameters, example)
|
263
|
+
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
|
264
|
+
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
|
265
|
+
# Rails test infrastructure allows us to send the values directly as a hash
|
266
|
+
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
|
267
|
+
tuples = parameters
|
268
|
+
.select { |p| p[:in] == :formData }
|
269
|
+
.map { |p| [p[:name], example.send(extract_getter(p))] }
|
270
|
+
Hash[tuples]
|
271
|
+
end
|
272
|
+
|
273
|
+
def build_raw_payload(parameters, example)
|
274
|
+
body_param = parameters.select { |p| p[:in] == :body }.first
|
275
|
+
return nil unless body_param
|
276
|
+
|
277
|
+
raise(MissingParameterError, body_param[:name]) unless example.respond_to?(body_param[:name])
|
278
|
+
|
279
|
+
example.send(body_param[:name])
|
280
|
+
end
|
281
|
+
|
282
|
+
def build_json_payload(parameters, example)
|
283
|
+
build_raw_payload(parameters, example)&.to_json
|
284
|
+
end
|
285
|
+
|
286
|
+
def doc_version(doc)
|
287
|
+
doc[:openapi] || doc[:swagger] || "3"
|
288
|
+
end
|
289
|
+
|
290
|
+
def extract_getter(parameter)
|
291
|
+
parameter[:getter] || parameter[:name]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
class MissingParameterError < StandardError
|
296
|
+
def message
|
297
|
+
<<~MSG
|
298
|
+
Missing parameter '#{super}'
|
299
|
+
|
300
|
+
Please check your spec. It looks like you defined a body parameter,
|
301
|
+
but did not declare usage via let. Try adding:
|
302
|
+
|
303
|
+
let(:#{super}) {}
|
304
|
+
MSG
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/slice"
|
4
|
+
require "json-schema"
|
5
|
+
require "json"
|
6
|
+
require "rspec/swag/extended_schema"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module Swag
|
10
|
+
class ResponseValidator
|
11
|
+
def initialize(config = ::RSpec::Swag.config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!(metadata, response)
|
16
|
+
swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
|
17
|
+
|
18
|
+
validate_code!(metadata, response)
|
19
|
+
validate_headers!(metadata, response.headers)
|
20
|
+
validate_body!(metadata, swagger_doc, response.body)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_code!(metadata, response)
|
26
|
+
expected = metadata[:response][:code].to_s
|
27
|
+
return unless response.status.to_s != expected
|
28
|
+
|
29
|
+
raise UnexpectedResponse,
|
30
|
+
"Expected response code '#{response.status}' to match '#{expected}'\n" \
|
31
|
+
"Response body: #{response.body}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
35
|
+
def validate_headers!(metadata, headers)
|
36
|
+
header_schemas = metadata[:response][:headers] || {}
|
37
|
+
expected = header_schemas.keys
|
38
|
+
expected.each do |name|
|
39
|
+
nullable_attribute = header_schemas.dig(name.to_s, :schema, :nullable)
|
40
|
+
required_attribute = header_schemas.dig(name.to_s, :required)
|
41
|
+
|
42
|
+
is_nullable = nullable_attribute.nil? ? false : nullable_attribute
|
43
|
+
is_required = required_attribute.nil? ? true : required_attribute
|
44
|
+
|
45
|
+
if !headers.include?(name.to_s) && is_required
|
46
|
+
raise UnexpectedResponse, "Expected response header #{name} to be present"
|
47
|
+
end
|
48
|
+
|
49
|
+
if headers.include?(name.to_s) && headers[name.to_s].nil? && !is_nullable
|
50
|
+
raise UnexpectedResponse, "Expected response header #{name} to not be null"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
55
|
+
|
56
|
+
def validate_body!(metadata, swagger_doc, body)
|
57
|
+
response_schema = metadata[:response][:schema]
|
58
|
+
return if response_schema.nil?
|
59
|
+
|
60
|
+
version = @config.get_openapi_spec_version(metadata[:openapi_spec] || metadata[:swagger_doc])
|
61
|
+
schemas = definitions_or_component_schemas(swagger_doc, version)
|
62
|
+
|
63
|
+
validation_schema = response_schema
|
64
|
+
.merge("$schema" => "http://tempuri.org/rswag/specs/extended_schema")
|
65
|
+
.merge(schemas)
|
66
|
+
|
67
|
+
validation_options = validation_options_from(metadata)
|
68
|
+
|
69
|
+
errors = JSON::Validator.fully_validate(validation_schema, body, validation_options)
|
70
|
+
return unless errors.any?
|
71
|
+
|
72
|
+
raise UnexpectedResponse,
|
73
|
+
"Expected response body to match schema: #{errors.join("\n")}\n" \
|
74
|
+
"Response body: #{JSON.pretty_generate(JSON.parse(body))}"
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:disable Style/DoubleNegation
|
78
|
+
def validation_options_from(metadata)
|
79
|
+
is_strict = !!metadata.fetch(:openapi_strict_schema_validation, @config.openapi_strict_schema_validation)
|
80
|
+
|
81
|
+
{ strict: is_strict }
|
82
|
+
end
|
83
|
+
# rubocop:enable Style/DoubleNegation
|
84
|
+
|
85
|
+
def definitions_or_component_schemas(swagger_doc, version)
|
86
|
+
if version.start_with?("2")
|
87
|
+
swagger_doc.slice(:definitions)
|
88
|
+
elsif swagger_doc.key?(:definitions) # Openapi3
|
89
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: definitions is replaced in OpenAPI3! "\
|
90
|
+
"Rename to components/schemas (in swagger_helper.rb)")
|
91
|
+
swagger_doc.slice(:definitions)
|
92
|
+
else
|
93
|
+
components = swagger_doc[:components] || {}
|
94
|
+
{ components: components }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class UnexpectedResponse < StandardError; end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/deep_merge"
|
4
|
+
require "rspec/core/formatters/base_text_formatter"
|
5
|
+
require "swagger_helper"
|
6
|
+
|
7
|
+
module RSpec
|
8
|
+
module Swag
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter
|
11
|
+
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
|
12
|
+
|
13
|
+
def initialize(output, config = RSpec::Swag.config)
|
14
|
+
super(output)
|
15
|
+
@config = config
|
16
|
+
|
17
|
+
@output.puts "Generating Swagger docs ..."
|
18
|
+
end
|
19
|
+
|
20
|
+
def example_group_finished(notification)
|
21
|
+
metadata = notification.group.metadata
|
22
|
+
|
23
|
+
# !metadata[:document] won't work, since nil means we should generate docs.
|
24
|
+
return if metadata[:document] == false
|
25
|
+
return unless metadata.key?(:response)
|
26
|
+
|
27
|
+
swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
|
28
|
+
|
29
|
+
unless doc_version(swagger_doc).start_with?("2")
|
30
|
+
# This is called multiple times per file!
|
31
|
+
# metadata[:operation] is also re-used between examples within file
|
32
|
+
# therefore be careful NOT to modify its content here.
|
33
|
+
upgrade_request_type!(metadata)
|
34
|
+
upgrade_servers!(swagger_doc)
|
35
|
+
upgrade_oauth!(swagger_doc)
|
36
|
+
upgrade_response_produces!(swagger_doc, metadata)
|
37
|
+
end
|
38
|
+
|
39
|
+
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
|
40
|
+
end
|
41
|
+
|
42
|
+
# rubocop:disable all
|
43
|
+
def stop(_notification = nil)
|
44
|
+
@config.openapi_specs.each do |url_path, doc|
|
45
|
+
unless doc_version(doc).start_with?("2")
|
46
|
+
doc[:paths]&.each_pair do |_k, v|
|
47
|
+
v.each_pair do |_verb, value|
|
48
|
+
is_hash = value.is_a?(Hash)
|
49
|
+
if is_hash && value[:parameters]
|
50
|
+
schema_param = value[:parameters]&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
|
51
|
+
mime_list = value[:consumes] || doc[:consumes]
|
52
|
+
|
53
|
+
if value && schema_param && mime_list
|
54
|
+
value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content)
|
55
|
+
value[:requestBody][:required] = true if schema_param[:required]
|
56
|
+
value[:requestBody][:description] = schema_param[:description] if schema_param[:description]
|
57
|
+
examples = value[:request_examples]
|
58
|
+
mime_list.each do |mime|
|
59
|
+
value[:requestBody][:content][mime] = { schema: schema_param[:schema] }
|
60
|
+
next unless examples
|
61
|
+
|
62
|
+
value[:requestBody][:content][mime][:examples] ||= {}
|
63
|
+
examples.map do |example|
|
64
|
+
value[:requestBody][:content][mime][:examples][example[:name]] = {
|
65
|
+
summary: example[:summary] || value[:summary],
|
66
|
+
value: example[:value]
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
|
73
|
+
end
|
74
|
+
remove_invalid_operation_keys!(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
file_path = File.join(@config.openapi_root, url_path)
|
80
|
+
dirname = File.dirname(file_path)
|
81
|
+
FileUtils.mkdir_p dirname unless File.exist?(dirname)
|
82
|
+
|
83
|
+
File.open(file_path, "w") do |file|
|
84
|
+
file.write(pretty_generate(doc))
|
85
|
+
end
|
86
|
+
|
87
|
+
@output.puts "Swagger doc generated at #{file_path}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# rubocop:enable all
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def pretty_generate(doc)
|
95
|
+
if @config.openapi_format == :yaml
|
96
|
+
clean_doc = yaml_prepare(doc)
|
97
|
+
YAML.dump(clean_doc)
|
98
|
+
else # config errors are thrown in 'def openapi_format', no throw needed here
|
99
|
+
JSON.pretty_generate(doc)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def yaml_prepare(doc)
|
104
|
+
json_doc = JSON.pretty_generate(doc)
|
105
|
+
JSON.parse(json_doc)
|
106
|
+
end
|
107
|
+
|
108
|
+
def metadata_to_swagger(metadata)
|
109
|
+
response_code = metadata[:response][:code]
|
110
|
+
response = metadata[:response].reject { |k, _v| k == :code }
|
111
|
+
|
112
|
+
verb = metadata[:operation][:verb]
|
113
|
+
operation = metadata[:operation]
|
114
|
+
.reject { |k, _v| k == :verb }
|
115
|
+
.merge(responses: { response_code => response })
|
116
|
+
|
117
|
+
path_template = metadata[:path_item][:template]
|
118
|
+
path_item = metadata[:path_item]
|
119
|
+
.reject { |k, _v| k == :template }
|
120
|
+
.merge(verb => operation)
|
121
|
+
|
122
|
+
{ paths: { path_template => path_item } }
|
123
|
+
end
|
124
|
+
|
125
|
+
def doc_version(doc)
|
126
|
+
doc[:openapi] || doc[:swagger] || "3"
|
127
|
+
end
|
128
|
+
|
129
|
+
def upgrade_response_produces!(swagger_doc, metadata)
|
130
|
+
# Accept header
|
131
|
+
mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces])
|
132
|
+
target_node = metadata[:response]
|
133
|
+
upgrade_content!(mime_list, target_node)
|
134
|
+
metadata[:response].delete(:schema)
|
135
|
+
end
|
136
|
+
|
137
|
+
def upgrade_content!(mime_list, target_node)
|
138
|
+
schema = target_node[:schema]
|
139
|
+
return if mime_list.empty? || schema.nil?
|
140
|
+
|
141
|
+
target_node[:content] ||= {}
|
142
|
+
mime_list.each do |mime_type|
|
143
|
+
# TODO: upgrade to have content-type specific schema
|
144
|
+
(target_node[:content][mime_type] ||= {}).merge!(schema: schema)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def upgrade_request_type!(metadata)
|
149
|
+
# No deprecation here as it seems valid to allow type as a shorthand
|
150
|
+
operation_nodes = metadata[:operation][:parameters] || []
|
151
|
+
path_nodes = metadata[:path_item][:parameters] || []
|
152
|
+
header_node = metadata[:response][:headers] || {}
|
153
|
+
|
154
|
+
(operation_nodes + path_nodes + [header_node]).each do |node|
|
155
|
+
if node && node[:type] && node[:schema].nil?
|
156
|
+
node[:schema] = { type: node[:type] }
|
157
|
+
node.delete(:type)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def upgrade_servers!(swagger_doc)
|
163
|
+
return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
|
164
|
+
|
165
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: schemes, host, and basePath are replaced " \
|
166
|
+
"in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)")
|
167
|
+
|
168
|
+
swagger_doc[:servers] = { urls: [] }
|
169
|
+
swagger_doc[:schemes].each do |scheme|
|
170
|
+
swagger_doc[:servers][:urls] << "#{scheme}://#{swagger_doc[:host]}#{swagger_doc[:basePath]}"
|
171
|
+
end
|
172
|
+
|
173
|
+
swagger_doc.delete(:schemes)
|
174
|
+
swagger_doc.delete(:host)
|
175
|
+
swagger_doc.delete(:basePath)
|
176
|
+
end
|
177
|
+
|
178
|
+
# rubocop:disable all
|
179
|
+
def upgrade_oauth!(swagger_doc)
|
180
|
+
# find flow in securitySchemes (securityDefinitions will have been re-written)
|
181
|
+
schemes = swagger_doc.dig(:components, :securitySchemes)
|
182
|
+
return unless schemes&.any? { |_k, v| v.key?(:flow) }
|
183
|
+
|
184
|
+
schemes.each do |name, v|
|
185
|
+
next unless v.key?(:flow)
|
186
|
+
|
187
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions flow is replaced in OpenAPI3! " \
|
188
|
+
"Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)")
|
189
|
+
flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
|
190
|
+
if flow == "accessCode"
|
191
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions accessCode is replaced " \
|
192
|
+
"in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)")
|
193
|
+
flow = "authorizationCode"
|
194
|
+
end
|
195
|
+
if flow == "application"
|
196
|
+
RSpec::Swag.deprecator.warn("RSpec::Swag: WARNING: securityDefinitions application is replaced " \
|
197
|
+
"in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)")
|
198
|
+
flow = "clientCredentials"
|
199
|
+
end
|
200
|
+
flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
|
201
|
+
a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
|
202
|
+
end
|
203
|
+
swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
|
204
|
+
end
|
205
|
+
end
|
206
|
+
# rubocop:enable all
|
207
|
+
|
208
|
+
def remove_invalid_operation_keys!(value)
|
209
|
+
return unless value.is_a?(Hash)
|
210
|
+
|
211
|
+
value&.delete(:consumes)
|
212
|
+
value&.delete(:produces)
|
213
|
+
value&.delete(:request_examples)
|
214
|
+
value[:parameters]&.each { |p| p.delete(:getter) }
|
215
|
+
end
|
216
|
+
end
|
217
|
+
# rubocop:enable Metrics/ClassLength
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec/swag"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "rspec/swag/project_initializer"
|
6
|
+
|
7
|
+
namespace :rspec do
|
8
|
+
desc "Generate Swagger JSON files from integration specs"
|
9
|
+
RSpec::Core::RakeTask.new("swaggerize") do |t|
|
10
|
+
t.pattern = ENV.fetch(
|
11
|
+
"PATTERN",
|
12
|
+
"spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb"
|
13
|
+
)
|
14
|
+
|
15
|
+
additional_rspec_opts = ENV.fetch(
|
16
|
+
"ADDITIONAL_RSPEC_OPTS",
|
17
|
+
""
|
18
|
+
)
|
19
|
+
|
20
|
+
t.rspec_opts = [additional_rspec_opts]
|
21
|
+
|
22
|
+
t.rspec_opts += if RSpec::Swag.config.swagger_dry_run
|
23
|
+
["--format RSpec::Swag::SwaggerFormatter", "--dry-run", "--order defined"]
|
24
|
+
else
|
25
|
+
["--format RSpec::Swag::SwaggerFormatter", "--order defined"]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
namespace :swag do
|
30
|
+
desc "Copy swagger_helper.rb to spec/"
|
31
|
+
task :install do
|
32
|
+
RSpec::Swag::ProjectInitializer.new.run
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
task swaggerize: ["rspec:swaggerize"]
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "rspec/swag"
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
# Specify a root folder where Swagger JSON files are generated
|
8
|
+
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
9
|
+
# to ensure that it's configured to serve Swagger from the same folder
|
10
|
+
config.openapi_root = File.expand_path("../", File.dirname(__FILE__))
|
11
|
+
|
12
|
+
# Define one or more Swagger documents and provide global metadata for each one
|
13
|
+
# When you run the 'rspec:swaggerize' rake task, the complete Swagger will
|
14
|
+
# be generated at the provided relative path under openapi_root
|
15
|
+
# By default, the operations defined in spec files are added to the first
|
16
|
+
# document below. You can override this behavior by adding a openapi_spec tag to the
|
17
|
+
# the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json'
|
18
|
+
config.openapi_specs = {
|
19
|
+
"v1/swagger.yaml" => {
|
20
|
+
openapi: "3.0.1",
|
21
|
+
info: {
|
22
|
+
title: "API V1",
|
23
|
+
version: "v1"
|
24
|
+
},
|
25
|
+
paths: {},
|
26
|
+
servers: [
|
27
|
+
{
|
28
|
+
url: "{protocol}://{host}:{port}",
|
29
|
+
variables: {
|
30
|
+
protocol: {
|
31
|
+
enum: ["http", "https"],
|
32
|
+
default: "http"
|
33
|
+
},
|
34
|
+
host: {
|
35
|
+
default: "localhost"
|
36
|
+
},
|
37
|
+
port: {
|
38
|
+
default: "3000"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
# Specify the format of the output Swagger file when running 'rspec:swaggerize'.
|
47
|
+
# The openapi_specs configuration option has the filename including format in
|
48
|
+
# the key, this may want to be changed to avoid putting yaml in json files.
|
49
|
+
# Defaults to json. Accepts ':json' and ':yaml'.
|
50
|
+
config.openapi_format = :yaml
|
51
|
+
end
|
data/lib/rspec/swag.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec/core"
|
4
|
+
require "rspec/swag/example_group_helpers"
|
5
|
+
require "rspec/swag/example_helpers"
|
6
|
+
require "rspec/swag/configuration"
|
7
|
+
|
8
|
+
module RSpec
|
9
|
+
module Swag
|
10
|
+
# Extend RSpec with a swagger-based DSL
|
11
|
+
::RSpec.configure do |c|
|
12
|
+
c.add_setting :openapi_root
|
13
|
+
c.add_setting :openapi_specs
|
14
|
+
c.add_setting :swagger_dry_run
|
15
|
+
c.add_setting :openapi_format, default: :json
|
16
|
+
c.add_setting :openapi_strict_schema_validation
|
17
|
+
c.extend ExampleGroupHelpers
|
18
|
+
c.include ExampleHelpers
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
@config ||= Configuration.new(RSpec.configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.deprecator
|
26
|
+
@deprecator ||= ActiveSupport::Deprecation.new("3.0", "rspec-swag")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-swag
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richie Morris
|
8
|
+
- Greg Myers
|
9
|
+
- Jay Danielian
|
10
|
+
- GracefulPotato
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2024-02-24 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activesupport
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.1'
|
23
|
+
- - "<"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.2'
|
26
|
+
type: :runtime
|
27
|
+
prerelease: false
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.1'
|
33
|
+
- - "<"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '7.2'
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: json-schema
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.2'
|
43
|
+
- - "<"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '5.0'
|
46
|
+
type: :runtime
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '2.2'
|
53
|
+
- - "<"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '5.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec-core
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.0'
|
63
|
+
- - "<"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '4.0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '3.0'
|
73
|
+
- - "<"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.0'
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: rspec
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.13.0
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.13.0
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: climate_control
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.0.0
|
97
|
+
- - "<"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '2.0'
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.0.0
|
107
|
+
- - "<"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '2.0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubocop
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 1.60.2
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - '='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 1.60.2
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: simplecov
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - '='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.21.2
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - '='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 0.21.2
|
138
|
+
description: 'Simplify API integration testing with a succinct rspec DSL and generate
|
139
|
+
OpenAPI specification files directly from your rspec tests. More about the OpenAPI
|
140
|
+
initiative here: http://spec.openapis.org/'
|
141
|
+
email:
|
142
|
+
- gracefulpotatow@gmail.com
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".rubocop_rspec_alias_config.yml"
|
148
|
+
- MIT-LICENSE
|
149
|
+
- Rakefile
|
150
|
+
- lib/rspec/swag.rb
|
151
|
+
- lib/rspec/swag/configuration.rb
|
152
|
+
- lib/rspec/swag/example_group_helpers.rb
|
153
|
+
- lib/rspec/swag/example_helpers.rb
|
154
|
+
- lib/rspec/swag/extended_schema.rb
|
155
|
+
- lib/rspec/swag/project_initializer.rb
|
156
|
+
- lib/rspec/swag/rake_task.rb
|
157
|
+
- lib/rspec/swag/request_factory.rb
|
158
|
+
- lib/rspec/swag/response_validator.rb
|
159
|
+
- lib/rspec/swag/swagger_formatter.rb
|
160
|
+
- lib/rspec/swag/tasks/rspec_swag_tasks.rake
|
161
|
+
- lib/rspec/swag/templates/spec/swagger_helper.rb
|
162
|
+
- lib/rspec/swag/version.rb
|
163
|
+
homepage: https://github.com/graceful-potato/rspec-swag
|
164
|
+
licenses:
|
165
|
+
- MIT
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubygems_version: 3.4.20
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: An OpenAPI-based (formerly called Swagger) DSL for rspec & accompanying rake
|
186
|
+
task for generating OpenAPI specification files
|
187
|
+
test_files: []
|