kensa 1.1.4 → 1.2.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ require 'test/unit'
2
+ require 'test/unit/ui/console/testrunner'
3
+
4
+ def format_kensa_test_name(name)
5
+ name.sub(/\Atest_/,"").match(/\A([^\(]*)/)[1].gsub("_", " ")
6
+ end
7
+
8
+ module Test
9
+ module Unit
10
+ class TestCase
11
+ alias_method :add_error_with_connection_exception, :add_error
12
+ alias_method :add_failure_with_connection_exception, :add_failure
13
+
14
+ private
15
+ def add_error(exception)
16
+ if exception.class == Errno::ECONNREFUSED
17
+ @test_passed = false
18
+ message = "Unable to connect to your API."
19
+ @_result.add_failure(Failure.new(name, filter_backtrace(caller()), message))
20
+ else
21
+ add_error_with_connection_exception(exception)
22
+ end
23
+ end
24
+ end
25
+
26
+ class Failure
27
+ def long_display
28
+ name = format_kensa_test_name(@test_name)
29
+ "#{name} - FAILED: #@message"
30
+ end
31
+ end
32
+
33
+ class Error
34
+ def long_display
35
+ backtrace = filter_backtrace(@exception.backtrace).join("\n ")
36
+ name = format_kensa_test_name(@test_name)
37
+ "#{@exception.class.name} in #{name}:\n#{message}\n #{backtrace}"
38
+ end
39
+ end
40
+
41
+ module UI
42
+ module Console
43
+ class TestRunner
44
+
45
+ alias_method :test_started_old, :test_started
46
+
47
+ def add_fault(fault)
48
+ @faults << fault
49
+ @already_outputted = true
50
+ end
51
+
52
+ def test_started(name)
53
+ if name =~ /\((.*)::([^\)]*)/
54
+ ctx, should = [$1, $2]
55
+ end
56
+ unless ctx.nil? or should.nil?
57
+ if ctx != @ctx
58
+ nl
59
+ output("#{ctx}:")
60
+ end
61
+ @ctx = ctx
62
+ @current_test_text = " ==> #{should}"
63
+ else
64
+ test_started_old(name)
65
+ end
66
+ end
67
+
68
+ def test_finished(name)
69
+ @current_test_text = name.sub(/\Atest_/,"").match(/\A([^\(]*)/)[1].gsub("_", " ")
70
+ if fault = @faults.find {|f| f.test_name == name}
71
+ fault_type = fault.is_a?(Test::Unit::Failure) ? "FAILED" : "ERROR!"
72
+ # NOTE -- Concatenation because "\e[0m]" does funky stuff.
73
+ output("[\e[0;31m#{fault_type}\e[0m" + "] #{@current_test_text}.")
74
+ else
75
+ output("[ \e[0;32mOK\e[0m ] #{@current_test_text}.")
76
+ end
77
+ @already_outputted = false
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -0,0 +1,6 @@
1
+ Response = Struct.new(:code, :body, :cookies) do
2
+ def json_body
3
+ Yajl::Parser.parse(self.body)
4
+ end
5
+ end
6
+
@@ -0,0 +1,56 @@
1
+ class Test::Unit::TestCase
2
+ def make_token(id, salt, timestamp)
3
+ Digest::SHA1.hexdigest([id, salt, timestamp].join(':'))
4
+ end
5
+
6
+ def provider_request(meth, path, params = {}, auth_credentials = nil)
7
+ if auth_credentials.nil?
8
+ auth_credentials = [manifest["id"], manifest["api"]["password"]]
9
+ end
10
+ if path =~ /http/
11
+ uri = URI.parse(path)
12
+ else
13
+ uri = URI.parse(base_url)
14
+ uri.path = path
15
+ end
16
+ if auth_credentials
17
+ uri.userinfo = auth_credentials
18
+ end
19
+ opts = meth == :get ? { :params => params } : params
20
+ response = RestClient.send(meth, "#{uri.to_s}", opts)
21
+ Response.new(response.code, response.body, response.cookies)
22
+ rescue RestClient::Forbidden
23
+ Response.new(403)
24
+ rescue RestClient::Unauthorized
25
+ Response.new(401)
26
+ end
27
+
28
+ def get(path, params = {})
29
+ provider_request(:get, path, params, auth = false)
30
+ end
31
+
32
+ def delete(path, auth_credentials = nil)
33
+ provider_request(:delete, path, params = nil, auth_credentials)
34
+ end
35
+
36
+ def post(path, params = {}, auth_credentials = nil)
37
+ provider_request(:post, path, params, auth_credentials)
38
+ end
39
+
40
+ def put(path, params = {}, auth_credentials = nil)
41
+ provider_request(:put, path, params, auth_credentials)
42
+ end
43
+
44
+ def manifest
45
+ return @manifest if @manifest
46
+ @manifest ||= $manifest || Heroku::Kensa::Manifest.new.skeleton
47
+ end
48
+
49
+ def base_url
50
+ if manifest["api"]["test"].is_a?(Hash)
51
+ manifest["api"]["test"]["base_url"].chomp("/")
52
+ else
53
+ manifest["api"]["test"].chomp("/")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ $:.unshift(File.expand_path("../..",__FILE__))
2
+ require 'test/helper'
3
+ class ManifestGenerationTest < Test::Unit::TestCase
4
+ include Heroku::Kensa
5
+
6
+ def setup
7
+ super
8
+ @manifest = Manifest.new
9
+ end
10
+
11
+ def test_generates_a_new_sso_salt_every_time
12
+ assert @manifest.skeleton['api']['sso_salt'] != Manifest.new.skeleton['api']['sso_salt']
13
+ end
14
+
15
+ def test_generates_a_new_password_every_time
16
+ assert @manifest.skeleton['api']['password'] != Manifest.new.skeleton['api']['password']
17
+ end
18
+ end
19
+
20
+ class ManifestGenerationWithoutSSOTest < Test::Unit::TestCase
21
+ include Heroku::Kensa
22
+
23
+ def setup
24
+ super
25
+ options = { :sso => false }
26
+ @manifest = Manifest.new 'test.txt', options
27
+ end
28
+
29
+ def test_exclude_sso_salt
30
+ assert_nil @manifest.skeleton['api']['sso_salt']
31
+ end
32
+ end
@@ -1,36 +1,51 @@
1
+ $:.unshift(File.expand_path("../..",__FILE__))
1
2
  require 'test/helper'
2
-
3
3
  class ManifestTest < Test::Unit::TestCase
4
- include Heroku::Kensa
5
4
 
6
- context 'manifest' do
7
- setup { @manifest = Manifest.new }
5
+ def test_has_an_id
6
+ assert manifest["id"], "Manifest needs to specify the ID of the add-on."
7
+ end
8
8
 
9
- test 'have sso salt' do
10
- assert_not_nil @manifest.skeleton['api']['sso_salt']
11
- end
9
+ def test_has_a_hash_of_api_settings
10
+ assert manifest["api"], "Manifest needs to contain a Hash of API settings."
11
+ assert manifest["api"].is_a?(Hash), "Manifest needs to contain a Hash of API settings."
12
+ end
12
13
 
13
- test 'generates a new sso salt every time' do
14
- assert @manifest.skeleton['api']['ssl_salt'] != Manifest.new.skeleton['api']['sso_salt']
15
- end
14
+ def test_api_has_a_password
15
+ assert manifest["api"]["password"], "Manifest must define a password within the API settings."
16
+ end
16
17
 
17
- test 'has an api password' do
18
- assert_not_nil @manifest.skeleton['api']['password']
19
- end
18
+ def test_api_contains_test
19
+ assert manifest["api"]["test"], "Manifest must define a test environment with the API settings."
20
+ end
21
+
22
+ def test_api_contains_production
23
+ assert manifest["api"]["production"], "Manifest must define a production environment with the API settings."
24
+ end
20
25
 
21
- test 'generates a new password every time' do
22
- assert @manifest.skeleton['api']['password'] != Manifest.new.skeleton['api']['password']
26
+ def test_api_contains_production_of_https
27
+ if manifest["api"]["production"].is_a?(Hash)
28
+ url = manifest["api"]["production"]["base_url"]
29
+ else
30
+ url = manifest["api"]["production"]
23
31
  end
32
+ assert url.match(%r{\Ahttps://}), "Production environment must communicate over HTTPS."
24
33
  end
25
34
 
26
- context 'manifest without sso' do
27
- setup do
28
- options = { :sso => false }
29
- @manifest = Manifest.new 'test.txt', options
35
+ def test_all_config_vars_are_in_upper_case
36
+ manifest["api"]["config_vars"].each do |var|
37
+ assert_equal var.upcase, var, "All config vars must be uppercase, #{var} is not."
30
38
  end
39
+ end
31
40
 
32
- test 'exclude sso salt' do
33
- assert_nil @manifest.skeleton['api']['sso_salt']
41
+ def test_assert_config_var_prefixes_match_addon_id
42
+ id = manifest["id"].upcase.gsub("-", "_")
43
+ manifest["api"]["config_vars"].each do |var|
44
+ assert var.match(%r{\A#{id}_}), "All config vars must be prefixed with the add-on ID (#{id}), #{var} is not."
34
45
  end
35
46
  end
47
+
48
+ def test_username_is_deprecated
49
+ assert !manifest["api"]["username"], "Username has been deprecated."
50
+ end
36
51
  end
@@ -0,0 +1,30 @@
1
+ $:.unshift(File.expand_path("../..",__FILE__))
2
+ require 'test/lib/dependencies'
3
+ class PlanChangeTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ super
7
+ @params = { :plan => "new_plan" }
8
+ end
9
+
10
+ def plan_change(auth = nil, params = @params)
11
+ response = put "/heroku/resources/123", params, auth
12
+ end
13
+
14
+ def test_working_plan_change_call
15
+ response = plan_change
16
+ assert_equal 200, response.code, "Expected a 200 response code on successful plan change."
17
+ end
18
+
19
+ def test_detects_missing_auth
20
+ response = plan_change(auth = false)
21
+ assert_equal 401, response.code, "Provisioning request should require authentication."
22
+
23
+ response = plan_change(auth = [manifest["id"]+"a", manifest["api"]["password"]])
24
+ assert_equal 401, response.code, "Provisioning request appears to allow any username, should require '#{manifest["id"]}'."
25
+
26
+ response = plan_change(auth = [manifest["id"], manifest["api"]["password"]+"a"])
27
+ assert_equal 401, response.code, "Provisioning request appears to allow any password, should require '#{manifest["api"]["password"]}'."
28
+ end
29
+
30
+ end
@@ -0,0 +1,84 @@
1
+ $:.unshift(File.expand_path("../..",__FILE__))
2
+ require 'test/lib/dependencies'
3
+ class ProvisionTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ super
7
+ @params = {}
8
+ end
9
+
10
+ def provision(auth = nil, params = @params)
11
+ post "/heroku/resources", params, auth
12
+ end
13
+
14
+ def test_working_provision_call
15
+ response = provision
16
+ assert_equal 201, response.code, "Expects a 201 - Created response/status code when successfully provisioned."
17
+ end
18
+
19
+ def test_allows_the_definition_of_a_custom_provisioning_endpoint
20
+ #Artifice.activate_with(KensaServer.new)
21
+ #@data['api']['test'] = {
22
+ # "base_url" => "https://example.org/providers/provision",
23
+ # "sso_url" => "https://example.org/sso"
24
+ #}
25
+ #assert_valid
26
+ end
27
+
28
+ def test_expects_a_valid_json_response
29
+ response = provision
30
+ assert response.json_body, "Expects a valid JSON object as response body."
31
+ end
32
+
33
+ def test_detects_missing_id
34
+ response = provision
35
+ assert response.json_body["id"], "Expects JSON response to contain the Provider's unique ID for this app."
36
+ assert response.json_body["id"].to_s.strip != "", "Expects JSON response to contain the Provider's unique ID for this app."
37
+ end
38
+
39
+ def test_provides_app_config
40
+ response = provision
41
+ assert response.json_body["config"].is_a?(Hash), "Expects JSON response to contain a hash of config variables."
42
+ end
43
+
44
+ def test_all_config_values_are_strings
45
+ response = provision
46
+ response.json_body["config"].each do |k,v|
47
+ assert k.is_a?(String), "Expect all config names to be strings ('#{k}' is not)."
48
+ assert v.is_a?(String), "Expect all config values to be strings ('#{v}' is not)."
49
+ end
50
+ end
51
+
52
+ def test_all_config_vars_are_defined_in_the_manifest
53
+ response = provision
54
+ response.json_body["config"].each do |k,v|
55
+ assert manifest["api"]["config_vars"].include?(k), "Only config vars defined in the manfiest can be set ('#{k}' is not)."
56
+ end
57
+ end
58
+
59
+ def test_all_config_url_values_are_valid
60
+ response = provision
61
+ response.json_body["config"].each do |k,v|
62
+ next unless k =~ /_URL\z/
63
+ begin
64
+ uri = URI.parse(v)
65
+ assert uri.host, "#{v} is not a valid URI - missing host"
66
+ assert uri.scheme, "#{v} is not a valid URI - missing scheme"
67
+ rescue URI::InvalidURIError
68
+ assert false, "#{v} is not a valud URI"
69
+ end
70
+ end
71
+ end
72
+
73
+ def test_detects_missing_auth
74
+ response = provision(auth = false)
75
+ assert_equal 401, response.code, "Provisioning request should require authentication."
76
+
77
+ response = provision(auth = [manifest["id"]+"a", manifest["api"]["password"]])
78
+ assert_equal 401, response.code, "Provisioning request appears to allow any username, should require '#{manifest["id"]}'."
79
+
80
+ response = provision(auth = [manifest["id"], manifest["api"]["password"]+"a"])
81
+ assert_equal 401, response.code, "Provisioning request appears to allow any password, should require '#{manifest["api"]["password"]}'."
82
+ end
83
+
84
+ end
@@ -0,0 +1,82 @@
1
+ require 'sinatra'
2
+ require 'json'
3
+
4
+ class ProviderServer < Sinatra::Base
5
+ set :views, File.dirname(__FILE__) + "/views"
6
+
7
+ def initialize(manifest = nil)
8
+ @manifest = manifest
9
+ super
10
+ end
11
+
12
+ helpers do
13
+ def unauthorized!(status=403)
14
+ halt status, "Not authorized\n"
15
+ end
16
+
17
+ def check_timestamp!
18
+ unauthorized! if params[:timestamp].to_i < (Time.now-60*2).to_i
19
+ end
20
+
21
+ def check_token!
22
+ salt = @manifest && @manifest["sso_salt"]
23
+ token = Digest::SHA1.hexdigest([params[:id], salt, params[:timestamp]].join(':'))
24
+ unauthorized! if params[:token] != token
25
+ end
26
+
27
+ def authenticate!
28
+ unless auth_heroku?
29
+ response['WWW-Authenticate'] = %(Basic realm="Kensa Test Server")
30
+ unauthorized!(401)
31
+ end
32
+ end
33
+
34
+ def auth_heroku?
35
+ auth = Rack::Auth::Basic::Request.new(request.env)
36
+ return false unless auth.provided? && auth.basic? && auth.credentials
37
+ if @manifest
38
+ auth.credentials == [@manifest["id"], @manifest["api"]["password"]]
39
+ else
40
+ auth.credentials == ['myaddon', 'secret']
41
+ end
42
+ end
43
+ end
44
+
45
+ delete '/heroku/resources/:id' do
46
+ authenticate!
47
+ status 200
48
+ end
49
+
50
+ put '/heroku/resources/:id' do
51
+ authenticate!
52
+ status 200
53
+ end
54
+
55
+ post '/heroku/resources' do
56
+ authenticate!
57
+ status 201
58
+ { "id" => 52343.to_s,
59
+ "config" => {
60
+ "MYADDON_USER" => "1",
61
+ "MYADDON_URL" => "http://host.example.org/"
62
+ }
63
+ }.to_json
64
+ end
65
+
66
+ get '/heroku/resources/:id' do
67
+ check_timestamp!
68
+ check_token!
69
+ response.set_cookie('heroku-nav-data', params['nav-data'])
70
+ session[:heroku] = true
71
+ haml :index
72
+ end
73
+
74
+ post '/sso/login' do
75
+ check_timestamp!
76
+ check_token!
77
+ response.set_cookie('heroku-nav-data', params['nav-data'])
78
+ session[:heroku] = true
79
+ haml :index
80
+ end
81
+
82
+ end
@@ -0,0 +1,6 @@
1
+ %html
2
+ %body
3
+ - if session[:heroku]
4
+ #heroku-header
5
+ %h1 Heroku
6
+ %h1 Sample Addon
@@ -0,0 +1,130 @@
1
+ $:.unshift(File.expand_path("../..",__FILE__))
2
+ require 'test/helper'
3
+ require 'cgi'
4
+
5
+ module SsoSetupActions
6
+ include Heroku::Kensa
7
+
8
+ def sso_setup
9
+ Timecop.freeze Time.utc(2010, 1)
10
+ @data = Manifest.new.skeleton.merge(:id => 1)
11
+ @data['api']['test'] = 'http://localhost:4567/'
12
+ @data['api']['sso_salt'] = 'SSO_SALT'
13
+ @sso = Sso.new @data
14
+ end
15
+
16
+ def asserts_builds_full_url(env)
17
+ url, query = @sso.full_url.split('?')
18
+ data = CGI.parse(query)
19
+
20
+ assert_equal "#{@data['api'][env]}heroku/resources/1", url
21
+ assert_equal 'b6010f6fbb850887a396c2bc0ab23974003008f6', data['token'].first
22
+ assert_equal '1262304000', data['timestamp'].first
23
+ assert_equal 'username@example.com', data['user'].first
24
+ end
25
+ end
26
+
27
+ class SsoLaunchTest < Test::Unit::TestCase
28
+ include SsoSetupActions
29
+
30
+ def setup
31
+ super
32
+ sso_setup
33
+ end
34
+
35
+ def test_builds_path
36
+ assert_equal '/heroku/resources/1', @sso.path
37
+ end
38
+
39
+ def test_builds_full_url
40
+ asserts_builds_full_url('test')
41
+ end
42
+ end
43
+
44
+ class SsoGetLaunchTest < Test::Unit::TestCase
45
+ include SsoSetupActions
46
+
47
+ def setup
48
+ super
49
+ sso_setup
50
+ @data["api"]["test"] = "http://example.org/"
51
+ @sso = Sso.new(@data).start
52
+ end
53
+
54
+ def test_sso_url_should_be_the_full_url
55
+ assert_equal @sso.full_url, @sso.sso_url
56
+ end
57
+
58
+ def test_message_is_opening_full_url
59
+ assert_equal "Opening #{@sso.full_url}", @sso.message
60
+ end
61
+ end
62
+
63
+ class SsoPostLaunchTest < Test::Unit::TestCase
64
+ include SsoSetupActions
65
+
66
+ def setup
67
+ super
68
+ sso_setup
69
+ @data['api']['test'] = {
70
+ "base_url" => "http://localhost:4567",
71
+ "sso_url" => "http://localhost:4567/users/login/sso"
72
+ }
73
+ end
74
+
75
+ def test_it_starts_the_proxy_server
76
+ Artifice.deactivate
77
+ @sso = Sso.new(@data).start
78
+ body = RestClient.get(@sso.sso_url)
79
+
80
+ assert body.include? 'b6010f6fbb850887a396c2bc0ab23974003008f6'
81
+ assert body.include? '1262304000'
82
+ assert body.include? @sso.url
83
+ assert body.include? @sso.sample_nav_data
84
+ end
85
+ end
86
+
87
+ class SsoPostProxyLaunchTest < Test::Unit::TestCase
88
+ include SsoSetupActions
89
+
90
+ def setup
91
+ super
92
+ sso_setup
93
+ @data['api']['test'] = {
94
+ "base_url" => "http://localhost:4567",
95
+ "sso_url" => "http://localhost:4567/users/login/sso"
96
+ }
97
+ any_instance_of(Sso, :run_proxy => false)
98
+ @sso = Sso.new(@data).start
99
+ end
100
+
101
+ def test_sso_url_should_point_to_the_proxy
102
+ assert_equal "http://localhost:#{@sso.proxy_port}/", @sso.sso_url
103
+ end
104
+
105
+ def test_post_url_contains_url_and_path
106
+ assert_equal "http://localhost:4567/users/login/sso", @sso.post_url
107
+ end
108
+
109
+ def test_message_is_posting_data_to_post_url_via_proxy_on_port_proxy_port
110
+ assert_equal "POSTing #{@sso.query_data} to #{@sso.post_url} via proxy on port #{@sso.proxy_port}", @sso.message
111
+ end
112
+ end
113
+
114
+ class SsoEnvironmentLaunchTest < Test::Unit::TestCase
115
+ include SsoSetupActions
116
+
117
+ def setup
118
+ super
119
+ sso_setup
120
+ env = 'production'
121
+ @data[:env] = env
122
+ @data['api'][env] = 'http://localhost:7654/'
123
+
124
+ @sso = Sso.new @data
125
+ end
126
+
127
+ def test_builds_full_url
128
+ asserts_builds_full_url('production')
129
+ end
130
+ end