devise_saml_authenticatable 1.3.1 → 1.6.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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.travis.yml +37 -22
  4. data/Gemfile +2 -10
  5. data/README.md +127 -44
  6. data/app/controllers/devise/saml_sessions_controller.rb +38 -7
  7. data/devise_saml_authenticatable.gemspec +2 -1
  8. data/lib/devise_saml_authenticatable.rb +70 -0
  9. data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
  10. data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +10 -2
  11. data/lib/devise_saml_authenticatable/exception.rb +1 -1
  12. data/lib/devise_saml_authenticatable/model.rb +20 -32
  13. data/lib/devise_saml_authenticatable/routes.rb +17 -6
  14. data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +38 -0
  15. data/lib/devise_saml_authenticatable/saml_response.rb +16 -0
  16. data/lib/devise_saml_authenticatable/strategy.rb +10 -2
  17. data/lib/devise_saml_authenticatable/version.rb +1 -1
  18. data/spec/controllers/devise/saml_sessions_controller_spec.rb +118 -11
  19. data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
  20. data/spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb +34 -4
  21. data/spec/devise_saml_authenticatable/model_spec.rb +199 -5
  22. data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
  23. data/spec/devise_saml_authenticatable/strategy_spec.rb +18 -0
  24. data/spec/features/saml_authentication_spec.rb +45 -21
  25. data/spec/rails_helper.rb +6 -2
  26. data/spec/routes/routes_spec.rb +102 -0
  27. data/spec/spec_helper.rb +7 -0
  28. data/spec/support/Gemfile.rails4 +24 -6
  29. data/spec/support/Gemfile.rails5 +25 -0
  30. data/spec/support/Gemfile.rails5.1 +25 -0
  31. data/spec/support/Gemfile.rails5.2 +25 -0
  32. data/spec/support/attribute-map.yml +12 -0
  33. data/spec/support/attribute_map_resolver.rb.erb +14 -0
  34. data/spec/support/idp_settings_adapter.rb.erb +5 -5
  35. data/spec/support/idp_template.rb +8 -1
  36. data/spec/support/rails_app.rb +110 -16
  37. data/spec/support/saml_idp_controller.rb.erb +22 -10
  38. data/spec/support/sp_template.rb +52 -21
  39. metadata +26 -10
  40. data/spec/support/Gemfile.ruby-saml-1.3 +0 -23
@@ -3,12 +3,16 @@ ENV["RAILS_ENV"] ||= 'test'
3
3
  require 'spec_helper'
4
4
 
5
5
  create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "false")
6
- require 'support/sp/config/environment'
6
+ require "#{working_directory}/sp/config/environment"
7
7
  require 'rspec/rails'
8
8
 
9
9
  ActiveRecord::Migration.verbose = false
10
10
  ActiveRecord::Base.logger = Logger.new(nil)
11
- ActiveRecord::Migrator.migrate(File.expand_path("../support/sp/db/migrate/", __FILE__))
11
+ if ActiveRecord::Base.connection.respond_to?(:migration_context)
12
+ ActiveRecord::Base.connection.migration_context.migrate
13
+ else
14
+ ActiveRecord::Migrator.migrate("#{working_directory}/sp/db/migrate/")
15
+ end
12
16
 
13
17
  RSpec.configure do |config|
14
18
  config.use_transactional_fixtures = true
@@ -0,0 +1,102 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'SamlAuthenticatable Routes', type: :routing do
4
+ describe 'GET /users/saml/sign_in (login)' do
5
+ it 'routes to Devise::SamlSessionsController#new' do
6
+ expect(get: '/users/saml/sign_in').to route_to(controller: 'devise/saml_sessions', action: 'new')
7
+ expect(get: new_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'new')
8
+ end
9
+ end
10
+
11
+ describe 'POST /users/saml/auth (session creation)' do
12
+ it 'routes to Devise::SamlSessionsController#create' do
13
+ expect(post: '/users/saml/auth').to route_to(controller: 'devise/saml_sessions', action: 'create')
14
+ end
15
+ end
16
+
17
+ describe 'DELETE /users/sign_out (logout)' do
18
+ it 'routes to Devise::SamlSessionsController#destroy' do
19
+ expect(delete: '/users/sign_out').to route_to(controller: 'devise/saml_sessions', action: 'destroy')
20
+ expect(delete: destroy_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'destroy')
21
+ end
22
+ end
23
+
24
+ describe 'GET /users/saml/metadata' do
25
+ it 'routes to Devise::SamlSessionsController#metadata' do
26
+ expect(get: '/users/saml/metadata').to route_to(controller: 'devise/saml_sessions', action: 'metadata')
27
+ end
28
+ end
29
+
30
+ describe 'GET /users/saml/idp_sign_out (IdP-initiated logout)' do
31
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
32
+ expect(get: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
33
+ end
34
+ end
35
+
36
+ describe 'POST /users/saml/idp_sign_out (IdP-initiated logout)' do
37
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
38
+ expect(post: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
39
+ end
40
+ end
41
+
42
+ context 'when saml_route_helper_prefix is "sso"' do
43
+ before(:all) do
44
+ ::Devise.saml_route_helper_prefix = 'sso'
45
+
46
+ # A very simple Rails engine
47
+ module SamlRouteHelperPrefixEngine
48
+ class Engine < ::Rails::Engine
49
+ isolate_namespace SamlRouteHelperPrefixEngine
50
+ end
51
+
52
+ Engine.routes.draw do
53
+ devise_for :users, module: :devise
54
+ end
55
+ end
56
+ end
57
+ after(:all) do
58
+ ::Devise.saml_route_helper_prefix = nil
59
+ end
60
+ routes { SamlRouteHelperPrefixEngine::Engine.routes }
61
+
62
+ describe 'GET /users/saml/sign_in (login)' do
63
+ it 'routes to Devise::SamlSessionsController#new' do
64
+ expect(get: '/users/saml/sign_in').to route_to(controller: 'devise/saml_sessions', action: 'new')
65
+ expect(get: new_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'new')
66
+ end
67
+ end
68
+
69
+ describe 'POST /users/saml/auth (session creation)' do
70
+ it 'routes to Devise::SamlSessionsController#create' do
71
+ expect(post: '/users/saml/auth').to route_to(controller: 'devise/saml_sessions', action: 'create')
72
+ end
73
+ end
74
+
75
+ describe 'DELETE /users/sign_out (logout)' do
76
+ it 'routes to Devise::SamlSessionsController#destroy' do
77
+ expect(delete: '/users/sign_out').to route_to(controller: 'devise/saml_sessions', action: 'destroy')
78
+ expect(delete: destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'destroy')
79
+ end
80
+ end
81
+
82
+ describe 'GET /users/saml/metadata' do
83
+ it 'routes to Devise::SamlSessionsController#metadata' do
84
+ expect(get: '/users/saml/metadata').to route_to(controller: 'devise/saml_sessions', action: 'metadata')
85
+ end
86
+ end
87
+
88
+ describe 'GET /users/saml/idp_sign_out (IdP-initiated logout)' do
89
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
90
+ expect(get: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
91
+ expect(get: idp_destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
92
+ end
93
+ end
94
+
95
+ describe 'POST /users/saml/idp_sign_out (IdP-initiated logout)' do
96
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
97
+ expect(post: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
98
+ expect(post: idp_destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,5 @@
1
+ require "fileutils"
2
+
1
3
  RSpec.configure do |config|
2
4
  config.run_all_when_everything_filtered = true
3
5
  config.filter_run :focus
@@ -28,8 +30,13 @@ RSpec.configure do |config|
28
30
  Devise.saml_session_index_key = @original_saml_session_index_key
29
31
  Devise.idp_settings_adapter = nil
30
32
  end
33
+
34
+ config.after :suite do
35
+ FileUtils.rm_rf($working_directory) if $working_directory
36
+ end
31
37
  end
32
38
 
33
39
  require 'support/rails_app'
34
40
 
41
+ require "action_controller" # https://github.com/heartcombo/responders/pull/95
35
42
  require 'devise_saml_authenticatable'
@@ -4,20 +4,38 @@ source 'https://rubygems.org'
4
4
  gemspec path: '../..'
5
5
 
6
6
  group :test do
7
- gem 'rake'
8
7
  gem 'rspec', '~> 3.0'
9
8
  gem 'rails', '~> 4.0'
10
- gem 'rspec-rails'
11
- gem 'sqlite3'
9
+ gem 'rspec-rails', '~> 3.9'
10
+ gem 'sqlite3', '~> 1.3.6'
12
11
  gem 'capybara'
13
12
  gem 'poltergeist'
14
13
 
15
14
  # Lock down versions of gems for older versions of Ruby
16
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
17
- gem 'addressable', '~> 2.4.0'
18
- gem 'mime-types', '~> 2.99'
15
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
16
+ gem 'rake', '~> 12.2'
17
+ else
18
+ gem 'rake'
19
19
  end
20
+
20
21
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
22
  gem 'devise', '~> 3.5'
23
+ gem 'minitest', '~> 5.11.0'
24
+ gem 'nokogiri', '~> 1.6.8'
25
+ gem 'public_suffix', '~> 2.0.5'
26
+ end
27
+
28
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
29
+ gem 'responders', '~> 1.0'
30
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
31
+ gem 'responders', '~> 2.0'
32
+ end
33
+
34
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.2")
35
+ gem 'byebug', '~> 9.0'
36
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
37
+ gem 'byebug', '~> 10.0'
38
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
39
+ gem 'byebug', '~> 11.0.0'
22
40
  end
23
41
  end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.0.0'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.1.0'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.2'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ "urn:mace:dir:attribute-def:first_name": "first_name"
2
+ "first_name": "first_name"
3
+ "firstName": "first_name"
4
+ "firstname": "first_name"
5
+ "urn:mace:dir:attribute-def:last_name": "last_name"
6
+ "last_name": "last_name"
7
+ "lastName": "last_name"
8
+ "lastname": "last_name"
9
+ "urn:mace:dir:attribute-def:email": "email"
10
+ "email_address": "email"
11
+ "emailAddress": "email"
12
+ "email": "email"
@@ -0,0 +1,14 @@
1
+ class AttributeMapResolver < DeviseSamlAuthenticatable::DefaultAttributeMapResolver
2
+ def attribute_map
3
+ issuer = saml_response.issuers.first
4
+ Rails.logger.info("[#{self.class.name}] issuer=#{issuer.inspect}")
5
+ if issuer == "http://localhost:8009/saml/auth"
6
+ {
7
+ "myemailaddress" => "email",
8
+ "myname" => "name",
9
+ }
10
+ else
11
+ {}
12
+ end
13
+ end
14
+ end
@@ -2,15 +2,15 @@ class IdpSettingsAdapter
2
2
  def self.settings(idp_entity_id)
3
3
  if idp_entity_id == "http://localhost:8020/saml/metadata"
4
4
  {
5
- assertion_consumer_service_url: "acs_url",
5
+ assertion_consumer_service_url: "http://localhost:8020/users/saml/auth",
6
6
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
7
- name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
7
+ name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
8
8
  issuer: "sp_issuer",
9
9
  idp_entity_id: "http://localhost:8020/saml/metadata",
10
10
  authn_context: "",
11
- idp_slo_target_url: "http://www.example.com",
12
- idp_sso_target_url: "http://www.example.com",
13
- idp_cert: "idp_cert"
11
+ idp_slo_target_url: "http://localhost:8010/saml/logout",
12
+ idp_sso_target_url: "http://localhost:8010/saml/auth",
13
+ idp_cert_fingerprint: "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
14
14
  }
15
15
  else
16
16
  {}
@@ -1,9 +1,15 @@
1
1
  # Set up a SAML IdP
2
2
 
3
+ @email_address_attribute_key = ENV.fetch("EMAIL_ADDRESS_ATTRIBUTE_KEY", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
4
+ @name_attribute_key = ENV.fetch("NAME_ATTRIBUTE_KEY", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")
3
5
  @include_subject_in_attributes = ENV.fetch('INCLUDE_SUBJECT_IN_ATTRIBUTES')
4
6
  @valid_destination = ENV.fetch('VALID_DESTINATION', "true")
5
7
 
6
- gem 'ruby-saml-idp'
8
+ if Rails::VERSION::MAJOR < 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR < 2)
9
+ gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "34814fd41f91c493b89aa01ac73c44d241a31245b5bc5542fa4b7317525e1dcfa60ba947b3d085e4e229456fdee0d8af6aac6a63cf750d807ea6fe5d853dff4a"'
10
+ end
11
+
12
+ gem 'ruby-saml-idp', '~> 0.3.3'
7
13
  gem 'thin'
8
14
 
9
15
  insert_into_file('Gemfile', after: /\z/) {
@@ -11,6 +17,7 @@ insert_into_file('Gemfile', after: /\z/) {
11
17
  # Lock down versions of gems for older versions of Ruby
12
18
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
13
19
  gem 'devise', '~> 3.5'
20
+ gem 'nokogiri', '~> 1.6.8'
14
21
  end
15
22
  GEMFILE
16
23
  }
@@ -1,4 +1,9 @@
1
- require 'open3'
1
+ require "open3"
2
+ require "socket"
3
+ require "tempfile"
4
+ require "timeout"
5
+
6
+ APP_READY_TIMEOUT ||= 30
2
7
 
3
8
  def sh!(cmd)
4
9
  unless system(cmd)
@@ -7,38 +12,127 @@ def sh!(cmd)
7
12
  end
8
13
 
9
14
  def app_ready?(pid, port)
10
- Process.getpgid(pid) &&
11
- system("lsof -i:#{port}", out: '/dev/null')
15
+ Process.getpgid(pid) && port_open?(port)
16
+ rescue Errno::ESRCH
17
+ false
12
18
  end
13
19
 
14
20
  def create_app(name, env = {})
15
- rails_new_options = %w(-T -J -S --skip-spring --skip-listen)
16
- rails_new_options << "-O" if name == 'idp'
17
- Dir.chdir(File.expand_path('../../support', __FILE__)) do
18
- FileUtils.rm_rf(name)
19
- system(env, "rails", "new", name, *rails_new_options, "-m", "#{name}_template.rb")
21
+ puts "[#{name}] Creating Rails app"
22
+ rails_new_options = %w[-T -J -S --skip-spring --skip-listen --skip-bootsnap]
23
+ rails_new_options << "-O" if name == "idp"
24
+ with_clean_env do
25
+ Dir.chdir(working_directory) do
26
+ FileUtils.rm_rf(name)
27
+ puts("rails _#{Rails.version}_ new #{name} #{rails_new_options.join(" ")} -m #{File.expand_path("../#{name}_template.rb", __FILE__)}")
28
+ system(env, "rails", "_#{Rails.version}_", "new", name, *rails_new_options, "-m", File.expand_path("../#{name}_template.rb", __FILE__))
29
+ end
20
30
  end
21
31
  end
22
32
 
23
33
  def start_app(name, port, options = {})
34
+ puts "[#{name}] Starting Rails app"
24
35
  pid = nil
25
- Bundler.with_clean_env do
26
- Dir.chdir(File.expand_path("../../support/#{name}", __FILE__)) do
27
- pid = Process.spawn("bundle exec rails server -p #{port}")
28
- sleep 1 until app_ready?(pid, port)
29
- if app_ready?(pid, port)
30
- puts "Launched #{name} on port #{port} (pid #{pid})..."
31
- else
36
+ app_bundle_install(name)
37
+
38
+ with_clean_env do
39
+ Dir.chdir(app_dir(name)) do
40
+ pid = Process.spawn(app_env(name), "bundle exec rails server -p #{port} -e production", chdir: app_dir(name), out: "log/#{name}.log", err: "log/#{name}.err.log")
41
+ begin
42
+ Timeout.timeout(APP_READY_TIMEOUT) do
43
+ sleep 1 until app_ready?(pid, port)
44
+ end
45
+ if app_ready?(pid, port)
46
+ puts "[#{name}] Launched #{name} on port #{port} (pid #{pid})..."
47
+ else
48
+ raise "#{name} failed after starting"
49
+ end
50
+ rescue Timeout::Error
32
51
  raise "#{name} failed to start"
33
52
  end
34
53
  end
35
54
  end
36
55
  pid
56
+ rescue RuntimeError => e
57
+ warn "=== #{name}"
58
+ Dir.chdir(app_dir(name)) do
59
+ warn File.read("log/#{name}.log") if File.exist?("log/#{name}.log")
60
+ warn File.read("log/#{name}.err.log") if File.exist?("log/#{name}.err.log")
61
+ end
62
+ raise e
37
63
  end
38
64
 
39
- def stop_app(pid)
65
+ def stop_app(name, pid)
40
66
  if pid
41
67
  Process.kill(:INT, pid)
42
68
  Process.wait(pid)
43
69
  end
70
+ Dir.chdir(app_dir(name)) do
71
+ if File.exist?("log/#{name}.log")
72
+ puts "=== [#{name}] stdout"
73
+ puts File.read("log/#{name}.log")
74
+ end
75
+ if File.exist?("log/#{name}.err.log")
76
+ warn "=== [#{name}] stderr"
77
+ warn File.read("log/#{name}.err.log")
78
+ end
79
+ if File.exist?("log/production.log")
80
+ puts "=== [#{name}] Rails logs"
81
+ puts File.read("log/production.log")
82
+ end
83
+ end
84
+ end
85
+
86
+ def port_open?(port)
87
+ Timeout::timeout(1) do
88
+ begin
89
+ s = TCPSocket.new('localhost', port)
90
+ s.close
91
+ return true
92
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL
93
+ # try 127.0.0.1
94
+ end
95
+ begin
96
+ s = TCPSocket.new('127.0.0.1', port)
97
+ s.close
98
+ return true
99
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
100
+ return false
101
+ end
102
+ end
103
+ rescue Timeout::Error
104
+ false
105
+ end
106
+
107
+ def app_bundle_install(name)
108
+ with_clean_env do
109
+ Open3.popen3(app_env(name), "bundle install", chdir: app_dir(name)) do |stdin, stdout, stderr, thread|
110
+ stdin.close
111
+ exit_status = thread.value
112
+
113
+ puts stdout.read
114
+ warn stderr.read
115
+ raise "bundle install failed" unless exit_status.success?
116
+ end
117
+ end
118
+ end
119
+
120
+ def app_dir(name)
121
+ File.join(working_directory, name)
122
+ end
123
+
124
+ def app_env(name)
125
+ {"BUNDLE_GEMFILE" => File.join(app_dir(name), "Gemfile"), "RAILS_ENV" => "production"}
126
+ end
127
+
128
+ def working_directory
129
+ $working_directory ||= Dir.mktmpdir("dsa_test")
130
+ end
131
+
132
+ def with_clean_env(&blk)
133
+ if Bundler.respond_to?(:with_original_env)
134
+ Bundler.with_original_env(&blk)
135
+ else
136
+ Bundler.with_clean_env(&blk)
137
+ end
44
138
  end