testbeat 0.5.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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/spec_helper.rb +479 -0
  3. data/lib/testbeat.rb +14 -0
  4. metadata +88 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a07df1c8d2decd193451ed6563c37ecf5d77e4e6
4
+ data.tar.gz: efc84e08c072861996aca8d767fe7195da3e87d0
5
+ SHA512:
6
+ metadata.gz: 90f7258973368c942e722f6b1be18804ca132da3e05d5da189ac339f49705b7cb191ceb2268cdb65567a131578e8f7204085ab73bc91727754724384abea11e7
7
+ data.tar.gz: fc8a054528bedcbc506d183138d5414fdcd1d30dd69ef73dd900b6000f305e5fde425a04838d8b36dae9b1c68d169026638f601bdbc13905648637410959befb
@@ -0,0 +1,479 @@
1
+
2
+ require "awesome_print"
3
+ require "net/http"
4
+ require "openssl"
5
+ require "json"
6
+ require "hashie"
7
+ require "logger"
8
+
9
+ # TODO can we support http://www.relishapp.com/rspec/rspec-core/v/3-1/docs/example-groups/shared-context
10
+
11
+ # Node should have getters for repository and hosting specifics
12
+ # Configuration attributes for the node should are available as [:properties]
13
+
14
+ # global so we don't need to pass this around to utility classes
15
+ $logger = Logger.new('testbeat-debug.log')
16
+
17
+ class TestbeatNode
18
+
19
+ def initialize(nodename)
20
+ @name = nodename
21
+ if !@name || @name.length == 0
22
+ logger.error { "Node name not set, at #{Dir.pwd}" }
23
+ raise "Node name not set"
24
+ end
25
+ attributesFile = "#{folder}/chef.json"
26
+ if File.exists?(attributesFile)
27
+ @attributes = read_chef_attributes(attributesFile)
28
+ else
29
+ logger.warn { "Failed to locate node attributes, from #{attributesFile}, at #{Dir.pwd}" }
30
+ @attributes = nil
31
+ end
32
+ logger.info { "Node initialized #{nodename}, attributes from #{attributesFile}" }
33
+ end
34
+
35
+ def logger
36
+ $logger
37
+ end
38
+
39
+ def folder
40
+ "nodes/#{@name}"
41
+ end
42
+
43
+ def vagrant?
44
+ vagrantfile = "#{folder}/Vagrantfile"
45
+ return File.exists?(vagrantfile)
46
+ end
47
+
48
+ # Attributes defined specifically on the node, not aggregated like in chef runs
49
+ def attributes
50
+ @attributes
51
+ end
52
+
53
+ def attributes?
54
+ @attributes != nil
55
+ end
56
+
57
+ # Provides access to attributes, if available, as @node[key] much like in chef cookbooks
58
+ # Use .keys to see if a key exists
59
+ def [](key)
60
+ return nil if @attributes == nil
61
+ # Raise exception so test issues are clearly displayed in rspec output (puts is displayed before test output, difficult to identify which test it comes from)
62
+ raise "Missing attribute key '#{key}', got #{@attributes.keys}" if not @attributes.key?(key)
63
+ @attributes[key]
64
+ end
65
+
66
+ # More methods to work like hash
67
+ def keys
68
+ @attributes.keys
69
+ end
70
+
71
+ def key?(key)
72
+ @attributes.key?(key)
73
+ end
74
+
75
+ # host is assumed to be equivalent with node name now, but we could read it from attributes, see ticket:1017
76
+ def host
77
+ @name
78
+ end
79
+
80
+ # returns hash "username" and "password", or false if unsupported
81
+ def testauth
82
+ return false # we don't support authenticated nodes yet
83
+ end
84
+
85
+ # return command line access, instance of TestbeatNodeRsh, or false if unsupported
86
+ # This is probably easier to support than get_bats; on vagrant nodes we have 'vagrant ssh -c'
87
+ def shell
88
+ if not vagrant?
89
+ return TestbeatShellStub.new()
90
+ end
91
+ return TestbeatShellVagrant.new(folder)
92
+ end
93
+
94
+ def provision
95
+ if not vagrant?
96
+ raise "Provision support will probably require a vagrant box"
97
+ else
98
+ raise "Provision not implemented"
99
+ end
100
+ end
101
+
102
+
103
+ def to_s
104
+ "Testbeat node #{@name}"
105
+ #ap @attributes
106
+ end
107
+
108
+ # The following methods are private
109
+ private
110
+
111
+ # Returns node attributes from node file compatible with "knife node from file"
112
+ # Returns as Mash because that's what chef uses
113
+ def read_chef_attributes(jsonPath)
114
+ #p "read attributes from #{jsonPath}"
115
+ data = nil
116
+ File::open(jsonPath) { |f|
117
+ data = f.read
118
+ }
119
+ raise "Failed to read file #{jsonPath}" if data == nil
120
+ json = JSON.parse(data)
121
+ mash = Hashie::Mash.new(json)
122
+ raise "Missing 'normal' attributes in node file #{jsonPath}" if not mash.key?("normal")
123
+ return mash["normal"]
124
+ end
125
+
126
+ end
127
+
128
+ # Support different types of command execution on node
129
+ class TestbeatShell
130
+
131
+ def exec(cmd)
132
+ raise "Command execution on this node is not supported"
133
+ end
134
+
135
+ end
136
+
137
+ class TestbeatShellVagrant
138
+
139
+ def initialize(vagrantFolder)
140
+ @vagrantFolder = vagrantFolder
141
+ end
142
+
143
+ def exec(cmd)
144
+ $logger.debug {"Exec: #{cmd}"}
145
+ stdout = nil
146
+ Dir.chdir(@vagrantFolder){
147
+ @out = %x[vagrant ssh -c '#{cmd}' 2>&1]
148
+ @status = $?
149
+ }
150
+ $logger.debug {"Exec result #{exitstatus}: #{@out[0,50].strip}"}
151
+ return self
152
+ end
153
+
154
+ # output of latest exec (stdout and stderr merged, as we haven't worked on separating them)
155
+ def out
156
+ @out
157
+ end
158
+
159
+ def exitstatus
160
+ @status.exitstatus
161
+ end
162
+
163
+ def ok?
164
+ exitstatus == 0
165
+ end
166
+
167
+ end
168
+
169
+ class TestbeatShellStub
170
+
171
+ def initialize()
172
+ @hasrun = Array.new
173
+ end
174
+
175
+ def hasrun
176
+ @hasrun
177
+ end
178
+
179
+ def exec(cmd)
180
+ @hasrun.push(cmd)
181
+ $logger.warn "No guest shell available to exec: '#{cmd}'"
182
+ end
183
+
184
+ def out
185
+ "(No guest shell for current testbeat context)"
186
+ end
187
+
188
+ def exitstatus
189
+ @hasrun.length
190
+ end
191
+
192
+ def ok?
193
+ false
194
+ end
195
+ end
196
+
197
+ # Context has getters for test case parameters that can be digged out from Rspec examples,
198
+ # initialized for each "it"
199
+ class TestbeatContext
200
+
201
+ # reads context from RSpec example metadata
202
+ def initialize(example)
203
+ # enables de-duplication of requests within describe, using tostring to compare becase == wouldn't work
204
+ @context_block_id = "#{example.metadata[:example_group][:block]}"
205
+
206
+ # defaults
207
+ @user = { :username => 'testuser', :password => 'testpassword' }
208
+ @unencrypted = false
209
+ @unauthenticated = false
210
+ @rest = false
211
+ @reprovision = false
212
+
213
+ # actual context
214
+ parse_example_group(example.metadata[:example_group], 0)
215
+
216
+ @context_block_id_short = /([^\/]+)>$/.match(@context_block_id)[1]
217
+ logger.info{ "#{example.metadata[:example_group][:location]}: #{@rest ? @method : ''} #{@resource} #{@unencrypted ? '[unencrypted] ' : ' '}#{@unauthenticated ? '[unauthenticated] ' : ' '}" }
218
+ end
219
+
220
+ def logger
221
+ $logger
222
+ end
223
+
224
+ def context_block_id
225
+ @context_block_id
226
+ end
227
+
228
+ # Returns true if the current context has a REST request specified
229
+ def rest?
230
+ @rest
231
+ end
232
+
233
+ def nop?
234
+ !rest?
235
+ end
236
+
237
+ def user
238
+ @user
239
+ end
240
+
241
+ # Returns the REST resource if specified in context
242
+ def resource
243
+ if not rest?
244
+ return nil
245
+ end
246
+ @resource
247
+ end
248
+
249
+ def method
250
+ if not rest?
251
+ return nil
252
+ end
253
+ @method
254
+ end
255
+
256
+ def headers
257
+ @rest_headers
258
+ end
259
+
260
+ def headers?
261
+ !!headers
262
+ end
263
+
264
+ def body
265
+ @rest_body
266
+ end
267
+
268
+ def body?
269
+ !!body
270
+ end
271
+
272
+ def form
273
+ @rest_form
274
+ end
275
+
276
+ def form?
277
+ !!form
278
+ end
279
+
280
+ # Returns true if requests will be made without authentication even if the node expects authentication
281
+ def unauthenticated?
282
+ unencrypted? || @unauthenticated
283
+ end
284
+
285
+ def unencrypted?
286
+ @unencrypted
287
+ end
288
+
289
+ def reprovision?
290
+ @reprovision
291
+ end
292
+
293
+ def to_s
294
+ s = "Testbeat context"
295
+ if rest?
296
+ s += " #{method} #{resource}"
297
+ else
298
+ s += " non-REST"
299
+ end
300
+ if @unencrypted
301
+ s += " unencrypted"
302
+ elsif @unauthenticated
303
+ s += " unauthenticated"
304
+ end
305
+ s
306
+ end
307
+
308
+ private
309
+
310
+ def parse_example_group(example_group, uplevel)
311
+ if example_group[:parent_example_group]
312
+ parse_example_group(example_group[:parent_example_group], uplevel + 1)
313
+ end
314
+ logger.debug{ "Parsing context #{uplevel > 0 ? '-' : ' '}#{uplevel}: #{example_group[:description]}" }
315
+ parse_description_args(example_group[:description_args])
316
+ if rest?
317
+ if example_group[:body]
318
+ @rest_body = example_group[:body]
319
+ end
320
+ if example_group[:form]
321
+ @rest_form = example_group[:form]
322
+ end
323
+ if example_group[:headers]
324
+ @rest_headers = example_group[:headers]
325
+ end
326
+ end
327
+
328
+ end
329
+
330
+ def parse_description_args(example_group_description_args)
331
+ a = example_group_description_args[0]
332
+ /unencrypted/i.match(a) {
333
+ @unencrypted = true
334
+ }
335
+
336
+ /unauthenticated/i.match(a) {
337
+ @unauthenticated = true
338
+ }
339
+ # We could iterate methods in Net::HTTP and support all
340
+ # Change method to rest_method for consistency
341
+ /(GET|HEAD|POST)\s+(\S*)(.*)/.match(a) { |rest|
342
+ @rest = true
343
+ @method = rest[1]
344
+ @resource = rest[2]
345
+ }
346
+ /reprovision/i.match(a) {
347
+ @reprovision = true
348
+ }
349
+ # idea: nodes that should not be modified (production etc), particularily not through shell
350
+ #/untouchable/
351
+ end
352
+
353
+ end
354
+
355
+ $_testbeat_rest_reuse = Hash.new
356
+
357
+ class TestbeatRestRequest
358
+
359
+ def initialize(node, testbeat)
360
+ #@headers = {}
361
+ @timeout = 10
362
+ @node = node
363
+ @testbeat = testbeat
364
+ end
365
+
366
+ # Initiate the request and return Net::HTTPResponse object,
367
+ # supporting response [:responseHeaderName], .body (string), .code (int), .msg (string)
368
+ def run
369
+ reuse_id = @testbeat.context_block_id
370
+ previous = $_testbeat_rest_reuse[reuse_id]
371
+ if previous
372
+ @response = previous
373
+ @testbeat.logger.info{ "Request reused within #{reuse_id} responded #{@response.code} #{@response.message}" }
374
+ return @response
375
+ end
376
+ # If there's no built in auth support in Net::HTTP we can check for 401 here and re-run the request with auth header
377
+ Net::HTTP.start(@node.host,
378
+ :use_ssl => !@testbeat.unencrypted?,
379
+ :verify_mode => OpenSSL::SSL::VERIFY_NONE, # Ideally verify should be enabled for non-labs hosts (anything with a FQDN including dots)
380
+ :open_timeout => @timeout,
381
+ :read_timeout => @timeout
382
+ ) do |http|
383
+
384
+ req = Net::HTTP::Get.new(@testbeat.resource)
385
+ if @testbeat.method == 'POST'
386
+ req = Net::HTTP::Post.new(@testbeat.resource)
387
+ if @testbeat.form?
388
+ req.set_form_data(@testbeat.form)
389
+ end
390
+ if @testbeat.body?
391
+ req.body = @testbeat.body
392
+ end
393
+ end
394
+ if @testbeat.headers?
395
+ @testbeat.headers.each {|name, value| req[name] = value }
396
+ end
397
+
398
+ @response = http.request(req) # Net::HTTPResponse object
399
+
400
+ if @response.code == "401" and @testbeat.user and not @testbeat.unauthenticated?
401
+ u = @testbeat.user
402
+ @testbeat.logger.info{ "Authenticating to #{@testbeat.resource} with #{u[:username]}:#{u[:password]}" }
403
+ req.basic_auth u[:username], u[:password]
404
+ @response = http.request(req)
405
+ end
406
+
407
+ @testbeat.logger.info{ "Request #{@testbeat.resource} responded #{@response.code} #{@response.message}" }
408
+ $_testbeat_rest_reuse[reuse_id] = @response
409
+ return @response
410
+ end
411
+ end
412
+
413
+ end
414
+
415
+ RSpec.configure do |config|
416
+
417
+ activenodes = {}
418
+
419
+ # We can't use before(:suite) because it does not support instance variables
420
+ config.before(:context) do |context|
421
+
422
+ nodearg = ENV['NODE']
423
+ next if nodearg.nil?
424
+
425
+ if not activenodes.has_key?(nodearg)
426
+ activenodes[nodearg] = TestbeatNode.new(nodearg)
427
+ end
428
+ @node = activenodes[nodearg]
429
+
430
+ end
431
+
432
+ # https://www.relishapp.com/rspec/rspec-core/docs/hooks/before-and-after-hooks
433
+ config.before(:example) do |example|
434
+ #puts "------------- before"
435
+ #ap example.metadata
436
+ #ap example.metadata[:description_args]
437
+ #ap example[:description_args]
438
+ #ap example.full_description
439
+
440
+ # Testbeat can do nothing without a node, so the example will continue as a regular Rspec test
441
+ next if not @node
442
+
443
+ @testbeat = TestbeatContext.new(example)
444
+
445
+ # If there's no REST call in the example we're happy just to define @testbeat with access to command line etc
446
+ next if @testbeat.nop?
447
+
448
+ if @testbeat.unencrypted? and @testbeat.unauthenticated?
449
+ # Nodes that require non-test authentication are currently out of scope for the test framework
450
+ # This means we must skip specs in an "unauthenticated" context, or let them fail, because we won't get the 401 responses we'd expect
451
+ #p "--- Should be skipped; nodes that require authentication are currently unsupported"
452
+ elsif @testbeat.unencrypted?
453
+ # When we do add support for authentication to nodes, there should be no authentication on insecure channel
454
+ elsif @testbeat.reprovision?
455
+ # We have to run a new provisioning. For repos-backup amongst others.
456
+ @testbeat.logger{ "Reprovision triggered" }
457
+ @node.provision
458
+ else
459
+ # Authenticated is default, meaning that specs should be written with the assumption that all services are accessible
460
+ end
461
+
462
+ if @testbeat.rest?
463
+ @testbeat.logger{ "Request triggered" }
464
+ req = TestbeatRestRequest.new(@node, @testbeat)
465
+ begin
466
+ @response = req.run
467
+ rescue OpenSSL::SSL::SSLError => e
468
+ @response = { :error => e }
469
+ end
470
+ end
471
+
472
+ #p "Got reponse code #{@response.code}"
473
+ #ap @response.header.to_hash
474
+ end
475
+
476
+ config.after(:example) do
477
+ #p "------------- after"
478
+ end
479
+ end
data/lib/testbeat.rb ADDED
@@ -0,0 +1,14 @@
1
+
2
+ #require 'testbeat/rspec'
3
+ #require 'testbeat/vagrant'
4
+ require_relative './spec_helper.rb'
5
+
6
+ #module testbeat
7
+ #
8
+ # autoload :RSpec, './spec_helper.rb'
9
+ #
10
+ # def configure
11
+ # yield configuration
12
+ # end
13
+ #
14
+ #end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testbeat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Staffan Olsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: awesome_print
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Rspec spec_helper and Vagrant integration for HTTP level testing, on
56
+ a box from the outside
57
+ email: solsson@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - lib/testbeat.rb
63
+ - lib/spec_helper.rb
64
+ homepage: https://github.com/Reposoft/testbeat
65
+ licenses:
66
+ - MIT
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '2.0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 2.0.14
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: REST acceptance testing framework
88
+ test_files: []