rspec-rails-api 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +54 -0
- data/README.md +39 -36
- data/Rakefile +3 -0
- data/lib/rspec/rails/api/dsl/example.rb +94 -16
- data/lib/rspec/rails/api/dsl/example_group.rb +134 -48
- data/lib/rspec/rails/api/entity_config.rb +31 -4
- data/lib/rspec/rails/api/field_config.rb +12 -3
- data/lib/rspec/rails/api/matchers.rb +16 -3
- data/lib/rspec/rails/api/metadata.rb +149 -32
- data/lib/rspec/rails/api/open_api_renderer.rb +185 -25
- data/lib/rspec/rails/api/utils.rb +66 -7
- data/lib/rspec/rails/api/version.rb +1 -1
- data/lib/rspec_rails_api.rb +8 -0
- data/rspec-rails-api.gemspec +6 -4
- metadata +21 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06e9bc31ff4e310ca952011e76cc3fb6dcc39b15ea090229edb1385b51ed16f6
|
4
|
+
data.tar.gz: 8be9cba238ce37496e2e7eb2db221754cde5c673b1e404541ea84e71095e6ccb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a9058a1121e702991d55696687b64cb8d2bb3872596b4d92f79aa33de8ef54a5b9ef33d2275f92087256cd6d7afcd1fc16472157d52529e19a97b5b66118b70
|
7
|
+
data.tar.gz: 85777059873dd5fba1842ce286c0c0c4c445aa2dad584a2992b2add593de51cb267e1b398dd6038fe71f5ec36bfa38bf8f03fa4ce0877d0438ae62f4f58fb82f
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,60 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
|
4
4
|
## Not released
|
5
5
|
|
6
|
+
- All parameters attributes are considered required unless specified
|
7
|
+
- Fix object `attributes` key in spec and documentation.
|
8
|
+
When defining object attributes, documentation and tests used `properties` key
|
9
|
+
while the code was waiting for an `attributes` key. The later makes more sense
|
10
|
+
so the spec and documentation were fixed.
|
11
|
+
- Check for arrays of primitives is now a thing:
|
12
|
+
```rb
|
13
|
+
# In entities, when attribute is an array of primitives
|
14
|
+
entity :user,
|
15
|
+
aliases: { type: :array, description: 'Pseudonyms', of: :type_string }
|
16
|
+
|
17
|
+
# In examples, when response is an array of primitives
|
18
|
+
for_code 200, expect_many: :type_string do |url|
|
19
|
+
#...
|
20
|
+
end
|
21
|
+
```
|
22
|
+
- RSpec metadata is now stored in `rra` instead of `rrad` (the gem's first name
|
23
|
+
was RSpec Rails API Doc at the time). Update RSpec configuration accordingly.
|
24
|
+
- OpenApi:
|
25
|
+
- Responses now includes the schema
|
26
|
+
- `on_xxx` methods second parameter is now used as summary instead of description.
|
27
|
+
Description can be defined on the third parameter.
|
28
|
+
- DSL changes:
|
29
|
+
- `visit` is renamed to `test_response_of`
|
30
|
+
- Support for `doc_only` is removed
|
31
|
+
- Response expectations _should_ now be declared with `for_code`, and should be
|
32
|
+
removed from example bodies:
|
33
|
+
```rb
|
34
|
+
# Before:
|
35
|
+
for_code 200 do |url|
|
36
|
+
visit url
|
37
|
+
expect(response).to have_many defined: :post
|
38
|
+
end
|
39
|
+
|
40
|
+
# Now:
|
41
|
+
for_code 200, expect_many: :post do |url|
|
42
|
+
test_response_of url
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
|
47
|
+
## 0.3.4 - 2021-10-20
|
48
|
+
- Add the "required" attribute in parameters
|
49
|
+
|
50
|
+
## 0.3.3 - 2021-06-02
|
51
|
+
- Fix correct types on request parameters
|
52
|
+
|
53
|
+
## 0.3.2 - 2021-03-09
|
54
|
+
- Fix YAML rendering (ruby objects were sometimes rendered in documentation)
|
55
|
+
- Render examples results as YAML/JSON objects instead of text blocks.
|
56
|
+
|
57
|
+
## 0.3.1 - 2020-04-09
|
58
|
+
- Add support for "test only" examples, allowing to write examples without documentation.
|
59
|
+
|
6
60
|
## 0.3.0 - 2019-12-26
|
7
61
|
|
8
62
|
### Changed
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ renderer.api_contact = { name: 'Admin', email: 'admin@example.com', url: 'http:/
|
|
48
48
|
renderer.api_license = { name: 'Apache', url: 'https://opensource.org/licenses/Apache-2.0' }
|
49
49
|
|
50
50
|
RSpec.configuration.after(:context, type: :acceptance) do |context|
|
51
|
-
renderer.merge_context context.class.metadata[:
|
51
|
+
renderer.merge_context context.class.metadata[:rra].to_h
|
52
52
|
end
|
53
53
|
|
54
54
|
RSpec.configuration.after(:suite) do
|
@@ -125,7 +125,7 @@ end
|
|
125
125
|
#...
|
126
126
|
for_code 200, 'Success' do |url|
|
127
127
|
sing_in #...
|
128
|
-
|
128
|
+
test_response_of url
|
129
129
|
|
130
130
|
#...
|
131
131
|
end
|
@@ -139,14 +139,6 @@ by Arne Hartherz (MIT license).
|
|
139
139
|
|
140
140
|
Write some spec files and run RSpec as you would usually do.
|
141
141
|
|
142
|
-
If you want to generate the documentation without testing the endpoints
|
143
|
-
(and thus, without examples in generated files), use the `DOC_ONLY`
|
144
|
-
environment variable:
|
145
|
-
|
146
|
-
```sh
|
147
|
-
DOC_ONLY=true bundle exec rails spec
|
148
|
-
```
|
149
|
-
|
150
142
|
## Writing specs
|
151
143
|
|
152
144
|
There is a [commented example](dummy/spec/acceptance/posts_spec.rb) available in
|
@@ -171,9 +163,8 @@ RSpec.describe 'Users', type: :acceptance do
|
|
171
163
|
url: { type: :string, description: 'URL to this category' }
|
172
164
|
|
173
165
|
on_get '/api/users/', 'Users list' do
|
174
|
-
for_code 200, 'Success response' do |url|
|
175
|
-
|
176
|
-
expect(response).to have_many defined :user
|
166
|
+
for_code 200, 'Success response', expect_many: :user do |url|
|
167
|
+
test_response_of url
|
177
168
|
end
|
178
169
|
end
|
179
170
|
|
@@ -181,16 +172,15 @@ RSpec.describe 'Users', type: :acceptance do
|
|
181
172
|
path_param id: { type: :integer, description: 'User Id' }
|
182
173
|
|
183
174
|
request_params user: {
|
184
|
-
type: :object,
|
175
|
+
type: :object, attributes: {
|
185
176
|
name: { type: :string, required: false, description: 'New name' },
|
186
177
|
email: { type: :string, required: false, description: 'New email' },
|
187
178
|
role: { type: :string, required: false, description: 'New role' },
|
188
179
|
}
|
189
180
|
}
|
190
181
|
|
191
|
-
for_code 200, 'Success response' do |url|
|
192
|
-
|
193
|
-
expect(response).to have_one defined :user
|
182
|
+
for_code 200, 'Success response', expect_one: :user do |url|
|
183
|
+
test_response_of url
|
194
184
|
end
|
195
185
|
end
|
196
186
|
end
|
@@ -273,6 +263,16 @@ inline.
|
|
273
263
|
Both `:of` and `attributes` may be a hash of fields or a symbol. If they
|
274
264
|
are omitted, they will be documented, but responses won't be validated.
|
275
265
|
|
266
|
+
Arrays of primitives are supported; the type should be prefixed by `type_` to
|
267
|
+
avoid collisions with defined entities of the same name:
|
268
|
+
|
269
|
+
```rb
|
270
|
+
entity :user,
|
271
|
+
surnames: { type: :array, of: :type_int32 }
|
272
|
+
```
|
273
|
+
|
274
|
+
Check `lib/rspec_rails_api.rb` for the full list.
|
275
|
+
|
276
276
|
##### `parameters(type, fields)`
|
277
277
|
Describe path or request parameters. The type is only a reference,
|
278
278
|
use whatever makes sense. These parameters will be present in
|
@@ -282,16 +282,16 @@ documentation, only if they are referenced by a `request_params` or
|
|
282
282
|
Fields have the structure of the hash you would give to `request_params`
|
283
283
|
or `path_params` (see each method later in this documentation).
|
284
284
|
|
285
|
-
##### `on_<xxx>(url, description, &block)`
|
285
|
+
##### `on_<xxx>(url, summary = nil, description = nil, &block)`
|
286
286
|
|
287
287
|
Defines an URL.
|
288
288
|
|
289
289
|
- `url` should be a relative URL to an existing endpoint (i.e.:
|
290
290
|
`/api/users`)
|
291
|
-
- `
|
292
|
-
|
291
|
+
- `summary` is a one line description of the endpoint
|
292
|
+
- `description` should be some valid [CommonMark](https://commonmark.org/)
|
293
293
|
|
294
|
-
|
294
|
+
These methods are available:
|
295
295
|
|
296
296
|
- `on_get`
|
297
297
|
- `on_post`
|
@@ -341,7 +341,7 @@ nested elements can be described:
|
|
341
341
|
```ruby
|
342
342
|
on_post '/api/items' do
|
343
343
|
request_params attributes: {
|
344
|
-
item: { type: :object,
|
344
|
+
item: { type: :object, attributes: {
|
345
345
|
name: { type: integer, description: 'The name of the new item', required: true },
|
346
346
|
notes: { type: string, description: 'Additional notes' }
|
347
347
|
} }
|
@@ -353,21 +353,21 @@ end
|
|
353
353
|
An attribute should have the following form:
|
354
354
|
|
355
355
|
```
|
356
|
-
<attr_name>: {type: <type>, desc: <description>, required: <required>,
|
356
|
+
<attr_name>: {type: <type>, desc: <description>, required: <required>, attributes: <another_hash>, of: <another_hash> }
|
357
357
|
```
|
358
358
|
|
359
359
|
- `attr_name` is the attribute name (sic)
|
360
360
|
- `type` is the field type (check _entity definition_ for a list).
|
361
361
|
`type` can be `:object` if the attribute contains other attributes.
|
362
362
|
- `required` is optional an defaults to `false`.
|
363
|
-
- `
|
363
|
+
- `attributes` is a hash of params and is only used if `type: :object`
|
364
364
|
- `of` is a hash of params and is only used if `type: :array`
|
365
365
|
|
366
366
|
Alternative with defined parameters:
|
367
367
|
|
368
368
|
```ruby
|
369
369
|
parameters :item_form_params,
|
370
|
-
item: { type: :object,
|
370
|
+
item: { type: :object, attributes: {
|
371
371
|
name: { type: integer, description: 'The name of the new item', required: true },
|
372
372
|
notes: { type: string, description: 'Additional notes' }
|
373
373
|
}
|
@@ -380,12 +380,12 @@ on_post '/api/items' do
|
|
380
380
|
end
|
381
381
|
```
|
382
382
|
|
383
|
-
##### `for_code(http_status, description = nil,
|
383
|
+
##### `for_code(http_status, description = nil, test_only: false &block)`
|
384
384
|
|
385
385
|
Describes the desired output for a precedently defined URL.
|
386
386
|
|
387
|
-
Block takes one required argument, that should be passed to `
|
388
|
-
This argument will contain the block context and allow `
|
387
|
+
Block takes one required argument, that should be passed to `test_response_of`.
|
388
|
+
This argument will contain the block context and allow `test_response_of` to access
|
389
389
|
the metadatas.
|
390
390
|
|
391
391
|
You can have only one documented code per action/url, unless you use
|
@@ -396,11 +396,9 @@ You can have only one documented code per action/url, unless you use
|
|
396
396
|
- `description` should be some valid
|
397
397
|
[CommonMark](https://commonmark.org/). If not defined, a human readable
|
398
398
|
translation of the `http_status` will be used.
|
399
|
-
- `doc_only` can be set to true to temporarily disable block execution
|
400
|
-
and only create the documentation (without examples).
|
401
399
|
- `test_only` will omit the test from the documentation. Useful when you
|
402
400
|
need to test things _around_ the call (response content, db,...)
|
403
|
-
- `block` where additional tests can be performed. If `
|
401
|
+
- `block` where additional tests can be performed. If `test_response_of` is
|
404
402
|
called within the block, its output will be used in documentation
|
405
403
|
examples, and the response type and code will actually be tested.
|
406
404
|
|
@@ -409,17 +407,17 @@ examples. This can be useful to document endpoints that are impossible
|
|
409
407
|
to test.
|
410
408
|
|
411
409
|
Once again, you have to pass an argument to the block if you use
|
412
|
-
`
|
410
|
+
`test_response_of`.
|
413
411
|
|
414
412
|
```ruby
|
415
413
|
# ...
|
416
414
|
for_code 200, 'A successful response' do |url|
|
417
|
-
|
415
|
+
test_response_of url
|
418
416
|
# ...
|
419
417
|
end
|
420
418
|
|
421
419
|
for_code 200, 'Side test', test_only: true do |url|
|
422
|
-
|
420
|
+
test_response_of url
|
423
421
|
# ...
|
424
422
|
end
|
425
423
|
# ...
|
@@ -429,7 +427,7 @@ Once again, you have to pass an argument to the block if you use
|
|
429
427
|
|
430
428
|
Example methods are available in `for_code` blocks
|
431
429
|
|
432
|
-
##### `
|
430
|
+
##### `test_response_of(example, path_params: {}, payload: {}, headers: {})`
|
433
431
|
|
434
432
|
Visits the described URL and:
|
435
433
|
|
@@ -446,12 +444,17 @@ Visits the described URL and:
|
|
446
444
|
|
447
445
|
```ruby
|
448
446
|
for_code 200, 'Success' do |url|
|
449
|
-
|
447
|
+
test_response_of url
|
450
448
|
end
|
451
449
|
```
|
452
450
|
|
453
451
|
#### Matchers
|
454
452
|
|
453
|
+
The matchers _should not_ be used in acceptance specs unless the default DSL is
|
454
|
+
not adapted for a particular use case (and I can't imagine one now).
|
455
|
+
|
456
|
+
For the sake of comprehension, the two custom matchers still described.
|
457
|
+
|
455
458
|
##### `have_one(type)`
|
456
459
|
|
457
460
|
Expects the compared content to be a hash with the same keys as a
|
data/Rakefile
CHANGED
@@ -6,7 +6,16 @@ module RSpec
|
|
6
6
|
module DSL
|
7
7
|
# These methods will be available in examples (i.e.: 'for_code')
|
8
8
|
module Example
|
9
|
-
|
9
|
+
##
|
10
|
+
# Visits the current example and tests the response
|
11
|
+
#
|
12
|
+
# @param example [Hash] Current example
|
13
|
+
# @param path_params [Hash] Path parameters definition
|
14
|
+
# @param payload [Hash] Request body
|
15
|
+
# @param headers [Hash] Custom headers
|
16
|
+
#
|
17
|
+
# @return [void]
|
18
|
+
def test_response_of(example, path_params: {}, payload: {}, headers: {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
10
19
|
raise 'Missing context. Call visit with for_code context.' unless example
|
11
20
|
|
12
21
|
status_code = prepare_status_code example.class.description
|
@@ -23,39 +32,74 @@ module RSpec
|
|
23
32
|
|
24
33
|
return if example.class.description.match?(/-> test (\d+)(.*)/)
|
25
34
|
|
26
|
-
set_request_example example.class.metadata[:
|
35
|
+
set_request_example example.class.metadata[:rra], request_params, status_code, response.body
|
27
36
|
end
|
28
37
|
|
38
|
+
private
|
39
|
+
|
40
|
+
##
|
41
|
+
# Searches for a defined entity in metadata
|
42
|
+
#
|
43
|
+
# @param entity [Symbol] Entity reference
|
44
|
+
#
|
45
|
+
# @return [RSpec::Rails::Api::EntityConfig, Hash] Defined entity
|
29
46
|
def defined(entity)
|
30
|
-
|
47
|
+
return { type: entity.to_s.split('_').last.to_sym } if PRIMITIVES.include? entity
|
48
|
+
|
49
|
+
current_resource = rra_metadata.current_resource
|
31
50
|
raise '@current_resource is unset' unless current_resource
|
32
51
|
|
33
|
-
entities =
|
52
|
+
entities = rra_metadata.resources[current_resource][:entities]
|
34
53
|
|
35
54
|
out = entities[entity]
|
36
|
-
raise "
|
55
|
+
raise "Unknown entity '#{entity}' in resource '#{current_resource}'" unless out
|
37
56
|
|
38
57
|
out.expand_with(entities)
|
39
58
|
end
|
40
59
|
|
41
|
-
|
42
|
-
|
43
|
-
|
60
|
+
##
|
61
|
+
# Performs various tests on the response
|
62
|
+
#
|
63
|
+
# @param response [ActionDispatch::TestResponse] The response
|
64
|
+
# @param expected_code [Number] Code to test for
|
65
|
+
def check_response(response, expected_code) # rubocop:disable Metrics/AbcSize
|
44
66
|
expect(response.status).to eq expected_code
|
45
67
|
expect(response.headers['Content-Type']).to eq 'application/json; charset=utf-8' if expected_code != 204
|
68
|
+
expectations = rra_current_example[:expectations]
|
69
|
+
expect(response).to have_many defined(expectations[:many]) if expectations[:many]
|
70
|
+
expect(response).to have_one defined(expectations[:one]) if expectations[:one]
|
71
|
+
expect(response.body).to eq '' if expectations[:none]
|
46
72
|
end
|
47
73
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
74
|
+
##
|
75
|
+
# Adds a request example information to metadata
|
76
|
+
#
|
77
|
+
# @param rra_metadata [RSpec::Rails::Api::Metadata]
|
78
|
+
# @param request_params [Hash]
|
79
|
+
# @param status_code [Integer, nil]
|
80
|
+
# @param response [String, nil]
|
81
|
+
#
|
82
|
+
# @return [void]
|
83
|
+
def set_request_example(rra_metadata, request_params, status_code = nil, response = nil)
|
84
|
+
rra_metadata.add_request_example(url: request_params[:example_url],
|
85
|
+
action: request_params[:action],
|
86
|
+
status_code: status_code,
|
87
|
+
response: response,
|
88
|
+
path_params: request_params[:path_params],
|
89
|
+
params: request_params[:params])
|
55
90
|
end
|
56
91
|
|
92
|
+
##
|
93
|
+
# Prepares the options for a request
|
94
|
+
#
|
95
|
+
# @param description [String] RSpec example description
|
96
|
+
# @param request_params [Hash] Request parameters
|
97
|
+
# @param payload [Hash] Request body
|
98
|
+
# @param request_headers [Hash] Custom headers
|
99
|
+
#
|
100
|
+
# @return [Hash] Options for the request
|
57
101
|
def prepare_request_params(description, request_params = {}, payload = {}, request_headers = {})
|
58
|
-
example_params = description.split
|
102
|
+
example_params = description.split
|
59
103
|
|
60
104
|
{
|
61
105
|
action: example_params[0].downcase,
|
@@ -66,7 +110,13 @@ module RSpec
|
|
66
110
|
}
|
67
111
|
end
|
68
112
|
|
113
|
+
##
|
69
114
|
# Replace path params by values
|
115
|
+
#
|
116
|
+
# @param url [String] Url definition
|
117
|
+
# @param request_params [Hash] Request parameters
|
118
|
+
#
|
119
|
+
# @return [String] Actual path to visit
|
70
120
|
def prepare_request_url(url, request_params)
|
71
121
|
url.gsub(/(?::(\w*))/) do |e|
|
72
122
|
symbol = e.sub(':', '').to_sym
|
@@ -80,6 +130,12 @@ module RSpec
|
|
80
130
|
end
|
81
131
|
end
|
82
132
|
|
133
|
+
##
|
134
|
+
# Prepares request headers
|
135
|
+
#
|
136
|
+
# @param headers [Hash] Custom headers
|
137
|
+
#
|
138
|
+
# @return [Hash] Headers to use in request
|
83
139
|
def prepare_request_headers(headers = {})
|
84
140
|
{
|
85
141
|
'Accept' => 'application/json',
|
@@ -87,6 +143,12 @@ module RSpec
|
|
87
143
|
}.merge headers
|
88
144
|
end
|
89
145
|
|
146
|
+
##
|
147
|
+
# Extracts status code from RSpec example description
|
148
|
+
#
|
149
|
+
# @param description [String] RSpec example description
|
150
|
+
#
|
151
|
+
# @return [Integer] Status code
|
90
152
|
def prepare_status_code(description)
|
91
153
|
code_match = /->(?: test)? (\d+) - .*/.match description
|
92
154
|
|
@@ -94,6 +156,22 @@ module RSpec
|
|
94
156
|
|
95
157
|
code_match[1].to_i
|
96
158
|
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Returns the current example configuration from metadata
|
162
|
+
#
|
163
|
+
# @return [Hash] Current example configuration
|
164
|
+
def rra_current_example
|
165
|
+
self.class.metadata[:rra_current_example]
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Returns the whole Rspec-rails-API metadata object
|
170
|
+
#
|
171
|
+
# @return [RSpec::Rails::Api::Metadata] Rspec-rails-API metadata object
|
172
|
+
def rra_metadata
|
173
|
+
self.class.metadata[:rra]
|
174
|
+
end
|
97
175
|
end
|
98
176
|
end
|
99
177
|
end
|