kensa 0.4.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.
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