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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d00a1ef08b51bc62ee1b737217eb5f441ee625ca3cbd848d0db1cbacff619f2
4
- data.tar.gz: 6507ec718a8f8a0ecf5de375e2efba9f3d686cc577258baf89a05d9ccc47d64d
3
+ metadata.gz: 47e2dfd9b5b01a159c9d81dffc650b0a807d6491669100f3e4c0939b9012eb3e
4
+ data.tar.gz: 0b5f6d62d08a5139fa60ce2d2a193d92949193767a08378f5212e576bc762f50
5
5
  SHA512:
6
- metadata.gz: 742d99b9ceecc699b920038a11476f304a657b1023599d819fc69fd17f19154387745ceffeabda7cc98b36ccf065ba6c164f2eb3c618c34822e68ecd9e4f50b6
7
- data.tar.gz: 6c80fa3a4b6f088172aa9b795ff8fc48e82cc7f64052bdf3b0028e3741983d6c73088bb72a29be44f9575da1ecc57c70b97d4a8f428b96f4385a5aded4805e81
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-rest (0.2.0)
4
+ rspec-rest (0.3.0)
5
5
  rack-test (~> 2.1)
6
6
 
7
7
  GEM
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 example below shows the same behavior test written two ways.
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
- with_query locale: "en"
117
- with_headers "X-Tenant-Id" => "tenant-123"
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(expect_json_contract(:post_summary))
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
- - optional form: `get(path, description = nil) { ... }`
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
- - `expect_json_contract(name)`
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 `expect_json_contract`.
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(expect_json_contract(:post_summary))
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
- "expect_json_contract requires a contract name that responds to #to_sym."
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
@@ -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, description = nil, &block|
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 build_example_name(method:, path:, resource_path:, description:)
120
- route = compose_route_for_example(resource_path: resource_path, endpoint_path: path)
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
- "#{base} - #{normalized_description}"
187
+ send(:warn_on_deprecated_positional_description, method)
126
188
  end
127
189
 
128
- def compose_route_for_example(resource_path:, endpoint_path:)
129
- PathComposer.compose(
130
- base_path: rest_config.base_path,
131
- resource_path: resource_path,
132
- endpoint_path: endpoint_path
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| %(-H #{shell_escape("#{key}: #{redacted_value(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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Rest
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/rspec/rest.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "rest/version"
4
4
  require_relative "rest/errors"
5
+ require_relative "rest/deprecation"
5
6
  require_relative "rest/config"
6
7
  require_relative "rest/response"
7
8
  require_relative "rest/session"
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.2.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-08 00:00:00.000000000 Z
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