rspec-rails-api 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 232e1ea8b43dd1e3b5258f0029d28a54d886156634e87d4a6854a3543752fb0f
4
- data.tar.gz: 71828ab75d08ff900cbe9f018af219bc79b98231118712d127e35c274690d7ad
3
+ metadata.gz: aa6db3329e09e4356d205cf49f100e11132a3e8a8fcb681ef8ede5e22ba470dd
4
+ data.tar.gz: e25b15faecc41214f92bb089f8fc9e28702f9c5ad2072daf4014251d30f23291
5
5
  SHA512:
6
- metadata.gz: dbb37718158f52e0a2056bbe9b8ccde44782cafe915cc146f05333532484b10ae8260e7abbd3f571e657c778b8d8ac37c731adfa452d3e94af253dcf6849aede
7
- data.tar.gz: 731f623490f7e15b3f0acb8851955835083249b60e62f722ab9870c6658d3e690ed0b6aca8ac4edcdc3f6b1c892517511937d190459b8020fcb2b3c2ef99047a
6
+ metadata.gz: 2c947cf296de131ece545c7a21ed093d79de1e1fd5f57af53c58985eab815c781ce4dcbba7f7269c679dbf8155d4eabf0ae5035caa70bbd88090650630ed8e11
7
+ data.tar.gz: b0c4d8bef1c715b0ea369b5f7a659e8fa69344735af2a7735a51a1cb755c3fc3b642597c47a133be9568c0361d8565725ed803d2b0dd530050d1e286795ee911
data/CHANGELOG.md CHANGED
@@ -1,22 +1,74 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## Not released
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
- ## 0.5.0 - 2023-01-02
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.0] - 2023-07-17
22
+
23
+ ### Added
24
+
25
+ - Declaring the same resource multiple times is now possible
26
+ - Add a simple support for security schemes
27
+ - Add support for global entities declarations. Keep things DRYer :)
28
+ - `test_response_of`: add `ignore_content_type` flag to ignore response's content type
29
+ - `test_response_of`: add `ignore_response` flag to ignore tests on response
30
+ - Renderer: Add simple support for redactable content. It will replace content in responses by something else. For now,
31
+ sub-entities are not redacted.
32
+ - Support for `file` type. When declaring a field with the `file` type, it will change the request content-type to
33
+ `multipart/form-data` automatically.
34
+
35
+ ### Changed
36
+
37
+ - Generated JSON files will always be prettified to ease changes reviews when they are versioned
38
+ - When an unexpected 422 error happens, also display its content in error message
39
+ - [BREAKING] It is now impossible to declare the same entity twice. To remediate, rename your declared entities and/or
40
+ use global declarations. Check [README](./README.md) for example.
41
+ - Renderer: Use response data as-is when it's not valid JSON
42
+
43
+ ### Fixed
44
+
45
+ - Strip generated descriptions and summaries
46
+ - Primitives are no more referenced in schemas for "expect_one" types
47
+ - Compare type `:float` against `Numeric` class
48
+ - Entities: Raise error when using `:array` type with `attributes` property and `:object` type with `of` property
49
+ - Rendering of sub-references now use the correct reference
50
+ - Metadata: Add missing type on array parameters
51
+ - Don't transform parameters into JSON when making `get` requests
52
+ - Operation IDs don't use summaries for uniqueness
53
+ - Downcase response content type before comparison
54
+ - Type format is now added on request parameters if applicable
55
+
56
+ ## [0.5.0] - 2023-01-02
57
+
58
+ ### Changed
7
59
 
8
60
  - Improved error messages
9
61
  - Improved usage of primitive types: use `:string` instead of `:type_string`, and more generally, remove the `type_`
10
62
  prefix
63
+
64
+ ### Fixed
65
+ -
11
66
  - Fixed an error when an object is defined with an attribute named `type`.
12
67
 
13
- ## 0.4.0 - 2021-12-19
68
+ ## [0.4.0] - 2021-12-19
69
+
70
+ ### Added
14
71
 
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
72
  - Check for arrays of primitives is now a thing:
21
73
  ```rb
22
74
  # In entities, when attribute is an array of primitives
@@ -28,15 +80,19 @@ All notable changes to this project will be documented in this file.
28
80
  #...
29
81
  end
30
82
  ```
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
83
  - OpenApi:
34
84
  - Responses now includes the schema
35
85
  - `on_xxx` methods second parameter is now used as summary instead of description.
36
86
  Description can be defined on the third parameter.
87
+
88
+ ### Changed
89
+
90
+ - All parameters attributes are considered required unless specified
91
+ - RSpec metadata is now stored in `rra` instead of `rrad` (the gem's first name
92
+ was RSpec Rails API Doc at the time). Update RSpec configuration accordingly.
37
93
  - DSL changes:
38
94
  - `visit` is renamed to `test_response_of`
39
- - Support for `doc_only` is removed
95
+ - Support for `doc_only` is removed
40
96
  - Response expectations _should_ now be declared with `for_code`, and should be
41
97
  removed from example bodies:
42
98
  ```rb
@@ -52,44 +108,71 @@ All notable changes to this project will be documented in this file.
52
108
  end
53
109
  ```
54
110
 
111
+ ### Fixed
112
+
113
+ - Fix object `attributes` key in spec and documentation.
114
+ When defining object attributes, documentation and tests used `properties` key
115
+ while the code was waiting for an `attributes` key. The later makes more sense
116
+ so the spec and documentation were fixed.
117
+
118
+ ## [0.3.4] - 2021-10-20
119
+
120
+ ### Added
55
121
 
56
- ## 0.3.4 - 2021-10-20
57
122
  - Add the "required" attribute in parameters
58
123
 
59
- ## 0.3.3 - 2021-06-02
124
+ ## [0.3.3] - 2021-06-02
125
+
126
+ ### Fixed
127
+
60
128
  - Fix correct types on request parameters
61
129
 
62
- ## 0.3.2 - 2021-03-09
63
- - Fix YAML rendering (ruby objects were sometimes rendered in documentation)
130
+ ## [0.3.2] - 2021-03-09
131
+
132
+ ### Changed
133
+
64
134
  - Render examples results as YAML/JSON objects instead of text blocks.
65
135
 
66
- ## 0.3.1 - 2020-04-09
136
+ ### Fixed
137
+
138
+ - Fix YAML rendering (ruby objects were sometimes rendered in documentation)
139
+
140
+ ## [0.3.1] - 2020-04-09
141
+
142
+ ### Added
143
+
67
144
  - Add support for "test only" examples, allowing to write examples without documentation.
68
145
 
69
- ## 0.3.0 - 2019-12-26
146
+ ## [0.3.0] - 2019-12-26
70
147
 
71
148
  ### Changed
149
+
72
150
  - Rails 6 support, deprecated methods from Rails 5 are not supported. Use version `0.2.3`
73
151
  of this gem if your application is still on 5.
74
152
 
75
153
  ## 0.2.3 - 2019-12-04
76
154
 
77
- ### Improved
155
+ ### Added
78
156
 
79
157
  - Generated Swagger file now use the payloads of POST/PATCH/PUT requests.
80
- - Minimum Ruby version is now specified: Ruby 2.3.3.
81
158
 
82
- ## 0.2.1/0.2.2 - 2019-11-03
159
+ ### Changed
160
+
161
+ - Minimum Ruby version is now specified: Ruby 2.3.3.
83
162
 
84
- _Version 0.2.1 was released and yanked by mistake. Version 0.2.2 is the exact
85
- same one, with a version bump_
163
+ ## [0.2.2] - 2019-11-03
86
164
 
87
165
  ### Changed
88
166
 
89
167
  - `for_code` method now have its `description` optional. If none is provided,
90
168
  the description will be set from the status code.
91
169
 
92
- ## 0.2.0 - 2019-11-02
170
+ ## [0.2.1] - 2019-11-03 [YANKED]
171
+
172
+ _Version 0.2.1 was released and yanked by mistake. Version 0.2.2 is the exact
173
+ same one, with a version bump_
174
+
175
+ ## [0.2.0] - 2019-11-02
93
176
 
94
177
  ### Added
95
178
 
@@ -108,35 +191,35 @@ of the fixtures.
108
191
  the existing calls accordingly. To use params defined with `parameters`, use the
109
192
  `defined` option: `request_params defined: :common_form_params`
110
193
 
111
- ## 0.1.5 - 2019-10-31
194
+ ## [0.1.5] - 2019-10-31
112
195
 
113
196
  ### Fixed
114
197
 
115
198
  - Fixed issue with POST/PUT/DELETE requests with no `request_params`
116
199
  - Improved documentation (integration with Devise, typos and mistakes)
117
200
 
118
- ## 0.1.4 - 2019-10-24
201
+ ## [0.1.4] - 2019-10-24
119
202
 
120
203
  ### Added
121
204
 
122
205
  - Added support for arrays of objects in request parameters
123
206
 
124
- ## 0.1.3 - 2019-10-23
207
+ ## [0.1.3] - 2019-10-23
125
208
 
126
209
  ### Added
127
210
 
128
211
  - Added ability to document API descriptions, servers, etc... from the RSpec helper files
129
212
 
130
- ## 0.1.2 - 2019-10-22
213
+ ## [0.1.2] - 2019-10-22
131
214
 
132
215
  ### Added
133
216
 
134
217
  - Added `item` property for arrays descriptions
135
218
 
136
- ## 0.1.1 - 2019-10-22
219
+ ## [0.1.1] - 2019-10-22
137
220
 
138
221
  - Added support for custom headers in request examples (useful for `visit` method)
139
222
 
140
- ## 0.1.0 - 2019-10-21
223
+ ## [0.1.0] - 2019-10-21
141
224
 
142
225
  Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-rails-api (0.5.0)
4
+ rspec-rails-api (0.6.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -72,7 +72,101 @@ end
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
+ Here is an example configuration:
78
+
79
+ ```rb
80
+ # spec/rails_helper.rb
81
+ require 'spec/support/rspec_rails_api'
82
+ ```
83
+
84
+ ```rb
85
+ # spec/support/acceptance_entities.rb
86
+
87
+ # This file contains common object definitions
88
+ {
89
+ error: {
90
+ error: { type: :string, description: "Error message" }
91
+ },
92
+ form_error: {
93
+ title: { type: :array, required: false, description: "Title errors", of: :string }
94
+ },
95
+ }.each do |name, attributes|
96
+ RSpec::Rails::Api::Metadata.add_entity name, attributes
97
+ end
98
+ ```
99
+
100
+ ```rb
101
+ # spec/support/rspec_rails_api.rb
102
+ require 'rspec_rails_api'
103
+ require 'support/acceptance_entities'
104
+
105
+ # Include DSL in RSpec
106
+ RSpec.configure do |config|
107
+ config.include RSpec::Rails::Api::DSL::Example
108
+ end
109
+
110
+ # Initialize and configure the renderer
111
+ renderer = RSpec::Rails::Api::OpenApiRenderer.new
112
+ # Server URL for quick reference
113
+ server_url = 'https://example.com'
114
+
115
+ # Options here should be present for a valid OpenAPI file
116
+ renderer.api_title = 'MyProject API'
117
+ renderer.api_version = '1'
118
+
119
+ # Options below are optional
120
+ #
121
+ # API description. Markdown supported
122
+ # renderer.api_description = 'Manage data on MyProject'
123
+ #
124
+ # List of servers, to live-test the documentation
125
+ # renderer.api_servers = [{ url: server_url }, { url: 'http://localhost:3000' }]
126
+ #
127
+ # Link to the API terms of service, if any
128
+ # renderer.api_tos = 'http://example.com/tos.html'
129
+ #
130
+ # Contact information
131
+ # renderer.api_contact = { name: 'Admin', email: 'admin@example.com', url: 'http://example.com/contact' }
132
+ #
133
+ # API license information
134
+ # renderer.api_license = { name: 'Apache', url: 'https://opensource.org/licenses/Apache-2.0' }
135
+ #
136
+ # Possible security schemes
137
+ # renderer.add_security_scheme :pkce_code_grant, 'PKCE code grant',
138
+ # type: 'oauth2',
139
+ # flows: {
140
+ # implicit: {
141
+ # authorizationUrl: "#{server_url}/oauth/authorize",
142
+ # scopes: { read: 'will read data on your behalf', write: 'will write data on your behalf' }
143
+ # }
144
+ # }
145
+ # renderer.add_security_scheme :bearer, 'Bearer token',
146
+ # type: 'http',
147
+ # scheme: 'bearer'
148
+ #
149
+ # Declare keys whose values should be filtered in responses.
150
+ # renderer.redact_responses entity_name: { key: 'REDACTED' },
151
+ # other_entity: { other_key: ['REDACTED'] }
152
+
153
+
154
+ # We need to merge each context metadata so we can reference to them to build the final file
155
+ RSpec.configuration.after(:context, type: :acceptance) do |context|
156
+ renderer.merge_context context.class.metadata[:rra].to_h
157
+ # During development of rspec_rails_api, you may want to dump raw metadata to a file
158
+ renderer.merge_context context.class.metadata[:rra].to_h, dump_metadata: true
159
+ end
160
+
161
+ # Skip this block if you don't need the OpenAPI documentation file and only have your responses tested
162
+ RSpec.configuration.after(:suite) do
163
+ renderer.write_files Rails.root.join('public/swagger') # Write both YAML and prettified JSON files
164
+ # or
165
+ renderer.write_files Rails.root.join('public/swagger'), only: [:json] # Prettified JSON only
166
+ # or
167
+ renderer.write_files Rails.root.join('public/swagger'), only: [:yaml] # YAML only
168
+ end
169
+ ```
76
170
 
77
171
  ### Integration with Devise
78
172
 
@@ -124,7 +218,7 @@ end
124
218
  # In examples
125
219
  #...
126
220
  for_code 200, 'Success' do |url|
127
- sing_in #...
221
+ sign_in #...
128
222
  test_response_of url
129
223
 
130
224
  #...
@@ -149,7 +243,7 @@ The idea is to have a simple DSL, and declare things like:
149
243
  **spec/acceptance/users_spec.rb**
150
244
 
151
245
  ```ruby
152
- require 'acceptance_helper'
246
+ require 'rails_helper'
153
247
 
154
248
  RSpec.describe 'Users', type: :acceptance do
155
249
  resource 'Users', 'Manage users'
@@ -186,6 +280,78 @@ RSpec.describe 'Users', type: :acceptance do
186
280
  end
187
281
  ```
188
282
 
283
+ ### Entity declarations
284
+
285
+ You can declare entities locally (in every spec files), but sometimes you will need to use/reference the same entity
286
+ in multiple spec files (e.g.: an error message). In that case, you can create _global_ entities in separate files, and they
287
+ will be picked-up when needed.
288
+
289
+ Example of a local entity:
290
+
291
+ ```rb
292
+ # spec/acceptance/api/users_acceptance_spec.rb
293
+ require 'rails_helper'
294
+
295
+ RSpec.describe 'Users', type: :acceptance do
296
+ resource 'Users', 'Manage users'
297
+
298
+ # This is a local entity
299
+ entity :user,
300
+ id: { type: :integer, description: 'The id' },
301
+ email: { type: :string, description: 'The name' },
302
+ role: { type: :string, description: 'The name' },
303
+ created_at: { type: :datetime, description: 'Creation date' },
304
+ updated_at: { type: :datetime, description: 'Modification date' },
305
+ url: { type: :string, description: 'URL to this category' }
306
+
307
+ on_get '/api/users/', 'Users list' do
308
+ for_code 200, 'Success response', expect_many: :user do |url|
309
+ test_response_of url
310
+ end
311
+ end
312
+
313
+ #...
314
+ end
315
+ ```
316
+
317
+ Defining global entities:
318
+
319
+ ```rb
320
+ # spec/support/entities/user.rb
321
+ # This file should be required at some point in the "rails_helper" or "acceptance_helper"
322
+
323
+ require 'rspec/rails/api/metadata'
324
+
325
+ RSpec::Rails::Api::Metadata.add_entity :user,
326
+ id: { type: :integer, description: 'The id' },
327
+ email: { type: :string, description: 'The name' },
328
+ role: { type: :string, description: 'The name' },
329
+ created_at: { type: :datetime, description: 'Creation date' },
330
+ updated_at: { type: :datetime, description: 'Modification date' },
331
+ url: { type: :string, description: 'URL to this category' }
332
+
333
+ ```
334
+
335
+ Organization of the global entities declaration is up to you.
336
+
337
+ ```rb
338
+ # spec/acceptance/api/users_acceptance_spec.rb
339
+ require 'rails_helper'
340
+
341
+ RSpec.describe 'Users', type: :acceptance do
342
+ resource 'Users', 'Manage users'
343
+
344
+ on_get '/api/users/', 'Users list' do
345
+ # "user" will use the global user entity
346
+ for_code 200, 'Success response', expect_many: :user do |url|
347
+ test_response_of url
348
+ end
349
+ end
350
+
351
+ #...
352
+ end
353
+ ```
354
+
189
355
  ### DSL
190
356
 
191
357
  #### Example groups
@@ -197,6 +363,23 @@ Starts a resource description.
197
363
  - It must be called before any other documentation calls.
198
364
  - It should be in the first `describe block`
199
365
 
366
+ A resource may be completed across multiple spec files:
367
+
368
+ ```rb
369
+ # an_acceptance_spec.rb
370
+ RSpec.describe 'Something', type: :acceptance do
371
+ resource 'User', 'Manage users'
372
+ end
373
+
374
+ # another_acceptance_spec.rb
375
+ RSpec.describe 'Something else', type: :acceptance do
376
+ resource 'User', 'Another description'
377
+ end
378
+
379
+ ```
380
+
381
+ The first evaluated `resource` statement will be used as description; all the tests in both files will complete it.
382
+
200
383
  ##### `entity(type, fields)`
201
384
 
202
385
  Describes an entity for the documentation. The type is only a reference,
@@ -423,11 +606,30 @@ Once again, you have to pass an argument to the block if you use
423
606
  # ...
424
607
  ```
425
608
 
609
+ ##### `requires_security(scheme_references)`
610
+
611
+ Specifies the valid security schemes to use for this request. Security schemes are declared at the renderer level
612
+ (see [the configuration example](#Configuration)).
613
+
614
+ ```rb
615
+ # Given a previously :basic scheme
616
+
617
+ # ...
618
+ on_get '/some/path' do
619
+ require_security :basic, :implicit
620
+
621
+ for_code 200 do |url|
622
+ #...
623
+ end
624
+ end
625
+ # ...
626
+ ```
627
+
426
628
  #### Examples
427
629
 
428
630
  Example methods are available in `for_code` blocks
429
631
 
430
- ##### `test_response_of(example, path_params: {}, payload: {}, headers: {})`
632
+ ##### `test_response_of(example, path_params: {}, payload: {}, headers: {}, ignore_content_type: false)`
431
633
 
432
634
  Visits the described URL and:
433
635
 
@@ -441,6 +643,7 @@ Visits the described URL and:
441
643
  - `payload`: a hash of values to send. Ignored for GET and DELETE
442
644
  requests
443
645
  - `headers`: a hash of custom headers.
646
+ - `ignore_content_type`: whether to ignore response's content-type. By default, checks for a JSON response
444
647
 
445
648
  ```ruby
446
649
  for_code 200, 'Success' do |url|
@@ -9,14 +9,15 @@ module RSpec
9
9
  ##
10
10
  # Visits the current example and tests the response
11
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
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 visit with for_code context.' unless example
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].to_json,
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
- entities = rra_metadata.resources[current_resource][:entities]
53
- raise "Unknown entity '#{entity}' in resource '#{current_resource}'" unless entities.key? entity.to_sym
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
- entities[entity.to_sym].expand_with(entities)
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
- def check_response(response, expected_code) # rubocop:disable Metrics/AbcSize
64
- expect(response.status).to eq expected_code
65
- expect(response.headers['Content-Type']).to eq 'application/json; charset=utf-8' if expected_code != 204
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: example_params[0].downcase,
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
- metadata[:rra].add_entity type, fields
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
  #
@@ -13,8 +13,8 @@ module RSpec
13
13
 
14
14
  def initialize(fields)
15
15
  @fields = {}
16
- fields.each_key do |name|
17
- @fields[name] = FieldConfig.new fields[name]
16
+ fields.each_pair do |name, definition|
17
+ @fields[name] = FieldConfig.new(**definition)
18
18
  end
19
19
  end
20
20
 
@@ -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
@@ -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 :entities, :resources, :parameters, :current_resource, :current_url, :current_method, :current_code
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] = { description: description, paths: {}, entities: {} }
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 = { resources: {}, entities: {} }
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
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
- File.write "#{path}.#{type}", prepare_metadata.send("to_#{type}")
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(JSON.pretty_generate(hash))
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
- extract_from_resources
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 # rubocop:disable Metrics/MethodLength
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,29 @@ 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] = PARAM_TYPES[field.type][:format] if PARAM_TYPES[field.type][:format]
204
- schema[:properties][name] = property
205
- # Primitives support
206
- property[:items] = { type: field.attributes } if PRIMITIVES.include? field.attributes
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
+ end
207
245
 
208
246
  required.push name unless field.required == false
247
+
248
+ schema[:properties][name] = property
209
249
  end
210
250
 
211
251
  schema[:required] = required unless required.size.zero?
@@ -221,10 +261,10 @@ module RSpec
221
261
  #
222
262
  #
223
263
  # @return [void]
224
- def process_path_param(name, param) # rubocop:disable Metrics/MethodLength
264
+ def process_path_param(name, param) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
225
265
  parameter = {
226
266
  name: name.to_s,
227
- description: param[:description],
267
+ description: param[:description].presence&.strip || '',
228
268
  required: param[:required] || true,
229
269
  in: param[:scope].to_s,
230
270
  schema: {
@@ -250,7 +290,7 @@ module RSpec
250
290
  #
251
291
  # FIXME: Rename "action_config" to "action"
252
292
  # FIXME: Rename "parameters" to "path_parameters"
253
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
293
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
254
294
  def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil)
255
295
  responses = {}
256
296
  request_body = nil
@@ -268,21 +308,31 @@ module RSpec
268
308
  responses[status_key] = process_response status: status_key, status_config: status, content: content
269
309
  end
270
310
 
271
- summary = path_config[:actions][action_config][:summary]
272
- action = {
273
- summary: summary,
274
- description: path_config[:actions][action_config][:description],
275
- operationId: "#{resource} #{summary}".downcase.gsub(/[^\w]/, '_'),
311
+ action = {
312
+ summary: path_config[:actions][action_config][:summary]&.strip || '',
313
+ description: path_config[:actions][action_config][:description].presence&.strip || '',
314
+ operationId: "#{resource} #{action_config} #{path}".downcase.gsub(/[^\w]/, '_'),
276
315
  parameters: parameters,
277
316
  responses: responses,
278
317
  tags: [resource.to_s],
279
318
  }
280
319
 
320
+ if path_config[:actions][action_config].key? :security
321
+ references = path_config[:actions][action_config][:security]
322
+
323
+ action[:security] = []
324
+ references.each do |reference|
325
+ raise "No security scheme defined with reference #{reference}" unless @api_security.key? reference
326
+
327
+ action[:security].push({ reference => [] })
328
+ end
329
+ end
330
+
281
331
  action[:requestBody] = request_body if request_body
282
332
 
283
333
  action
284
334
  end
285
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
335
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
286
336
 
287
337
  ##
288
338
  # Processes a request body from metadata
@@ -294,11 +344,12 @@ module RSpec
294
344
  # @return [void]
295
345
  def process_request_body(schema: nil, ref: nil, examples: {})
296
346
  Utils.deep_set @api_components, ['schemas', ref], schema
347
+
297
348
  {
298
349
  # description: '',
299
350
  required: true,
300
351
  content: {
301
- 'application/json' => {
352
+ content_type_from_schema(schema) => {
302
353
  schema: { '$ref' => "#/components/schemas/#{ref}" },
303
354
  examples: examples,
304
355
  },
@@ -306,6 +357,23 @@ module RSpec
306
357
  }
307
358
  end
308
359
 
360
+ def content_type_from_schema(schema)
361
+ schema_includes_file?(schema) ? 'multipart/form-data' : 'application/json'
362
+ end
363
+
364
+ def schema_includes_file?(schema)
365
+ return true if schema[:type] == 'string' && schema[:format] == 'binary'
366
+ return false unless schema[:properties].is_a?(Hash) && schema[:required].is_a?(Array)
367
+
368
+ schema[:properties].each_value do |definition|
369
+ next unless schema_includes_file?(definition)
370
+
371
+ return true
372
+ end
373
+
374
+ false
375
+ end
376
+
309
377
  ##
310
378
  # Process a response from metadata
311
379
  #
@@ -314,22 +382,45 @@ module RSpec
314
382
  # @param content [String] Response content
315
383
  #
316
384
  # @return [void]
317
- def process_response(status: nil, status_config: nil, content: nil)
318
- response = { description: status_config[:description] }
385
+ def process_response(status: nil, status_config: nil, content: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
386
+ response = { description: status_config[:description].presence&.strip || '' }
319
387
 
320
388
  return response if status.to_s == '204' && content # No content
321
389
 
390
+ data = begin
391
+ JSON.parse(content)
392
+ rescue JSON::ParserError, TypeError
393
+ content
394
+ end
395
+
396
+ entity = status_config[:expectations][:one] || status_config[:expectations][:many]
397
+
398
+ # TODO: handle sub-entities
399
+ if @redactables.key?(entity) && data.is_a?(Hash)
400
+ if status_config[:expectations][:one]
401
+ @redactables[entity].each_pair do |attribute, replacement|
402
+ data[attribute.to_s] = replacement
403
+ end
404
+ else
405
+ data.each_index do |index|
406
+ @redactables[entity].each_pair do |attribute, replacement|
407
+ data[index][attribute.to_s] = replacement
408
+ end
409
+ end
410
+ end
411
+ end
412
+
322
413
  response[:content] = {
323
414
  'application/json': {
324
415
  schema: response_schema(status_config[:expectations]),
325
- examples: { default: { value: JSON.parse(content) } },
416
+ examples: { default: { value: data } },
326
417
  },
327
418
  }
328
419
 
329
420
  response
330
421
  end
331
422
 
332
- def response_schema(expectations)
423
+ def response_schema(expectations) # rubocop:disable Metrics/MethodLength
333
424
  if expectations[:many]
334
425
  items = if PRIMITIVES.include?(expectations[:many])
335
426
  { type: expectations[:many] }
@@ -338,7 +429,11 @@ module RSpec
338
429
  end
339
430
  { type: 'array', items: items }
340
431
  elsif expectations[:one]
341
- { '$ref' => "#/components/schemas/#{expectations[:one]}" }
432
+ if PRIMITIVES.include?(expectations[:one])
433
+ { type: expectations[:one] }
434
+ else
435
+ { '$ref' => "#/components/schemas/#{expectations[:one]}" }
436
+ end
342
437
  end
343
438
  end
344
439
 
@@ -361,6 +456,16 @@ module RSpec
361
456
  request_examples
362
457
  end
363
458
 
459
+ def global_entities
460
+ return if RSpec::Rails::Api::Metadata.entities.keys.count.zero?
461
+
462
+ @api_components[:schemas] = {}
463
+
464
+ RSpec::Rails::Api::Metadata.entities.each_pair do |name, entity|
465
+ @api_components[:schemas][name] = process_entity(entity)
466
+ end
467
+ end
468
+
364
469
  ##
365
470
  # Converts path with params like ":id" to their OpenAPI representation
366
471
  #
@@ -387,12 +492,12 @@ module RSpec
387
492
  # Fills the API general information sections
388
493
  #
389
494
  # @return [void]
390
- def api_infos
495
+ def api_infos # rubocop:disable Metrics/CyclomaticComplexity
391
496
  @api_infos = {
392
497
  title: @api_title || 'Some sample app',
393
498
  version: @api_version || '1.0',
394
499
  }
395
- @api_infos[:description] = @api_description if @api_description
500
+ @api_infos[:description] = @api_description.strip || '' if @api_description.present?
396
501
  @api_infos[:termsOfService] = @api_tos if @api_tos
397
502
  @api_infos[:contact] = @api_contact if @api_contact[:name]
398
503
  @api_infos[:license] = @api_license if @api_license[:name]
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module Rails
5
5
  module Api
6
- VERSION = '0.5.0'
6
+ VERSION = '0.6.0'
7
7
  end
8
8
  end
9
9
  end
@@ -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: Float },
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Tancoigne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-02 00:00:00.000000000 Z
11
+ date: 2023-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport