kensa 1.1.4 → 1.2.0rc1

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.
@@ -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