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 +2 -0
- data/Rakefile +34 -0
- data/TODO +8 -0
- data/a-server.rb +21 -0
- data/bin/kensa +153 -0
- data/kensa.gemspec +87 -0
- data/lib/heroku/kensa.rb +589 -0
- data/server.rb +13 -0
- data/set-env.sh +4 -0
- data/test/deprovision_check.rb +36 -0
- data/test/helper.rb +48 -0
- data/test/manifest_check_test.rb +142 -0
- data/test/provision_check_test.rb +50 -0
- data/test/provision_response_check_test.rb +72 -0
- data/test/resources/test_server.rb +43 -0
- data/test/sso_check_test.rb +28 -0
- metadata +189 -0
data/.gitignore
ADDED
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
|
+
|
data/lib/heroku/kensa.rb
ADDED
@@ -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)}×tamp=#{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×tamp=#{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)}×tamp=#{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)}×tamp=#{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
|