rspec-rest 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +1 -1
- data/README.md +129 -27
- data/lib/rspec/rest/contract_expectations.rb +26 -5
- data/lib/rspec/rest/deprecation.rb +52 -0
- data/lib/rspec/rest/dsl.rb +75 -26
- data/lib/rspec/rest/formatters/request_recorder.rb +40 -1
- data/lib/rspec/rest/version.rb +1 -1
- data/lib/rspec/rest.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47e2dfd9b5b01a159c9d81dffc650b0a807d6491669100f3e4c0939b9012eb3e
|
|
4
|
+
data.tar.gz: 0b5f6d62d08a5139fa60ce2d2a193d92949193767a08378f5212e576bc762f50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca75926bef46af5c64380a992281242afada213a4e4975ef41f0e1e8c525395c41a88618fd04accec24613cb13b107a8a0ae04b25a4e32cb4ded5464e6093760
|
|
7
|
+
data.tar.gz: f9762615e58277d690c7cdc7b7426dac3818e3f1fe7f12eb6e3e753d9cf0e19d366a7eae198911b5f33d7911ef72ded6826dabe63dd5b66bde19f98c7954fd87
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,36 @@ Semantic Versioning.
|
|
|
9
9
|
|
|
10
10
|
No changes yet.
|
|
11
11
|
|
|
12
|
+
## [0.3.0] - 2026-03-10
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Contract lookup helper for matcher composition in examples:
|
|
16
|
+
- `contract(:name)`
|
|
17
|
+
- Internal deprecation utility for gem APIs:
|
|
18
|
+
- `RSpec::Rest::Deprecation.warn(key:, message:)`
|
|
19
|
+
- once-per-key warning emission in RSpec output.
|
|
20
|
+
- Keyword request description support on verb DSL calls:
|
|
21
|
+
- `get(path, description: "...") { ... }` (and same for other verbs).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Failure-time `curl` output now uses an auth token environment placeholder for redacted auth-like headers, improving copy/paste usability:
|
|
25
|
+
- `Authorization: Bearer $API_AUTH_TOKEN`
|
|
26
|
+
- Redacted auth scheme prefixes are preserved in `curl` output for `Authorization` and `Proxy-Authorization` (for example `Basic`, `Digest`).
|
|
27
|
+
- README examples now prefer:
|
|
28
|
+
- `contract(:name)` over nested `expect_json_contract(...)`
|
|
29
|
+
- keyword request descriptions (`description:`).
|
|
30
|
+
- Added README RuboCop compatibility guidance for:
|
|
31
|
+
- `Rails/HttpPositionalArguments`
|
|
32
|
+
- `RSpec/EmptyExampleGroup`.
|
|
33
|
+
|
|
34
|
+
### Deprecated
|
|
35
|
+
- `expect_json_contract(name)` is deprecated and scheduled for removal in `1.0`.
|
|
36
|
+
Use `contract(:name)` for contract lookup in examples.
|
|
37
|
+
- Positional verb descriptions are deprecated and scheduled for removal in `1.0`:
|
|
38
|
+
- `get(path, "description") { ... }`
|
|
39
|
+
Use keyword descriptions instead:
|
|
40
|
+
- `get(path, description: "...") { ... }`
|
|
41
|
+
|
|
12
42
|
## [0.2.0] - 2026-03-08
|
|
13
43
|
|
|
14
44
|
### Added
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -11,10 +11,6 @@ It focuses on:
|
|
|
11
11
|
- high-signal failure output with request/response context
|
|
12
12
|
- auto-generated `curl` reproduction commands on failures
|
|
13
13
|
|
|
14
|
-
## Status
|
|
15
|
-
|
|
16
|
-
The gem is pre-release and in active development toward `0.1.0`.
|
|
17
|
-
|
|
18
14
|
## Installation
|
|
19
15
|
|
|
20
16
|
When published:
|
|
@@ -69,7 +65,7 @@ end
|
|
|
69
65
|
|
|
70
66
|
## Before and After (Rack::Test to rspec-rest)
|
|
71
67
|
|
|
72
|
-
The
|
|
68
|
+
The first two examples below test the same behavior in two styles.
|
|
73
69
|
|
|
74
70
|
Before (`Rack::Test` + manual response parsing):
|
|
75
71
|
|
|
@@ -113,25 +109,54 @@ RSpec.describe "Posts API" do
|
|
|
113
109
|
default_format :json
|
|
114
110
|
end
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
resource "/posts" do
|
|
113
|
+
with_auth auth_token
|
|
114
|
+
|
|
115
|
+
get "/", description: "returns posts page 1" do
|
|
116
|
+
query page: 1, per_page: 10
|
|
117
|
+
|
|
118
|
+
expect_status 200
|
|
119
|
+
expect_json array_of(hash_including("id" => integer, "author" => hash_including("id" => integer)))
|
|
120
|
+
expect_json_first hash_including("id" => posts.first.id)
|
|
121
|
+
expect_json_item(0) { |item| expect(item["author"]["id"]).to eq(posts.first.author.id) }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
What improves in the apples-to-apples rewrite:
|
|
128
|
+
|
|
129
|
+
- Request setup is declarative (`api`, `resource`, `with_auth`, `query`).
|
|
130
|
+
- JSON assertions read closer to the business intent (`expect_json_first`, `expect_json_item`).
|
|
131
|
+
- Failure output includes request/response context and a reproducible `curl`.
|
|
132
|
+
|
|
133
|
+
Beyond the baseline rewrite, `rspec-rest` can also express additional API concerns
|
|
134
|
+
with concise helpers:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
RSpec.describe "Posts API" do
|
|
138
|
+
include RSpec::Rest
|
|
139
|
+
|
|
140
|
+
let(:auth_token) { "test-token" }
|
|
141
|
+
|
|
142
|
+
api do
|
|
143
|
+
app MyApp::Base
|
|
144
|
+
base_path "/api/v1"
|
|
145
|
+
default_format :json
|
|
146
|
+
end
|
|
147
|
+
|
|
118
148
|
contract :post_summary do
|
|
119
149
|
hash_including("id" => integer, "author" => hash_including("id" => integer))
|
|
120
150
|
end
|
|
121
151
|
|
|
122
152
|
resource "/posts" do
|
|
123
153
|
with_auth auth_token
|
|
124
|
-
with_query per_page: 10
|
|
125
|
-
|
|
126
|
-
get "/", "returns first page of posts for authenticated user" do
|
|
127
|
-
query page: 1
|
|
128
154
|
|
|
155
|
+
get "/" do
|
|
156
|
+
query page: 1, per_page: 10
|
|
129
157
|
expect_status 200
|
|
130
|
-
expect_json array_of(
|
|
131
|
-
expect_json_first hash_including("id" => posts.first.id)
|
|
132
|
-
expect_json_item(0) { |item| expect(item["author"]["id"]).to eq(posts.first.author.id) }
|
|
158
|
+
expect_json array_of(contract(:post_summary))
|
|
133
159
|
expect_page_size 10
|
|
134
|
-
expect_max_page_size 20
|
|
135
160
|
end
|
|
136
161
|
|
|
137
162
|
get "/{id}" do
|
|
@@ -153,13 +178,6 @@ RSpec.describe "Posts API" do
|
|
|
153
178
|
end
|
|
154
179
|
```
|
|
155
180
|
|
|
156
|
-
What improves:
|
|
157
|
-
|
|
158
|
-
- Request setup is declarative (`api`, `resource`, shared presets, `query`, `multipart!`, `file`).
|
|
159
|
-
- JSON expectations are concise and structure-aware (`expect_json`, `expect_json_at`).
|
|
160
|
-
- Common API outcomes are one-liners (`expect_error`, pagination helpers).
|
|
161
|
-
- Failures include request/response context plus a reproducible `curl`.
|
|
162
|
-
|
|
163
181
|
## API Config (`api`)
|
|
164
182
|
|
|
165
183
|
`api` defines shared runtime configuration for a spec group.
|
|
@@ -188,7 +206,9 @@ Supported config:
|
|
|
188
206
|
|
|
189
207
|
- `resource "/users" do ... end`
|
|
190
208
|
- `get`, `post`, `put`, `patch`, `delete`
|
|
191
|
-
-
|
|
209
|
+
- preferred description form: `get(path, description: "...") { ... }`
|
|
210
|
+
- legacy positional form `get(path, "description")` is deprecated and will be removed in `1.0`.
|
|
211
|
+
It is deprecated to avoid `Rails/HttpPositionalArguments` false-positives in RuboCop.
|
|
192
212
|
|
|
193
213
|
Resource paths are composable and support placeholders:
|
|
194
214
|
|
|
@@ -207,7 +227,7 @@ Example with an explicit behavior name:
|
|
|
207
227
|
|
|
208
228
|
```ruby
|
|
209
229
|
resource "/users" do
|
|
210
|
-
get "/", "returns public users for authenticated client" do
|
|
230
|
+
get "/", description: "returns public users for authenticated client" do
|
|
211
231
|
expect_status 200
|
|
212
232
|
end
|
|
213
233
|
end
|
|
@@ -217,6 +237,45 @@ RSpec example output uses the composed full route (including `base_path`) and ap
|
|
|
217
237
|
- `GET /v1/users - returns public users for authenticated client`
|
|
218
238
|
|
|
219
239
|
Note: Example names (including `base_path`) are composed when the verb macro is evaluated. To ensure the example names include `base_path`, declare your `api` (and its `base_path`) before defining `resource` blocks and their verbs. If you configure `api`/`base_path` afterward, requests will use the configured `base_path`, but the previously defined example names will not reflect it.
|
|
240
|
+
|
|
241
|
+
## RuboCop Compatibility
|
|
242
|
+
|
|
243
|
+
`rspec-rest` verbs (`get`, `post`, etc.) define examples via DSL macros, and can trigger
|
|
244
|
+
false-positives in some RuboCop cops:
|
|
245
|
+
|
|
246
|
+
- `Rails/HttpPositionalArguments`: use keyword descriptions (`description:`), not positional descriptions.
|
|
247
|
+
- `RSpec/EmptyExampleGroup`: a `context` that only contains `resource` + verb DSL may be flagged as empty.
|
|
248
|
+
|
|
249
|
+
Recommended mitigation is scoped configuration for files that use `rspec-rest`:
|
|
250
|
+
|
|
251
|
+
```yaml
|
|
252
|
+
# .rubocop.yml
|
|
253
|
+
RSpec/EmptyExampleGroup:
|
|
254
|
+
Exclude:
|
|
255
|
+
- "spec/api/rest/**/*"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
If you only have a few affected groups, use an inline disable around the DSL group:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# rubocop:disable RSpec/EmptyExampleGroup
|
|
262
|
+
context "when authenticated" do
|
|
263
|
+
resource "/posts" do
|
|
264
|
+
get "/", description: "returns posts" do
|
|
265
|
+
expect_status 200
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# rubocop:enable RSpec/EmptyExampleGroup
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Prefer scoped exclusions/disables over broad project-wide disables so non-DSL specs
|
|
273
|
+
keep full lint coverage.
|
|
274
|
+
|
|
275
|
+
We are also considering a dedicated RuboCop extension for `rspec-rest` (for example,
|
|
276
|
+
`rubocop-rspec-rest`) to reduce manual configuration over time. Until then, the
|
|
277
|
+
scoped patterns above are the recommended approach.
|
|
278
|
+
|
|
220
279
|
## Shared Request Presets
|
|
221
280
|
|
|
222
281
|
Define shared request defaults at group/resource scope:
|
|
@@ -301,7 +360,8 @@ Available expectation helpers:
|
|
|
301
360
|
- `expect_status(code)`
|
|
302
361
|
- `expect_header(key, value_or_regex)`
|
|
303
362
|
- `expect_json(expected = nil, &block)`
|
|
304
|
-
- `
|
|
363
|
+
- `contract(name)` (lookup helper for reusable JSON contracts)
|
|
364
|
+
- `expect_json_contract(name)` (deprecated; use `contract(name)`)
|
|
305
365
|
- `expect_json_at(selector, expected = nil, &block)`
|
|
306
366
|
- `expect_json_first(expected = nil, &block)`
|
|
307
367
|
- `expect_json_item(index, expected = nil, &block)`
|
|
@@ -365,7 +425,7 @@ end
|
|
|
365
425
|
## Lightweight Contracts
|
|
366
426
|
|
|
367
427
|
A contract is a named, reusable JSON expectation (usually a response shape matcher).
|
|
368
|
-
Define it once in your spec group, then apply it anywhere with `
|
|
428
|
+
Define it once in your spec group, then apply it anywhere with `contract(:name)`.
|
|
369
429
|
|
|
370
430
|
```ruby
|
|
371
431
|
contract :post_summary do
|
|
@@ -378,7 +438,7 @@ end
|
|
|
378
438
|
|
|
379
439
|
get "/" do
|
|
380
440
|
expect_status 200
|
|
381
|
-
expect_json array_of(
|
|
441
|
+
expect_json array_of(contract(:post_summary))
|
|
382
442
|
end
|
|
383
443
|
```
|
|
384
444
|
|
|
@@ -424,6 +484,48 @@ When an expectation fails, output includes:
|
|
|
424
484
|
Sensitive headers are redacted by default and can be customized via
|
|
425
485
|
`redact_headers`.
|
|
426
486
|
|
|
487
|
+
Example (truncated):
|
|
488
|
+
|
|
489
|
+
```text
|
|
490
|
+
expected: 201
|
|
491
|
+
got: 422
|
|
492
|
+
|
|
493
|
+
Request:
|
|
494
|
+
POST /api/v1/posts
|
|
495
|
+
Headers:
|
|
496
|
+
Accept: application/json
|
|
497
|
+
Authorization: [REDACTED]
|
|
498
|
+
Content-Type: application/json
|
|
499
|
+
Body:
|
|
500
|
+
{
|
|
501
|
+
"title": "",
|
|
502
|
+
"body": "Example"
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
Response:
|
|
506
|
+
Status: 422
|
|
507
|
+
Headers:
|
|
508
|
+
Content-Type: application/json
|
|
509
|
+
Body:
|
|
510
|
+
{
|
|
511
|
+
"error": "Validation failed",
|
|
512
|
+
"details": {
|
|
513
|
+
"title": ["can't be blank"]
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
Reproduce with:
|
|
518
|
+
curl -X POST 'http://localhost:3000/api/v1/posts' -H 'Accept: application/json' -H "Authorization: Bearer $API_AUTH_TOKEN" -H 'Content-Type: application/json' -d '{"title":"","body":"Example"}'
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Before running an authenticated command, set your token:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
export API_AUTH_TOKEN="your_token_here"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Then paste the generated `curl` command directly in your terminal for fast manual debugging.
|
|
528
|
+
|
|
427
529
|
## Contributing
|
|
428
530
|
|
|
429
531
|
Contributions are welcome.
|
|
@@ -5,9 +5,31 @@ require_relative "contract_matcher"
|
|
|
5
5
|
module RSpec
|
|
6
6
|
module Rest
|
|
7
7
|
module ContractExpectations
|
|
8
|
+
def contract(name = nil, &)
|
|
9
|
+
if block_given?
|
|
10
|
+
raise ArgumentError,
|
|
11
|
+
"contract(:name) lookup does not accept a block in examples. " \
|
|
12
|
+
"Define contracts at the example group level with `contract(:name) { ... }`."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
contract_matcher_for(name, lookup_name: :contract)
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
def expect_json_contract(name)
|
|
19
|
+
Deprecation.warn(
|
|
20
|
+
key: :expect_json_contract,
|
|
21
|
+
message: "`expect_json_contract` is deprecated and will be removed in 1.0. " \
|
|
22
|
+
"Use `contract(:name)` instead."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
contract_matcher_for(name, lookup_name: :expect_json_contract)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def contract_matcher_for(name, lookup_name:)
|
|
9
31
|
contract_name = normalize_contract_name(name)
|
|
10
|
-
return unknown_contract_matcher(name_error_message(name)) if contract_name.nil?
|
|
32
|
+
return unknown_contract_matcher(name_error_message(name, lookup_name: lookup_name)) if contract_name.nil?
|
|
11
33
|
|
|
12
34
|
definition = self.class.rest_contract_definition(contract_name)
|
|
13
35
|
return ContractMatcher.new(name: contract_name, definition: definition, context: self) unless definition.nil?
|
|
@@ -17,8 +39,6 @@ module RSpec
|
|
|
17
39
|
unknown_contract_matcher(message)
|
|
18
40
|
end
|
|
19
41
|
|
|
20
|
-
private
|
|
21
|
-
|
|
22
42
|
def normalize_contract_name(name)
|
|
23
43
|
return nil if name.nil? || !name.respond_to?(:to_sym)
|
|
24
44
|
|
|
@@ -29,9 +49,10 @@ module RSpec
|
|
|
29
49
|
UnknownContractMatcher.new(message)
|
|
30
50
|
end
|
|
31
51
|
|
|
32
|
-
def name_error_message(name)
|
|
52
|
+
def name_error_message(name, lookup_name:)
|
|
53
|
+
lookup_hint = lookup_name == :expect_json_contract ? "expect_json_contract" : "contract(:name)"
|
|
33
54
|
"Invalid contract name #{name.inspect} (#{name.class}). " \
|
|
34
|
-
"
|
|
55
|
+
"#{lookup_hint} requires a contract name that responds to #to_sym."
|
|
35
56
|
end
|
|
36
57
|
end
|
|
37
58
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Rest
|
|
5
|
+
module Deprecation
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def warn(key:, message:)
|
|
9
|
+
return if already_emitted?(key)
|
|
10
|
+
|
|
11
|
+
emit("DEPRECATION: #{message}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset!
|
|
15
|
+
emitted_keys.clear
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def already_emitted?(key)
|
|
19
|
+
normalized_key = key.to_s
|
|
20
|
+
return true if emitted_keys[normalized_key]
|
|
21
|
+
|
|
22
|
+
emitted_keys[normalized_key] = true
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
private_class_method :already_emitted?
|
|
26
|
+
|
|
27
|
+
def emitted_keys
|
|
28
|
+
@emitted_keys ||= {}
|
|
29
|
+
end
|
|
30
|
+
private_class_method :emitted_keys
|
|
31
|
+
|
|
32
|
+
def emit(message)
|
|
33
|
+
reporter = rspec_reporter
|
|
34
|
+
if reporter.respond_to?(:message)
|
|
35
|
+
reporter.message(message)
|
|
36
|
+
else
|
|
37
|
+
Kernel.warn(message)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
private_class_method :emit
|
|
41
|
+
|
|
42
|
+
def rspec_reporter
|
|
43
|
+
return nil unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
|
|
44
|
+
|
|
45
|
+
::RSpec.configuration.reporter
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
private_class_method :rspec_reporter
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/rspec/rest/dsl.rb
CHANGED
|
@@ -56,9 +56,72 @@ module RSpec
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
module RouteNamingSupport
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_example_name(method:, path:, resource_path:, description:)
|
|
63
|
+
route = compose_route_for_example(resource_path: resource_path, endpoint_path: path)
|
|
64
|
+
base = "#{method.to_s.upcase} #{route}"
|
|
65
|
+
normalized_description = description.to_s.strip
|
|
66
|
+
return base if normalized_description.empty?
|
|
67
|
+
|
|
68
|
+
"#{base} - #{normalized_description}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def compose_route_for_example(resource_path:, endpoint_path:)
|
|
72
|
+
PathComposer.compose(
|
|
73
|
+
base_path: rest_config.base_path,
|
|
74
|
+
resource_path: resource_path,
|
|
75
|
+
endpoint_path: endpoint_path
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def current_resource_path
|
|
80
|
+
stack = @rest_resource_stack || []
|
|
81
|
+
return nil if stack.empty?
|
|
82
|
+
|
|
83
|
+
first_had_leading_slash = stack.first.to_s.start_with?("/")
|
|
84
|
+
normalized_segments = stack.map do |segment|
|
|
85
|
+
segment.to_s.sub(%r{\A/+}, "").sub(%r{/+\z}, "")
|
|
86
|
+
end.reject(&:empty?)
|
|
87
|
+
|
|
88
|
+
path = normalized_segments.join("/")
|
|
89
|
+
first_had_leading_slash && !path.empty? ? "/#{path}" : path
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module DescriptionArgumentSupport
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def warn_on_deprecated_positional_description(_method)
|
|
97
|
+
Deprecation.warn(
|
|
98
|
+
key: :verb_positional_description,
|
|
99
|
+
message: "Positional request descriptions (for example: get(path, description)) are deprecated and " \
|
|
100
|
+
"will be removed in 1.0. Use keyword descriptions " \
|
|
101
|
+
"(for example: get(path, description: \"...\")). " \
|
|
102
|
+
"This avoids RuboCop Rails/HttpPositionalArguments false-positives."
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def resolve_description_options(method:, positional_description:, keyword_description:)
|
|
107
|
+
if !positional_description.nil? && !keyword_description.nil?
|
|
108
|
+
raise ArgumentError,
|
|
109
|
+
"#{method}(...) received both positional and keyword descriptions. " \
|
|
110
|
+
"Use only `description:`."
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
description: keyword_description.nil? ? positional_description : keyword_description,
|
|
115
|
+
using_positional_description: !positional_description.nil? && keyword_description.nil?
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
59
120
|
module ClassMethods
|
|
60
121
|
include ClassLevelContracts
|
|
61
122
|
include ClassLevelPresets
|
|
123
|
+
include RouteNamingSupport
|
|
124
|
+
include DescriptionArgumentSupport
|
|
62
125
|
|
|
63
126
|
def api(&)
|
|
64
127
|
builder = ApiConfigBuilder.new(rest_config)
|
|
@@ -78,14 +141,15 @@ module RSpec
|
|
|
78
141
|
end
|
|
79
142
|
|
|
80
143
|
HTTP_METHODS.each do |method|
|
|
81
|
-
define_method(method) do |path,
|
|
144
|
+
define_method(method) do |path, positional_description = nil, description: nil, &block|
|
|
145
|
+
description_options = resolve_verb_description_options(method, positional_description, description)
|
|
82
146
|
resource_path = current_resource_path
|
|
83
147
|
request_presets = deep_dup_presets(current_request_presets)
|
|
84
148
|
example_name = build_example_name(
|
|
85
149
|
method: method,
|
|
86
150
|
path: path,
|
|
87
151
|
resource_path: resource_path,
|
|
88
|
-
description: description
|
|
152
|
+
description: description_options[:description]
|
|
89
153
|
)
|
|
90
154
|
it(example_name) do
|
|
91
155
|
start_rest_request(
|
|
@@ -95,6 +159,7 @@ module RSpec
|
|
|
95
159
|
presets: request_presets
|
|
96
160
|
)
|
|
97
161
|
instance_eval(&block) if block
|
|
162
|
+
self.class.send(:emit_positional_description_warning, method, description_options)
|
|
98
163
|
execute_rest_request_if_pending
|
|
99
164
|
end
|
|
100
165
|
end
|
|
@@ -116,35 +181,19 @@ module RSpec
|
|
|
116
181
|
|
|
117
182
|
private
|
|
118
183
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
base = "#{method.to_s.upcase} #{route}"
|
|
122
|
-
normalized_description = description.to_s.strip
|
|
123
|
-
return base if normalized_description.empty?
|
|
184
|
+
def emit_positional_description_warning(method, description_options)
|
|
185
|
+
return unless description_options[:using_positional_description]
|
|
124
186
|
|
|
125
|
-
|
|
187
|
+
send(:warn_on_deprecated_positional_description, method)
|
|
126
188
|
end
|
|
127
189
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
190
|
+
def resolve_verb_description_options(method, positional_description, description)
|
|
191
|
+
resolve_description_options(
|
|
192
|
+
method: method,
|
|
193
|
+
positional_description: positional_description,
|
|
194
|
+
keyword_description: description
|
|
133
195
|
)
|
|
134
196
|
end
|
|
135
|
-
|
|
136
|
-
def current_resource_path
|
|
137
|
-
stack = @rest_resource_stack || []
|
|
138
|
-
return nil if stack.empty?
|
|
139
|
-
|
|
140
|
-
first_had_leading_slash = stack.first.to_s.start_with?("/")
|
|
141
|
-
normalized_segments = stack.map do |segment|
|
|
142
|
-
segment.to_s.sub(%r{\A/+}, "").sub(%r{/+\z}, "")
|
|
143
|
-
end.reject(&:empty?)
|
|
144
|
-
|
|
145
|
-
path = normalized_segments.join("/")
|
|
146
|
-
first_had_leading_slash && !path.empty? ? "/#{path}" : path
|
|
147
|
-
end
|
|
148
197
|
end
|
|
149
198
|
|
|
150
199
|
module InstanceMethods
|
|
@@ -10,6 +10,18 @@ module RSpec
|
|
|
10
10
|
class RequestRecorder
|
|
11
11
|
include Helpers
|
|
12
12
|
|
|
13
|
+
AUTH_TOKEN_ENV_VAR = "API_AUTH_TOKEN"
|
|
14
|
+
AUTH_HEADER_KEYS = %w[
|
|
15
|
+
authorization
|
|
16
|
+
proxy-authorization
|
|
17
|
+
x-api-key
|
|
18
|
+
x-auth-token
|
|
19
|
+
].freeze
|
|
20
|
+
AUTH_SCHEME_HEADER_KEYS = %w[
|
|
21
|
+
authorization
|
|
22
|
+
proxy-authorization
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
13
25
|
def initialize(last_request:, redacted_headers: nil)
|
|
14
26
|
@last_request = last_request || {}
|
|
15
27
|
@redacted_headers = normalize_redacted_headers(redacted_headers || Config::DEFAULT_REDACT_HEADERS)
|
|
@@ -40,7 +52,7 @@ module RSpec
|
|
|
40
52
|
return nil if headers.nil? || headers.empty?
|
|
41
53
|
|
|
42
54
|
headers.sort_by { |key, _| key.to_s.downcase }
|
|
43
|
-
.map { |key, value|
|
|
55
|
+
.map { |key, value| format_header_option(key, redacted_value(key, value)) }
|
|
44
56
|
.join(" ")
|
|
45
57
|
end
|
|
46
58
|
|
|
@@ -63,13 +75,40 @@ module RSpec
|
|
|
63
75
|
|
|
64
76
|
def redacted_value(key, value)
|
|
65
77
|
return value unless @redacted_headers.include?(key.to_s.downcase)
|
|
78
|
+
return auth_header_placeholder(key, value) if auth_header?(key)
|
|
66
79
|
|
|
67
80
|
"[REDACTED]"
|
|
68
81
|
end
|
|
69
82
|
|
|
83
|
+
def auth_header?(key)
|
|
84
|
+
AUTH_HEADER_KEYS.include?(key.to_s.downcase)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def auth_header_placeholder(key, value)
|
|
88
|
+
value_string = value.to_s
|
|
89
|
+
if AUTH_SCHEME_HEADER_KEYS.include?(key.to_s.downcase)
|
|
90
|
+
scheme_match = value_string.match(/\A([A-Za-z][A-Za-z0-9._-]*)\s+/)
|
|
91
|
+
return "#{scheme_match[1]} $#{AUTH_TOKEN_ENV_VAR}" if scheme_match
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
"$#{AUTH_TOKEN_ENV_VAR}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def format_header_option(key, value)
|
|
98
|
+
header = "#{key}: #{value}"
|
|
99
|
+
return %(-H #{shell_escape_with_env_expansion(header)}) if value.to_s.include?("$#{AUTH_TOKEN_ENV_VAR}")
|
|
100
|
+
|
|
101
|
+
%(-H #{shell_escape(header)})
|
|
102
|
+
end
|
|
103
|
+
|
|
70
104
|
def shell_escape(value)
|
|
71
105
|
"'#{value.to_s.gsub("'", %q('"'"'))}'"
|
|
72
106
|
end
|
|
107
|
+
|
|
108
|
+
def shell_escape_with_env_expansion(value)
|
|
109
|
+
escaped = value.to_s.gsub("\\", "\\\\").gsub('"', '\"').gsub("`", "\\`")
|
|
110
|
+
"\"#{escaped}\""
|
|
111
|
+
end
|
|
73
112
|
end
|
|
74
113
|
end
|
|
75
114
|
end
|
data/lib/rspec/rest/version.rb
CHANGED
data/lib/rspec/rest.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-rest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack-test
|
|
@@ -111,6 +111,7 @@ files:
|
|
|
111
111
|
- lib/rspec/rest/config.rb
|
|
112
112
|
- lib/rspec/rest/contract_expectations.rb
|
|
113
113
|
- lib/rspec/rest/contract_matcher.rb
|
|
114
|
+
- lib/rspec/rest/deprecation.rb
|
|
114
115
|
- lib/rspec/rest/dsl.rb
|
|
115
116
|
- lib/rspec/rest/error_expectations.rb
|
|
116
117
|
- lib/rspec/rest/errors.rb
|