kensa 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ addon-manifest.json
2
+ pkg/
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ task :test do
2
+ fork do
3
+ exec "ruby test/resources/test_server.rb > /dev/null 2>&1"
4
+ end
5
+ system "turn"
6
+ system "ps -ax | grep test_server | grep -v grep | awk '{print $1}' | xargs kill"
7
+ end
8
+
9
+ task :default => :test
10
+
11
+ begin
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gemspec|
14
+ gemspec.name = "kensa"
15
+ gemspec.summary = ""
16
+ gemspec.description = ""
17
+ gemspec.email = "pedro@heroku.com"
18
+ gemspec.homepage = "http://heroku.com"
19
+ gemspec.authors = ["Blake Mizerany", "Pedro Belo", "Adam Wiggins"]
20
+
21
+ gemspec.add_development_dependency(%q<turn>, [">= 0"])
22
+ gemspec.add_development_dependency(%q<contest>, [">= 0"])
23
+ gemspec.add_dependency(%q<sinatra>, ["~> 0.9"])
24
+ gemspec.add_dependency(%q<rest-client>, ["~> 1.2.0"])
25
+ gemspec.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
26
+ gemspec.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
27
+ gemspec.add_dependency(%q<launchy>, [">= 0.3.2"])
28
+ gemspec.add_dependency(%q<mechanize>, ["~> 1.0.0"])
29
+
30
+ gemspec.version = '0.4.1'
31
+ end
32
+ rescue LoadError
33
+ puts "Jeweler not available. Install it with: gem install jeweler"
34
+ end
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+
2
+ [ ] heroku-addon test -n delete # run specific test [create | delete | sharing]
3
+ [ ] heroku-addon test # run full test suite
4
+ [ ] heroku-addon repl command # runs create, set ENVs, then delete on exit
5
+ [ ] heroku-addon push # push manifest to heroku
6
+
7
+ [X] heroku-addon check # check correctnes of JSON
8
+ [X] heroku-addon init # generate skeleton manifest
data/a-server.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'sinatra'
2
+ require 'yajl'
3
+ require 'restclient'
4
+
5
+ post "/heroku/resources" do
6
+ request.body.rewind
7
+ input = Yajl::Parser.parse(request.body.read)
8
+ resp = { :id => 123, :config => { "FOO" => "bar" } }
9
+ #resp = { :id => 456 }
10
+ json = Yajl::Encoder.encode(resp)
11
+ fork do
12
+ sleep 2
13
+ p input
14
+ RestClient.put(input["callback_url"], json)
15
+ end
16
+ "{}"
17
+ end
18
+
19
+ delete "/heroku/resources/:id" do
20
+ "ok"
21
+ end
data/bin/kensa ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'term/ansicolor'
5
+ require 'launchy'
6
+ require 'heroku/kensa'
7
+
8
+ fn="addon-manifest.json"
9
+ async=false
10
+ env="test"
11
+
12
+ ARGV.options do |o|
13
+ o.on("-f file", "--file") {|filename| fn = filename }
14
+ o.on("-h", "--help") { command = "help" }
15
+ o.on("--async") { async = true }
16
+ o.on("--production") { env = "production" }
17
+ o.parse!
18
+ end
19
+
20
+ command = ARGV.shift
21
+
22
+ $stdout.sync = true
23
+
24
+ class Screen
25
+ include Term::ANSIColor
26
+
27
+ def test(msg)
28
+ $stdout.puts
29
+ $stdout.puts
30
+ $stdout.print "Testing #{msg}"
31
+ end
32
+
33
+ def check(msg)
34
+ $stdout.puts
35
+ $stdout.print " Check #{msg}"
36
+ end
37
+
38
+ def error(msg)
39
+ $stdout.print "\n", magenta(" ! #{msg}")
40
+ end
41
+
42
+ def result(status)
43
+ msg = status ? bold("[PASS]") : red(bold("[FAIL]"))
44
+ $stdout.print " #{msg}"
45
+ end
46
+
47
+ def message(msg)
48
+ $stdout.puts msg
49
+ end
50
+
51
+ def finish
52
+ $stdout.puts
53
+ $stdout.puts
54
+ $stdout.puts "done."
55
+ end
56
+
57
+ end
58
+
59
+ def resolve_manifest(fn)
60
+ if File.exists?(fn)
61
+ File.read(fn)
62
+ else
63
+ abort("fatal: #{fn} not found")
64
+ end
65
+ end
66
+
67
+ def run(klass, fn, extras={})
68
+ screen = Screen.new
69
+ data = Yajl::Parser.parse(resolve_manifest(fn))
70
+ check = klass.new(data.merge(extras), screen)
71
+ check.call
72
+ screen.finish
73
+ end
74
+
75
+ include Heroku::Sensei
76
+
77
+ case command
78
+ when "init"
79
+ Manifest.init(fn)
80
+ Screen.new.message "Initialized new addon manifest in #{fn}"
81
+ when "test"
82
+ case check = ARGV.shift
83
+ when "manifest"
84
+ run ManifestCheck, fn
85
+ when "provision"
86
+ run ManifestCheck, fn
87
+ run ProvisionCheck, fn, :async => async, :env => env
88
+ when "deprovision"
89
+ id = ARGV.shift || abort("! no id specified; see usage")
90
+ run ManifestCheck, fn
91
+ run DeprovisionCheck, fn, :id => id, :async => async, :env => env
92
+ when "sso"
93
+ id = ARGV.shift || abort("! no id specified; see usage")
94
+ run ManifestCheck, fn
95
+ run SsoCheck, fn, :id => id, :env => env
96
+ else
97
+ abort "! Unknown test '#{check}'; see usage"
98
+ end
99
+ when "run"
100
+ abort "! missing command to run; see usage" if ARGV.empty?
101
+ run ManifestCheck, fn
102
+ run AllCheck, fn, :args => ARGV, :async => async, :env => env
103
+ when "sso"
104
+ id = ARGV.shift || abort("! no id specified; see usage")
105
+ data = Yajl::Parser.parse(resolve_manifest(fn)).merge(:id => id)
106
+ sso = Sso.new(data)
107
+ puts "Opening #{sso.full_url}"
108
+ Launchy.open sso.full_url
109
+ else
110
+ abort File.read(__FILE__).split('__END__').last
111
+ end
112
+
113
+ __END__
114
+ Usage: kensa [OPTIONS] command
115
+ kensa init
116
+ kensa test <type> [arg1 arg2 ...]
117
+ kensa run <command> [arg1 arg1 ...]
118
+
119
+ OPTIONS
120
+
121
+ -f, --filename path-to-file
122
+ Sets the manifest file to operate on, default is addon-manifest.json.
123
+
124
+ -h, --help
125
+ Show this message
126
+
127
+ --async
128
+ Check provision call with async response.
129
+
130
+ COMMANDS
131
+
132
+ init Creates a skeleton manifest
133
+
134
+ test <type> Simulate call from Heroku (provision or deprovision)
135
+
136
+ run <command> Provisions a resource and runs command in returned ENV
137
+
138
+ sso <id> Launches the browser on a Heroku session for the specified id
139
+
140
+ TEST TYPES
141
+
142
+ provision
143
+ Simulate a provision call from Heroku.
144
+
145
+ deprovision <id>
146
+ Simulate a deprovision call from Heroku.
147
+
148
+ sso <id>
149
+ Simulate a single sign-on call from Heroku.
150
+
151
+ manifest
152
+ Confirm that the manifest is valid. Automatically runs before all tests.
153
+
data/kensa.gemspec ADDED
@@ -0,0 +1,87 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{kensa}
8
+ s.version = "0.4.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Blake Mizerany", "Pedro Belo", "Adam Wiggins"]
12
+ s.date = %q{2010-04-07}
13
+ s.default_executable = %q{kensa}
14
+ s.description = %q{}
15
+ s.email = %q{pedro@heroku.com}
16
+ s.executables = ["kensa"]
17
+ s.extra_rdoc_files = [
18
+ "TODO"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "Rakefile",
23
+ "TODO",
24
+ "a-server.rb",
25
+ "bin/kensa",
26
+ "kensa.gemspec",
27
+ "lib/heroku/kensa.rb",
28
+ "server.rb",
29
+ "set-env.sh",
30
+ "test/deprovision_check.rb",
31
+ "test/helper.rb",
32
+ "test/manifest_check_test.rb",
33
+ "test/provision_check_test.rb",
34
+ "test/provision_response_check_test.rb",
35
+ "test/resources/test_server.rb",
36
+ "test/sso_check_test.rb"
37
+ ]
38
+ s.homepage = %q{http://heroku.com}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.rubygems_version = %q{1.3.6}
42
+ s.summary = %q{}
43
+ s.test_files = [
44
+ "test/deprovision_check.rb",
45
+ "test/helper.rb",
46
+ "test/manifest_check_test.rb",
47
+ "test/provision_check_test.rb",
48
+ "test/provision_response_check_test.rb",
49
+ "test/resources/test_server.rb",
50
+ "test/sso_check_test.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
58
+ s.add_development_dependency(%q<turn>, [">= 0"])
59
+ s.add_development_dependency(%q<contest>, [">= 0"])
60
+ s.add_runtime_dependency(%q<sinatra>, ["~> 0.9"])
61
+ s.add_runtime_dependency(%q<rest-client>, ["~> 1.2.0"])
62
+ s.add_runtime_dependency(%q<yajl-ruby>, ["~> 0.6"])
63
+ s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0"])
64
+ s.add_runtime_dependency(%q<launchy>, [">= 0.3.2"])
65
+ s.add_runtime_dependency(%q<mechanize>, ["~> 1.0.0"])
66
+ else
67
+ s.add_dependency(%q<turn>, [">= 0"])
68
+ s.add_dependency(%q<contest>, [">= 0"])
69
+ s.add_dependency(%q<sinatra>, ["~> 0.9"])
70
+ s.add_dependency(%q<rest-client>, ["~> 1.2.0"])
71
+ s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
72
+ s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
73
+ s.add_dependency(%q<launchy>, [">= 0.3.2"])
74
+ s.add_dependency(%q<mechanize>, ["~> 1.0.0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<turn>, [">= 0"])
78
+ s.add_dependency(%q<contest>, [">= 0"])
79
+ s.add_dependency(%q<sinatra>, ["~> 0.9"])
80
+ s.add_dependency(%q<rest-client>, ["~> 1.2.0"])
81
+ s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
82
+ s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
83
+ s.add_dependency(%q<launchy>, [">= 0.3.2"])
84
+ s.add_dependency(%q<mechanize>, ["~> 1.0.0"])
85
+ end
86
+ end
87
+
@@ -0,0 +1,589 @@
1
+ require 'yajl'
2
+ require 'restclient'
3
+ require 'socket'
4
+ require 'timeout'
5
+ require 'uri'
6
+ require 'mechanize'
7
+
8
+ module Heroku
9
+
10
+ module Sensei
11
+
12
+ module Manifest
13
+
14
+ def self.init(filename)
15
+ open(filename, 'w') {|f| f << skeleton_str }
16
+ end
17
+
18
+ def self.skeleton
19
+ Yajl::Parser.parse(skeleton_str)
20
+ end
21
+
22
+ def self.skeleton_str
23
+ return <<EOJSON
24
+ {
25
+ "id": "myaddon",
26
+ "name": "My Addon",
27
+ "plans": [
28
+ {
29
+ "id": "basic",
30
+ "name": "Basic",
31
+ "price": "0",
32
+ "price_unit": "month"
33
+ }
34
+ ],
35
+ "api": {
36
+ "config_vars": [
37
+ "MYADDON_URL"
38
+ ],
39
+ "production": "https://yourapp.com/",
40
+ "test": "http://localhost:4567/",
41
+ "username": "heroku",
42
+ "password": "#{generate_password(16)}",
43
+ "sso_salt": "#{generate_password(16)}"
44
+ }
45
+ }
46
+ EOJSON
47
+ end
48
+
49
+ PasswordChars = chars = ['a'..'z', 'A'..'Z', '0'..'9'].map { |r| r.to_a }.flatten
50
+ def self.generate_password(size=16)
51
+ Array.new(size) { PasswordChars[rand(PasswordChars.size)] }.join
52
+ end
53
+
54
+ end
55
+
56
+
57
+ class NilScreen
58
+
59
+ def test(msg)
60
+ end
61
+
62
+ def check(msg)
63
+ end
64
+
65
+ def error(msg)
66
+ end
67
+
68
+ def result(status)
69
+ end
70
+
71
+ end
72
+
73
+
74
+ class Check
75
+ attr_accessor :screen, :data
76
+
77
+ class CheckError < StandardError ; end
78
+
79
+ def initialize(data, screen=NilScreen.new)
80
+ @data = data
81
+ @screen = screen
82
+ end
83
+
84
+ def test(msg)
85
+ screen.test msg
86
+ end
87
+
88
+ def check(msg)
89
+ screen.check(msg)
90
+ if yield
91
+ screen.result(true)
92
+ else
93
+ raise CheckError
94
+ end
95
+ end
96
+
97
+ def run(klass, data)
98
+ c = klass.new(data, screen)
99
+ instance_eval(&c)
100
+ end
101
+
102
+ def error(msg)
103
+ raise CheckError, msg
104
+ end
105
+
106
+ def call
107
+ call!
108
+ true
109
+ rescue CheckError => boom
110
+ screen.result(false)
111
+ screen.error boom.message if boom.message != boom.class.name
112
+
113
+ false
114
+ end
115
+
116
+ def to_proc
117
+ me = self
118
+ Proc.new { me.call! }
119
+ end
120
+
121
+ end
122
+
123
+
124
+ class ManifestCheck < Check
125
+
126
+ ValidPriceUnits = %w[month dyno_hour]
127
+
128
+ def call!
129
+ test "manifest id key"
130
+ check "if exists" do
131
+ data.has_key?("id")
132
+ end
133
+ check "is a string" do
134
+ data["id"].is_a?(String)
135
+ end
136
+ check "is not blank" do
137
+ !data["id"].empty?
138
+ end
139
+
140
+ test "manifest name key"
141
+ check "if exists" do
142
+ data.has_key?("name")
143
+ end
144
+ check "is a string" do
145
+ data["name"].is_a?(String)
146
+ end
147
+ check "is not blank" do
148
+ !data["name"].empty?
149
+ end
150
+
151
+ test "manifest api key"
152
+ check "if exists" do
153
+ data.has_key?("api")
154
+ end
155
+ check "is a hash" do
156
+ data["api"].is_a?(Hash)
157
+ end
158
+ check "contains username" do
159
+ data["api"].has_key?("username") && data["api"]["username"] != ""
160
+ end
161
+ check "contains password" do
162
+ data["api"].has_key?("password") && data["api"]["password"] != ""
163
+ end
164
+ check "contains test url" do
165
+ data["api"].has_key?("test")
166
+ end
167
+ check "contains production url" do
168
+ data["api"].has_key?("production")
169
+ end
170
+ check "production url uses SSL" do
171
+ data["api"]["production"] =~ /^https:/
172
+ end
173
+ check "contains config_vars array" do
174
+ data["api"].has_key?("config_vars") && data["api"]["config_vars"].is_a?(Array)
175
+ end
176
+ check "containst at least one config var" do
177
+ !data["api"]["config_vars"].empty?
178
+ end
179
+ check "all config vars are uppercase strings" do
180
+ data["api"]["config_vars"].each do |k, v|
181
+ if k =~ /^[A-Z][0-9A-Z_]+$/
182
+ true
183
+ else
184
+ error "#{k.inspect} is not a valid ENV key"
185
+ end
186
+ end
187
+ end
188
+ check "all config vars are prefixed with the addon id" do
189
+ data["api"]["config_vars"].each do |k|
190
+ if k =~ /^#{data['id'].upcase}_/
191
+ true
192
+ else
193
+ error "#{k} is not a valid ENV key - must be prefixed with #{data['id'].upcase}_"
194
+ end
195
+ end
196
+ end
197
+
198
+ test "plans"
199
+ check "key must exist" do
200
+ data.has_key?("plans")
201
+ end
202
+ check "is an array" do
203
+ data["plans"].is_a?(Array)
204
+ end
205
+ check "contains at least one plan" do
206
+ !data["plans"].empty?
207
+ end
208
+ check "all plans are a hash" do
209
+ data["plans"].all? {|plan| plan.is_a?(Hash) }
210
+ end
211
+ check "all plans must have an id" do
212
+ data["plans"].all? {|plan| plan.has_key?("id") }
213
+ end
214
+ check "all plans have an unique id" do
215
+ ids = data["plans"].map {|plan| plan["id"] }
216
+ ids.size == ids.uniq.size
217
+ end
218
+ check "all plans have a name" do
219
+ data["plans"].all? {|plan| plan.has_key?("name") }
220
+ end
221
+ check "all plans have a unique name" do
222
+ names = data["plans"].map {|plan| plan["name"] }
223
+ names.size == names.uniq.size
224
+ end
225
+
226
+ data["plans"].each do |plan|
227
+ check "#{plan["name"]} has a valid price" do
228
+ if plan["price"] !~ /^\d+$/
229
+ error "expected an integer"
230
+ else
231
+ true
232
+ end
233
+ end
234
+
235
+ check "#{plan["name"]} has a valid price_unit" do
236
+ if ValidPriceUnits.include?(plan["price_unit"])
237
+ true
238
+ else
239
+ error "expected #{ValidPriceUnits.join(" or ")} but got #{plan["price_unit"].inspect}"
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ end
246
+
247
+
248
+ class ProvisionResponseCheck < Check
249
+
250
+ def call!
251
+ response = data[:provision_response]
252
+ test "response"
253
+ check "contains an id" do
254
+ response.has_key?("id")
255
+ end
256
+
257
+ if response.has_key?("config")
258
+ test "config data"
259
+ check "is a hash" do
260
+ response["config"].is_a?(Hash)
261
+ end
262
+
263
+ check "all config keys were previously defined in the manifest" do
264
+ response["config"].keys.each do |key|
265
+ error "#{key} is not in the manifest" unless data["api"]["config_vars"].include?(key)
266
+ end
267
+ true
268
+ end
269
+
270
+ check "all config values are strings" do
271
+ response["config"].each do |k, v|
272
+ if v.is_a?(String)
273
+ true
274
+ else
275
+ error "#{v.inspect} is not a string"
276
+ end
277
+ end
278
+ end
279
+
280
+ check "URL configs vars" do
281
+ response["config"].each do |key, value|
282
+ next unless key =~ /_URL$/
283
+ begin
284
+ uri = URI.parse(value)
285
+ error "#{value} is not a valid URI - missing host" unless uri.host
286
+ error "#{value} is not a valid URI - missing scheme" unless uri.scheme
287
+ error "#{value} is not a valid URI - pointing to localhost" if @data[:env] == 'production' && uri.host == 'localhost'
288
+ rescue URI::Error
289
+ error "#{value} is not a valid URI"
290
+ end
291
+ end
292
+ end
293
+
294
+ end
295
+ end
296
+
297
+ end
298
+
299
+
300
+ module HTTP
301
+
302
+ def get(path, params={})
303
+ path = "#{path}?" + params.map { |k, v| "#{k}=#{v}" }.join("&") unless params.empty?
304
+ request(:get, [], path)
305
+ end
306
+
307
+ def post(credentials, path, payload=nil)
308
+ request(:post, credentials, path, payload)
309
+ end
310
+
311
+ def delete(credentials, path, payload=nil)
312
+ request(:delete, credentials, path, payload)
313
+ end
314
+
315
+ def request(meth, credentials, path, payload=nil)
316
+ code = nil
317
+ body = nil
318
+
319
+ begin
320
+ args = [
321
+ (Yajl::Encoder.encode(payload) if payload),
322
+ {
323
+ :accept => "application/json",
324
+ :content_type => "application/json"
325
+ }
326
+ ].compact
327
+
328
+ user, pass = credentials
329
+ body = RestClient::Resource.new(url, user, pass)[path].send(
330
+ meth,
331
+ *args
332
+ )
333
+
334
+ code = 200
335
+ rescue RestClient::ExceptionWithResponse => boom
336
+ code = boom.http_code
337
+ body = boom.http_body
338
+ rescue Errno::ECONNREFUSED
339
+ code = -1
340
+ body = nil
341
+ end
342
+
343
+ [code, body]
344
+ end
345
+
346
+ end
347
+
348
+ class ApiCheck < Check
349
+ def url
350
+ env = data[:env] || 'test'
351
+ data["api"][env].chomp("/")
352
+ end
353
+
354
+ def credentials
355
+ %w( username password ).map { |attr| data["api"][attr] }
356
+ end
357
+ end
358
+
359
+ class ProvisionCheck < ApiCheck
360
+ include HTTP
361
+
362
+ READLEN = 1024 * 10
363
+ APPID = "app#{rand(10000)}@kensa.heroku.com"
364
+ APPNAME = "myapp"
365
+
366
+ def call!
367
+ json = nil
368
+ response = nil
369
+
370
+ code = nil
371
+ json = nil
372
+ path = "/heroku/resources"
373
+ callback = "http://localhost:7779/callback/999"
374
+ reader, writer = nil
375
+
376
+ payload = {
377
+ :heroku_id => APPID,
378
+ :appname => APPNAME,
379
+ :plan => @data['plans'].first['id'],
380
+ :callback_url => callback
381
+ }
382
+
383
+ if data[:async]
384
+ reader, writer = IO.pipe
385
+ end
386
+
387
+ test "POST /heroku/resources"
388
+ check "response" do
389
+ if data[:async]
390
+ child = fork do
391
+ Timeout.timeout(10) do
392
+ reader.close
393
+ server = TCPServer.open(7779)
394
+ client = server.accept
395
+ writer.write(client.readpartial(READLEN))
396
+ client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
397
+ client.close
398
+ writer.close
399
+ end
400
+ end
401
+ sleep(1)
402
+ end
403
+
404
+ code, json = post(credentials, path, payload)
405
+
406
+ if code == 200
407
+ # noop
408
+ elsif code == -1
409
+ error("unable to connect to #{url}")
410
+ else
411
+ error("expected 200, got #{code}")
412
+ end
413
+
414
+ true
415
+ end
416
+
417
+ if data[:async]
418
+ check "async response to PUT #{callback}" do
419
+ out = reader.readpartial(READLEN)
420
+ _, json = out.split("\r\n\r\n")
421
+ end
422
+ end
423
+
424
+ check "valid JSON" do
425
+ begin
426
+ response = Yajl::Parser.parse(json)
427
+ rescue Yajl::ParseError => boom
428
+ error boom.message
429
+ end
430
+ true
431
+ end
432
+
433
+ check "authentication" do
434
+ wrong_credentials = ['wrong', 'secret']
435
+ code, _ = post(wrong_credentials, path, payload)
436
+ error("expected 401, got #{code}") if code != 401
437
+ true
438
+ end
439
+
440
+ data[:provision_response] = response
441
+
442
+ run ProvisionResponseCheck, data
443
+ end
444
+
445
+ ensure
446
+ reader.close rescue nil
447
+ writer.close rescue nil
448
+ end
449
+
450
+
451
+ class DeprovisionCheck < ApiCheck
452
+ include HTTP
453
+
454
+ def call!
455
+ id = data[:id]
456
+ raise ArgumentError, "No id specified" if id.nil?
457
+
458
+ path = "/heroku/resources/#{id}"
459
+
460
+ test "DELETE #{path}"
461
+ check "response" do
462
+ code, _ = delete(credentials, path, nil)
463
+ if code == 200
464
+ true
465
+ elsif code == -1
466
+ error("unable to connect to #{url}")
467
+ else
468
+ error("expected 200, got #{code}")
469
+ end
470
+ end
471
+
472
+ check "authentication" do
473
+ wrong_credentials = ['wrong', 'secret']
474
+ code, _ = delete(wrong_credentials, path, nil)
475
+ error("expected 401, got #{code}") if code != 401
476
+ true
477
+ end
478
+
479
+ end
480
+
481
+ end
482
+
483
+
484
+ class Sso
485
+ attr_accessor :id, :url
486
+
487
+ def initialize(data)
488
+ @id = data[:id]
489
+ @salt = data['api']['sso_salt']
490
+ @url = data["api"]["test"].chomp('/')
491
+ end
492
+
493
+ def path
494
+ "/heroku/resources/#{id}"
495
+ end
496
+
497
+ def full_url
498
+ t = Time.now.to_i
499
+ "#{url}#{path}?token=#{make_token(t)}&timestamp=#{t}"
500
+ end
501
+
502
+ def make_token(t)
503
+ Digest::SHA1.hexdigest([@id, @salt, t].join(':'))
504
+ end
505
+ end
506
+
507
+
508
+ class SsoCheck < ApiCheck
509
+ include HTTP
510
+
511
+ def mechanize_get url
512
+ agent = Mechanize.new
513
+ page = agent.get(url)
514
+ return page, 200
515
+ rescue Mechanize::ResponseCodeError => error
516
+ return nil, error.response_code.to_i
517
+ rescue Errno::ECONNREFUSED
518
+ error("connection refused to #{url}")
519
+ end
520
+
521
+ def call!
522
+ sso = Sso.new(data)
523
+ t = Time.now.to_i
524
+
525
+ test "GET #{sso.path}"
526
+ check "validates token" do
527
+ page, respcode = mechanize_get sso.url + sso.path + "?token=invalid&timestamp=#{t}"
528
+ error("expected 403, got 200") unless respcode == 403
529
+ true
530
+ end
531
+
532
+ check "validates timestamp" do
533
+ prev = (Time.now - 60*6).to_i
534
+ page, respcode = mechanize_get sso.url + sso.path + "?token=#{sso.make_token(prev)}&timestamp=#{prev}"
535
+ error("expected 403, got 200") unless respcode == 403
536
+ true
537
+ end
538
+
539
+ check "logs in" do
540
+ page, respcode = mechanize_get sso.url + sso.path + "?token=#{sso.make_token(t)}&timestamp=#{t}"
541
+ error("expected 200, got #{respcode}") unless respcode == 200
542
+ true
543
+ end
544
+ end
545
+ end
546
+
547
+
548
+ ##
549
+ # On Testing:
550
+ # I've opted to not write tests for this
551
+ # due to the simple nature it's currently in.
552
+ # If this becomes more complex in even the
553
+ # least amount, find me (blake) and I'll
554
+ # help get tests in.
555
+ class AllCheck < Check
556
+
557
+ def call!
558
+ args = data[:args]
559
+ run ProvisionCheck, data
560
+
561
+ response = data[:provision_response]
562
+ data.merge!(:id => response["id"])
563
+ config = response["config"] || Hash.new
564
+
565
+ if args
566
+ screen.message "\n\n"
567
+ screen.message "Starting #{args.first}..."
568
+ screen.message ""
569
+
570
+ run_in_env(config) { system(*args) }
571
+
572
+ screen.message ""
573
+ screen.message "End of #{args.first}"
574
+ end
575
+
576
+ run DeprovisionCheck, data
577
+ end
578
+
579
+ def run_in_env(env)
580
+ env.each {|key, value| ENV[key] = value }
581
+ yield
582
+ env.keys.each {|key| ENV.delete(key) }
583
+ end
584
+
585
+ end
586
+
587
+ end
588
+
589
+ end