rspec-rails-api 0.5.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +120 -29
- data/Gemfile.lock +1 -1
- data/README.md +210 -37
- data/lib/rspec/rails/api/dsl/example.rb +35 -16
- data/lib/rspec/rails/api/dsl/example_group.rb +11 -1
- data/lib/rspec/rails/api/entity_config.rb +2 -2
- data/lib/rspec/rails/api/field_config.rb +5 -2
- data/lib/rspec/rails/api/metadata.rb +46 -18
- data/lib/rspec/rails/api/open_api_renderer.rb +147 -36
- data/lib/rspec/rails/api/version.rb +1 -1
- data/lib/rspec_rails_api.rb +2 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4771b145e32ae7cbfd7f03d6252618d3a4cd8b557930a67a562e45046d4e302b
|
4
|
+
data.tar.gz: 6bc0e55b1c2e5d51ba5bdfc52ac532efbf5ad539e138557a99895ef637e0311a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cafa1ebfc333e41474f85dfb9389f976df1ffa8000de62c8a412c6436088db9182a7f15ec162dff46848265e6cf86564e5a79c93e0814afcc454a242ad77c8b6
|
7
|
+
data.tar.gz: a24717ab5c36a863eaa6939c4ee3f5df3b5fbf058f45b17923d50dd0656680becca60f325d2d9c11b13db84dfaf243a4f24dd77450c33b28911086b986f9cb9e
|
data/CHANGELOG.md
CHANGED
@@ -1,22 +1,82 @@
|
|
1
1
|
# Change Log
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
-
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
5
6
|
|
6
|
-
|
7
|
+
<!--
|
8
|
+
Quick remainder of the possible sections:
|
9
|
+
-----------------------------------------
|
10
|
+
Added, for new features.
|
11
|
+
Changed, for changes in existing functionality.
|
12
|
+
Deprecated, for soon-to-be removed features.
|
13
|
+
Removed, for now removed features.
|
14
|
+
Fixed, for any bug fixes.
|
15
|
+
Security, in case of vulnerabilities.
|
16
|
+
Maintenance, in case of rework, dependencies change
|
17
|
+
-->
|
18
|
+
|
19
|
+
## [Unreleased]
|
20
|
+
|
21
|
+
## [0.6.1] - 2024-01-17
|
22
|
+
|
23
|
+
### Fixed
|
24
|
+
|
25
|
+
- Renderer: handle arrays of object with inline attributes (not a reference)
|
26
|
+
- Renderer: Remove the `name` attribute from `basic` authentication declaration
|
27
|
+
- Renderer: Add missing `items` key on arrays of any type
|
28
|
+
|
29
|
+
## [0.6.0] - 2023-07-17
|
30
|
+
|
31
|
+
### Added
|
32
|
+
|
33
|
+
- Declaring the same resource multiple times is now possible
|
34
|
+
- Add a simple support for security schemes
|
35
|
+
- Add support for global entities declarations. Keep things DRYer :)
|
36
|
+
- `test_response_of`: add `ignore_content_type` flag to ignore response's content type
|
37
|
+
- `test_response_of`: add `ignore_response` flag to ignore tests on response
|
38
|
+
- Renderer: Add simple support for redactable content. It will replace content in responses by something else. For now,
|
39
|
+
sub-entities are not redacted.
|
40
|
+
- Support for `file` type. When declaring a field with the `file` type, it will change the request content-type to
|
41
|
+
`multipart/form-data` automatically.
|
42
|
+
|
43
|
+
### Changed
|
44
|
+
|
45
|
+
- Generated JSON files will always be prettified to ease changes reviews when they are versioned
|
46
|
+
- When an unexpected 422 error happens, also display its content in error message
|
47
|
+
- [BREAKING] It is now impossible to declare the same entity twice. To remediate, rename your declared entities and/or
|
48
|
+
use global declarations. Check [README](./README.md) for example.
|
49
|
+
- Renderer: Use response data as-is when it's not valid JSON
|
50
|
+
|
51
|
+
### Fixed
|
52
|
+
|
53
|
+
- Strip generated descriptions and summaries
|
54
|
+
- Primitives are no more referenced in schemas for "expect_one" types
|
55
|
+
- Compare type `:float` against `Numeric` class
|
56
|
+
- Entities: Raise error when using `:array` type with `attributes` property and `:object` type with `of` property
|
57
|
+
- Rendering of sub-references now use the correct reference
|
58
|
+
- Metadata: Add missing type on array parameters
|
59
|
+
- Don't transform parameters into JSON when making `get` requests
|
60
|
+
- Operation IDs don't use summaries for uniqueness
|
61
|
+
- Downcase response content type before comparison
|
62
|
+
- Type format is now added on request parameters if applicable
|
63
|
+
|
64
|
+
## [0.5.0] - 2023-01-02
|
65
|
+
|
66
|
+
### Changed
|
7
67
|
|
8
68
|
- Improved error messages
|
9
69
|
- Improved usage of primitive types: use `:string` instead of `:type_string`, and more generally, remove the `type_`
|
10
70
|
prefix
|
71
|
+
|
72
|
+
### Fixed
|
73
|
+
-
|
11
74
|
- Fixed an error when an object is defined with an attribute named `type`.
|
12
75
|
|
13
|
-
## 0.4.0 - 2021-12-19
|
76
|
+
## [0.4.0] - 2021-12-19
|
77
|
+
|
78
|
+
### Added
|
14
79
|
|
15
|
-
- All parameters attributes are considered required unless specified
|
16
|
-
- Fix object `attributes` key in spec and documentation.
|
17
|
-
When defining object attributes, documentation and tests used `properties` key
|
18
|
-
while the code was waiting for an `attributes` key. The later makes more sense
|
19
|
-
so the spec and documentation were fixed.
|
20
80
|
- Check for arrays of primitives is now a thing:
|
21
81
|
```rb
|
22
82
|
# In entities, when attribute is an array of primitives
|
@@ -28,15 +88,19 @@ All notable changes to this project will be documented in this file.
|
|
28
88
|
#...
|
29
89
|
end
|
30
90
|
```
|
31
|
-
- RSpec metadata is now stored in `rra` instead of `rrad` (the gem's first name
|
32
|
-
was RSpec Rails API Doc at the time). Update RSpec configuration accordingly.
|
33
91
|
- OpenApi:
|
34
92
|
- Responses now includes the schema
|
35
93
|
- `on_xxx` methods second parameter is now used as summary instead of description.
|
36
94
|
Description can be defined on the third parameter.
|
95
|
+
|
96
|
+
### Changed
|
97
|
+
|
98
|
+
- All parameters attributes are considered required unless specified
|
99
|
+
- RSpec metadata is now stored in `rra` instead of `rrad` (the gem's first name
|
100
|
+
was RSpec Rails API Doc at the time). Update RSpec configuration accordingly.
|
37
101
|
- DSL changes:
|
38
102
|
- `visit` is renamed to `test_response_of`
|
39
|
-
- Support for `doc_only` is removed
|
103
|
+
- Support for `doc_only` is removed
|
40
104
|
- Response expectations _should_ now be declared with `for_code`, and should be
|
41
105
|
removed from example bodies:
|
42
106
|
```rb
|
@@ -52,44 +116,71 @@ All notable changes to this project will be documented in this file.
|
|
52
116
|
end
|
53
117
|
```
|
54
118
|
|
119
|
+
### Fixed
|
120
|
+
|
121
|
+
- Fix object `attributes` key in spec and documentation.
|
122
|
+
When defining object attributes, documentation and tests used `properties` key
|
123
|
+
while the code was waiting for an `attributes` key. The later makes more sense
|
124
|
+
so the spec and documentation were fixed.
|
125
|
+
|
126
|
+
## [0.3.4] - 2021-10-20
|
127
|
+
|
128
|
+
### Added
|
55
129
|
|
56
|
-
## 0.3.4 - 2021-10-20
|
57
130
|
- Add the "required" attribute in parameters
|
58
131
|
|
59
|
-
## 0.3.3 - 2021-06-02
|
132
|
+
## [0.3.3] - 2021-06-02
|
133
|
+
|
134
|
+
### Fixed
|
135
|
+
|
60
136
|
- Fix correct types on request parameters
|
61
137
|
|
62
|
-
## 0.3.2 - 2021-03-09
|
63
|
-
|
138
|
+
## [0.3.2] - 2021-03-09
|
139
|
+
|
140
|
+
### Changed
|
141
|
+
|
64
142
|
- Render examples results as YAML/JSON objects instead of text blocks.
|
65
143
|
|
66
|
-
|
144
|
+
### Fixed
|
145
|
+
|
146
|
+
- Fix YAML rendering (ruby objects were sometimes rendered in documentation)
|
147
|
+
|
148
|
+
## [0.3.1] - 2020-04-09
|
149
|
+
|
150
|
+
### Added
|
151
|
+
|
67
152
|
- Add support for "test only" examples, allowing to write examples without documentation.
|
68
153
|
|
69
|
-
## 0.3.0 - 2019-12-26
|
154
|
+
## [0.3.0] - 2019-12-26
|
70
155
|
|
71
156
|
### Changed
|
157
|
+
|
72
158
|
- Rails 6 support, deprecated methods from Rails 5 are not supported. Use version `0.2.3`
|
73
159
|
of this gem if your application is still on 5.
|
74
160
|
|
75
161
|
## 0.2.3 - 2019-12-04
|
76
162
|
|
77
|
-
###
|
163
|
+
### Added
|
78
164
|
|
79
165
|
- Generated Swagger file now use the payloads of POST/PATCH/PUT requests.
|
80
|
-
- Minimum Ruby version is now specified: Ruby 2.3.3.
|
81
166
|
|
82
|
-
|
167
|
+
### Changed
|
83
168
|
|
84
|
-
|
85
|
-
|
169
|
+
- Minimum Ruby version is now specified: Ruby 2.3.3.
|
170
|
+
|
171
|
+
## [0.2.2] - 2019-11-03
|
86
172
|
|
87
173
|
### Changed
|
88
174
|
|
89
175
|
- `for_code` method now have its `description` optional. If none is provided,
|
90
176
|
the description will be set from the status code.
|
91
177
|
|
92
|
-
## 0.2.
|
178
|
+
## [0.2.1] - 2019-11-03 [YANKED]
|
179
|
+
|
180
|
+
_Version 0.2.1 was released and yanked by mistake. Version 0.2.2 is the exact
|
181
|
+
same one, with a version bump_
|
182
|
+
|
183
|
+
## [0.2.0] - 2019-11-02
|
93
184
|
|
94
185
|
### Added
|
95
186
|
|
@@ -108,35 +199,35 @@ of the fixtures.
|
|
108
199
|
the existing calls accordingly. To use params defined with `parameters`, use the
|
109
200
|
`defined` option: `request_params defined: :common_form_params`
|
110
201
|
|
111
|
-
## 0.1.5 - 2019-10-31
|
202
|
+
## [0.1.5] - 2019-10-31
|
112
203
|
|
113
204
|
### Fixed
|
114
205
|
|
115
206
|
- Fixed issue with POST/PUT/DELETE requests with no `request_params`
|
116
207
|
- Improved documentation (integration with Devise, typos and mistakes)
|
117
208
|
|
118
|
-
## 0.1.4 - 2019-10-24
|
209
|
+
## [0.1.4] - 2019-10-24
|
119
210
|
|
120
211
|
### Added
|
121
212
|
|
122
213
|
- Added support for arrays of objects in request parameters
|
123
214
|
|
124
|
-
## 0.1.3 - 2019-10-23
|
215
|
+
## [0.1.3] - 2019-10-23
|
125
216
|
|
126
217
|
### Added
|
127
218
|
|
128
219
|
- Added ability to document API descriptions, servers, etc... from the RSpec helper files
|
129
220
|
|
130
|
-
## 0.1.2 - 2019-10-22
|
221
|
+
## [0.1.2] - 2019-10-22
|
131
222
|
|
132
223
|
### Added
|
133
224
|
|
134
225
|
- Added `item` property for arrays descriptions
|
135
226
|
|
136
|
-
## 0.1.1 - 2019-10-22
|
227
|
+
## [0.1.1] - 2019-10-22
|
137
228
|
|
138
229
|
- Added support for custom headers in request examples (useful for `visit` method)
|
139
230
|
|
140
|
-
## 0.1.0 - 2019-10-21
|
231
|
+
## [0.1.0] - 2019-10-21
|
141
232
|
|
142
233
|
Initial release
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -26,53 +26,114 @@ bundle
|
|
26
26
|
|
27
27
|
Configuration should be made manually for now:
|
28
28
|
|
29
|
-
**spec/
|
29
|
+
**spec/support/rspec_rails_api.rb**:
|
30
30
|
|
31
31
|
```ruby
|
32
|
-
require 'rails_helper'
|
33
32
|
require 'rspec_rails_api'
|
34
33
|
|
34
|
+
# Associate spec/acceptance/* to acceptance tests
|
35
|
+
RSpec::Rails::DIRECTORY_MAPPINGS[:acceptance] = %w[spec acceptance]
|
36
|
+
|
35
37
|
RSpec.configure do |config|
|
36
38
|
config.include RSpec::Rails::Api::DSL::Example
|
37
|
-
|
38
|
-
|
39
|
-
renderer = RSpec::Rails::Api::OpenApiRenderer.new
|
40
|
-
# Options here should be customized
|
41
|
-
renderer.api_title = 'YourProject API'
|
42
|
-
renderer.api_version = '1'
|
43
|
-
renderer.api_description = 'Manage data on YourProject'
|
44
|
-
# Options below are optional
|
45
|
-
renderer.api_servers = [{ url: 'https://example.com' }]
|
46
|
-
renderer.api_tos = 'http://example.com/tos.html'
|
47
|
-
renderer.api_contact = { name: 'Admin', email: 'admin@example.com', url: 'http://example.com/contact' }
|
48
|
-
renderer.api_license = { name: 'Apache', url: 'https://opensource.org/licenses/Apache-2.0' }
|
39
|
+
config.include RSpec::Rails::RequestExampleGroup, type: :acceptance
|
49
40
|
|
50
|
-
|
51
|
-
renderer
|
52
|
-
|
41
|
+
# Define the renderer if you want to generate the OpenApi documentation
|
42
|
+
renderer = RSpec::Rails::Api::OpenApiRenderer.new
|
43
|
+
# Options here should be customized
|
44
|
+
renderer.api_title = 'YourProject API'
|
45
|
+
renderer.api_version = '1'
|
46
|
+
renderer.api_description = 'Manage data on YourProject'
|
47
|
+
# Options below are optional
|
48
|
+
renderer.api_servers = [{ url: 'https://example.com' }]
|
49
|
+
renderer.api_tos = 'http://example.com/tos.html'
|
50
|
+
renderer.api_contact = { name: 'Admin', email: 'admin@example.com', url: 'http://example.com/contact' }
|
51
|
+
renderer.api_license = { name: 'Apache', url: 'https://opensource.org/licenses/Apache-2.0' }
|
52
|
+
# ... Check the "Configuration" section for all the options
|
53
|
+
|
54
|
+
config.after(:context, type: :acceptance) do |context|
|
55
|
+
renderer.merge_context context.class.metadata[:rra].to_h
|
56
|
+
end
|
53
57
|
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
config.after(:suite) do
|
59
|
+
# Default path is 'tmp/rspec_rails_api_output.json/yaml'
|
60
|
+
renderer.write_files Rails.root.join('public', 'swagger_doc'), only: [:json]
|
61
|
+
end
|
57
62
|
end
|
58
63
|
```
|
59
64
|
|
60
|
-
**spec/rails_helper.rb
|
65
|
+
**spec/rails_helper.rb**:
|
61
66
|
|
62
67
|
```ruby
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
RSpec.configure do |config|
|
68
|
-
# ...
|
69
|
-
config.include RSpec::Rails::RequestExampleGroup, type: :acceptance
|
70
|
-
end
|
68
|
+
#...
|
69
|
+
require 'support/rspec_rails_api'
|
70
|
+
#...
|
71
71
|
```
|
72
72
|
|
73
73
|
## Configuration
|
74
74
|
|
75
|
-
**TODO: This section is incomplete and the gem has no generator yet
|
75
|
+
**TODO: This section is incomplete and the gem has no generator yet.**
|
76
|
+
|
77
|
+
|
78
|
+
```rb
|
79
|
+
# Server URL for quick reference
|
80
|
+
server_url = 'https://example.com'
|
81
|
+
|
82
|
+
# Options here should be present for a valid OpenAPI file
|
83
|
+
renderer.api_title = 'MyProject API'
|
84
|
+
renderer.api_version = '1'
|
85
|
+
|
86
|
+
# Options below are optional
|
87
|
+
#
|
88
|
+
# API description. Markdown supported
|
89
|
+
# renderer.api_description = 'Manage data on MyProject'
|
90
|
+
#
|
91
|
+
# List of servers, to live-test the documentation
|
92
|
+
# renderer.api_servers = [{ url: server_url }, { url: 'http://localhost:3000' }]
|
93
|
+
#
|
94
|
+
# Link to the API terms of service, if any
|
95
|
+
# renderer.api_tos = 'http://example.com/tos.html'
|
96
|
+
#
|
97
|
+
# Contact information
|
98
|
+
# renderer.api_contact = { name: 'Admin', email: 'admin@example.com', url: 'http://example.com/contact' }
|
99
|
+
#
|
100
|
+
# API license information
|
101
|
+
# renderer.api_license = { name: 'Apache', url: 'https://opensource.org/licenses/Apache-2.0' }
|
102
|
+
#
|
103
|
+
# Possible security schemes
|
104
|
+
# renderer.add_security_scheme :pkce_code_grant, 'PKCE code grant',
|
105
|
+
# type: 'oauth2',
|
106
|
+
# flows: {
|
107
|
+
# implicit: {
|
108
|
+
# authorizationUrl: "#{server_url}/oauth/authorize",
|
109
|
+
# scopes: { read: 'will read data on your behalf', write: 'will write data on your behalf' }
|
110
|
+
# }
|
111
|
+
# }
|
112
|
+
# renderer.add_security_scheme :bearer, 'Bearer token',
|
113
|
+
# type: 'http',
|
114
|
+
# scheme: 'bearer'
|
115
|
+
#
|
116
|
+
# Declare keys whose values should be filtered in responses.
|
117
|
+
# renderer.redact_responses entity_name: { key: 'REDACTED' },
|
118
|
+
# other_entity: { other_key: ['REDACTED'] }
|
119
|
+
|
120
|
+
|
121
|
+
# We need to merge each context metadata so we can reference to them to build the final file
|
122
|
+
RSpec.configuration.after(:context, type: :acceptance) do |context|
|
123
|
+
renderer.merge_context context.class.metadata[:rra].to_h
|
124
|
+
# During development of rspec_rails_api, you may want to dump raw metadata to a file
|
125
|
+
renderer.merge_context context.class.metadata[:rra].to_h, dump_metadata: true
|
126
|
+
end
|
127
|
+
|
128
|
+
# Skip this block if you don't need the OpenAPI documentation file and only have your responses tested
|
129
|
+
RSpec.configuration.after(:suite) do
|
130
|
+
renderer.write_files Rails.root.join('public/swagger') # Write both YAML and prettified JSON files
|
131
|
+
# or
|
132
|
+
renderer.write_files Rails.root.join('public/swagger'), only: [:json] # Prettified JSON only
|
133
|
+
# or
|
134
|
+
renderer.write_files Rails.root.join('public/swagger'), only: [:yaml] # YAML only
|
135
|
+
end
|
136
|
+
```
|
76
137
|
|
77
138
|
### Integration with Devise
|
78
139
|
|
@@ -124,7 +185,7 @@ end
|
|
124
185
|
# In examples
|
125
186
|
#...
|
126
187
|
for_code 200, 'Success' do |url|
|
127
|
-
|
188
|
+
sign_in #...
|
128
189
|
test_response_of url
|
129
190
|
|
130
191
|
#...
|
@@ -135,10 +196,6 @@ end
|
|
135
196
|
This solution comes from [this article](https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs)
|
136
197
|
by Arne Hartherz (MIT license).
|
137
198
|
|
138
|
-
## Usage
|
139
|
-
|
140
|
-
Write some spec files and run RSpec as you would usually do.
|
141
|
-
|
142
199
|
## Writing specs
|
143
200
|
|
144
201
|
There is a [commented example](dummy/spec/acceptance/posts_spec.rb) available in
|
@@ -149,7 +206,7 @@ The idea is to have a simple DSL, and declare things like:
|
|
149
206
|
**spec/acceptance/users_spec.rb**
|
150
207
|
|
151
208
|
```ruby
|
152
|
-
require '
|
209
|
+
require 'rails_helper'
|
153
210
|
|
154
211
|
RSpec.describe 'Users', type: :acceptance do
|
155
212
|
resource 'Users', 'Manage users'
|
@@ -186,6 +243,80 @@ RSpec.describe 'Users', type: :acceptance do
|
|
186
243
|
end
|
187
244
|
```
|
188
245
|
|
246
|
+
### Entity declarations
|
247
|
+
|
248
|
+
You can declare entities locally (in every spec files), but sometimes you will need to use/reference the same entity
|
249
|
+
in multiple spec files (e.g.: an error message). In that case, you can create _global_ entities in separate files, and they
|
250
|
+
will be picked-up when needed.
|
251
|
+
|
252
|
+
Example of a local entity:
|
253
|
+
|
254
|
+
```rb
|
255
|
+
# spec/acceptance/api/users_acceptance_spec.rb
|
256
|
+
require 'rails_helper'
|
257
|
+
|
258
|
+
RSpec.describe 'Users', type: :acceptance do
|
259
|
+
resource 'Users', 'Manage users'
|
260
|
+
|
261
|
+
# This is a local entity
|
262
|
+
entity :user,
|
263
|
+
id: { type: :integer, description: 'The id' },
|
264
|
+
email: { type: :string, description: 'The name' },
|
265
|
+
role: { type: :string, description: 'The name' },
|
266
|
+
created_at: { type: :datetime, description: 'Creation date' },
|
267
|
+
updated_at: { type: :datetime, description: 'Modification date' },
|
268
|
+
url: { type: :string, description: 'URL to this category' }
|
269
|
+
|
270
|
+
on_get '/api/users/', 'Users list' do
|
271
|
+
for_code 200, 'Success response', expect_many: :user do |url|
|
272
|
+
test_response_of url
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#...
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
Defining global entities:
|
281
|
+
|
282
|
+
```rb
|
283
|
+
# spec/support/entities/user.rb
|
284
|
+
# This file should be required at some point in the "rails_helper" or "acceptance_helper"
|
285
|
+
|
286
|
+
require 'rspec/rails/api/metadata'
|
287
|
+
|
288
|
+
RSpec::Rails::Api::Metadata.add_entity :user,
|
289
|
+
id: { type: :integer, description: 'The id' },
|
290
|
+
email: { type: :string, description: 'The name' },
|
291
|
+
role: { type: :string, description: 'The name' },
|
292
|
+
created_at: { type: :datetime, description: 'Creation date' },
|
293
|
+
updated_at: { type: :datetime, description: 'Modification date' },
|
294
|
+
url: { type: :string, description: 'URL to this category' }
|
295
|
+
|
296
|
+
```
|
297
|
+
|
298
|
+
Organization of the global entities declaration is up to you.
|
299
|
+
|
300
|
+
For small projects, we usually put them all in one file:
|
301
|
+
|
302
|
+
```rb
|
303
|
+
# spec/support/acceptance_entities.rb
|
304
|
+
|
305
|
+
require 'rspec/rails/api/metadata'
|
306
|
+
|
307
|
+
# This file contains common object definitions
|
308
|
+
{
|
309
|
+
error: {
|
310
|
+
error: { type: :string, description: "Error message" }
|
311
|
+
},
|
312
|
+
form_error: {
|
313
|
+
title: { type: :array, required: false, description: "Title errors", of: :string }
|
314
|
+
},
|
315
|
+
}.each do |name, attributes|
|
316
|
+
RSpec::Rails::Api::Metadata.add_entity name, attributes
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
189
320
|
### DSL
|
190
321
|
|
191
322
|
#### Example groups
|
@@ -197,6 +328,23 @@ Starts a resource description.
|
|
197
328
|
- It must be called before any other documentation calls.
|
198
329
|
- It should be in the first `describe block`
|
199
330
|
|
331
|
+
A resource may be completed across multiple spec files:
|
332
|
+
|
333
|
+
```rb
|
334
|
+
# an_acceptance_spec.rb
|
335
|
+
RSpec.describe 'Something', type: :acceptance do
|
336
|
+
resource 'User', 'Manage users'
|
337
|
+
end
|
338
|
+
|
339
|
+
# another_acceptance_spec.rb
|
340
|
+
RSpec.describe 'Something else', type: :acceptance do
|
341
|
+
resource 'User', 'Another description'
|
342
|
+
end
|
343
|
+
|
344
|
+
```
|
345
|
+
|
346
|
+
The first evaluated `resource` statement will be used as description; all the tests in both files will complete it.
|
347
|
+
|
200
348
|
##### `entity(type, fields)`
|
201
349
|
|
202
350
|
Describes an entity for the documentation. The type is only a reference,
|
@@ -423,11 +571,30 @@ Once again, you have to pass an argument to the block if you use
|
|
423
571
|
# ...
|
424
572
|
```
|
425
573
|
|
574
|
+
##### `requires_security(scheme_references)`
|
575
|
+
|
576
|
+
Specifies the valid security schemes to use for this request. Security schemes are declared at the renderer level
|
577
|
+
(see [the configuration example](#Configuration)).
|
578
|
+
|
579
|
+
```rb
|
580
|
+
# Given a previously :basic scheme
|
581
|
+
|
582
|
+
# ...
|
583
|
+
on_get '/some/path' do
|
584
|
+
require_security :basic, :implicit
|
585
|
+
|
586
|
+
for_code 200 do |url|
|
587
|
+
#...
|
588
|
+
end
|
589
|
+
end
|
590
|
+
# ...
|
591
|
+
```
|
592
|
+
|
426
593
|
#### Examples
|
427
594
|
|
428
595
|
Example methods are available in `for_code` blocks
|
429
596
|
|
430
|
-
##### `test_response_of(example, path_params: {}, payload: {}, headers: {})`
|
597
|
+
##### `test_response_of(example, path_params: {}, payload: {}, headers: {}, ignore_content_type: false)`
|
431
598
|
|
432
599
|
Visits the described URL and:
|
433
600
|
|
@@ -441,6 +608,7 @@ Visits the described URL and:
|
|
441
608
|
- `payload`: a hash of values to send. Ignored for GET and DELETE
|
442
609
|
requests
|
443
610
|
- `headers`: a hash of custom headers.
|
611
|
+
- `ignore_content_type`: whether to ignore response's content-type. By default, checks for a JSON response
|
444
612
|
|
445
613
|
```ruby
|
446
614
|
for_code 200, 'Success' do |url|
|
@@ -543,6 +711,11 @@ MRs to improve this are welcome.
|
|
543
711
|
|
544
712
|
There is no support for file fields yet.
|
545
713
|
|
714
|
+
### Security
|
715
|
+
|
716
|
+
`Security` mechanisms are only declared for the OpenApi documentation as a reference, and is
|
717
|
+
not checked nor enforced in the tests.
|
718
|
+
|
546
719
|
## Development
|
547
720
|
|
548
721
|
After checking out the repo, run `bin/setup` to install dependencies.
|
@@ -9,14 +9,15 @@ module RSpec
|
|
9
9
|
##
|
10
10
|
# Visits the current example and tests the response
|
11
11
|
#
|
12
|
-
# @param example
|
13
|
-
# @param path_params
|
14
|
-
# @param payload
|
15
|
-
# @param headers
|
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
|
+
# @param ignore_content_type [Boolean] Whether to ignore the response's content-type for this response only
|
16
17
|
#
|
17
18
|
# @return [void]
|
18
|
-
def test_response_of(example, path_params: {}, payload: {}, headers: {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
19
|
-
raise 'Missing context. Call
|
19
|
+
def test_response_of(example, path_params: {}, payload: {}, headers: {}, ignore_content_type: false, ignore_response: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists, Layout/LineLength
|
20
|
+
raise 'Missing context. Call "test_response_of" within a "for_code" block.' unless example
|
20
21
|
|
21
22
|
status_code = prepare_status_code example.class.description
|
22
23
|
|
@@ -25,10 +26,10 @@ module RSpec
|
|
25
26
|
|
26
27
|
send(request_params[:action],
|
27
28
|
request_params[:url],
|
28
|
-
params: request_params[:params]
|
29
|
+
params: request_params[:params],
|
29
30
|
headers: request_params[:headers])
|
30
31
|
|
31
|
-
check_response(response, status_code)
|
32
|
+
check_response(response, status_code, ignore_content_type: ignore_content_type) unless ignore_response
|
32
33
|
|
33
34
|
return if example.class.description.match?(/-> test (\d+)(.*)/)
|
34
35
|
|
@@ -38,7 +39,10 @@ module RSpec
|
|
38
39
|
private
|
39
40
|
|
40
41
|
##
|
41
|
-
# Searches for a defined entity in metadata
|
42
|
+
# Searches for a defined entity in example metadata or global entities
|
43
|
+
#
|
44
|
+
# If an entity needs expansion (e.g.: with an attribute like "type: :array, of: :something"), it will use the
|
45
|
+
# scope where the entity was found: global entities or example metadata.
|
42
46
|
#
|
43
47
|
# @param entity [Symbol] Entity reference
|
44
48
|
#
|
@@ -49,10 +53,10 @@ module RSpec
|
|
49
53
|
current_resource = rra_metadata.current_resource
|
50
54
|
raise '@current_resource is unset' unless current_resource
|
51
55
|
|
52
|
-
|
53
|
-
raise "
|
56
|
+
definition = RSpec::Rails::Api::Metadata.entities[entity.to_sym]
|
57
|
+
raise "Entity '#{entity}' was never defined (globally or in '#{current_resource}')" unless definition
|
54
58
|
|
55
|
-
|
59
|
+
definition.expand_with(RSpec::Rails::Api::Metadata.entities)
|
56
60
|
end
|
57
61
|
|
58
62
|
##
|
@@ -60,9 +64,21 @@ module RSpec
|
|
60
64
|
#
|
61
65
|
# @param response [ActionDispatch::TestResponse] The response
|
62
66
|
# @param expected_code [Number] Code to test for
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
# @param ignore_content_type [Boolean] Whether to ignore the response's content-type for
|
68
|
+
# this response only
|
69
|
+
def check_response(response, expected_code, ignore_content_type: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
70
|
+
code_error_message = if response.status != expected_code && response.status == 422
|
71
|
+
<<~TXT
|
72
|
+
expected: #{expected_code}
|
73
|
+
got: #{response.status}
|
74
|
+
response: #{response.body}
|
75
|
+
TXT
|
76
|
+
end
|
77
|
+
|
78
|
+
expect(response.status).to eq(expected_code), code_error_message
|
79
|
+
if expected_code != 204 && !ignore_content_type
|
80
|
+
expect(response.headers['Content-Type'].downcase).to eq 'application/json; charset=utf-8'
|
81
|
+
end
|
66
82
|
expectations = rra_current_example[:expectations]
|
67
83
|
expect(response).to have_many defined(expectations[:many]) if expectations[:many]
|
68
84
|
expect(response).to have_one defined(expectations[:one]) if expectations[:one]
|
@@ -98,9 +114,12 @@ module RSpec
|
|
98
114
|
# @return [Hash] Options for the request
|
99
115
|
def prepare_request_params(description, request_params = {}, payload = {}, request_headers = {})
|
100
116
|
example_params = description.split
|
117
|
+
verb = example_params[0].downcase
|
118
|
+
|
119
|
+
payload = payload.to_json if verb != 'get'
|
101
120
|
|
102
121
|
{
|
103
|
-
action:
|
122
|
+
action: verb,
|
104
123
|
url: prepare_request_url(example_params[1], request_params),
|
105
124
|
example_url: example_params[1],
|
106
125
|
params: payload,
|
@@ -28,7 +28,7 @@ module RSpec
|
|
28
28
|
#
|
29
29
|
# @return [void]
|
30
30
|
def entity(type, fields)
|
31
|
-
|
31
|
+
RSpec::Rails::Api::Metadata.add_entity type, fields
|
32
32
|
end
|
33
33
|
|
34
34
|
##
|
@@ -78,6 +78,16 @@ module RSpec
|
|
78
78
|
metadata[:rra].add_request_params attributes
|
79
79
|
end
|
80
80
|
|
81
|
+
##
|
82
|
+
# Declares security schemes valid for this path. It won't be enforced during testing but will complete the
|
83
|
+
# documentation. When the reference does not exist, an exception will be thrown _during_ render, not before.
|
84
|
+
#
|
85
|
+
# @param scheme_references [Array<Symbol>] References to a security scheme defined with the renderer's
|
86
|
+
# `add_security_scheme`.
|
87
|
+
def requires_security(*scheme_references)
|
88
|
+
metadata[:rra].add_security_references(*scheme_references)
|
89
|
+
end
|
90
|
+
|
81
91
|
##
|
82
92
|
# Defines a GET action
|
83
93
|
#
|
@@ -11,10 +11,13 @@ module RSpec
|
|
11
11
|
class FieldConfig
|
12
12
|
attr_accessor :required, :type, :attributes, :description
|
13
13
|
|
14
|
-
def initialize(type:, description:, required: true, attributes: nil, of: nil)
|
14
|
+
def initialize(type:, description:, required: true, attributes: nil, of: nil) # rubocop:disable Metrics/CyclomaticComplexity
|
15
15
|
@required = required
|
16
16
|
@description = description
|
17
|
+
|
17
18
|
raise "Field type not allowed: '#{type}'" unless Validator.valid_type?(type)
|
19
|
+
raise "Don't use 'of' on non-arrays" if of && type != :array
|
20
|
+
raise "Don't use 'attributes' on non-objects" if attributes && type != :object
|
18
21
|
|
19
22
|
define_attributes attributes if type == :object
|
20
23
|
define_attributes of if type == :array
|
@@ -50,7 +53,7 @@ module RSpec
|
|
50
53
|
def define_attributes(attributes)
|
51
54
|
@attributes = case attributes
|
52
55
|
when Hash
|
53
|
-
|
56
|
+
EntityConfig.new attributes
|
54
57
|
when Symbol
|
55
58
|
attributes
|
56
59
|
end
|
@@ -10,11 +10,10 @@ module RSpec
|
|
10
10
|
module Api
|
11
11
|
# Handles contexts and examples metadata.
|
12
12
|
class Metadata # rubocop:disable Metrics/ClassLength
|
13
|
-
attr_reader :
|
13
|
+
attr_reader :resources, :parameters, :current_resource, :current_url, :current_method, :current_code
|
14
14
|
|
15
15
|
def initialize
|
16
16
|
@resources = {}
|
17
|
-
@entities = {}
|
18
17
|
@parameters = {}
|
19
18
|
# Only used when building metadata during RSpec boot
|
20
19
|
@current_resource = nil
|
@@ -23,6 +22,33 @@ module RSpec
|
|
23
22
|
@current_code = nil
|
24
23
|
end
|
25
24
|
|
25
|
+
class << self
|
26
|
+
##
|
27
|
+
# Define an entity globally.
|
28
|
+
#
|
29
|
+
# Global entities will be available within the specs, but if they are re-declared locally, the local variant
|
30
|
+
# will be used.
|
31
|
+
#
|
32
|
+
# @param name [Symbol] Entity name
|
33
|
+
# @param fields [Hash] Fields definitions
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def add_entity(name, fields)
|
37
|
+
@entities ||= {}
|
38
|
+
raise "#{name} is already declared" if @entities.key? name
|
39
|
+
|
40
|
+
@entities[name] = EntityConfig.new fields
|
41
|
+
end
|
42
|
+
|
43
|
+
def entities
|
44
|
+
@entities || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset
|
48
|
+
@entities = {}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
26
52
|
##
|
27
53
|
# Adds a resource to metadata
|
28
54
|
#
|
@@ -31,24 +57,10 @@ module RSpec
|
|
31
57
|
#
|
32
58
|
# @return [void]
|
33
59
|
def add_resource(name, description)
|
34
|
-
@resources[name.to_sym]
|
60
|
+
@resources[name.to_sym] ||= { description: description, paths: {} }
|
35
61
|
@current_resource = name.to_sym
|
36
62
|
end
|
37
63
|
|
38
|
-
##
|
39
|
-
# Adds an entity definition
|
40
|
-
#
|
41
|
-
# @param name [Symbol] Entity name
|
42
|
-
# @param fields [Hash] Fields definitions
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# @return [void]
|
46
|
-
def add_entity(name, fields)
|
47
|
-
Utils.deep_set(@resources,
|
48
|
-
[@current_resource, 'entities', name],
|
49
|
-
EntityConfig.new(fields))
|
50
|
-
end
|
51
|
-
|
52
64
|
##
|
53
65
|
# Adds a parameter definition
|
54
66
|
#
|
@@ -61,6 +73,7 @@ module RSpec
|
|
61
73
|
|
62
74
|
fields.each_value do |field|
|
63
75
|
field[:required] = true unless field[:required] == false
|
76
|
+
field[:schema] = { type: field[:of] } if field[:type] == :array && PRIMITIVES.include?(field[:of])
|
64
77
|
end
|
65
78
|
@parameters[name] = fields
|
66
79
|
end
|
@@ -114,6 +127,20 @@ module RSpec
|
|
114
127
|
params)
|
115
128
|
end
|
116
129
|
|
130
|
+
# Associate a defined security scheme to this request
|
131
|
+
#
|
132
|
+
# @param references [Array<Symbol>] Security scheme reference
|
133
|
+
def add_security_references(*references)
|
134
|
+
check_current_context :resource, :url, :method
|
135
|
+
|
136
|
+
refs = @resources.dig @current_resource, 'paths', @current_url, 'actions', @current_method, 'security'
|
137
|
+
refs ||= []
|
138
|
+
refs += references
|
139
|
+
Utils.deep_set(@resources,
|
140
|
+
[@current_resource, 'paths', @current_url, 'actions', @current_method, 'security'],
|
141
|
+
refs)
|
142
|
+
end
|
143
|
+
|
117
144
|
##
|
118
145
|
# Adds an action and sets `@current_url` and `@current_method`
|
119
146
|
#
|
@@ -294,7 +321,7 @@ module RSpec
|
|
294
321
|
# @param field [Hash] Parameter definition
|
295
322
|
#
|
296
323
|
# @return [Hash] Completed parameter
|
297
|
-
def fill_request_param(field)
|
324
|
+
def fill_request_param(field) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
298
325
|
if field[:type] == :object && field[:attributes]
|
299
326
|
organize_params field[:attributes]
|
300
327
|
else
|
@@ -302,6 +329,7 @@ module RSpec
|
|
302
329
|
type: PARAM_TYPES[field[:type]][:type],
|
303
330
|
description: field[:description] || nil,
|
304
331
|
}
|
332
|
+
properties[:format] = PARAM_TYPES[field[:type]][:format] if PARAM_TYPES[field[:type]][:format]
|
305
333
|
|
306
334
|
properties[:items] = organize_params field[:of] if field[:type] == :array && field[:of]
|
307
335
|
properties
|
@@ -15,16 +15,36 @@ module RSpec
|
|
15
15
|
# ```
|
16
16
|
class OpenApiRenderer # rubocop:disable Metrics/ClassLength
|
17
17
|
attr_writer :api_servers, :api_title, :api_version, :api_description, :api_tos
|
18
|
+
attr_reader :redactables
|
18
19
|
|
19
20
|
def initialize
|
20
|
-
@metadata
|
21
|
-
@api_infos
|
22
|
-
@api_servers
|
23
|
-
@api_paths
|
21
|
+
@metadata = { resources: {}, entities: {} }
|
22
|
+
@api_infos = {}
|
23
|
+
@api_servers = []
|
24
|
+
@api_paths = {}
|
24
25
|
@api_components = {}
|
25
26
|
@api_tags = []
|
26
27
|
@api_contact = {}
|
27
28
|
@api_license = {}
|
29
|
+
@api_security = {}
|
30
|
+
@redactables = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def redact_responses(pairs)
|
34
|
+
@redactables = pairs
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Adds a security scheme definition to the API documentation
|
39
|
+
#
|
40
|
+
# @param reference [Symbol] Reference to use in the tests
|
41
|
+
# @param name [String] Human friendly name
|
42
|
+
# @param definition [Hash] Security scheme definition as per https://swagger.io/specification/#security-scheme-object
|
43
|
+
def add_security_scheme(reference, name, definition)
|
44
|
+
raise "Security scheme #{reference} is already defined" if @api_security.key? reference
|
45
|
+
|
46
|
+
definition[:name] = name unless reference == :basic
|
47
|
+
@api_security[reference] = definition
|
28
48
|
end
|
29
49
|
|
30
50
|
##
|
@@ -36,7 +56,6 @@ module RSpec
|
|
36
56
|
# @return [void
|
37
57
|
def merge_context(context, dump_metadata: false)
|
38
58
|
@metadata[:resources].deep_merge! context.respond_to?(:resources) ? context.resources : context[:resources]
|
39
|
-
@metadata[:entities].deep_merge! context.respond_to?(:entities) ? context.entities : context[:entities]
|
40
59
|
|
41
60
|
# Save context for debug and fixtures
|
42
61
|
File.write ::Rails.root.join('tmp', 'rra_metadata.yaml'), @metadata.to_yaml if dump_metadata
|
@@ -54,11 +73,16 @@ module RSpec
|
|
54
73
|
|
55
74
|
path ||= ::Rails.root.join('tmp', 'rspec_api_rails')
|
56
75
|
|
76
|
+
metadata = prepare_metadata
|
77
|
+
|
57
78
|
file_types = %i[yaml json]
|
58
79
|
only.each do |type|
|
59
80
|
next unless file_types.include? type
|
60
81
|
|
61
|
-
|
82
|
+
data = metadata.to_yaml if type == :yaml
|
83
|
+
data = JSON.pretty_generate(metadata) if type == :json
|
84
|
+
|
85
|
+
File.write "#{path}.#{type}", data
|
62
86
|
end
|
63
87
|
end
|
64
88
|
|
@@ -77,7 +101,7 @@ module RSpec
|
|
77
101
|
components: @api_components,
|
78
102
|
tags: @api_tags,
|
79
103
|
}
|
80
|
-
JSON.parse(
|
104
|
+
JSON.parse(hash.to_json)
|
81
105
|
end
|
82
106
|
|
83
107
|
##
|
@@ -125,24 +149,33 @@ module RSpec
|
|
125
149
|
#
|
126
150
|
# @return [void]
|
127
151
|
def extract_metadata
|
128
|
-
|
152
|
+
extract_security
|
129
153
|
api_infos
|
130
154
|
api_servers
|
155
|
+
global_entities
|
156
|
+
extract_from_resources
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Extracts metadata from security schemes for rendering
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
def extract_security
|
164
|
+
return unless @api_security.keys.count.positive?
|
165
|
+
|
166
|
+
@api_components['securitySchemes'] = @api_security
|
131
167
|
end
|
132
168
|
|
133
169
|
##
|
134
170
|
# Extracts metadata from resources for rendering
|
135
171
|
#
|
136
172
|
# @return [void]
|
137
|
-
def extract_from_resources
|
173
|
+
def extract_from_resources
|
138
174
|
@api_components[:schemas] ||= {}
|
139
175
|
@metadata[:resources].each do |resource_key, resource|
|
140
|
-
resource[:entities].each do |name, entity|
|
141
|
-
@api_components[:schemas][name] = process_entity(entity)
|
142
|
-
end
|
143
176
|
@api_tags.push(
|
144
177
|
name: resource_key.to_s,
|
145
|
-
description: resource[:description]
|
178
|
+
description: resource[:description].presence&.strip || ''
|
146
179
|
)
|
147
180
|
process_resource resource: resource_key, resource_config: resource
|
148
181
|
end
|
@@ -190,22 +223,31 @@ module RSpec
|
|
190
223
|
parameters
|
191
224
|
end
|
192
225
|
|
193
|
-
def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
226
|
+
def process_entity(entity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
194
227
|
schema = {
|
195
228
|
properties: {},
|
196
229
|
}
|
197
230
|
required = []
|
198
231
|
entity.fields.each do |name, field|
|
199
232
|
property = {
|
200
|
-
description: field.description,
|
233
|
+
description: field.description.presence&.strip || '',
|
201
234
|
type: PARAM_TYPES[field.type][:type],
|
202
235
|
}
|
203
|
-
property[:format]
|
204
|
-
|
205
|
-
|
206
|
-
|
236
|
+
property[:format] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
|
237
|
+
|
238
|
+
if PRIMITIVES.include? field.attributes
|
239
|
+
property[:items] = { type: field.attributes }
|
240
|
+
elsif field.type == :object && field.attributes.is_a?(Symbol)
|
241
|
+
property = { '$ref' => "#/components/schemas/#{field.attributes}" }
|
242
|
+
elsif field.type == :array && field.attributes.is_a?(Symbol)
|
243
|
+
property = { type: :array, items: { '$ref' => "#/components/schemas/#{field.attributes}" } }
|
244
|
+
elsif field.type == :array
|
245
|
+
property = { type: :array, items: process_entity(field.attributes) }
|
246
|
+
end
|
207
247
|
|
208
248
|
required.push name unless field.required == false
|
249
|
+
|
250
|
+
schema[:properties][name] = property
|
209
251
|
end
|
210
252
|
|
211
253
|
schema[:required] = required unless required.size.zero?
|
@@ -221,10 +263,10 @@ module RSpec
|
|
221
263
|
#
|
222
264
|
#
|
223
265
|
# @return [void]
|
224
|
-
def process_path_param(name, param) # rubocop:disable Metrics/MethodLength
|
266
|
+
def process_path_param(name, param) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
225
267
|
parameter = {
|
226
268
|
name: name.to_s,
|
227
|
-
description: param[:description],
|
269
|
+
description: param[:description].presence&.strip || '',
|
228
270
|
required: param[:required] || true,
|
229
271
|
in: param[:scope].to_s,
|
230
272
|
schema: {
|
@@ -250,7 +292,7 @@ module RSpec
|
|
250
292
|
#
|
251
293
|
# FIXME: Rename "action_config" to "action"
|
252
294
|
# FIXME: Rename "parameters" to "path_parameters"
|
253
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
295
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
254
296
|
def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil)
|
255
297
|
responses = {}
|
256
298
|
request_body = nil
|
@@ -268,21 +310,31 @@ module RSpec
|
|
268
310
|
responses[status_key] = process_response status: status_key, status_config: status, content: content
|
269
311
|
end
|
270
312
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
operationId: "#{resource} #{summary}".downcase.gsub(/[^\w]/, '_'),
|
313
|
+
action = {
|
314
|
+
summary: path_config[:actions][action_config][:summary]&.strip || '',
|
315
|
+
description: path_config[:actions][action_config][:description].presence&.strip || '',
|
316
|
+
operationId: "#{resource} #{action_config} #{path}".downcase.gsub(/[^\w]/, '_'),
|
276
317
|
parameters: parameters,
|
277
318
|
responses: responses,
|
278
319
|
tags: [resource.to_s],
|
279
320
|
}
|
280
321
|
|
322
|
+
if path_config[:actions][action_config].key? :security
|
323
|
+
references = path_config[:actions][action_config][:security]
|
324
|
+
|
325
|
+
action[:security] = []
|
326
|
+
references.each do |reference|
|
327
|
+
raise "No security scheme defined with reference #{reference}" unless @api_security.key? reference
|
328
|
+
|
329
|
+
action[:security].push({ reference => [] })
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
281
333
|
action[:requestBody] = request_body if request_body
|
282
334
|
|
283
335
|
action
|
284
336
|
end
|
285
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
337
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
286
338
|
|
287
339
|
##
|
288
340
|
# Processes a request body from metadata
|
@@ -294,11 +346,12 @@ module RSpec
|
|
294
346
|
# @return [void]
|
295
347
|
def process_request_body(schema: nil, ref: nil, examples: {})
|
296
348
|
Utils.deep_set @api_components, ['schemas', ref], schema
|
349
|
+
|
297
350
|
{
|
298
351
|
# description: '',
|
299
352
|
required: true,
|
300
353
|
content: {
|
301
|
-
|
354
|
+
content_type_from_schema(schema) => {
|
302
355
|
schema: { '$ref' => "#/components/schemas/#{ref}" },
|
303
356
|
examples: examples,
|
304
357
|
},
|
@@ -306,6 +359,23 @@ module RSpec
|
|
306
359
|
}
|
307
360
|
end
|
308
361
|
|
362
|
+
def content_type_from_schema(schema)
|
363
|
+
schema_includes_file?(schema) ? 'multipart/form-data' : 'application/json'
|
364
|
+
end
|
365
|
+
|
366
|
+
def schema_includes_file?(schema)
|
367
|
+
return true if schema[:type] == 'string' && schema[:format] == 'binary'
|
368
|
+
return false unless schema[:properties].is_a?(Hash) && schema[:required].is_a?(Array)
|
369
|
+
|
370
|
+
schema[:properties].each_value do |definition|
|
371
|
+
next unless schema_includes_file?(definition)
|
372
|
+
|
373
|
+
return true
|
374
|
+
end
|
375
|
+
|
376
|
+
false
|
377
|
+
end
|
378
|
+
|
309
379
|
##
|
310
380
|
# Process a response from metadata
|
311
381
|
#
|
@@ -314,22 +384,45 @@ module RSpec
|
|
314
384
|
# @param content [String] Response content
|
315
385
|
#
|
316
386
|
# @return [void]
|
317
|
-
def process_response(status: nil, status_config: nil, content: nil)
|
318
|
-
response = { description: status_config[:description] }
|
387
|
+
def process_response(status: nil, status_config: nil, content: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
388
|
+
response = { description: status_config[:description].presence&.strip || '' }
|
319
389
|
|
320
390
|
return response if status.to_s == '204' && content # No content
|
321
391
|
|
392
|
+
data = begin
|
393
|
+
JSON.parse(content)
|
394
|
+
rescue JSON::ParserError, TypeError
|
395
|
+
content
|
396
|
+
end
|
397
|
+
|
398
|
+
entity = status_config[:expectations][:one] || status_config[:expectations][:many]
|
399
|
+
|
400
|
+
# TODO: handle sub-entities
|
401
|
+
if @redactables.key?(entity) && data.is_a?(Hash)
|
402
|
+
if status_config[:expectations][:one]
|
403
|
+
@redactables[entity].each_pair do |attribute, replacement|
|
404
|
+
data[attribute.to_s] = replacement
|
405
|
+
end
|
406
|
+
else
|
407
|
+
data.each_index do |index|
|
408
|
+
@redactables[entity].each_pair do |attribute, replacement|
|
409
|
+
data[index][attribute.to_s] = replacement
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
322
415
|
response[:content] = {
|
323
416
|
'application/json': {
|
324
417
|
schema: response_schema(status_config[:expectations]),
|
325
|
-
examples: { default: { value:
|
418
|
+
examples: { default: { value: data } },
|
326
419
|
},
|
327
420
|
}
|
328
421
|
|
329
422
|
response
|
330
423
|
end
|
331
424
|
|
332
|
-
def response_schema(expectations)
|
425
|
+
def response_schema(expectations) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
333
426
|
if expectations[:many]
|
334
427
|
items = if PRIMITIVES.include?(expectations[:many])
|
335
428
|
{ type: expectations[:many] }
|
@@ -338,7 +431,15 @@ module RSpec
|
|
338
431
|
end
|
339
432
|
{ type: 'array', items: items }
|
340
433
|
elsif expectations[:one]
|
341
|
-
|
434
|
+
# Unspecified arrays
|
435
|
+
if expectations[:one] == :array
|
436
|
+
{ type: :array, items: {} }
|
437
|
+
# Array of specified type
|
438
|
+
elsif PRIMITIVES.include?(expectations[:one])
|
439
|
+
{ type: expectations[:one] }
|
440
|
+
else
|
441
|
+
{ '$ref' => "#/components/schemas/#{expectations[:one]}" }
|
442
|
+
end
|
342
443
|
end
|
343
444
|
end
|
344
445
|
|
@@ -361,6 +462,16 @@ module RSpec
|
|
361
462
|
request_examples
|
362
463
|
end
|
363
464
|
|
465
|
+
def global_entities
|
466
|
+
return if RSpec::Rails::Api::Metadata.entities.keys.count.zero?
|
467
|
+
|
468
|
+
@api_components[:schemas] = {}
|
469
|
+
|
470
|
+
RSpec::Rails::Api::Metadata.entities.each_pair do |name, entity|
|
471
|
+
@api_components[:schemas][name] = process_entity(entity)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
364
475
|
##
|
365
476
|
# Converts path with params like ":id" to their OpenAPI representation
|
366
477
|
#
|
@@ -387,12 +498,12 @@ module RSpec
|
|
387
498
|
# Fills the API general information sections
|
388
499
|
#
|
389
500
|
# @return [void]
|
390
|
-
def api_infos
|
501
|
+
def api_infos # rubocop:disable Metrics/CyclomaticComplexity
|
391
502
|
@api_infos = {
|
392
503
|
title: @api_title || 'Some sample app',
|
393
504
|
version: @api_version || '1.0',
|
394
505
|
}
|
395
|
-
@api_infos[:description] = @api_description if @api_description
|
506
|
+
@api_infos[:description] = @api_description.strip || '' if @api_description.present?
|
396
507
|
@api_infos[:termsOfService] = @api_tos if @api_tos
|
397
508
|
@api_infos[:contact] = @api_contact if @api_contact[:name]
|
398
509
|
@api_infos[:license] = @api_license if @api_license[:name]
|
data/lib/rspec_rails_api.rb
CHANGED
@@ -30,9 +30,10 @@ module RSpec
|
|
30
30
|
boolean: { type: 'boolean', format: nil },
|
31
31
|
string: { type: 'string', format: nil, class: String },
|
32
32
|
integer: { type: 'integer', format: nil, class: Integer },
|
33
|
-
number: { type: 'number', format: nil, class:
|
33
|
+
number: { type: 'number', format: nil, class: Numeric },
|
34
34
|
array: { type: 'array', format: nil, class: Array },
|
35
35
|
object: { type: 'object', format: nil, class: Hash },
|
36
|
+
file: { type: 'string', format: 'binary' },
|
36
37
|
}.freeze
|
37
38
|
|
38
39
|
PRIMITIVES = PARAM_TYPES.keys
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-rails-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Tancoigne
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|