spectre-core 1.11.0 → 1.12.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/exe/spectre +516 -524
- data/lib/spectre/assertion.rb +33 -14
- data/lib/spectre/bag.rb +21 -19
- data/lib/spectre/curl.rb +397 -391
- data/lib/spectre/diagnostic.rb +39 -29
- data/lib/spectre/environment.rb +30 -26
- data/lib/spectre/helpers.rb +133 -134
- data/lib/spectre/http/basic_auth.rb +5 -2
- data/lib/spectre/http/keystone.rb +76 -73
- data/lib/spectre/http.rb +82 -76
- data/lib/spectre/logger/console.rb +143 -142
- data/lib/spectre/logger/file.rb +1 -1
- data/lib/spectre/logger.rb +3 -1
- data/lib/spectre/mixin.rb +58 -34
- data/lib/spectre/reporter/console.rb +102 -104
- data/lib/spectre/reporter/junit.rb +100 -100
- data/lib/spectre/resources.rb +49 -46
- data/lib/spectre.rb +440 -438
- metadata +3 -3
data/lib/spectre/curl.rb
CHANGED
@@ -1,391 +1,397 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
alias_method :
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
#
|
139
|
-
# '
|
140
|
-
# '
|
141
|
-
# '
|
142
|
-
# '
|
143
|
-
# '
|
144
|
-
# '
|
145
|
-
# '
|
146
|
-
# '
|
147
|
-
# '
|
148
|
-
#
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
@@
|
155
|
-
@@
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
req['base_url']
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
uri
|
247
|
-
end
|
248
|
-
|
249
|
-
if req['
|
250
|
-
uri += '?'
|
251
|
-
uri += req['
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
#
|
265
|
-
|
266
|
-
|
267
|
-
end
|
268
|
-
|
269
|
-
# Add
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
@@
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
1
|
+
require_relative '../spectre'
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
|
7
|
+
module Spectre::Curl
|
8
|
+
class SpectreHttpRequest < Spectre::DslClass
|
9
|
+
def initialize request
|
10
|
+
@__req = request
|
11
|
+
end
|
12
|
+
|
13
|
+
def method method_name
|
14
|
+
@__req['method'] = method_name.upcase
|
15
|
+
end
|
16
|
+
|
17
|
+
def url base_url
|
18
|
+
@__req['base_url'] = base_url
|
19
|
+
end
|
20
|
+
|
21
|
+
def path url_path
|
22
|
+
@__req['path'] = url_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def header name, value
|
26
|
+
@__req['headers'] = [] unless @__req['headers']
|
27
|
+
@__req['headers'].append [name, value.to_s.strip]
|
28
|
+
end
|
29
|
+
|
30
|
+
def param name, value
|
31
|
+
@__req['query'] = [] unless @__req['query']
|
32
|
+
@__req['query'].append [name, value.to_s.strip]
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_type media_type
|
36
|
+
@__req['headers'] = [] unless @__req['headers']
|
37
|
+
@__req['headers'].append ['Content-Type', media_type]
|
38
|
+
end
|
39
|
+
|
40
|
+
def json data
|
41
|
+
body JSON.pretty_generate(data)
|
42
|
+
content_type 'application/json'
|
43
|
+
end
|
44
|
+
|
45
|
+
def body body_content
|
46
|
+
@__req['body'] = body_content
|
47
|
+
end
|
48
|
+
|
49
|
+
def ensure_success!
|
50
|
+
@__req['ensure_success'] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure_success?
|
54
|
+
@__req['ensure_success']
|
55
|
+
end
|
56
|
+
|
57
|
+
def authenticate method
|
58
|
+
@__req['auth'] = method
|
59
|
+
end
|
60
|
+
|
61
|
+
def certificate path
|
62
|
+
@__req['cert'] = path
|
63
|
+
use_ssl!
|
64
|
+
end
|
65
|
+
|
66
|
+
def use_ssl!
|
67
|
+
@__req['use_ssl'] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :auth, :authenticate
|
71
|
+
alias_method :cert, :certificate
|
72
|
+
alias_method :media_type, :content_type
|
73
|
+
end
|
74
|
+
|
75
|
+
class SpectreHttpHeader
|
76
|
+
def initialize headers
|
77
|
+
@headers = headers || {}
|
78
|
+
end
|
79
|
+
|
80
|
+
def [] key
|
81
|
+
@headers[key.downcase]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class SpectreHttpResponse
|
86
|
+
def initialize res
|
87
|
+
@res = res
|
88
|
+
@data = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def code
|
92
|
+
@res[:code]
|
93
|
+
end
|
94
|
+
|
95
|
+
def message
|
96
|
+
@res[:message]
|
97
|
+
end
|
98
|
+
|
99
|
+
def protocol
|
100
|
+
@res[:protocol]
|
101
|
+
end
|
102
|
+
|
103
|
+
def version
|
104
|
+
@res[:version]
|
105
|
+
end
|
106
|
+
|
107
|
+
def headers
|
108
|
+
SpectreHttpHeader.new @res[:headers]
|
109
|
+
end
|
110
|
+
|
111
|
+
def body
|
112
|
+
@res[:body]
|
113
|
+
end
|
114
|
+
|
115
|
+
def json
|
116
|
+
return nil unless @res[:body]
|
117
|
+
|
118
|
+
if @data == nil
|
119
|
+
begin
|
120
|
+
@data = JSON.parse(@res[:body], object_class: OpenStruct)
|
121
|
+
rescue
|
122
|
+
raise 'invalid json'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
@data
|
127
|
+
end
|
128
|
+
|
129
|
+
def success?
|
130
|
+
@res[:code] < 400
|
131
|
+
end
|
132
|
+
|
133
|
+
def pretty
|
134
|
+
@res.pretty
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# DEFAULT_HTTP_REQUEST = {
|
139
|
+
# 'method' => 'GET', # -X, --request <cmd>
|
140
|
+
# 'base_url' => nil,
|
141
|
+
# 'path' => nil,
|
142
|
+
# 'headers' => nil, # -H, --header <header/@file>
|
143
|
+
# 'query' => nil,
|
144
|
+
# 'body' => nil, # -d, --data <data>
|
145
|
+
# 'cert' => nil, # --cacert
|
146
|
+
# 'follow' => false, # -L, --location
|
147
|
+
# 'username' => nil, # -u, --user <user:password>
|
148
|
+
# 'password' => nil,
|
149
|
+
# 'use_ssl' => false, # -k
|
150
|
+
# }
|
151
|
+
|
152
|
+
|
153
|
+
class << self
|
154
|
+
@@http_cfg = {}
|
155
|
+
@@response = nil
|
156
|
+
@@request = nil
|
157
|
+
@@modules = []
|
158
|
+
|
159
|
+
def curl name, secure: false, &block
|
160
|
+
req = {
|
161
|
+
'use_ssl' => secure,
|
162
|
+
}
|
163
|
+
|
164
|
+
if @@http_cfg.key? name
|
165
|
+
req.merge! @@http_cfg[name]
|
166
|
+
raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." unless req['base_url']
|
167
|
+
else
|
168
|
+
req['base_url'] = name
|
169
|
+
end
|
170
|
+
|
171
|
+
SpectreHttpRequest.new(req)._evaluate(&block) if block_given?
|
172
|
+
|
173
|
+
invoke(req)
|
174
|
+
end
|
175
|
+
|
176
|
+
def curl_request
|
177
|
+
raise 'No request has been invoked yet' unless @@request
|
178
|
+
|
179
|
+
@@request
|
180
|
+
end
|
181
|
+
|
182
|
+
def curl_response
|
183
|
+
raise 'There is no response. No request has been invoked yet.' unless @@response
|
184
|
+
|
185
|
+
@@response
|
186
|
+
end
|
187
|
+
|
188
|
+
def register mod
|
189
|
+
raise 'Module must not be nil' unless mod
|
190
|
+
|
191
|
+
@@modules << mod
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def try_format_json str, pretty: false
|
197
|
+
return str unless str or str.empty?
|
198
|
+
|
199
|
+
begin
|
200
|
+
json = JSON.parse(str)
|
201
|
+
json.obfuscate!(@@secure_keys) unless @@debug
|
202
|
+
|
203
|
+
if pretty
|
204
|
+
str = JSON.pretty_generate(json)
|
205
|
+
else
|
206
|
+
str = JSON.dump(json)
|
207
|
+
end
|
208
|
+
rescue
|
209
|
+
# do nothing
|
210
|
+
end
|
211
|
+
|
212
|
+
str
|
213
|
+
end
|
214
|
+
|
215
|
+
def secure? key
|
216
|
+
@@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase }
|
217
|
+
end
|
218
|
+
|
219
|
+
def header_to_s headers
|
220
|
+
s = ''
|
221
|
+
|
222
|
+
return s unless headers
|
223
|
+
|
224
|
+
headers.each do |header|
|
225
|
+
key = header[0].to_s
|
226
|
+
value = header[1].to_s
|
227
|
+
value = '*****' if secure?(key) and not @@debug
|
228
|
+
s += "#{key.ljust(30, '.')}: #{value}\n"
|
229
|
+
end
|
230
|
+
|
231
|
+
s
|
232
|
+
end
|
233
|
+
|
234
|
+
def invoke req
|
235
|
+
cmd = [@@curl_path]
|
236
|
+
|
237
|
+
if req['cert'] or req['use_ssl']
|
238
|
+
scheme = 'https'
|
239
|
+
else
|
240
|
+
scheme = 'http'
|
241
|
+
end
|
242
|
+
|
243
|
+
uri = req['base_url']
|
244
|
+
|
245
|
+
unless uri.match /http(?:s)?:\/\//
|
246
|
+
uri = scheme + '://' + uri
|
247
|
+
end
|
248
|
+
|
249
|
+
if req['path']
|
250
|
+
uri += '/' unless uri.end_with? '/'
|
251
|
+
uri += req['path']
|
252
|
+
end
|
253
|
+
|
254
|
+
if req['query']
|
255
|
+
uri += '?'
|
256
|
+
uri += req['query']
|
257
|
+
.map { |x| x.join '='}
|
258
|
+
.join('&')
|
259
|
+
end
|
260
|
+
|
261
|
+
cmd.append('"' + uri + '"')
|
262
|
+
cmd.append('-X', req['method']) unless req['method'] == 'GET' or (req['body'] and req['method'] == 'POST')
|
263
|
+
|
264
|
+
# Call all registered modules
|
265
|
+
@@modules.each do |mod|
|
266
|
+
mod.on_req(req, cmd) if mod.respond_to? :on_req
|
267
|
+
end
|
268
|
+
|
269
|
+
# Add headers to curl command
|
270
|
+
req['headers'].each do |header|
|
271
|
+
cmd.append('-H', '"' + header.join(':') + '"')
|
272
|
+
end if req['headers']
|
273
|
+
|
274
|
+
# Add request body
|
275
|
+
if req['body'] != nil and not req['body'].empty?
|
276
|
+
req_body = try_format_json(req['body']).gsub(/"/, '\\"')
|
277
|
+
cmd.append('-d', '"' + req_body + '"')
|
278
|
+
elsif ['POST', 'PUT', 'PATCH'].include? req['method'].upcase
|
279
|
+
cmd.append('-d', '"\n"')
|
280
|
+
end
|
281
|
+
|
282
|
+
# Add certificate path if one if given
|
283
|
+
if req['cert']
|
284
|
+
raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
|
285
|
+
|
286
|
+
cmd.append('--cacert', req['cert'])
|
287
|
+
elsif req['use_ssl'] or uri.start_with? 'https'
|
288
|
+
cmd.append('-k')
|
289
|
+
end
|
290
|
+
|
291
|
+
cmd.append('-i')
|
292
|
+
cmd.append('-v')
|
293
|
+
|
294
|
+
@@request = OpenStruct.new(req)
|
295
|
+
|
296
|
+
sys_cmd = cmd.join(' ')
|
297
|
+
|
298
|
+
@@logger.debug(sys_cmd)
|
299
|
+
|
300
|
+
req_id = SecureRandom.uuid()[0..5]
|
301
|
+
|
302
|
+
req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
|
303
|
+
req_log += header_to_s(req['headers'])
|
304
|
+
req_log += try_format_json(req['body'], pretty: true)
|
305
|
+
|
306
|
+
@@logger.info(req_log)
|
307
|
+
|
308
|
+
start_time = Time.now
|
309
|
+
|
310
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(sys_cmd)
|
311
|
+
|
312
|
+
end_time = Time.now
|
313
|
+
|
314
|
+
output = stdout.gets(nil)
|
315
|
+
stdout.close
|
316
|
+
|
317
|
+
debug_log = stderr.gets(nil)
|
318
|
+
stderr.close
|
319
|
+
|
320
|
+
# debug_log.lines.each { |x| @@logger.debug x unless x.empty? }
|
321
|
+
|
322
|
+
raise "Unable to request #{uri}. Please check if this URL is correctly configured and reachable." unless output
|
323
|
+
|
324
|
+
@@logger.debug("[<] #{req_id} stdout:\n#{output}")
|
325
|
+
|
326
|
+
header, body = output.split /\r?\n\r?\n/
|
327
|
+
|
328
|
+
result = header.lines.first
|
329
|
+
|
330
|
+
exit_code = wait_thr.value.exitstatus
|
331
|
+
|
332
|
+
raise Exception.new "An error occured while executing curl:\n#{debug_log.lines.map { |x| not x.empty? }}" unless exit_code == 0
|
333
|
+
|
334
|
+
# Parse protocol, version, status code and status message from response
|
335
|
+
match = /^(?<protocol>[A-Za-z0-9]+)\/(?<version>\d+\.?\d*) (?<code>\d+) (?<message>.*)/.match result
|
336
|
+
|
337
|
+
raise "Unexpected result from curl request:\n#{result}" unless match
|
338
|
+
|
339
|
+
res_headers = header.lines[1..-1]
|
340
|
+
.map { |x| /^(?<key>[A-Za-z0-9-]+):\s*(?<value>.*)$/.match x }
|
341
|
+
.select { |x| x != nil }
|
342
|
+
.map { |x| [x[:key].downcase, x[:value]] }
|
343
|
+
|
344
|
+
res = {
|
345
|
+
protocol: match[:protocol],
|
346
|
+
version: match[:version],
|
347
|
+
code: match[:code].to_i,
|
348
|
+
message: match[:message],
|
349
|
+
headers: Hash[res_headers],
|
350
|
+
body: body,
|
351
|
+
}
|
352
|
+
|
353
|
+
# Call all registered modules
|
354
|
+
@@modules.each do |mod|
|
355
|
+
mod.on_res(res, output) if mod.respond_to? :on_res
|
356
|
+
end
|
357
|
+
|
358
|
+
res_log = "[<] #{req_id} #{res[:code]} #{res[:message]} (#{end_time - start_time}s)\n"
|
359
|
+
res_headers.each do |header|
|
360
|
+
res_log += "#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}\n"
|
361
|
+
end
|
362
|
+
|
363
|
+
if res[:body] != nil and not res[:body].empty?
|
364
|
+
res_log += try_format_json(res[:body], pretty: true)
|
365
|
+
end
|
366
|
+
|
367
|
+
@@logger.info res_log
|
368
|
+
|
369
|
+
@@response = SpectreHttpResponse.new(res)
|
370
|
+
|
371
|
+
raise "Response did not indicate success: #{@@response.code} #{@@response.message}" if req['ensure_success'] and not @@response.success?
|
372
|
+
|
373
|
+
@@response
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
Spectre.register do |config|
|
378
|
+
@@debug = config['debug']
|
379
|
+
|
380
|
+
@@logger = ::Logger.new(config['log_file'], progname: 'spectre/curl')
|
381
|
+
@@logger.level = @@debug ? Logger::DEBUG : Logger::INFO
|
382
|
+
|
383
|
+
@@secure_keys = config['secure_keys'] || []
|
384
|
+
|
385
|
+
@@curl_path = config['curl_path'] || 'curl'
|
386
|
+
|
387
|
+
if config.key? 'http'
|
388
|
+
@@http_cfg = {}
|
389
|
+
|
390
|
+
config['http'].each do |name, cfg|
|
391
|
+
@@http_cfg[name] = cfg
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
Spectre.delegate :curl, :curl_response, :curl_request, to: self
|
397
|
+
end
|