devise-otp 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|