spectre-core 1.8.4 → 1.12.0

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