devise-otp 0.2.0 → 0.3.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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +0 -0
- data/Gemfile +1 -22
- data/LICENSE.txt +0 -0
- data/README.md +43 -66
- data/Rakefile +0 -0
- data/app/assets/javascripts/devise-otp.js +1 -0
- data/app/assets/javascripts/qrcode.js +609 -0
- data/app/controllers/devise_otp/devise/credentials_controller.rb +102 -0
- data/app/controllers/devise_otp/devise/tokens_controller.rb +112 -0
- data/app/views/devise/credentials/refresh.html.erb +19 -0
- data/app/views/devise/credentials/show.html.erb +31 -0
- data/app/views/devise/tokens/_token_secret.html.erb +23 -0
- data/app/views/devise/tokens/_trusted_devices.html.erb +12 -0
- data/app/views/devise/tokens/recovery.html.erb +21 -0
- data/app/views/devise/tokens/recovery_codes.text.erb +3 -0
- data/app/views/devise/tokens/show.html.erb +21 -0
- data/config/locales/en.yml +10 -10
- data/devise-otp.gemspec +14 -9
- data/docs/QR_CODES.md +48 -0
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +22 -14
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +29 -16
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +6 -9
- data/lib/devise_otp_authenticatable/engine.rb +22 -13
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +8 -7
- data/lib/devise_otp_authenticatable/hooks.rb +1 -1
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +28 -28
- data/lib/devise_otp_authenticatable/routes.rb +9 -10
- data/lib/generators/active_record/devise_otp_generator.rb +1 -1
- data/lib/generators/active_record/templates/migration.rb +1 -2
- data/lib/generators/devise_otp/devise_otp_generator.rb +0 -0
- data/lib/generators/devise_otp/install_generator.rb +30 -5
- data/lib/generators/devise_otp/views_generator.rb +2 -3
- data/test/dummy/README.rdoc +0 -0
- data/test/dummy/Rakefile +0 -0
- data/test/dummy/app/assets/config/manifest.js +2 -0
- data/test/dummy/app/assets/javascripts/application.js +1 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -0
- data/test/dummy/app/controllers/application_controller.rb +1 -1
- data/test/dummy/app/controllers/posts_controller.rb +2 -0
- data/test/dummy/app/helpers/application_helper.rb +0 -0
- data/test/dummy/app/helpers/posts_helper.rb +0 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/post.rb +0 -0
- data/test/dummy/app/models/user.rb +1 -1
- data/test/dummy/app/views/layouts/application.html.erb +0 -0
- data/test/dummy/app/views/posts/_form.html.erb +0 -0
- data/test/dummy/app/views/posts/edit.html.erb +0 -0
- data/test/dummy/app/views/posts/index.html.erb +0 -0
- data/test/dummy/app/views/posts/new.html.erb +0 -0
- data/test/dummy/app/views/posts/show.html.erb +0 -0
- data/test/dummy/config/application.rb +2 -1
- data/test/dummy/config/boot.rb +0 -0
- data/test/dummy/config/database.yml +1 -1
- data/test/dummy/config/environment.rb +0 -0
- data/test/dummy/config/environments/development.rb +0 -7
- data/test/dummy/config/environments/production.rb +0 -4
- data/test/dummy/config/environments/test.rb +0 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/test/dummy/config/initializers/devise.rb +0 -0
- data/test/dummy/config/initializers/inflections.rb +0 -0
- data/test/dummy/config/initializers/mime_types.rb +0 -0
- data/test/dummy/config/initializers/secret_token.rb +0 -0
- data/test/dummy/config/initializers/session_store.rb +0 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -0
- data/test/dummy/config/locales/en.yml +0 -0
- data/test/dummy/config/routes.rb +0 -0
- data/test/dummy/config.ru +0 -0
- data/test/dummy/db/migrate/20130125101430_create_users.rb +1 -1
- data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +1 -1
- data/test/dummy/db/migrate/20130131142320_create_posts.rb +1 -1
- data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +2 -2
- data/test/dummy/db/test.sqlite3-journal +0 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/public/404.html +0 -0
- data/test/dummy/public/422.html +0 -0
- data/test/dummy/public/500.html +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/persistence_test.rb +81 -0
- data/test/integration/refresh_test.rb +2 -18
- data/test/integration/sign_in_test.rb +14 -4
- data/test/integration/token_test.rb +31 -0
- data/test/integration_tests_helper.rb +19 -2
- data/test/model_tests_helper.rb +0 -0
- data/test/models/otp_authenticatable_test.rb +14 -9
- data/test/orm/active_record.rb +3 -1
- data/test/test_helper.rb +71 -2
- metadata +135 -24
- data/.travis.yml +0 -11
- data/app/controllers/devise_otp/credentials_controller.rb +0 -106
- data/app/controllers/devise_otp/tokens_controller.rb +0 -105
- data/app/views/devise_otp/credentials/refresh.html.erb +0 -20
- data/app/views/devise_otp/credentials/show.html.erb +0 -23
- data/app/views/devise_otp/tokens/_token_secret.html.erb +0 -17
- data/app/views/devise_otp/tokens/recovery.html.erb +0 -21
- data/app/views/devise_otp/tokens/show.html.erb +0 -31
- data/lib/devise_otp_authenticatable/mapping.rb +0 -19
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/test/dummy/config/routes.rb
CHANGED
|
File without changes
|
data/test/dummy/config.ru
CHANGED
|
File without changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class DeviseOtpAddToUsers < ActiveRecord::Migration
|
|
1
|
+
class DeviseOtpAddToUsers < ActiveRecord::Migration[5.0]
|
|
2
2
|
def self.up
|
|
3
3
|
change_table :users do |t|
|
|
4
4
|
t.string :otp_auth_secret
|
|
@@ -18,7 +18,7 @@ class DeviseOtpAddToUsers < ActiveRecord::Migration
|
|
|
18
18
|
add_index :users, :otp_session_challenge, :unique => true
|
|
19
19
|
add_index :users, :otp_challenge_expires
|
|
20
20
|
end
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
def self.down
|
|
23
23
|
change_table :users do |t|
|
|
24
24
|
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
|
Binary file
|
|
File without changes
|
data/test/dummy/public/404.html
CHANGED
|
File without changes
|
data/test/dummy/public/422.html
CHANGED
|
File without changes
|
data/test/dummy/public/500.html
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'integration_tests_helper'
|
|
3
|
+
|
|
4
|
+
class PersistenceTest < ActionDispatch::IntegrationTest
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@old_persistence = User.otp_trust_persistence
|
|
8
|
+
User.otp_trust_persistence = 3.seconds
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def teardown
|
|
12
|
+
User.otp_trust_persistence = @old_persistence
|
|
13
|
+
Capybara.reset_sessions!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
test 'a user should be requested the otp challenge every log in' do
|
|
17
|
+
# log in 1fa
|
|
18
|
+
user = enable_otp_and_sign_in
|
|
19
|
+
otp_challenge_for user
|
|
20
|
+
|
|
21
|
+
visit user_otp_token_path
|
|
22
|
+
assert_equal user_otp_token_path, current_path
|
|
23
|
+
|
|
24
|
+
sign_out
|
|
25
|
+
sign_user_in
|
|
26
|
+
|
|
27
|
+
assert_equal user_otp_credential_path, current_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test 'a user should be able to set their browser as trusted' do
|
|
31
|
+
# log in 1fa
|
|
32
|
+
user = enable_otp_and_sign_in
|
|
33
|
+
otp_challenge_for user
|
|
34
|
+
|
|
35
|
+
visit user_otp_token_path
|
|
36
|
+
assert_equal user_otp_token_path, current_path
|
|
37
|
+
|
|
38
|
+
click_link('Trust this browser')
|
|
39
|
+
assert_text 'Your browser is trusted.'
|
|
40
|
+
sign_out
|
|
41
|
+
|
|
42
|
+
sign_user_in
|
|
43
|
+
|
|
44
|
+
assert_equal root_path, current_path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test 'a user should be able to download its recovery codes' do
|
|
48
|
+
# log in 1fa
|
|
49
|
+
user = enable_otp_and_sign_in
|
|
50
|
+
otp_challenge_for user
|
|
51
|
+
|
|
52
|
+
visit user_otp_token_path
|
|
53
|
+
assert_equal user_otp_token_path, current_path
|
|
54
|
+
|
|
55
|
+
enable_chrome_headless_downloads(page, "/tmp/devise-otp")
|
|
56
|
+
|
|
57
|
+
DownloadHelper.wait_for_download(count: 1) do
|
|
58
|
+
click_link('Download recovery codes')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
assert_equal 1, DownloadHelper.downloads.size
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test 'trusted status should expire' do
|
|
65
|
+
# log in 1fa
|
|
66
|
+
user = enable_otp_and_sign_in
|
|
67
|
+
otp_challenge_for user
|
|
68
|
+
|
|
69
|
+
visit user_otp_token_path
|
|
70
|
+
assert_equal user_otp_token_path, current_path
|
|
71
|
+
|
|
72
|
+
click_link('Trust this browser')
|
|
73
|
+
assert_text 'Your browser is trusted.'
|
|
74
|
+
sign_out
|
|
75
|
+
|
|
76
|
+
sleep User.otp_trust_persistence.to_i + 1
|
|
77
|
+
sign_user_in
|
|
78
|
+
|
|
79
|
+
assert_equal user_otp_credential_path, current_path
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -21,7 +21,6 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
test 'a user should be prompted for credentials when the credentials_refresh time is expired' do
|
|
24
|
-
|
|
25
24
|
sign_user_in
|
|
26
25
|
visit user_otp_token_path
|
|
27
26
|
assert_equal user_otp_token_path, current_path
|
|
@@ -45,7 +44,6 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
|
45
44
|
fill_in 'user_refresh_password', :with => '12345678'
|
|
46
45
|
click_button 'Continue...'
|
|
47
46
|
assert_equal user_otp_token_path, current_path
|
|
48
|
-
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
test 'a user should NOT be able to access their OTP settings unless refreshing' do
|
|
@@ -63,20 +61,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
|
63
61
|
assert_equal refresh_user_otp_credential_path, current_path
|
|
64
62
|
end
|
|
65
63
|
|
|
66
|
-
test 'user should be
|
|
67
|
-
enable_otp_and_sign_in_with_otp
|
|
68
|
-
|
|
69
|
-
sleep(2)
|
|
70
|
-
visit user_otp_token_path
|
|
71
|
-
assert_equal refresh_user_otp_credential_path, current_path
|
|
72
|
-
|
|
73
|
-
fill_in 'user_refresh_password', :with => '12345678'
|
|
74
|
-
click_button 'Continue...'
|
|
75
|
-
|
|
76
|
-
assert_equal refresh_user_otp_credential_path, current_path
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
test 'user should be finally be able to access their settings, if they provide both a password and a valid OTP token' do
|
|
64
|
+
test 'user should be finally be able to access their settings, and just password is enough' do
|
|
80
65
|
user = enable_otp_and_sign_in_with_otp
|
|
81
66
|
|
|
82
67
|
sleep(2)
|
|
@@ -84,9 +69,8 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
|
84
69
|
assert_equal refresh_user_otp_credential_path, current_path
|
|
85
70
|
|
|
86
71
|
fill_in 'user_refresh_password', :with => '12345678'
|
|
87
|
-
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
|
88
72
|
click_button 'Continue...'
|
|
89
73
|
|
|
90
74
|
assert_equal user_otp_token_path, current_path
|
|
91
75
|
end
|
|
92
|
-
end
|
|
76
|
+
end
|
|
@@ -10,12 +10,12 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
|
10
10
|
test 'a new user should be able to sign in without using their token' do
|
|
11
11
|
create_full_user
|
|
12
12
|
|
|
13
|
-
visit
|
|
13
|
+
visit posts_path
|
|
14
14
|
fill_in 'user_email', :with => 'user@email.invalid'
|
|
15
15
|
fill_in 'user_password', :with => '12345678'
|
|
16
|
-
click_button 'Sign in'
|
|
16
|
+
page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
|
|
17
17
|
|
|
18
|
-
assert_equal
|
|
18
|
+
assert_equal posts_path, current_path
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
test 'a new user, just signed in, should be able to sign in and enable their OTP authentication' do
|
|
@@ -50,6 +50,16 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
|
50
50
|
assert_equal new_user_session_path, current_path
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
test 'fail blank token authentication' do
|
|
54
|
+
enable_otp_and_sign_in
|
|
55
|
+
assert_equal user_otp_credential_path, current_path
|
|
56
|
+
|
|
57
|
+
fill_in 'user_token', :with => ''
|
|
58
|
+
click_button 'Submit Token'
|
|
59
|
+
|
|
60
|
+
assert_equal user_otp_credential_path, current_path
|
|
61
|
+
end
|
|
62
|
+
|
|
53
63
|
test 'successful token authentication' do
|
|
54
64
|
user = enable_otp_and_sign_in
|
|
55
65
|
|
|
@@ -74,4 +84,4 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
|
74
84
|
User.otp_authentication_timeout = old_timeout
|
|
75
85
|
assert_equal new_user_session_path, current_path
|
|
76
86
|
end
|
|
77
|
-
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'integration_tests_helper'
|
|
3
|
+
|
|
4
|
+
class TokenTest < ActionDispatch::IntegrationTest
|
|
5
|
+
|
|
6
|
+
def teardown
|
|
7
|
+
Capybara.reset_sessions!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
test 'disabling OTP after successfully enabling' do
|
|
11
|
+
# log in 1fa
|
|
12
|
+
user = enable_otp_and_sign_in
|
|
13
|
+
assert_equal user_otp_credential_path, current_path
|
|
14
|
+
|
|
15
|
+
# otp 2fa
|
|
16
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
|
17
|
+
click_button 'Submit Token'
|
|
18
|
+
assert_equal root_path, current_path
|
|
19
|
+
|
|
20
|
+
# disable OTP
|
|
21
|
+
disable_otp
|
|
22
|
+
|
|
23
|
+
# logout
|
|
24
|
+
sign_out
|
|
25
|
+
|
|
26
|
+
# log back in 1fa
|
|
27
|
+
sign_user_in(user)
|
|
28
|
+
|
|
29
|
+
assert_equal root_path, current_path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
class ActionDispatch::IntegrationTest
|
|
2
|
+
include Warden::Test::Helpers
|
|
2
3
|
|
|
3
4
|
def warden
|
|
4
5
|
request.env['warden']
|
|
@@ -22,7 +23,6 @@ class ActionDispatch::IntegrationTest
|
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
|
|
26
26
|
def enable_otp_and_sign_in
|
|
27
27
|
user = create_full_user
|
|
28
28
|
sign_user_in(user)
|
|
@@ -36,13 +36,30 @@ class ActionDispatch::IntegrationTest
|
|
|
36
36
|
user
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def otp_challenge_for(user)
|
|
40
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
|
41
|
+
click_button 'Submit Token'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def disable_otp
|
|
45
|
+
visit user_otp_token_path
|
|
46
|
+
uncheck 'user_otp_enabled'
|
|
47
|
+
click_button 'Continue...'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sign_out
|
|
51
|
+
logout :user
|
|
52
|
+
end
|
|
53
|
+
|
|
39
54
|
def sign_user_in(user = nil)
|
|
40
55
|
user ||= create_full_user
|
|
41
56
|
resource_name = user.class.name.underscore
|
|
42
57
|
visit send("new_#{resource_name}_session_path")
|
|
43
58
|
fill_in "#{resource_name}_email", :with => user.email
|
|
44
59
|
fill_in "#{resource_name}_password", :with => user.password
|
|
45
|
-
|
|
60
|
+
|
|
61
|
+
page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
|
|
46
62
|
user
|
|
47
63
|
end
|
|
64
|
+
|
|
48
65
|
end
|
data/test/model_tests_helper.rb
CHANGED
|
File without changes
|
|
@@ -3,17 +3,17 @@ require 'model_tests_helper'
|
|
|
3
3
|
|
|
4
4
|
class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
def setup
|
|
7
|
+
new_user
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
test 'new users have a non-nil secret set' do
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
assert_not_nil User.first.otp_auth_secret
|
|
12
|
+
end
|
|
13
13
|
|
|
14
14
|
test 'new users have OTP disabled by default' do
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
assert !User.first.otp_enabled
|
|
16
|
+
end
|
|
17
17
|
|
|
18
18
|
test 'users should have an instance of TOTP/ROTP objects' do
|
|
19
19
|
u = User.first
|
|
@@ -80,6 +80,12 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
|
80
80
|
assert_equal false, u.otp_challenge_valid?
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
test 'null otp challenge' do
|
|
84
|
+
u = User.first
|
|
85
|
+
u.update_attribute(:otp_enabled, true)
|
|
86
|
+
assert_equal false, u.validate_otp_token('')
|
|
87
|
+
assert_equal false, u.validate_otp_token(nil)
|
|
88
|
+
end
|
|
83
89
|
|
|
84
90
|
test 'generated otp token should be valid for the user' do
|
|
85
91
|
u = User.first
|
|
@@ -109,8 +115,7 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
|
|
|
109
115
|
recovery = u.next_otp_recovery_tokens
|
|
110
116
|
|
|
111
117
|
assert u.valid_otp_recovery_token? recovery.fetch(0)
|
|
112
|
-
|
|
118
|
+
assert_nil u.valid_otp_recovery_token?(recovery.fetch(0))
|
|
113
119
|
assert u.valid_otp_recovery_token? recovery.fetch(2)
|
|
114
120
|
end
|
|
115
|
-
|
|
116
|
-
end
|
|
121
|
+
end
|
data/test/orm/active_record.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
ActiveRecord::Migration.verbose = false
|
|
2
2
|
ActiveRecord::Base.logger = Logger.new(nil)
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
migrations_path = File.expand_path("../../dummy/db/migrate/", __FILE__)
|
|
5
|
+
|
|
6
|
+
ActiveRecord::MigrationContext.new(migrations_path, ActiveRecord::SchemaMigration).migrate
|
data/test/test_helper.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
ENV["RAILS_ENV"] = "test"
|
|
2
2
|
DEVISE_ORM = (ENV["DEVISE_ORM"] || :active_record).to_sym
|
|
3
3
|
|
|
4
|
-
$:.unshift File.dirname(__FILE__)
|
|
5
4
|
puts "\n==> Devise.orm = #{DEVISE_ORM.inspect}"
|
|
6
5
|
require "dummy/config/environment"
|
|
7
6
|
require "orm/#{DEVISE_ORM}"
|
|
8
7
|
require 'rails/test_help'
|
|
9
8
|
require 'capybara/rails'
|
|
9
|
+
require 'capybara/cuprite'
|
|
10
10
|
require 'minitest/reporters'
|
|
11
11
|
|
|
12
12
|
MiniTest::Reporters.use!
|
|
@@ -15,8 +15,77 @@ MiniTest::Reporters.use!
|
|
|
15
15
|
|
|
16
16
|
#ActiveSupport::Deprecation.silenced = true
|
|
17
17
|
|
|
18
|
-
#
|
|
18
|
+
# Use a module to not pollute the global namespace
|
|
19
|
+
module CapybaraHelper
|
|
20
|
+
def self.register_driver(driver_name, args = [])
|
|
21
|
+
opts = { headless: true, js_errors: true, window_size: [1920, 1200], browser_options: {} }
|
|
22
|
+
args.each do |arg|
|
|
23
|
+
opts[:browser_options][arg] = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Capybara.register_driver(driver_name) do |app|
|
|
27
|
+
Capybara::Cuprite::Driver.new(app, opts)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Register our own custom drivers
|
|
33
|
+
CapybaraHelper.register_driver(:headless_chrome, %w[disable-gpu no-sandbox disable-dev-shm-usage])
|
|
34
|
+
|
|
35
|
+
# Configure Capybara JS driver
|
|
36
|
+
Capybara.current_driver = :headless_chrome
|
|
37
|
+
Capybara.javascript_driver = :headless_chrome
|
|
38
|
+
|
|
39
|
+
# Configure Capybara server
|
|
40
|
+
Capybara.run_server = true
|
|
41
|
+
Capybara.server = :puma, { Silent: true }
|
|
19
42
|
|
|
20
43
|
class ActionDispatch::IntegrationTest
|
|
21
44
|
include Capybara::DSL
|
|
45
|
+
|
|
46
|
+
# What capybara calls a "page" in its DSL is actually a Capybara::Session
|
|
47
|
+
# and doesn't know about the *command* method that allows us to play with
|
|
48
|
+
# the Chrome API.
|
|
49
|
+
# See: https://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session
|
|
50
|
+
#
|
|
51
|
+
# To enable downloads we need to do it on the browser's page object, so fetch it
|
|
52
|
+
# from this long method chain.
|
|
53
|
+
# See: https://github.com/rubycdp/ferrum/blob/master/lib/ferrum/page.rb
|
|
54
|
+
def enable_chrome_headless_downloads(session, directory)
|
|
55
|
+
page = session.driver.browser.page
|
|
56
|
+
page.command('Page.setDownloadBehavior', behavior: 'allow', downloadPath: directory)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# From https://collectiveidea.com/blog/archives/2012/01/27/testing-file-downloads-with-capybara-and-chromedriver
|
|
61
|
+
module DownloadHelper
|
|
62
|
+
extend self
|
|
63
|
+
|
|
64
|
+
TIMEOUT = 10
|
|
65
|
+
|
|
66
|
+
def downloads
|
|
67
|
+
Dir["/tmp/devise-otp/*"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def wait_for_download(count: 1)
|
|
71
|
+
yield if block_given?
|
|
72
|
+
|
|
73
|
+
Timeout.timeout(TIMEOUT) do
|
|
74
|
+
sleep 0.2 until downloaded?(count)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def downloaded?(count)
|
|
79
|
+
!downloading? && downloads.size == count
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def downloading?
|
|
83
|
+
downloads.grep(/\.crdownload$/).any?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def clear_downloads
|
|
87
|
+
FileUtils.rm_f(downloads)
|
|
88
|
+
end
|
|
22
89
|
end
|
|
90
|
+
|
|
91
|
+
require "devise-otp"
|