samorau 0.1.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.
- data/.gitignore +1 -0
- data/NOTES +28 -0
- data/Rakefile +22 -0
- data/TODO +8 -0
- data/a-server.rb +21 -0
- data/bin/samorau +136 -0
- data/lib/heroku/samorau.rb +465 -0
- data/samorau.gemspec +78 -0
- data/server.rb +13 -0
- data/set-env.sh +4 -0
- data/test/create_check_test.rb +51 -0
- data/test/create_response_check_test.rb +46 -0
- data/test/delete_check.rb +37 -0
- data/test/helper.rb +47 -0
- data/test/manifest_check_test.rb +117 -0
- metadata +134 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
addon-manifest.json
|
data/NOTES
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
samorau
|
2
|
+
1. to wait for a chance
|
3
|
+
2. to serve by ones side
|
4
|
+
|
5
|
+
check man
|
6
|
+
- invalid
|
7
|
+
- valid
|
8
|
+
- invalid price (show error)
|
9
|
+
- multiple plans
|
10
|
+
|
11
|
+
check create
|
12
|
+
- server not running
|
13
|
+
- no config (no output)
|
14
|
+
- config
|
15
|
+
|
16
|
+
check all
|
17
|
+
- uses id from response
|
18
|
+
|
19
|
+
run
|
20
|
+
- ENV["FOO"]
|
21
|
+
|
22
|
+
arch
|
23
|
+
|
24
|
+
- chain of responsiblity
|
25
|
+
- commands pass by data
|
26
|
+
|
27
|
+
show contest
|
28
|
+
show turn
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "samorau"
|
5
|
+
gemspec.summary = ""
|
6
|
+
gemspec.description = ""
|
7
|
+
gemspec.email = "pedro@heroku.com"
|
8
|
+
gemspec.homepage = "http://heroku.com"
|
9
|
+
gemspec.authors = ["Blake Mizerany", "Pedro Belo", "Adam Wiggins"]
|
10
|
+
|
11
|
+
gemspec.add_development_dependency(%q<turn>, [">= 0"])
|
12
|
+
gemspec.add_development_dependency(%q<contest>, [">= 0"])
|
13
|
+
gemspec.add_dependency(%q<sinatra>, ["~> 0.9"])
|
14
|
+
gemspec.add_dependency(%q<rest-client>, ["~> 1.2.0"])
|
15
|
+
gemspec.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
16
|
+
gemspec.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
17
|
+
|
18
|
+
gemspec.version = '0.1.0'
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
22
|
+
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/samorau
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'term/ansicolor'
|
4
|
+
require 'heroku/samorau'
|
5
|
+
|
6
|
+
fn="addon-manifest.json"
|
7
|
+
async=false
|
8
|
+
env="test"
|
9
|
+
|
10
|
+
ARGV.options do |o|
|
11
|
+
o.on("-f file", "--file") {|filename| fn = filename }
|
12
|
+
o.on("-h", "--help") { command = "help" }
|
13
|
+
o.on("--async") { async = true }
|
14
|
+
o.on("--production") { env = "production" }
|
15
|
+
o.parse!
|
16
|
+
end
|
17
|
+
|
18
|
+
command = ARGV.shift
|
19
|
+
|
20
|
+
$stdout.sync = true
|
21
|
+
|
22
|
+
class Screen
|
23
|
+
include Term::ANSIColor
|
24
|
+
|
25
|
+
def test(msg)
|
26
|
+
$stdout.puts
|
27
|
+
$stdout.puts
|
28
|
+
$stdout.print "Testing #{msg}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def check(msg)
|
32
|
+
$stdout.puts
|
33
|
+
$stdout.print " Check #{msg}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(msg)
|
37
|
+
$stdout.print "\n", magenta(" ! #{msg}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def result(status)
|
41
|
+
msg = status ? bold("[PASS]") : red(bold("[FAIL]"))
|
42
|
+
$stdout.print " #{msg}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def message(msg)
|
46
|
+
$stdout.puts msg
|
47
|
+
end
|
48
|
+
|
49
|
+
def finish
|
50
|
+
$stdout.puts
|
51
|
+
$stdout.puts
|
52
|
+
$stdout.puts "done."
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def resolve_manifest(fn)
|
58
|
+
if File.exists?(fn)
|
59
|
+
File.read(fn)
|
60
|
+
else
|
61
|
+
abort("fatal: #{fn} not found")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def run(klass, fn, extras={})
|
66
|
+
screen = Screen.new
|
67
|
+
data = Yajl::Parser.parse(resolve_manifest(fn))
|
68
|
+
check = klass.new(data.merge(extras), screen)
|
69
|
+
check.call
|
70
|
+
screen.finish
|
71
|
+
end
|
72
|
+
|
73
|
+
include Heroku::Samorau
|
74
|
+
|
75
|
+
case command
|
76
|
+
when "init"
|
77
|
+
Manifest.init(fn)
|
78
|
+
Screen.new.message "Initialized new addon manifest in #{fn}"
|
79
|
+
when "test"
|
80
|
+
case check = ARGV.shift
|
81
|
+
when "manifest"
|
82
|
+
run ManifestCheck, fn
|
83
|
+
when "provision"
|
84
|
+
run ManifestCheck, fn
|
85
|
+
run CreateCheck, fn, :async => async, :env => env
|
86
|
+
when "deprovision"
|
87
|
+
id = ARGV.shift || abort("! no id specified; see usage")
|
88
|
+
run ManifestCheck, fn
|
89
|
+
run DeleteCheck, fn, :id => id, :async => async, :env => env
|
90
|
+
else
|
91
|
+
abort "! Unknown test '#{check}'; see usage"
|
92
|
+
end
|
93
|
+
when "run"
|
94
|
+
abort "! missing command to run; see usage" if ARGV.empty?
|
95
|
+
run ManifestCheck, fn
|
96
|
+
run AllCheck, fn, :args => ARGV, :async => async, :env => env
|
97
|
+
else
|
98
|
+
abort DATA.read
|
99
|
+
end
|
100
|
+
|
101
|
+
__END__
|
102
|
+
Usage: samorau [OPTIONS] command
|
103
|
+
samorau init
|
104
|
+
samorau test <type> [arg1 arg2 ...]
|
105
|
+
samorau run <command> [arg1 arg1 ...]
|
106
|
+
|
107
|
+
OPTIONS
|
108
|
+
|
109
|
+
-f, --filename path-to-file
|
110
|
+
Sets the manifest file to operate on, default is addon-manifest.json.
|
111
|
+
|
112
|
+
-h, --help
|
113
|
+
Show this message
|
114
|
+
|
115
|
+
--async
|
116
|
+
Check provision call with async response.
|
117
|
+
|
118
|
+
COMMANDS
|
119
|
+
|
120
|
+
init Creates a skeleton manifest
|
121
|
+
|
122
|
+
test <type> Simulate call from Heroku (provision or deprovision)
|
123
|
+
|
124
|
+
run <command> Provisions a resource and runs command in returned ENV
|
125
|
+
|
126
|
+
TEST TYPES
|
127
|
+
|
128
|
+
provision
|
129
|
+
Simulate a provision call from Heroku.
|
130
|
+
|
131
|
+
deprovision <id>
|
132
|
+
Simulate a deprovision call from Heroku.
|
133
|
+
|
134
|
+
manifest
|
135
|
+
Confirm that the manifest is valid. Automatically runs before all tests.
|
136
|
+
|
@@ -0,0 +1,465 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'restclient'
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module Heroku
|
7
|
+
|
8
|
+
module Samorau
|
9
|
+
|
10
|
+
module Manifest
|
11
|
+
|
12
|
+
def self.init(filename)
|
13
|
+
json = Yajl::Encoder.encode(skeleton, :pretty => true)
|
14
|
+
open(filename, 'w') {|f| f << json }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.skeleton
|
18
|
+
{
|
19
|
+
"name" => "myaddon",
|
20
|
+
|
21
|
+
"api" => {
|
22
|
+
"username" => "heroku",
|
23
|
+
"password" => generate_password,
|
24
|
+
"test" => "http://localhost:4567/",
|
25
|
+
"production" => "https://yourapp.com/",
|
26
|
+
"config_vars" => ["MYADDON_URL"]
|
27
|
+
},
|
28
|
+
|
29
|
+
"plans" => [
|
30
|
+
{
|
31
|
+
"name" => "Basic",
|
32
|
+
"price" => "0",
|
33
|
+
"price_unit" => "month"
|
34
|
+
}
|
35
|
+
]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.generate_password
|
40
|
+
Array.new(8) { rand(256) }.pack('C*').unpack('H*').first
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
class NilScreen
|
47
|
+
|
48
|
+
def test(msg)
|
49
|
+
end
|
50
|
+
|
51
|
+
def check(msg)
|
52
|
+
end
|
53
|
+
|
54
|
+
def error(msg)
|
55
|
+
end
|
56
|
+
|
57
|
+
def result(status)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class Check
|
64
|
+
attr_accessor :screen, :data
|
65
|
+
|
66
|
+
class CheckError < StandardError ; end
|
67
|
+
|
68
|
+
def initialize(data, screen=NilScreen.new)
|
69
|
+
@data = data
|
70
|
+
@screen = screen
|
71
|
+
end
|
72
|
+
|
73
|
+
def test(msg)
|
74
|
+
screen.test msg
|
75
|
+
end
|
76
|
+
|
77
|
+
def check(msg)
|
78
|
+
screen.check(msg)
|
79
|
+
if yield
|
80
|
+
screen.result(true)
|
81
|
+
else
|
82
|
+
raise CheckError
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def run(klass, data)
|
87
|
+
c = klass.new(data, screen)
|
88
|
+
instance_eval(&c)
|
89
|
+
end
|
90
|
+
|
91
|
+
def error(msg)
|
92
|
+
raise CheckError, msg
|
93
|
+
end
|
94
|
+
|
95
|
+
def call
|
96
|
+
call!
|
97
|
+
true
|
98
|
+
rescue CheckError => boom
|
99
|
+
screen.result(false)
|
100
|
+
screen.error boom.message if boom.message != boom.class.name
|
101
|
+
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_proc
|
106
|
+
me = self
|
107
|
+
Proc.new { me.call! }
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
class ManifestCheck < Check
|
114
|
+
|
115
|
+
ValidPriceUnits = %w[month dyno_hour]
|
116
|
+
|
117
|
+
def call!
|
118
|
+
test "manifest name key"
|
119
|
+
check "if exists" do
|
120
|
+
data.has_key?("name")
|
121
|
+
end
|
122
|
+
check "is a string" do
|
123
|
+
data["name"].is_a?(String)
|
124
|
+
end
|
125
|
+
check "is not blank" do
|
126
|
+
!data["name"].empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
test "manifest api key"
|
130
|
+
check "if exists" do
|
131
|
+
data.has_key?("api")
|
132
|
+
end
|
133
|
+
check "is a hash" do
|
134
|
+
data["api"].is_a?(Hash)
|
135
|
+
end
|
136
|
+
check "contains username" do
|
137
|
+
data["api"].has_key?("username") && data["api"]["username"] != ""
|
138
|
+
end
|
139
|
+
check "contains password" do
|
140
|
+
data["api"].has_key?("password") && data["api"]["password"] != ""
|
141
|
+
end
|
142
|
+
check "contains test url" do
|
143
|
+
data["api"].has_key?("test")
|
144
|
+
end
|
145
|
+
check "contains production url" do
|
146
|
+
data["api"].has_key?("production")
|
147
|
+
end
|
148
|
+
check "production url uses SSL" do
|
149
|
+
data["api"]["production"] =~ /^https:/
|
150
|
+
end
|
151
|
+
check "contains config_vars array" do
|
152
|
+
data["api"].has_key?("config_vars") && data["api"]["config_vars"].is_a?(Array)
|
153
|
+
end
|
154
|
+
check "containst at least one config var" do
|
155
|
+
!data["api"]["config_vars"].empty?
|
156
|
+
end
|
157
|
+
check "all config vars are uppercase strings" do
|
158
|
+
data["api"]["config_vars"].each do |k, v|
|
159
|
+
if k =~ /^[A-Z][0-9A-Z_]+$/
|
160
|
+
true
|
161
|
+
else
|
162
|
+
error "#{k.inspect} is not a valid ENV key"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
test "plans"
|
168
|
+
check "key must exist" do
|
169
|
+
data.has_key?("plans")
|
170
|
+
end
|
171
|
+
check "is an array" do
|
172
|
+
data["plans"].is_a?(Array)
|
173
|
+
end
|
174
|
+
check "contains at least one plan" do
|
175
|
+
!data["plans"].empty?
|
176
|
+
end
|
177
|
+
check "all plans are a hash" do
|
178
|
+
data["plans"].all? {|plan| plan.is_a?(Hash) }
|
179
|
+
end
|
180
|
+
check "all plans have a name" do
|
181
|
+
data["plans"].all? {|plan| plan.has_key?("name") }
|
182
|
+
end
|
183
|
+
check "all plans have a unique name" do
|
184
|
+
names = data["plans"].map {|plan| plan["name"] }
|
185
|
+
names.size == names.uniq.size
|
186
|
+
end
|
187
|
+
|
188
|
+
data["plans"].each do |plan|
|
189
|
+
check "#{plan["name"]} has a valid price" do
|
190
|
+
if plan["price"] !~ /^\d+$/
|
191
|
+
error "expected an integer"
|
192
|
+
else
|
193
|
+
true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
check "#{plan["name"]} has a valid price_unit" do
|
198
|
+
if ValidPriceUnits.include?(plan["price_unit"])
|
199
|
+
true
|
200
|
+
else
|
201
|
+
error "expected #{ValidPriceUnits.join(" or ")} but got #{plan["price_unit"].inspect}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
class CreateResponseCheck < Check
|
211
|
+
|
212
|
+
def call!
|
213
|
+
response = data[:create_response]
|
214
|
+
test "response"
|
215
|
+
check "contains an id" do
|
216
|
+
response.has_key?("id")
|
217
|
+
end
|
218
|
+
|
219
|
+
if response.has_key?("config")
|
220
|
+
test "config data"
|
221
|
+
check "is a hash" do
|
222
|
+
response["config"].is_a?(Hash)
|
223
|
+
end
|
224
|
+
|
225
|
+
check "all config keys were previously defined in the manifest" do
|
226
|
+
response["config"].keys.each do |key|
|
227
|
+
error "#{key} is not in the manifest" unless data["api"]["config_vars"].include?(key)
|
228
|
+
end
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
check "all config values are strings" do
|
233
|
+
response["config"].each do |k, v|
|
234
|
+
if v.is_a?(String)
|
235
|
+
true
|
236
|
+
else
|
237
|
+
error "#{v.inspect} is not a string"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
module HTTP
|
248
|
+
|
249
|
+
def post(credentials, path, payload=nil)
|
250
|
+
request(:post, credentials, path, payload)
|
251
|
+
end
|
252
|
+
|
253
|
+
def delete(credentials, path, payload=nil)
|
254
|
+
request(:delete, credentials, path, payload)
|
255
|
+
end
|
256
|
+
|
257
|
+
def request(meth, credentials, path, payload=nil)
|
258
|
+
code = nil
|
259
|
+
body = nil
|
260
|
+
|
261
|
+
begin
|
262
|
+
args = [
|
263
|
+
(Yajl::Encoder.encode(payload) if payload),
|
264
|
+
{
|
265
|
+
:accept => "application/json",
|
266
|
+
:content_type => "application/json"
|
267
|
+
}
|
268
|
+
].compact
|
269
|
+
|
270
|
+
user, pass = credentials
|
271
|
+
body = RestClient::Resource.new(url, user, pass)[path].send(
|
272
|
+
meth,
|
273
|
+
*args
|
274
|
+
)
|
275
|
+
|
276
|
+
code = 200
|
277
|
+
rescue RestClient::ExceptionWithResponse => boom
|
278
|
+
code = boom.http_code
|
279
|
+
body = boom.http_body
|
280
|
+
rescue Errno::ECONNREFUSED
|
281
|
+
code = -1
|
282
|
+
body = nil
|
283
|
+
end
|
284
|
+
|
285
|
+
[code, body]
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
class ApiCheck < Check
|
291
|
+
def url
|
292
|
+
env = data[:env] || 'test'
|
293
|
+
data["api"][env].chomp("/")
|
294
|
+
end
|
295
|
+
|
296
|
+
def credentials
|
297
|
+
%w( username password ).map { |attr| data["api"][attr] }
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class CreateCheck < ApiCheck
|
302
|
+
include HTTP
|
303
|
+
|
304
|
+
READLEN = 1024 * 10
|
305
|
+
APPID = "app123@heroku.com"
|
306
|
+
|
307
|
+
def call!
|
308
|
+
json = nil
|
309
|
+
response = nil
|
310
|
+
|
311
|
+
code = nil
|
312
|
+
json = nil
|
313
|
+
path = "/heroku/resources"
|
314
|
+
callback = "http://localhost:7779/callback/999"
|
315
|
+
reader, writer = nil
|
316
|
+
|
317
|
+
payload = {
|
318
|
+
:id => APPID,
|
319
|
+
:plan => "Basic",
|
320
|
+
:callback_url => callback
|
321
|
+
}
|
322
|
+
|
323
|
+
if data[:async]
|
324
|
+
reader, writer = IO.pipe
|
325
|
+
end
|
326
|
+
|
327
|
+
test "POST /heroku/resources"
|
328
|
+
check "response" do
|
329
|
+
if data[:async]
|
330
|
+
child = fork do
|
331
|
+
Timeout.timeout(10) do
|
332
|
+
reader.close
|
333
|
+
server = TCPServer.open(7779)
|
334
|
+
client = server.accept
|
335
|
+
writer.write(client.readpartial(READLEN))
|
336
|
+
client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
|
337
|
+
client.close
|
338
|
+
writer.close
|
339
|
+
end
|
340
|
+
end
|
341
|
+
sleep(1)
|
342
|
+
end
|
343
|
+
|
344
|
+
code, json = post(credentials, path, payload)
|
345
|
+
|
346
|
+
if code == 200
|
347
|
+
# noop
|
348
|
+
elsif code == -1
|
349
|
+
error("unable to connect to #{url}")
|
350
|
+
else
|
351
|
+
error("expected 200, got #{code}")
|
352
|
+
end
|
353
|
+
|
354
|
+
true
|
355
|
+
end
|
356
|
+
|
357
|
+
if data[:async]
|
358
|
+
check "async response to PUT #{callback}" do
|
359
|
+
out = reader.readpartial(READLEN)
|
360
|
+
_, json = out.split("\r\n\r\n")
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
check "valid JSON" do
|
365
|
+
begin
|
366
|
+
response = Yajl::Parser.parse(json)
|
367
|
+
rescue Yajl::ParseError => boom
|
368
|
+
error boom.message
|
369
|
+
end
|
370
|
+
true
|
371
|
+
end
|
372
|
+
|
373
|
+
check "authentication" do
|
374
|
+
wrong_credentials = ['wrong', 'secret']
|
375
|
+
code, _ = post(wrong_credentials, path, payload)
|
376
|
+
error("expected 401, got #{code}") if code != 401
|
377
|
+
true
|
378
|
+
end
|
379
|
+
|
380
|
+
data[:create_response] = response
|
381
|
+
|
382
|
+
run CreateResponseCheck, data
|
383
|
+
end
|
384
|
+
|
385
|
+
ensure
|
386
|
+
reader.close rescue nil
|
387
|
+
writer.close rescue nil
|
388
|
+
end
|
389
|
+
|
390
|
+
|
391
|
+
class DeleteCheck < ApiCheck
|
392
|
+
include HTTP
|
393
|
+
|
394
|
+
def call!
|
395
|
+
id = data[:id]
|
396
|
+
raise ArgumentError, "No id specified" if id.nil?
|
397
|
+
|
398
|
+
path = "/heroku/resources/#{id}"
|
399
|
+
|
400
|
+
test "DELETE #{path}"
|
401
|
+
check "response" do
|
402
|
+
code, _ = delete(credentials, path, nil)
|
403
|
+
if code == 200
|
404
|
+
true
|
405
|
+
elsif code == -1
|
406
|
+
error("unable to connect to #{url}")
|
407
|
+
else
|
408
|
+
error("expected 200, got #{code}")
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
check "authentication" do
|
413
|
+
wrong_credentials = ['wrong', 'secret']
|
414
|
+
code, _ = delete(wrong_credentials, path, nil)
|
415
|
+
error("expected 401, got #{code}") if code != 401
|
416
|
+
true
|
417
|
+
end
|
418
|
+
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
##
|
425
|
+
# On Testing:
|
426
|
+
# I've opted to not write tests for this
|
427
|
+
# due to the simple nature it's currently in.
|
428
|
+
# If this becomes more complex in even the
|
429
|
+
# least amount, find me (blake) and I'll
|
430
|
+
# help get tests in.
|
431
|
+
class AllCheck < Check
|
432
|
+
|
433
|
+
def call!
|
434
|
+
args = data[:args]
|
435
|
+
run CreateCheck, data
|
436
|
+
|
437
|
+
response = data[:create_response]
|
438
|
+
id = response["id"]
|
439
|
+
config = response["config"] || Hash.new
|
440
|
+
|
441
|
+
if args
|
442
|
+
screen.message "\n\n"
|
443
|
+
screen.message "Starting #{args.first}..."
|
444
|
+
screen.message ""
|
445
|
+
|
446
|
+
run_in_env(config) { system(*args) }
|
447
|
+
|
448
|
+
screen.message ""
|
449
|
+
screen.message "End of #{args.first}"
|
450
|
+
end
|
451
|
+
|
452
|
+
run DeleteCheck, data.merge(:id => id)
|
453
|
+
end
|
454
|
+
|
455
|
+
def run_in_env(env)
|
456
|
+
env.each {|key, value| ENV[key] = value }
|
457
|
+
yield
|
458
|
+
env.keys.each {|key| ENV.delete(key) }
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|
data/samorau.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
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{samorau}
|
8
|
+
s.version = "0.1.0"
|
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-02-11}
|
13
|
+
s.default_executable = %q{samorau}
|
14
|
+
s.description = %q{}
|
15
|
+
s.email = %q{pedro@heroku.com}
|
16
|
+
s.executables = ["samorau"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"NOTES",
|
23
|
+
"Rakefile",
|
24
|
+
"TODO",
|
25
|
+
"a-server.rb",
|
26
|
+
"bin/samorau",
|
27
|
+
"lib/heroku/samorau.rb",
|
28
|
+
"samorau.gemspec",
|
29
|
+
"server.rb",
|
30
|
+
"set-env.sh",
|
31
|
+
"test/create_check_test.rb",
|
32
|
+
"test/create_response_check_test.rb",
|
33
|
+
"test/delete_check.rb",
|
34
|
+
"test/helper.rb",
|
35
|
+
"test/manifest_check_test.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://heroku.com}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.5}
|
41
|
+
s.summary = %q{}
|
42
|
+
s.test_files = [
|
43
|
+
"test/create_check_test.rb",
|
44
|
+
"test/create_response_check_test.rb",
|
45
|
+
"test/delete_check.rb",
|
46
|
+
"test/helper.rb",
|
47
|
+
"test/manifest_check_test.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_development_dependency(%q<turn>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<contest>, [">= 0"])
|
57
|
+
s.add_runtime_dependency(%q<sinatra>, ["~> 0.9"])
|
58
|
+
s.add_runtime_dependency(%q<rest-client>, ["~> 1.2.0"])
|
59
|
+
s.add_runtime_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
60
|
+
s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<turn>, [">= 0"])
|
63
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
64
|
+
s.add_dependency(%q<sinatra>, ["~> 0.9"])
|
65
|
+
s.add_dependency(%q<rest-client>, ["~> 1.2.0"])
|
66
|
+
s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
67
|
+
s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<turn>, [">= 0"])
|
71
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
72
|
+
s.add_dependency(%q<sinatra>, ["~> 0.9"])
|
73
|
+
s.add_dependency(%q<rest-client>, ["~> 1.2.0"])
|
74
|
+
s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
75
|
+
s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
data/server.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'yajl'
|
3
|
+
require 'restclient'
|
4
|
+
|
5
|
+
post "/heroku/resources" do
|
6
|
+
resp = { :id => 123, :config => { "FOO" => "bar" } }
|
7
|
+
#resp = { :id => 123 }
|
8
|
+
Yajl::Encoder.encode(resp)
|
9
|
+
end
|
10
|
+
|
11
|
+
delete "/heroku/resources/:id" do
|
12
|
+
"ok"
|
13
|
+
end
|
data/set-env.sh
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/helper"
|
2
|
+
require "heroku/samorau"
|
3
|
+
|
4
|
+
class CreateCheckTest < Test::Unit::TestCase
|
5
|
+
include Heroku::Samorau
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@data = Manifest.skeleton
|
9
|
+
@responses = [
|
10
|
+
[200, to_json({ :id => 456 })],
|
11
|
+
[401, "Unauthorized"]
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
def check ; CreateCheck ; end
|
16
|
+
|
17
|
+
test "valid on 200 for the regular check, and 401 for the auth check" do
|
18
|
+
assert_valid do |check|
|
19
|
+
stub :post, check, @responses
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
test "invalid JSON" do
|
24
|
+
@responses[0] = [200, "---"]
|
25
|
+
assert_invalid do |check|
|
26
|
+
stub :post, check, @responses
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test "status other than 200" do
|
31
|
+
@responses[0] = [500, to_json({ :id => 456 })]
|
32
|
+
assert_invalid do |check|
|
33
|
+
stub :post, check, @responses
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
test "runs create response check" do
|
38
|
+
@responses[0] = [200, to_json({ :noid => 456 })]
|
39
|
+
assert_invalid do |check|
|
40
|
+
stub :post, check, @responses
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
test "runs auth check" do
|
45
|
+
@responses[1] = [200, to_json({ :id => 456 })]
|
46
|
+
assert_invalid do |check|
|
47
|
+
stub :post, check, @responses
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/helper"
|
2
|
+
require 'heroku/samorau'
|
3
|
+
|
4
|
+
class CreateResponseCheckTest < Test::Unit::TestCase
|
5
|
+
include Heroku::Samorau
|
6
|
+
|
7
|
+
def check ; CreateResponseCheck ; end
|
8
|
+
|
9
|
+
setup do
|
10
|
+
@response = { "id" => "123" }
|
11
|
+
@data = Manifest.skeleton.merge(:create_response => @response)
|
12
|
+
end
|
13
|
+
|
14
|
+
test "is valid if no errors" do
|
15
|
+
assert_valid
|
16
|
+
end
|
17
|
+
|
18
|
+
test "has an id" do
|
19
|
+
@response.delete("id")
|
20
|
+
assert_invalid
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "when config is present" do
|
24
|
+
|
25
|
+
test "is a hash" do
|
26
|
+
@response["config"] = ""
|
27
|
+
assert_invalid
|
28
|
+
end
|
29
|
+
|
30
|
+
test "each key is previously set in the manifest" do
|
31
|
+
@response["config"] = { "MYSQL_URL" => "http://..." }
|
32
|
+
assert_invalid
|
33
|
+
end
|
34
|
+
|
35
|
+
test "each value is a string" do
|
36
|
+
@response["config"] = { "MYADDON_URL" => {} }
|
37
|
+
assert_invalid
|
38
|
+
end
|
39
|
+
|
40
|
+
test "is valid otherwise" do
|
41
|
+
@response["config"] = { "MYADDON_URL" => "http://..." }
|
42
|
+
assert_valid
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/helper"
|
2
|
+
require "heroku/samorau"
|
3
|
+
|
4
|
+
class DeleteCheckTest < Test::Unit::TestCase
|
5
|
+
include Heroku::Samorau
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@data = Manifest.skeleton.merge :id => 123
|
9
|
+
@responses = [
|
10
|
+
[200, ""],
|
11
|
+
[401, ""],
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
def check ; DeleteCheck ; end
|
16
|
+
|
17
|
+
test "valid on 200" do
|
18
|
+
assert_valid do |check|
|
19
|
+
stub :delete, check, @responses
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
test "status other than 200" do
|
24
|
+
@responses[0] = [500, ""]
|
25
|
+
assert_invalid do |check|
|
26
|
+
stub :delete, check, @responses
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test "runs auth check" do
|
31
|
+
@responses[1] = [200, ""]
|
32
|
+
assert_invalid do |check|
|
33
|
+
stub :delete, check, @responses
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'contest'
|
2
|
+
|
3
|
+
class Test::Unit::TestCase
|
4
|
+
|
5
|
+
def assert_valid(data=@data, &blk)
|
6
|
+
check = create_check(data, &blk)
|
7
|
+
assert check.call
|
8
|
+
end
|
9
|
+
|
10
|
+
def assert_invalid(data=@data, &blk)
|
11
|
+
check = create_check(data, &blk)
|
12
|
+
assert !check.call
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_check(data, &blk)
|
16
|
+
check = self.check.new(data)
|
17
|
+
blk.call(check) if blk
|
18
|
+
check
|
19
|
+
end
|
20
|
+
|
21
|
+
module Headerize
|
22
|
+
attr_accessor :headers
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_json(data, headers={})
|
26
|
+
body = Yajl::Encoder.encode(data)
|
27
|
+
add_headers(body, headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_headers(o, headers={})
|
31
|
+
o.extend Headerize
|
32
|
+
o.headers = {}
|
33
|
+
o.headers["Content-Type"] ||= "application/json"
|
34
|
+
o.headers.merge!(headers)
|
35
|
+
o
|
36
|
+
end
|
37
|
+
|
38
|
+
def stub(meth, o, returns)
|
39
|
+
o.instance_eval { @returns = Array(returns) }
|
40
|
+
eval <<-EVAL
|
41
|
+
def o.#{meth}(*args)
|
42
|
+
@returns.shift || fail("Nothing else to return from stub'ed method")
|
43
|
+
end
|
44
|
+
EVAL
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/helper"
|
2
|
+
require 'heroku/samorau'
|
3
|
+
|
4
|
+
class ManifestCheckTest < Test::Unit::TestCase
|
5
|
+
include Heroku::Samorau
|
6
|
+
|
7
|
+
def check ; ManifestCheck ; end
|
8
|
+
|
9
|
+
setup do
|
10
|
+
@data = Manifest.skeleton
|
11
|
+
end
|
12
|
+
|
13
|
+
test "is valid if no errors" do
|
14
|
+
assert_valid
|
15
|
+
end
|
16
|
+
|
17
|
+
test "has a name" do
|
18
|
+
@data.delete("name")
|
19
|
+
assert_invalid
|
20
|
+
end
|
21
|
+
|
22
|
+
test "api key exists" do
|
23
|
+
@data.delete("api")
|
24
|
+
assert_invalid
|
25
|
+
end
|
26
|
+
|
27
|
+
test "api is a Hash" do
|
28
|
+
@data["api"] = ""
|
29
|
+
assert_invalid
|
30
|
+
end
|
31
|
+
|
32
|
+
test "api has a username" do
|
33
|
+
@data["api"].delete("username")
|
34
|
+
assert_invalid
|
35
|
+
end
|
36
|
+
|
37
|
+
test "api has a password" do
|
38
|
+
@data["api"].delete("password")
|
39
|
+
assert_invalid
|
40
|
+
end
|
41
|
+
|
42
|
+
test "api contains test" do
|
43
|
+
@data["api"].delete("test")
|
44
|
+
assert_invalid
|
45
|
+
end
|
46
|
+
|
47
|
+
test "api contains production" do
|
48
|
+
@data["api"].delete("production")
|
49
|
+
assert_invalid
|
50
|
+
end
|
51
|
+
|
52
|
+
test "api contains production of https" do
|
53
|
+
@data["api"]["production"] = "http://foo.com"
|
54
|
+
assert_invalid
|
55
|
+
end
|
56
|
+
|
57
|
+
test "api contains config_vars array" do
|
58
|
+
@data["api"]["config_vars"] = "test"
|
59
|
+
assert_invalid
|
60
|
+
end
|
61
|
+
|
62
|
+
test "api contains at least one config var" do
|
63
|
+
@data["api"]["config_vars"].clear
|
64
|
+
assert_invalid
|
65
|
+
end
|
66
|
+
|
67
|
+
test "all config vars are in upper case" do
|
68
|
+
@data["api"]["config_vars"] << 'invalid_var'
|
69
|
+
assert_invalid
|
70
|
+
end
|
71
|
+
|
72
|
+
test "plans key must exist" do
|
73
|
+
@data.delete("plans")
|
74
|
+
assert_invalid
|
75
|
+
end
|
76
|
+
|
77
|
+
test "plans key must be an Array" do
|
78
|
+
@data["plans"] = ""
|
79
|
+
assert_invalid
|
80
|
+
end
|
81
|
+
|
82
|
+
test "has at least one plan" do
|
83
|
+
@data["plans"] = []
|
84
|
+
assert_invalid
|
85
|
+
end
|
86
|
+
|
87
|
+
test "all plans are a hash" do
|
88
|
+
@data["plans"][0] = ""
|
89
|
+
assert_invalid
|
90
|
+
end
|
91
|
+
|
92
|
+
test "all plans have a name" do
|
93
|
+
@data["plans"].first.delete("name")
|
94
|
+
assert_invalid
|
95
|
+
end
|
96
|
+
|
97
|
+
test "all plans have a unique name" do
|
98
|
+
@data["plans"] << @data["plans"].first.dup
|
99
|
+
assert_invalid
|
100
|
+
end
|
101
|
+
|
102
|
+
test "plans have a price" do
|
103
|
+
@data["plans"].first.delete("price")
|
104
|
+
assert_invalid
|
105
|
+
end
|
106
|
+
|
107
|
+
test "plans have an integer price" do
|
108
|
+
@data["plans"].first["price"] = "fiddy cent"
|
109
|
+
assert_invalid
|
110
|
+
end
|
111
|
+
|
112
|
+
test "plans have a valid price_unit" do
|
113
|
+
@data["plans"].first["price_unit"] = "first ov da munth"
|
114
|
+
assert_invalid
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: samorau
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Blake Mizerany
|
8
|
+
- Pedro Belo
|
9
|
+
- Adam Wiggins
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2010-02-11 00:00:00 -08:00
|
15
|
+
default_executable: samorau
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: turn
|
19
|
+
type: :development
|
20
|
+
version_requirement:
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: "0"
|
26
|
+
version:
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: contest
|
29
|
+
type: :development
|
30
|
+
version_requirement:
|
31
|
+
version_requirements: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
version:
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: sinatra
|
39
|
+
type: :runtime
|
40
|
+
version_requirement:
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0.9"
|
46
|
+
version:
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rest-client
|
49
|
+
type: :runtime
|
50
|
+
version_requirement:
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.2.0
|
56
|
+
version:
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: yajl-ruby
|
59
|
+
type: :runtime
|
60
|
+
version_requirement:
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0.6"
|
66
|
+
version:
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: term-ansicolor
|
69
|
+
type: :runtime
|
70
|
+
version_requirement:
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "1.0"
|
76
|
+
version:
|
77
|
+
description: ""
|
78
|
+
email: pedro@heroku.com
|
79
|
+
executables:
|
80
|
+
- samorau
|
81
|
+
extensions: []
|
82
|
+
|
83
|
+
extra_rdoc_files:
|
84
|
+
- TODO
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- NOTES
|
88
|
+
- Rakefile
|
89
|
+
- TODO
|
90
|
+
- a-server.rb
|
91
|
+
- bin/samorau
|
92
|
+
- lib/heroku/samorau.rb
|
93
|
+
- samorau.gemspec
|
94
|
+
- server.rb
|
95
|
+
- set-env.sh
|
96
|
+
- test/create_check_test.rb
|
97
|
+
- test/create_response_check_test.rb
|
98
|
+
- test/delete_check.rb
|
99
|
+
- test/helper.rb
|
100
|
+
- test/manifest_check_test.rb
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://heroku.com
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options:
|
107
|
+
- --charset=UTF-8
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: "0"
|
115
|
+
version:
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: "0"
|
121
|
+
version:
|
122
|
+
requirements: []
|
123
|
+
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.3.5
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: ""
|
129
|
+
test_files:
|
130
|
+
- test/create_check_test.rb
|
131
|
+
- test/create_response_check_test.rb
|
132
|
+
- test/delete_check.rb
|
133
|
+
- test/helper.rb
|
134
|
+
- test/manifest_check_test.rb
|