rspec-rails-api 0.3.4 → 0.5.0
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 +4 -4
- data/.gitignore +0 -5
- data/.gitlab-ci.yml +1 -1
- data/.rubocop.yml +5 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +51 -0
- data/Gemfile.lock +98 -0
- data/README.md +39 -36
- data/Rakefile +3 -0
- data/lib/rspec/rails/api/dsl/example.rb +94 -18
- data/lib/rspec/rails/api/dsl/example_group.rb +134 -48
- data/lib/rspec/rails/api/entity_config.rb +28 -4
- data/lib/rspec/rails/api/field_config.rb +10 -2
- data/lib/rspec/rails/api/matchers.rb +26 -18
- data/lib/rspec/rails/api/metadata.rb +153 -21
- data/lib/rspec/rails/api/open_api_renderer.rb +189 -20
- data/lib/rspec/rails/api/utils.rb +28 -70
- data/lib/rspec/rails/api/validator.rb +211 -0
- data/lib/rspec/rails/api/version.rb +1 -1
- data/lib/rspec_rails_api.rb +4 -0
- data/rspec-rails-api.gemspec +10 -5
- metadata +68 -9
@@ -9,99 +9,185 @@ module RSpec
|
|
9
9
|
# All these methods will be available in example groups
|
10
10
|
# (anything but 'it', 'example', 'for_code')
|
11
11
|
module ExampleGroup
|
12
|
-
|
13
|
-
# as it will initialize the
|
12
|
+
##
|
13
|
+
# First method to be called in a spec file # as it will initialize the
|
14
|
+
# metadata.
|
15
|
+
#
|
16
|
+
# @param name [String] Resource name
|
17
|
+
# @param description [String] Resource description
|
14
18
|
def resource(name, description = '')
|
15
|
-
metadata[:
|
16
|
-
metadata[:
|
19
|
+
metadata[:rra] ||= Metadata.new
|
20
|
+
metadata[:rra].add_resource name, description
|
17
21
|
end
|
18
22
|
|
19
|
-
|
23
|
+
##
|
24
|
+
# Describes an entity
|
25
|
+
#
|
26
|
+
# @param type [Symbol] Name of the entity for reference
|
27
|
+
# @param fields [Hash] Fields declarations
|
28
|
+
#
|
29
|
+
# @return [void]
|
20
30
|
def entity(type, fields)
|
21
|
-
metadata[:
|
31
|
+
metadata[:rra].add_entity type, fields
|
22
32
|
end
|
23
33
|
|
24
|
-
|
34
|
+
##
|
35
|
+
# Describes request or path parameters
|
36
|
+
#
|
37
|
+
# @param type [Symbol] Name of the parameters set for reference
|
38
|
+
# @param fields [Hash] Fields declarations
|
39
|
+
#
|
40
|
+
# @return [void]
|
25
41
|
def parameters(type, fields)
|
26
|
-
metadata[:
|
42
|
+
metadata[:rra].add_parameter type, fields
|
27
43
|
end
|
28
44
|
|
29
|
-
|
45
|
+
##
|
46
|
+
# Declares parameters used in URLS (_path_)
|
47
|
+
# Use `fields` or `defined` but not both.
|
48
|
+
#
|
49
|
+
# @param [Hash, nil] fields An attributes declaration
|
50
|
+
# @param [Symbol, nil] defined An entity reference
|
51
|
+
#
|
52
|
+
# @return [void]
|
30
53
|
def path_params(fields: nil, defined: nil)
|
31
|
-
if defined && !metadata[:
|
54
|
+
if defined && !metadata[:rra].parameters[defined]
|
32
55
|
raise "Parameter #{defined} was not defined with the 'parameters' method"
|
33
56
|
end
|
34
57
|
|
35
|
-
fields ||= metadata[:
|
58
|
+
fields ||= metadata[:rra].parameters[defined]
|
36
59
|
|
37
|
-
metadata[:
|
60
|
+
metadata[:rra].add_path_params fields
|
38
61
|
end
|
39
62
|
|
63
|
+
##
|
64
|
+
# Declares parameters for a request body
|
65
|
+
# Use `attributes` or `defined` but not both.
|
66
|
+
#
|
67
|
+
# @param [Hash, nil] attributes An attributes declaration
|
68
|
+
# @param [Symbol, nil] defined An entity reference.
|
69
|
+
#
|
70
|
+
# @return [void]
|
40
71
|
def request_params(attributes: nil, defined: nil)
|
41
|
-
if defined && !metadata[:
|
72
|
+
if defined && !metadata[:rra].parameters[defined]
|
42
73
|
raise "Parameter #{defined} was not defined with the 'parameters' method"
|
43
74
|
end
|
44
75
|
|
45
|
-
attributes ||= metadata[:
|
76
|
+
attributes ||= metadata[:rra].parameters[defined]
|
46
77
|
|
47
|
-
metadata[:
|
78
|
+
metadata[:rra].add_request_params attributes
|
48
79
|
end
|
49
80
|
|
50
|
-
|
51
|
-
|
81
|
+
##
|
82
|
+
# Defines a GET action
|
83
|
+
#
|
84
|
+
# @param [String] url URL to test
|
85
|
+
# @param [String] summary What the action does
|
86
|
+
# @param [String] description Longer description
|
87
|
+
#
|
88
|
+
# @return [void]
|
89
|
+
def on_get(url, summary = nil, description = nil, &block)
|
90
|
+
on_action(:get, url, summary, description, &block)
|
52
91
|
end
|
53
92
|
|
54
|
-
|
55
|
-
|
93
|
+
##
|
94
|
+
# Defines a POST action
|
95
|
+
#
|
96
|
+
# @param [String] url URL to test
|
97
|
+
# @param [String] summary What the action does
|
98
|
+
# @param [String] description Longer description
|
99
|
+
#
|
100
|
+
# @return [void]
|
101
|
+
def on_post(url, summary = nil, description = nil, &block)
|
102
|
+
on_action(:post, url, summary, description, &block)
|
56
103
|
end
|
57
104
|
|
58
|
-
|
59
|
-
|
105
|
+
##
|
106
|
+
# Defines a PUT action
|
107
|
+
#
|
108
|
+
# @param [String] url URL to test
|
109
|
+
# @param [String] summary What the action does
|
110
|
+
# @param [String] description Longer description
|
111
|
+
#
|
112
|
+
# @return [void]
|
113
|
+
def on_put(url, summary = nil, description = nil, &block)
|
114
|
+
on_action(:put, url, summary, description, &block)
|
60
115
|
end
|
61
116
|
|
62
|
-
|
63
|
-
|
117
|
+
##
|
118
|
+
# Defines a PATCH action
|
119
|
+
#
|
120
|
+
# @param [String] url URL to test
|
121
|
+
# @param [String] summary What the action does
|
122
|
+
# @param [String] description Longer description
|
123
|
+
#
|
124
|
+
# @return [void]
|
125
|
+
def on_patch(url, summary = nil, description = nil, &block)
|
126
|
+
on_action(:patch, url, summary, description, &block)
|
64
127
|
end
|
65
128
|
|
66
|
-
|
67
|
-
|
129
|
+
##
|
130
|
+
# Defines a DELETE action
|
131
|
+
#
|
132
|
+
# @param [String] url URL to test
|
133
|
+
# @param [String] summary What the action does
|
134
|
+
# @param [String] description Longer description
|
135
|
+
#
|
136
|
+
# @return [void]
|
137
|
+
def on_delete(url, summary = nil, description = nil, &block)
|
138
|
+
on_action(:delete, url, summary, description, &block)
|
68
139
|
end
|
69
140
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
141
|
+
##
|
142
|
+
# Adds an HTTP code declaration to metadata, with expected result
|
143
|
+
# If no expectation is provided, the response will be expected to be empty
|
144
|
+
#
|
145
|
+
# @param status_code [Number] Status code to test for
|
146
|
+
# @param description [String] Description of the route/status pair
|
147
|
+
# @param expect_many [Symbol] Check the response for a list of given entity
|
148
|
+
# @param expect_one [Symbol] Check the response for a given entity
|
149
|
+
# @param test_only [Boolean] When true, test the response without filling the documentation
|
150
|
+
#
|
151
|
+
# @return [void]
|
152
|
+
#
|
153
|
+
def for_code(status_code, description = nil, expect_many: nil, expect_one: false, test_only: false, &block)
|
78
154
|
description ||= Rack::Utils::HTTP_STATUS_CODES[status_code]
|
79
155
|
|
80
|
-
metadata[:
|
156
|
+
metadata[:rra].add_status_code(status_code, description) unless test_only
|
157
|
+
metadata[:rra].add_expectations(expect_one, expect_many)
|
158
|
+
metadata[:rra_current_example] = metadata[:rra].current_example
|
81
159
|
|
82
160
|
describe "->#{test_only ? ' test' : ''} #{status_code} - #{description}" do
|
83
|
-
execute_for_code_block(
|
161
|
+
execute_for_code_block(block)
|
84
162
|
end
|
85
163
|
end
|
86
164
|
|
87
165
|
private
|
88
166
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
167
|
+
##
|
168
|
+
# Currently fill metadata with the action
|
169
|
+
#
|
170
|
+
# @param [Symbol] action HTTP verb
|
171
|
+
# @param [String] url URL to test
|
172
|
+
# @param [String, nil] summary What the action does
|
173
|
+
# @param [String, nil] description Longer description
|
174
|
+
#
|
175
|
+
# @return [void]
|
176
|
+
def on_action(action, url, summary, description, &block)
|
177
|
+
metadata[:rra].add_action(action, url, summary, description)
|
93
178
|
|
94
|
-
|
95
|
-
end
|
179
|
+
describe("#{action.upcase} #{url}", &block)
|
96
180
|
end
|
97
181
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
182
|
+
##
|
183
|
+
# Visit the URL and test response
|
184
|
+
#
|
185
|
+
# @param callback_block [block] Block to execute for testing the response
|
186
|
+
#
|
187
|
+
# @return [void]
|
188
|
+
def execute_for_code_block(callback_block)
|
189
|
+
example 'Test response and create documentation', caller: callback_block.send(:caller) do
|
190
|
+
instance_eval(&callback_block) if callback_block
|
105
191
|
end
|
106
192
|
end
|
107
193
|
end
|
@@ -18,6 +18,8 @@ module RSpec
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
##
|
22
|
+
# @return [Hash] Entity configuration
|
21
23
|
def to_h
|
22
24
|
out = {}
|
23
25
|
@fields.each_key do |key|
|
@@ -26,18 +28,40 @@ module RSpec
|
|
26
28
|
out
|
27
29
|
end
|
28
30
|
|
31
|
+
##
|
32
|
+
# Replaces the arrays 'of' and objects 'attributes' with the corresponding
|
33
|
+
# entities, recursively
|
34
|
+
#
|
35
|
+
# @param entities [Hash] List of entities
|
36
|
+
#
|
37
|
+
# @return [Hash]
|
29
38
|
def expand_with(entities)
|
30
39
|
hash = to_h
|
31
40
|
hash.each_pair do |field, config|
|
32
41
|
next unless %i[array object].include? config[:type]
|
33
42
|
|
34
|
-
|
35
|
-
next unless
|
36
|
-
raise "Entity #{attributes} not found for entity completion." unless entities[attributes]
|
43
|
+
attribute = config[:attributes]
|
44
|
+
next unless attribute.is_a? Symbol
|
37
45
|
|
38
|
-
hash[field][:attributes] = entities
|
46
|
+
hash[field][:attributes] = expand_attribute attribute, entities
|
39
47
|
end
|
40
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
##
|
53
|
+
# Expands an attribute for "for" and "attributes" keys
|
54
|
+
#
|
55
|
+
# @param attribute [Symbol] Attribute name
|
56
|
+
# @param entities [Hash] List of entities
|
57
|
+
def expand_attribute(attribute, entities)
|
58
|
+
# Primitives support
|
59
|
+
return { type: attribute } if PRIMITIVES.include? attribute
|
60
|
+
|
61
|
+
raise "Entity #{attribute} not found for entity completion." unless entities[attribute]
|
62
|
+
|
63
|
+
entities[attribute].expand_with(entities)
|
64
|
+
end
|
41
65
|
end
|
42
66
|
end
|
43
67
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rspec/rails/api/entity_config'
|
4
|
-
require 'rspec/rails/api/
|
4
|
+
require 'rspec/rails/api/validator'
|
5
5
|
|
6
6
|
module RSpec
|
7
7
|
module Rails
|
@@ -14,7 +14,7 @@ module RSpec
|
|
14
14
|
def initialize(type:, description:, required: true, attributes: nil, of: nil)
|
15
15
|
@required = required
|
16
16
|
@description = description
|
17
|
-
raise "Field type not allowed: '#{type}'" unless
|
17
|
+
raise "Field type not allowed: '#{type}'" unless Validator.valid_type?(type)
|
18
18
|
|
19
19
|
define_attributes attributes if type == :object
|
20
20
|
define_attributes of if type == :array
|
@@ -22,6 +22,8 @@ module RSpec
|
|
22
22
|
@type = type
|
23
23
|
end
|
24
24
|
|
25
|
+
##
|
26
|
+
# @return [Hash] Field configuration
|
25
27
|
def to_h
|
26
28
|
out = { required: @required, type: @type }
|
27
29
|
out[:description] = @description unless @description.nil?
|
@@ -39,6 +41,12 @@ module RSpec
|
|
39
41
|
|
40
42
|
private
|
41
43
|
|
44
|
+
##
|
45
|
+
# Sets @attributes of the field when it's an Array or Hash
|
46
|
+
#
|
47
|
+
# @param attributes [Hash, Symbol] The attributes definition or reference
|
48
|
+
#
|
49
|
+
# @return [void]
|
42
50
|
def define_attributes(attributes)
|
43
51
|
@attributes = case attributes
|
44
52
|
when Hash
|
@@ -1,40 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require 'yaml'
|
4
5
|
|
6
|
+
require 'rspec/rails/api/validator'
|
5
7
|
require 'rspec/rails/api/utils'
|
6
8
|
|
7
|
-
|
9
|
+
##
|
10
|
+
# RSpec matcher to check something against an array of `expected`
|
8
11
|
RSpec::Matchers.define :have_many do |expected|
|
9
12
|
match do |actual|
|
10
|
-
|
11
|
-
@actual = JSON.parse(actual.body) if actual.respond_to? :body
|
13
|
+
actual = RSpec::Rails::Api::Utils.hash_from_response actual
|
12
14
|
|
13
|
-
raise "Response is not an array: #{
|
14
|
-
raise 'Response has no item to compare with' unless
|
15
|
+
raise "Response is not an array: #{actual.class}" unless actual.is_a? Array
|
16
|
+
raise 'Response has no item to compare with' unless actual.count.positive?
|
15
17
|
|
16
|
-
|
17
|
-
ok = true
|
18
|
-
@actual.each do |item|
|
19
|
-
ok = false unless RSpec::Rails::Api::Utils.validate_object_structure item, expected
|
20
|
-
end
|
18
|
+
@errors = RSpec::Rails::Api::Validator.validate_array actual, expected
|
21
19
|
|
22
|
-
|
20
|
+
@errors.blank?
|
23
21
|
end
|
24
22
|
|
25
|
-
|
23
|
+
failure_message do |actual|
|
24
|
+
object = RSpec::Rails::Api::Utils.hash_from_response(actual).to_json.chomp
|
25
|
+
RSpec::Rails::Api::Validator.format_failure_message @errors, object
|
26
|
+
end
|
26
27
|
end
|
27
28
|
|
28
|
-
|
29
|
+
##
|
30
|
+
# RSpec matcher to check something against the `expected` definition
|
29
31
|
RSpec::Matchers.define :have_one do |expected|
|
30
32
|
match do |actual|
|
31
|
-
|
32
|
-
@actual = JSON.parse(actual.body) if actual.respond_to? :body
|
33
|
+
actual = RSpec::Rails::Api::Utils.hash_from_response actual
|
33
34
|
|
34
|
-
|
35
|
+
@errors = if expected.keys.count == 1 && expected.key?(:type)
|
36
|
+
RSpec::Rails::Api::Validator.validate_type actual, expected[:type]
|
37
|
+
else
|
38
|
+
RSpec::Rails::Api::Validator.validate_object actual, expected
|
39
|
+
end
|
35
40
|
|
36
|
-
|
41
|
+
@errors.blank?
|
37
42
|
end
|
38
43
|
|
39
|
-
|
44
|
+
failure_message do |actual|
|
45
|
+
object = RSpec::Rails::Api::Utils.hash_from_response(actual).to_json.chomp
|
46
|
+
RSpec::Rails::Api::Validator.format_failure_message @errors, object
|
47
|
+
end
|
40
48
|
end
|