kensa 1.2.0rc7 → 1.2.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 -1
- data/Gemfile +5 -4
- data/Gemfile.lock +21 -27
- data/README.md +1 -1
- data/Rakefile +18 -10
- data/bin/kensa +10 -30
- data/kensa.gemspec +27 -20
- data/lib/heroku/kensa.rb +7 -9
- data/lib/heroku/kensa/check.rb +499 -0
- data/lib/heroku/kensa/client.rb +67 -89
- data/lib/heroku/kensa/git.rb +39 -0
- data/lib/heroku/kensa/manifest.rb +41 -8
- data/lib/heroku/kensa/screen.rb +37 -0
- data/lib/heroku/kensa/sso.rb +22 -22
- data/lib/heroku/kensa/version.rb +1 -2
- data/test/all_check_test.rb +25 -0
- data/test/create_test.rb +40 -6
- data/test/deprovision_check_test.rb +39 -0
- data/test/helper.rb +74 -11
- data/test/init_test.rb +54 -0
- data/test/manifest_check_test.rb +94 -0
- data/test/manifest_test.rb +37 -33
- data/test/plan_change_check_test.rb +31 -0
- data/test/provision_check_test.rb +51 -0
- data/test/provision_response_check_test.rb +81 -0
- data/test/resources/runner.rb +1 -0
- data/test/resources/server.rb +227 -0
- data/test/sso_check_test.rb +58 -0
- data/test/sso_test.rb +113 -53
- metadata +97 -91
- data/test.rb +0 -1
- data/test/deprovision_test.rb +0 -30
- data/test/lib/dependencies.rb +0 -12
- data/test/lib/formatter.rb +0 -84
- data/test/lib/http.rb +0 -60
- data/test/lib/response.rb +0 -12
- data/test/manifest_generation_test.rb +0 -32
- data/test/plan_change_test.rb +0 -30
- data/test/provision_test.rb +0 -84
- data/test/resources/provider_server.rb +0 -81
- data/test/resources/views/index.haml +0 -6
- data/test/sso_launch_test.rb +0 -130
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
source
|
1
|
+
source :rubygems
|
2
2
|
gemspec
|
3
|
-
|
4
|
-
|
5
|
-
gem 'ruby-
|
3
|
+
gem 'rake'
|
4
|
+
group :development do
|
5
|
+
gem 'ruby-debug', :platforms => [:ruby_18]
|
6
|
+
gem 'ruby-debug19', :platforms => [:ruby_19]
|
6
7
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,27 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
kensa (1.2.
|
5
|
-
|
6
|
-
launchy (~> 2.0.5)
|
4
|
+
kensa (1.2.0)
|
5
|
+
launchy (>= 0.3.2)
|
7
6
|
mechanize (~> 1.0.0)
|
8
|
-
rest-client (
|
9
|
-
term-ansicolor (~> 1.0
|
10
|
-
|
11
|
-
yajl-ruby (~> 0.8.3)
|
7
|
+
rest-client (>= 1.4.0, < 1.7.0)
|
8
|
+
term-ansicolor (~> 1.0)
|
9
|
+
yajl-ruby (~> 0.6)
|
12
10
|
|
13
11
|
GEM
|
14
12
|
remote: http://rubygems.org/
|
15
13
|
specs:
|
16
14
|
addressable (2.2.6)
|
17
15
|
archive-tar-minitar (0.5.2)
|
18
|
-
artifice (0.6)
|
19
|
-
rack-test
|
20
16
|
columnize (0.3.4)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
json (1.
|
17
|
+
contest (0.1.3)
|
18
|
+
fakefs (0.4.0)
|
19
|
+
haml (3.1.2)
|
20
|
+
json (1.5.3)
|
25
21
|
launchy (2.0.5)
|
26
22
|
addressable (~> 2.2.6)
|
27
23
|
linecache (0.43)
|
@@ -29,15 +25,13 @@ GEM
|
|
29
25
|
ruby_core_source (>= 0.1.4)
|
30
26
|
mechanize (1.0.0)
|
31
27
|
nokogiri (>= 1.2.1)
|
32
|
-
mime-types (1.
|
28
|
+
mime-types (1.17.2)
|
33
29
|
nokogiri (1.5.0)
|
34
|
-
rack (1.3.
|
35
|
-
|
36
|
-
rack (>= 1.0)
|
37
|
-
rake (0.9.2)
|
30
|
+
rack (1.3.2)
|
31
|
+
rake (0.9.2.2)
|
38
32
|
rest-client (1.6.7)
|
39
33
|
mime-types (>= 1.16)
|
40
|
-
rr (1.0.
|
34
|
+
rr (1.0.3)
|
41
35
|
ruby-debug (0.10.4)
|
42
36
|
columnize (>= 0.1)
|
43
37
|
ruby-debug-base (~> 0.10.4.0)
|
@@ -57,9 +51,7 @@ GEM
|
|
57
51
|
rack (~> 1.1)
|
58
52
|
tilt (>= 1.2.2, < 2.0)
|
59
53
|
term-ansicolor (1.0.7)
|
60
|
-
|
61
|
-
hoe (>= 1.5.1)
|
62
|
-
tilt (1.3.3)
|
54
|
+
tilt (1.3.2)
|
63
55
|
timecop (0.3.5)
|
64
56
|
yajl-ruby (0.8.3)
|
65
57
|
|
@@ -67,12 +59,14 @@ PLATFORMS
|
|
67
59
|
ruby
|
68
60
|
|
69
61
|
DEPENDENCIES
|
70
|
-
|
71
|
-
|
62
|
+
contest
|
63
|
+
fakefs
|
64
|
+
haml
|
72
65
|
json
|
73
66
|
kensa!
|
74
|
-
|
67
|
+
rake
|
68
|
+
rr
|
75
69
|
ruby-debug
|
76
70
|
ruby-debug19
|
77
|
-
sinatra (
|
78
|
-
timecop (
|
71
|
+
sinatra (>= 0.9)
|
72
|
+
timecop (>= 0.3.5)
|
data/README.md
CHANGED
@@ -24,6 +24,6 @@ http://provider.heroku.com/resources/technical/build/provisioning
|
|
24
24
|
|
25
25
|
## Meta #######################################################################
|
26
26
|
|
27
|
-
Maintained by
|
27
|
+
Maintained by Pedro Belo.
|
28
28
|
|
29
29
|
Released under the MIT license. http://github.com/heroku/kensa
|
data/Rakefile
CHANGED
@@ -1,12 +1,20 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << '.'
|
6
|
+
t.verbose = true
|
7
|
+
t.test_files = FileList["test/*_test.rb"]
|
8
|
+
end
|
9
|
+
|
10
|
+
desc 'Start the server'
|
11
|
+
task :start do
|
12
|
+
fork { exec "ruby test/resources/server.rb > test_log.txt 2>&1" }
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Stop the server'
|
16
|
+
task :stop do
|
17
|
+
system "ps -ax | grep test/resources/server.rb | grep -v grep | awk '{print $1}' | xargs kill"
|
10
18
|
end
|
11
19
|
|
12
|
-
task :default => :test
|
20
|
+
task :default => [:start, :test, :stop]
|
data/bin/kensa
CHANGED
@@ -4,47 +4,28 @@ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
|
4
4
|
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
5
|
|
6
6
|
require 'rubygems'
|
7
|
-
require 'optparse'
|
8
7
|
require 'heroku/kensa'
|
9
8
|
require 'heroku/kensa/client'
|
10
9
|
|
11
10
|
$stdout.sync = true
|
12
11
|
|
13
|
-
options = {
|
14
|
-
:filename => 'addon-manifest.json',
|
15
|
-
:env => "test",
|
16
|
-
:async => false,
|
17
|
-
}
|
18
|
-
|
19
|
-
ARGV.options do |o|
|
20
|
-
o.on("-f file", "--file") {|filename| options[:filename] = filename }
|
21
|
-
o.on("--async") { options[:async] = true }
|
22
|
-
o.on("--production") { options[:env] = "production" }
|
23
|
-
o.on("--without-sso") { options[:sso] = false }
|
24
|
-
o.on("-h", "--help") { command = "help" }
|
25
|
-
o.on("-p plan", "--plan") { |plan| options[:plan] = plan }
|
26
|
-
o.on("-v", "--version") { options[:command] = "version" }
|
27
|
-
o.on("-t template", "--template") { |t| options[:template] = t }
|
28
|
-
o.parse!
|
29
|
-
end
|
30
|
-
|
31
12
|
include Heroku::Kensa
|
32
13
|
|
33
14
|
begin
|
34
15
|
args = ARGV.dup
|
35
16
|
ARGV.clear
|
36
|
-
Client.new(args
|
37
|
-
rescue Client::
|
38
|
-
puts
|
39
|
-
rescue Client::CommandInvalid
|
17
|
+
Client.new(args).run!
|
18
|
+
rescue Client::CommandInvalid => e
|
19
|
+
puts e.message unless e.message.empty?
|
40
20
|
abort File.read(__FILE__).split('__END__').last
|
41
21
|
end
|
42
22
|
|
43
23
|
__END__
|
44
24
|
Usage: kensa [OPTIONS] command
|
45
25
|
kensa init
|
46
|
-
kensa
|
47
|
-
kensa
|
26
|
+
kensa create <app_name> --template
|
27
|
+
kensa test <type> [arg1 arg2 ...]
|
28
|
+
kensa run <command> [arg1 arg1 ...]
|
48
29
|
|
49
30
|
OPTIONS
|
50
31
|
|
@@ -66,16 +47,14 @@ OPTIONS
|
|
66
47
|
--post
|
67
48
|
Use HTTP POST for single sign-on instead of GET
|
68
49
|
|
69
|
-
|
70
|
-
|
71
|
-
git://github.com/heroku/kensa-template-<name>
|
72
|
-
try sinatra, clojure, or node
|
50
|
+
--template
|
51
|
+
Name of git template on github or full url of git repo.
|
73
52
|
|
74
53
|
COMMANDS
|
75
54
|
|
76
55
|
init Creates a skeleton manifest
|
77
56
|
|
78
|
-
create <
|
57
|
+
create <app> Clone a git repo that contains a template add-on
|
79
58
|
|
80
59
|
test <type> Simulate call from Heroku (provision or deprovision)
|
81
60
|
|
@@ -103,3 +82,4 @@ TEST TYPES
|
|
103
82
|
|
104
83
|
manifest
|
105
84
|
Confirm that the manifest is valid. Automatically runs before all tests.
|
85
|
+
|
data/kensa.gemspec
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
$:.push File.expand_path("../lib", __FILE__)
|
2
3
|
require "heroku/kensa/version"
|
3
4
|
|
4
5
|
Gem::Specification.new do |s|
|
5
|
-
s.name =
|
6
|
-
s.
|
7
|
-
s.
|
8
|
-
|
9
|
-
s.
|
10
|
-
s.
|
11
|
-
s.
|
6
|
+
s.name = %q{kensa}
|
7
|
+
s.version = Heroku::Kensa::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
|
10
|
+
s.authors = ["Blake Mizerany", "Pedro Belo", "Adam Wiggins", 'Glenn Gillen', 'Chris Continanza']
|
11
|
+
s.default_executable = %q{kensa}
|
12
|
+
s.description = %q{Kensa is a command-line tool to help add-on providers integrating their services with Heroku. It manages manifest files, and provides a TDD-like approach for programmers to test and develop their APIs.}
|
13
|
+
s.email = %q{pedro@heroku.com}
|
12
14
|
|
13
15
|
s.rubyforge_project = "kensa"
|
14
16
|
|
@@ -17,17 +19,22 @@ Gem::Specification.new do |s|
|
|
17
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
20
|
s.require_paths = ["lib"]
|
19
21
|
|
20
|
-
s.
|
21
|
-
s.
|
22
|
-
s.
|
23
|
-
|
24
|
-
s.add_development_dependency(
|
25
|
-
s.add_development_dependency(
|
26
|
-
s.
|
27
|
-
s.
|
28
|
-
s.
|
29
|
-
s.
|
30
|
-
s.
|
31
|
-
s.
|
32
|
-
s.add_runtime_dependency(
|
22
|
+
s.homepage = %q{http://provider.heroku.com/resources}
|
23
|
+
s.rubygems_version = %q{1.6.2}
|
24
|
+
s.summary = %q{Tool to help Heroku add-on providers integrating their services}
|
25
|
+
|
26
|
+
s.add_development_dependency(%q<contest>, [">= 0"])
|
27
|
+
s.add_development_dependency(%q<timecop>, [">= 0.3.5"])
|
28
|
+
s.add_development_dependency(%q<sinatra>, [">= 0.9"])
|
29
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
30
|
+
s.add_development_dependency(%q<contest>, [">= 0"])
|
31
|
+
s.add_development_dependency(%q<haml>, [">= 0"])
|
32
|
+
s.add_development_dependency(%q<rr>, [">= 0"])
|
33
|
+
s.add_development_dependency(%q<fakefs>, [">= 0"])
|
34
|
+
s.add_runtime_dependency(%q<rest-client>, ["< 1.7.0", ">= 1.4.0"])
|
35
|
+
s.add_runtime_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
36
|
+
s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
37
|
+
s.add_runtime_dependency(%q<launchy>, [">= 0.3.2"])
|
38
|
+
s.add_runtime_dependency(%q<mechanize>, ["~> 1.0.0"])
|
33
39
|
end
|
40
|
+
|
data/lib/heroku/kensa.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
require 'yajl'
|
2
|
-
require 'mechanize'
|
3
|
-
require 'socket'
|
4
|
-
require 'timeout'
|
5
|
-
require 'uri'
|
6
|
-
base_path = File.dirname(__FILE__)
|
7
|
-
%w{http manifest sso post_proxy}.each do |lib|
|
8
|
-
require "#{base_path}/kensa/#{lib}"
|
9
|
-
end
|
10
1
|
require 'heroku/kensa/version'
|
2
|
+
require 'heroku/kensa/http'
|
3
|
+
require 'heroku/kensa/manifest'
|
4
|
+
require 'heroku/kensa/check'
|
5
|
+
require 'heroku/kensa/sso'
|
6
|
+
require 'heroku/kensa/post_proxy'
|
7
|
+
require 'heroku/kensa/screen'
|
8
|
+
require 'heroku/kensa/git'
|
@@ -0,0 +1,499 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'mechanize'
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module Heroku
|
8
|
+
module Kensa
|
9
|
+
class Check
|
10
|
+
attr_accessor :screen, :data
|
11
|
+
|
12
|
+
class CheckError < StandardError ; end
|
13
|
+
|
14
|
+
def initialize(data, screen=NilScreen.new)
|
15
|
+
@data = data
|
16
|
+
@screen = screen
|
17
|
+
end
|
18
|
+
|
19
|
+
def env
|
20
|
+
@data.fetch(:env, 'test')
|
21
|
+
end
|
22
|
+
|
23
|
+
def test(msg)
|
24
|
+
screen.test msg
|
25
|
+
end
|
26
|
+
|
27
|
+
def check(msg)
|
28
|
+
screen.check(msg)
|
29
|
+
if yield
|
30
|
+
screen.result(true)
|
31
|
+
else
|
32
|
+
raise CheckError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(klass, data)
|
37
|
+
c = klass.new(data, screen)
|
38
|
+
instance_eval(&c)
|
39
|
+
end
|
40
|
+
|
41
|
+
def error(msg)
|
42
|
+
raise CheckError, msg
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
call!
|
47
|
+
true
|
48
|
+
rescue CheckError => boom
|
49
|
+
screen.result(false)
|
50
|
+
screen.error boom.message if boom.message != boom.class.name
|
51
|
+
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_proc
|
56
|
+
me = self
|
57
|
+
Proc.new { me.call! }
|
58
|
+
end
|
59
|
+
|
60
|
+
def url
|
61
|
+
if data['api'][env].is_a? Hash
|
62
|
+
base = data['api'][env]['base_url']
|
63
|
+
uri = URI.parse(base)
|
64
|
+
base.sub!(uri.query, '') if uri.query
|
65
|
+
base.sub(uri.path, '')
|
66
|
+
else
|
67
|
+
data['api'][env].chomp("/")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class ManifestCheck < Check
|
74
|
+
|
75
|
+
ValidPriceUnits = %w[month dyno_hour]
|
76
|
+
|
77
|
+
def call!
|
78
|
+
test "manifest id key"
|
79
|
+
check "if exists" do
|
80
|
+
data.has_key?("id")
|
81
|
+
end
|
82
|
+
check "is a string" do
|
83
|
+
data["id"].is_a?(String)
|
84
|
+
end
|
85
|
+
check "is not blank" do
|
86
|
+
!data["id"].empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
test "manifest api key"
|
90
|
+
check "if exists" do
|
91
|
+
data.has_key?("api")
|
92
|
+
end
|
93
|
+
check "is a hash" do
|
94
|
+
data["api"].is_a?(Hash)
|
95
|
+
end
|
96
|
+
check "contains password" do
|
97
|
+
data["api"].has_key?("password") && data["api"]["password"] != ""
|
98
|
+
end
|
99
|
+
check "contains test url" do
|
100
|
+
data["api"].has_key?("test")
|
101
|
+
end
|
102
|
+
check "contains production url" do
|
103
|
+
data["api"].has_key?("production")
|
104
|
+
end
|
105
|
+
if data['api']['production'].is_a? Hash
|
106
|
+
check "production url uses SSL" do
|
107
|
+
data['api']['production']['base_url'] =~ /^https:/
|
108
|
+
end
|
109
|
+
check "sso url uses SSL" do
|
110
|
+
data['api']['production']['sso_url'] =~ /^https:/
|
111
|
+
end
|
112
|
+
else
|
113
|
+
check "production url uses SSL" do
|
114
|
+
data['api']['production'] =~ /^https:/
|
115
|
+
end
|
116
|
+
end
|
117
|
+
check "contains config_vars array" do
|
118
|
+
data["api"].has_key?("config_vars") && data["api"]["config_vars"].is_a?(Array)
|
119
|
+
end
|
120
|
+
check "containst at least one config var" do
|
121
|
+
!data["api"]["config_vars"].empty?
|
122
|
+
end
|
123
|
+
check "all config vars are uppercase strings" do
|
124
|
+
data["api"]["config_vars"].each do |k, v|
|
125
|
+
if k =~ /^[A-Z][0-9A-Z_]+$/
|
126
|
+
true
|
127
|
+
else
|
128
|
+
error "#{k.inspect} is not a valid ENV key"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
check "all config vars are prefixed with the addon id" do
|
133
|
+
data["api"]["config_vars"].each do |k|
|
134
|
+
addon_key = data['id'].upcase.gsub('-', '_')
|
135
|
+
if k =~ /^#{addon_key}_/
|
136
|
+
true
|
137
|
+
else
|
138
|
+
error "#{k} is not a valid ENV key - must be prefixed with #{addon_key}_"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
check "deprecated fields" do
|
143
|
+
if data["api"].has_key?("username")
|
144
|
+
error "username is deprecated: Please authenticate using the add-on id."
|
145
|
+
end
|
146
|
+
true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
class ProvisionResponseCheck < Check
|
153
|
+
|
154
|
+
def call!
|
155
|
+
response = data[:provision_response]
|
156
|
+
test "response"
|
157
|
+
|
158
|
+
check "contains an id" do
|
159
|
+
response.is_a?(Hash) && response.has_key?("id")
|
160
|
+
end
|
161
|
+
|
162
|
+
screen.message " (id #{response['id']})"
|
163
|
+
|
164
|
+
if response.has_key?("config")
|
165
|
+
test "config data"
|
166
|
+
check "is a hash" do
|
167
|
+
response["config"].is_a?(Hash)
|
168
|
+
end
|
169
|
+
|
170
|
+
check "all config keys were previously defined in the manifest" do
|
171
|
+
response["config"].keys.each do |key|
|
172
|
+
error "#{key} is not in the manifest" unless data["api"]["config_vars"].include?(key)
|
173
|
+
end
|
174
|
+
true
|
175
|
+
end
|
176
|
+
|
177
|
+
check "all keys in the manifest are present" do
|
178
|
+
difference = data['api']['config_vars'] - response['config'].keys
|
179
|
+
unless difference.empty?
|
180
|
+
verb = (difference.size == 1) ? "is" : "are"
|
181
|
+
error "#{difference.join(', ')} #{verb} missing from the manifest"
|
182
|
+
end
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
check "all config values are strings" do
|
187
|
+
response["config"].each do |k, v|
|
188
|
+
if v.is_a?(String)
|
189
|
+
true
|
190
|
+
else
|
191
|
+
error "the key #{k} doesn't contain a string (#{v.inspect})"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
check "URL configs vars" do
|
197
|
+
response["config"].each do |key, value|
|
198
|
+
next unless key =~ /_URL$/
|
199
|
+
begin
|
200
|
+
uri = URI.parse(value)
|
201
|
+
error "#{value} is not a valid URI - missing host" unless uri.host
|
202
|
+
error "#{value} is not a valid URI - missing scheme" unless uri.scheme
|
203
|
+
error "#{value} is not a valid URI - pointing to localhost" if env == 'production' && uri.host == 'localhost'
|
204
|
+
rescue URI::Error
|
205
|
+
error "#{value} is not a valid URI"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
class ApiCheck < Check
|
217
|
+
def base_path
|
218
|
+
if data['api'][env].is_a? Hash
|
219
|
+
URI.parse(data['api'][env]['base_url']).path
|
220
|
+
else
|
221
|
+
'/heroku/resources'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def heroku_id
|
226
|
+
"app#{rand(10000)}@kensa.heroku.com"
|
227
|
+
end
|
228
|
+
|
229
|
+
def credentials
|
230
|
+
[ data['id'], data['api']['password'] ]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
class ProvisionCheck < ApiCheck
|
235
|
+
include HTTP
|
236
|
+
|
237
|
+
READLEN = 1024 * 10
|
238
|
+
|
239
|
+
def call!
|
240
|
+
json = nil
|
241
|
+
response = nil
|
242
|
+
|
243
|
+
code = nil
|
244
|
+
json = nil
|
245
|
+
callback = "http://localhost:7779/callback/999"
|
246
|
+
reader, writer = nil
|
247
|
+
|
248
|
+
payload = {
|
249
|
+
:heroku_id => heroku_id,
|
250
|
+
:plan => data[:plan] || 'test',
|
251
|
+
:callback_url => callback,
|
252
|
+
:logplex_token => nil,
|
253
|
+
:options => {}
|
254
|
+
}
|
255
|
+
|
256
|
+
if data[:async]
|
257
|
+
reader, writer = IO.pipe
|
258
|
+
end
|
259
|
+
|
260
|
+
test "POST /heroku/resources"
|
261
|
+
check "response" do
|
262
|
+
if data[:async]
|
263
|
+
child = fork do
|
264
|
+
reader.close
|
265
|
+
server = TCPServer.open(7779)
|
266
|
+
client = server.accept
|
267
|
+
writer.write(client.readpartial(READLEN))
|
268
|
+
client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
|
269
|
+
client.close
|
270
|
+
writer.close
|
271
|
+
end
|
272
|
+
sleep(1)
|
273
|
+
end
|
274
|
+
|
275
|
+
code, json = post(credentials, base_path, payload)
|
276
|
+
|
277
|
+
if code == 200
|
278
|
+
# noop
|
279
|
+
elsif code == -1
|
280
|
+
error("unable to connect to #{url}")
|
281
|
+
else
|
282
|
+
error("expected 200, got #{code}")
|
283
|
+
end
|
284
|
+
|
285
|
+
true
|
286
|
+
end
|
287
|
+
|
288
|
+
if data[:async]
|
289
|
+
check "async response to PUT #{callback}" do
|
290
|
+
out = reader.readpartial(READLEN)
|
291
|
+
_, json = out.split("\r\n\r\n")
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
check "valid JSON" do
|
296
|
+
begin
|
297
|
+
response = Yajl::Parser.parse(json)
|
298
|
+
rescue Yajl::ParseError => boom
|
299
|
+
error boom.message
|
300
|
+
end
|
301
|
+
true
|
302
|
+
end
|
303
|
+
|
304
|
+
check "authentication" do
|
305
|
+
wrong_credentials = ['wrong', 'secret']
|
306
|
+
code, _ = post(wrong_credentials, base_path, payload)
|
307
|
+
error("expected 401, got #{code}") if code != 401
|
308
|
+
true
|
309
|
+
end
|
310
|
+
|
311
|
+
data[:provision_response] = response
|
312
|
+
|
313
|
+
run ProvisionResponseCheck, data
|
314
|
+
end
|
315
|
+
|
316
|
+
ensure
|
317
|
+
reader.close rescue nil
|
318
|
+
writer.close rescue nil
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
class DeprovisionCheck < ApiCheck
|
323
|
+
include HTTP
|
324
|
+
|
325
|
+
def call!
|
326
|
+
id = data[:id]
|
327
|
+
raise ArgumentError, "No id specified" if id.nil?
|
328
|
+
|
329
|
+
path = "#{base_path}/#{CGI::escape(id.to_s)}"
|
330
|
+
|
331
|
+
test "DELETE #{path}"
|
332
|
+
check "response" do
|
333
|
+
code, _ = delete(credentials, path, nil)
|
334
|
+
if code == 200
|
335
|
+
true
|
336
|
+
elsif code == -1
|
337
|
+
error("unable to connect to #{url}")
|
338
|
+
else
|
339
|
+
error("expected 200, got #{code}")
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
check "authentication" do
|
344
|
+
wrong_credentials = ['wrong', 'secret']
|
345
|
+
code, _ = delete(wrong_credentials, path, nil)
|
346
|
+
error("expected 401, got #{code}") if code != 401
|
347
|
+
true
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|
351
|
+
|
352
|
+
end
|
353
|
+
|
354
|
+
|
355
|
+
class PlanChangeCheck < ApiCheck
|
356
|
+
include HTTP
|
357
|
+
|
358
|
+
def call!
|
359
|
+
id = data[:id]
|
360
|
+
raise ArgumentError, "No id specified" if id.nil?
|
361
|
+
|
362
|
+
new_plan = data[:plan]
|
363
|
+
raise ArgumentError, "No plan specified" if new_plan.nil?
|
364
|
+
|
365
|
+
path = "#{base_path}/#{CGI::escape(id.to_s)}"
|
366
|
+
payload = {:plan => new_plan, :heroku_id => heroku_id}
|
367
|
+
|
368
|
+
test "PUT #{path}"
|
369
|
+
check "response" do
|
370
|
+
code, _ = put(credentials, path, payload)
|
371
|
+
if code == 200
|
372
|
+
true
|
373
|
+
elsif code == -1
|
374
|
+
error("unable to connect to #{url}")
|
375
|
+
else
|
376
|
+
error("expected 200, got #{code}")
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
check "authentication" do
|
381
|
+
wrong_credentials = ['wrong', 'secret']
|
382
|
+
code, _ = put(wrong_credentials, path, payload)
|
383
|
+
error("expected 401, got #{code}") if code != 401
|
384
|
+
true
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
|
390
|
+
class SsoCheck < ApiCheck
|
391
|
+
include HTTP
|
392
|
+
|
393
|
+
def agent
|
394
|
+
@agent ||= Mechanize.new
|
395
|
+
end
|
396
|
+
|
397
|
+
def mechanize_get
|
398
|
+
if @sso.POST?
|
399
|
+
page = agent.post(@sso.post_url, @sso.query_params)
|
400
|
+
else
|
401
|
+
page = agent.get(@sso.get_url)
|
402
|
+
end
|
403
|
+
return page, 200
|
404
|
+
rescue Mechanize::ResponseCodeError => error
|
405
|
+
return nil, error.response_code.to_i
|
406
|
+
rescue Errno::ECONNREFUSED
|
407
|
+
error("connection refused to #{url}")
|
408
|
+
end
|
409
|
+
|
410
|
+
def check(msg)
|
411
|
+
@sso = Sso.new(data)
|
412
|
+
super
|
413
|
+
end
|
414
|
+
|
415
|
+
def call!
|
416
|
+
error("need an sso salt to perform sso test") unless data['api']['sso_salt']
|
417
|
+
|
418
|
+
sso = Sso.new(data)
|
419
|
+
verb = sso.POST? ? 'POST' : 'GET'
|
420
|
+
test "#{verb} #{sso.path}"
|
421
|
+
|
422
|
+
check "validates token" do
|
423
|
+
@sso.token = 'invalid'
|
424
|
+
page, respcode = mechanize_get
|
425
|
+
error("expected 403, got #{respcode}") unless respcode == 403
|
426
|
+
true
|
427
|
+
end
|
428
|
+
|
429
|
+
check "validates timestamp" do
|
430
|
+
@sso.timestamp = (Time.now - 60*6).to_i
|
431
|
+
page, respcode = mechanize_get
|
432
|
+
error("expected 403, got #{respcode}") unless respcode == 403
|
433
|
+
true
|
434
|
+
end
|
435
|
+
|
436
|
+
page_logged_in = nil
|
437
|
+
check "logs in" do
|
438
|
+
page_logged_in, respcode = mechanize_get
|
439
|
+
error("expected 200, got #{respcode}") unless respcode == 200
|
440
|
+
true
|
441
|
+
end
|
442
|
+
|
443
|
+
check "creates the heroku-nav-data cookie" do
|
444
|
+
cookie = agent.cookie_jar.cookies(URI.parse(@sso.full_url)).detect { |c| c.name == 'heroku-nav-data' }
|
445
|
+
error("could not find cookie heroku-nav-data") unless cookie
|
446
|
+
error("expected #{@sso.sample_nav_data}, got #{cookie.value}") unless cookie.value == @sso.sample_nav_data
|
447
|
+
true
|
448
|
+
end
|
449
|
+
|
450
|
+
check "displays the heroku layout" do
|
451
|
+
error("could not find Heroku layout") if page_logged_in.search('div#heroku-header').empty?
|
452
|
+
true
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
|
458
|
+
##
|
459
|
+
# On Testing:
|
460
|
+
# I've opted to not write tests for this
|
461
|
+
# due to the simple nature it's currently in.
|
462
|
+
# If this becomes more complex in even the
|
463
|
+
# least amount, find me (blake) and I'll
|
464
|
+
# help get tests in.
|
465
|
+
class AllCheck < Check
|
466
|
+
|
467
|
+
def call!
|
468
|
+
args = data[:args]
|
469
|
+
run ProvisionCheck, data
|
470
|
+
|
471
|
+
response = data[:provision_response]
|
472
|
+
data.merge!(:id => response["id"])
|
473
|
+
config = response["config"] || Hash.new
|
474
|
+
|
475
|
+
if args
|
476
|
+
screen.message "\n\n"
|
477
|
+
screen.message "Starting #{args.first}..."
|
478
|
+
screen.message "\n\n"
|
479
|
+
|
480
|
+
run_in_env(config) { system(*args) }
|
481
|
+
error("run exited abnormally, expected 0, got #{$?.to_i}") unless $?.to_i == 0
|
482
|
+
|
483
|
+
screen.message "\n"
|
484
|
+
screen.message "End of #{args.first}\n"
|
485
|
+
end
|
486
|
+
|
487
|
+
run DeprovisionCheck, data
|
488
|
+
end
|
489
|
+
|
490
|
+
def run_in_env(env)
|
491
|
+
env.each {|key, value| ENV[key] = value }
|
492
|
+
yield
|
493
|
+
env.keys.each {|key| ENV.delete(key) }
|
494
|
+
end
|
495
|
+
|
496
|
+
end
|
497
|
+
|
498
|
+
end
|
499
|
+
end
|