spectre-core 1.9.0 → 1.12.1
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 +51 -46
- data/lib/spectre/assertion.rb +50 -37
- data/lib/spectre/bag.rb +4 -2
- data/lib/spectre/curl.rb +62 -33
- data/lib/spectre/diagnostic.rb +12 -2
- data/lib/spectre/environment.rb +9 -5
- data/lib/spectre/helpers.rb +81 -22
- data/lib/spectre/http/basic_auth.rb +5 -2
- data/lib/spectre/http/keystone.rb +76 -73
- data/lib/spectre/http.rb +373 -359
- data/lib/spectre/logger/console.rb +7 -6
- data/lib/spectre/logger/file.rb +96 -96
- data/lib/spectre/logger.rb +146 -144
- data/lib/spectre/mixin.rb +35 -18
- data/lib/spectre/reporter/console.rb +3 -5
- data/lib/spectre/reporter/junit.rb +5 -3
- data/lib/spectre/resources.rb +7 -4
- data/lib/spectre.rb +58 -45
- metadata +26 -12
data/lib/spectre/http.rb
CHANGED
@@ -1,359 +1,373 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
require '
|
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
|
-
@__req['
|
46
|
-
end
|
47
|
-
|
48
|
-
def
|
49
|
-
@__req['
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
def
|
121
|
-
@
|
122
|
-
end
|
123
|
-
|
124
|
-
def
|
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
|
-
end
|
157
|
-
|
158
|
-
def
|
159
|
-
@
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
|
164
|
-
class << self
|
165
|
-
@@http_cfg = {}
|
166
|
-
@@response = nil
|
167
|
-
@@request = nil
|
168
|
-
@@modules = []
|
169
|
-
@@secure_keys = []
|
170
|
-
|
171
|
-
def https name, &block
|
172
|
-
http(name, secure: true, &block)
|
173
|
-
end
|
174
|
-
|
175
|
-
def http name, secure:
|
176
|
-
req = {}
|
177
|
-
|
178
|
-
if @@http_cfg.key? name
|
179
|
-
req.
|
180
|
-
raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment."
|
181
|
-
else
|
182
|
-
req['base_url'] = name
|
183
|
-
end
|
184
|
-
|
185
|
-
req['
|
186
|
-
|
187
|
-
SpectreHttpRequest.new(req).
|
188
|
-
|
189
|
-
invoke(req)
|
190
|
-
end
|
191
|
-
|
192
|
-
def request
|
193
|
-
raise 'No request has been invoked yet' unless @@request
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
@@response
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
base_url
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
end
|
355
|
-
end
|
356
|
-
|
357
|
-
Spectre.
|
358
|
-
|
359
|
-
|
1
|
+
require_relative '../spectre'
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'openssl'
|
5
|
+
require 'json'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'logger'
|
8
|
+
require 'ostruct'
|
9
|
+
|
10
|
+
module Spectre
|
11
|
+
module Http
|
12
|
+
DEFAULT_HTTP_CONFIG = {
|
13
|
+
'method' => 'GET',
|
14
|
+
'path' => '',
|
15
|
+
'host' => nil,
|
16
|
+
'port' => 80,
|
17
|
+
'scheme' => 'http',
|
18
|
+
'use_ssl' => false,
|
19
|
+
'cert' => nil,
|
20
|
+
'headers' => nil,
|
21
|
+
'query' => nil,
|
22
|
+
'content_type' => '',
|
23
|
+
'timeout' => 180,
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
@@modules = []
|
27
|
+
|
28
|
+
class HttpError < Exception
|
29
|
+
def initialize message
|
30
|
+
super message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class SpectreHttpRequest < Spectre::DslClass
|
35
|
+
class Headers
|
36
|
+
CONTENT_TYPE = 'Content-Type'
|
37
|
+
UNIQUE_HEADERS = [CONTENT_TYPE].freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize request
|
41
|
+
@__req = request
|
42
|
+
end
|
43
|
+
|
44
|
+
def method method_name
|
45
|
+
@__req['method'] = method_name.upcase
|
46
|
+
end
|
47
|
+
|
48
|
+
def url base_url
|
49
|
+
@__req['base_url'] = base_url
|
50
|
+
end
|
51
|
+
|
52
|
+
def path url_path
|
53
|
+
@__req['path'] = url_path
|
54
|
+
end
|
55
|
+
|
56
|
+
def timeout seconds
|
57
|
+
@__req['timeout'] = seconds
|
58
|
+
end
|
59
|
+
|
60
|
+
def header name, value
|
61
|
+
@__req['headers'] ||= []
|
62
|
+
@__req['headers'].append [name, value.to_s.strip]
|
63
|
+
end
|
64
|
+
|
65
|
+
def param name, value
|
66
|
+
@__req['query'] ||= []
|
67
|
+
@__req['query'].append [name, value.to_s.strip]
|
68
|
+
end
|
69
|
+
|
70
|
+
def content_type media_type
|
71
|
+
@__req['content_type'] = media_type
|
72
|
+
end
|
73
|
+
|
74
|
+
def json data
|
75
|
+
data = data.to_h if data.is_a? OpenStruct
|
76
|
+
body JSON.pretty_generate(data)
|
77
|
+
|
78
|
+
# TODO: Only set content type, if not explicitly set
|
79
|
+
content_type('application/json')
|
80
|
+
end
|
81
|
+
|
82
|
+
def body body_content
|
83
|
+
@__req['body'] = body_content.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def ensure_success!
|
87
|
+
@__req['ensure_success'] = true
|
88
|
+
end
|
89
|
+
|
90
|
+
def ensure_success?
|
91
|
+
@__req['ensure_success']
|
92
|
+
end
|
93
|
+
|
94
|
+
def authenticate method
|
95
|
+
@__req['auth'] = method
|
96
|
+
end
|
97
|
+
|
98
|
+
def no_auth!
|
99
|
+
@__req['auth'] = 'none'
|
100
|
+
end
|
101
|
+
|
102
|
+
def certificate path
|
103
|
+
@__req['cert'] = path
|
104
|
+
end
|
105
|
+
|
106
|
+
def use_ssl!
|
107
|
+
@__req['use_ssl'] = true
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
@__req.to_s
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :auth, :authenticate
|
115
|
+
alias_method :cert, :certificate
|
116
|
+
alias_method :media_type, :content_type
|
117
|
+
end
|
118
|
+
|
119
|
+
class SpectreHttpHeader
|
120
|
+
def initialize headers
|
121
|
+
@headers = headers || {}
|
122
|
+
end
|
123
|
+
|
124
|
+
def [] key
|
125
|
+
return nil unless @headers.key?(key.downcase)
|
126
|
+
|
127
|
+
@headers[key.downcase].first
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
@headers.to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class SpectreHttpResponse
|
136
|
+
attr_reader :code, :message, :headers, :body
|
137
|
+
|
138
|
+
def initialize net_res
|
139
|
+
@code = net_res.code.to_i
|
140
|
+
@message = net_res.message
|
141
|
+
@body = net_res.body
|
142
|
+
@headers = SpectreHttpHeader.new(net_res.to_hash)
|
143
|
+
@json_data = nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def json
|
147
|
+
if !@body.nil? and @json_data.nil?
|
148
|
+
begin
|
149
|
+
@json_data = JSON.parse(@body, object_class: OpenStruct)
|
150
|
+
rescue JSON::ParserError
|
151
|
+
raise HttpError.new("Body content is not a valid JSON:\n#{@body}")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
@json_data
|
156
|
+
end
|
157
|
+
|
158
|
+
def success?
|
159
|
+
@code < 400
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
class << self
|
165
|
+
@@http_cfg = {}
|
166
|
+
@@response = nil
|
167
|
+
@@request = nil
|
168
|
+
@@modules = []
|
169
|
+
@@secure_keys = []
|
170
|
+
|
171
|
+
def https name, &block
|
172
|
+
http(name, secure: true, &block)
|
173
|
+
end
|
174
|
+
|
175
|
+
def http name, secure: false, &block
|
176
|
+
req = {}
|
177
|
+
|
178
|
+
if @@http_cfg.key? name
|
179
|
+
req.deep_merge! @@http_cfg[name].deep_clone
|
180
|
+
raise HttpError.new("No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment.") unless req['base_url']
|
181
|
+
else
|
182
|
+
req['base_url'] = name
|
183
|
+
end
|
184
|
+
|
185
|
+
req['use_ssl'] = secure unless secure.nil?
|
186
|
+
|
187
|
+
SpectreHttpRequest.new(req)._evaluate(&block) if block_given?
|
188
|
+
|
189
|
+
invoke(req)
|
190
|
+
end
|
191
|
+
|
192
|
+
def request
|
193
|
+
raise 'No request has been invoked yet' unless @@request
|
194
|
+
|
195
|
+
@@request
|
196
|
+
end
|
197
|
+
|
198
|
+
def response
|
199
|
+
raise 'There is no response. No request has been invoked yet.' unless @@response
|
200
|
+
|
201
|
+
@@response
|
202
|
+
end
|
203
|
+
|
204
|
+
def register mod
|
205
|
+
raise 'Module must not be nil' unless mod
|
206
|
+
|
207
|
+
@@modules << mod
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def try_format_json str, pretty: false
|
213
|
+
return str unless str or str.empty?
|
214
|
+
|
215
|
+
begin
|
216
|
+
json = JSON.parse(str)
|
217
|
+
json.obfuscate!(@@secure_keys) unless @@debug
|
218
|
+
|
219
|
+
if pretty
|
220
|
+
str = JSON.pretty_generate(json)
|
221
|
+
else
|
222
|
+
str = JSON.dump(json)
|
223
|
+
end
|
224
|
+
rescue
|
225
|
+
# do nothing
|
226
|
+
end
|
227
|
+
|
228
|
+
str
|
229
|
+
end
|
230
|
+
|
231
|
+
def secure? key
|
232
|
+
@@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase }
|
233
|
+
end
|
234
|
+
|
235
|
+
def header_to_s headers
|
236
|
+
s = ''
|
237
|
+
headers.each_header.each do |header, value|
|
238
|
+
value = '*****' if secure?(header) and not @@debug
|
239
|
+
s += "#{header.to_s.ljust(30, '.')}: #{value.to_s}\n"
|
240
|
+
end
|
241
|
+
s
|
242
|
+
end
|
243
|
+
|
244
|
+
def invoke req
|
245
|
+
@@request = nil
|
246
|
+
|
247
|
+
# Build URI
|
248
|
+
|
249
|
+
scheme = req['use_ssl'] ? 'https' : 'http'
|
250
|
+
base_url = req['base_url']
|
251
|
+
|
252
|
+
unless base_url.match /http(?:s)?:\/\//
|
253
|
+
base_url = scheme + '://' + base_url
|
254
|
+
end
|
255
|
+
|
256
|
+
if req['path']
|
257
|
+
base_url = base_url + '/' unless base_url.end_with? '/'
|
258
|
+
base_url += req['path']
|
259
|
+
end
|
260
|
+
|
261
|
+
uri = URI(base_url)
|
262
|
+
|
263
|
+
raise HttpError.new("'#{uri}' is not a valid uri") unless uri.host
|
264
|
+
|
265
|
+
# Build query parameters
|
266
|
+
|
267
|
+
uri.query = URI.encode_www_form(req['query']) unless not req['query'] or req['query'].empty?
|
268
|
+
|
269
|
+
# Create HTTP client
|
270
|
+
|
271
|
+
net_http = Net::HTTP.new(uri.host, uri.port)
|
272
|
+
net_http.read_timeout = req['timeout']
|
273
|
+
|
274
|
+
if uri.scheme == 'https'
|
275
|
+
net_http.use_ssl = true
|
276
|
+
|
277
|
+
if req.key? 'cert'
|
278
|
+
raise HttpError.new("Certificate '#{req['cert']}' does not exist") unless File.exists? req['cert']
|
279
|
+
|
280
|
+
net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
281
|
+
net_http.ca_file = req['cert']
|
282
|
+
else
|
283
|
+
net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Create HTTP Request
|
288
|
+
|
289
|
+
net_req = Net::HTTPGenericRequest.new(req['method'], true, true, uri)
|
290
|
+
net_req.body = req['body']
|
291
|
+
net_req.content_type = req['content_type'] if req['content_type'] and not req['content_type'].empty?
|
292
|
+
|
293
|
+
if req['headers']
|
294
|
+
req['headers'].each do |header|
|
295
|
+
net_req[header[0]] = header[1]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
req_id = SecureRandom.uuid()[0..5]
|
300
|
+
|
301
|
+
# Run HTTP modules
|
302
|
+
|
303
|
+
@@modules.each do |mod|
|
304
|
+
mod.on_req(net_http, net_req, req) if mod.respond_to? :on_req
|
305
|
+
end
|
306
|
+
|
307
|
+
# Log request
|
308
|
+
|
309
|
+
req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
|
310
|
+
req_log += header_to_s(net_req)
|
311
|
+
req_log += try_format_json(req['body'], pretty: true) if req['body'] != nil and not req['body'].empty?
|
312
|
+
|
313
|
+
@@logger.info(req_log)
|
314
|
+
|
315
|
+
# Request
|
316
|
+
|
317
|
+
start_time = Time.now
|
318
|
+
|
319
|
+
begin
|
320
|
+
net_res = net_http.request(net_req)
|
321
|
+
rescue SocketError => e
|
322
|
+
raise HttpError.new("The request '#{req['method']} #{uri}' failed. Please check if the given URL '#{uri}' is valid and available or a corresponding HTTP config in the environment file exists. See log for more details. Original.\nOriginal error was: #{e.message}")
|
323
|
+
rescue Net::ReadTimeout
|
324
|
+
raise HttpError.new("HTTP timeout of #{net_http.read_timeout}s exceeded")
|
325
|
+
end
|
326
|
+
|
327
|
+
end_time = Time.now
|
328
|
+
|
329
|
+
req['started_at'] = start_time
|
330
|
+
req['finished_at'] = end_time
|
331
|
+
|
332
|
+
# Run HTTP modules
|
333
|
+
|
334
|
+
@@modules.each do |mod|
|
335
|
+
mod.on_res(net_http, net_res, req) if mod.respond_to? :on_res
|
336
|
+
end
|
337
|
+
|
338
|
+
# Log response
|
339
|
+
|
340
|
+
res_log = "[<] #{req_id} #{net_res.code} #{net_res.message} (#{end_time - start_time}s)\n"
|
341
|
+
res_log += header_to_s(net_res)
|
342
|
+
res_log += try_format_json(net_res.body, pretty: true) unless net_res.body.nil? or net_res.body.empty?
|
343
|
+
|
344
|
+
@@logger.info(res_log)
|
345
|
+
|
346
|
+
fail "Response code of #{req_id} did not indicate success: #{net_res.code} #{net_res.message}" if req['ensure_success'] and net_res.code.to_i >= 400
|
347
|
+
|
348
|
+
# Set global request and response variables
|
349
|
+
|
350
|
+
@@request = OpenStruct.new(req)
|
351
|
+
@@request.freeze
|
352
|
+
|
353
|
+
@@response = SpectreHttpResponse.new(net_res)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
Spectre.register do |config|
|
358
|
+
@@logger = ::Logger.new(config['log_file'], progname: 'spectre/http')
|
359
|
+
@@secure_keys = config['secure_keys'] || []
|
360
|
+
@@debug = config['debug']
|
361
|
+
|
362
|
+
if config.key? 'http'
|
363
|
+
@@http_cfg = {}
|
364
|
+
|
365
|
+
config['http'].each do |name, cfg|
|
366
|
+
@@http_cfg[name] = cfg
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
Spectre.delegate :http, :https, :request, :response, to: self
|
372
|
+
end
|
373
|
+
end
|