devise-security 0.14.1 → 0.18.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 +4 -4
- data/LICENSE.txt +3 -1
- data/README.md +136 -61
- data/app/controllers/devise/paranoid_verification_code_controller.rb +26 -12
- data/app/controllers/devise/password_expired_controller.rb +32 -10
- data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
- data/app/views/devise/password_expired/show.html.erb +5 -5
- data/config/locales/bg.yml +42 -0
- data/config/locales/by.yml +50 -0
- data/config/locales/cs.yml +46 -0
- data/config/locales/de.yml +16 -2
- data/config/locales/en.yml +15 -2
- data/config/locales/es.yml +22 -9
- data/config/locales/fa.yml +42 -0
- data/config/locales/fr.yml +15 -2
- data/config/locales/hi.yml +43 -0
- data/config/locales/it.yml +36 -4
- data/config/locales/ja.yml +14 -1
- data/config/locales/nl.yml +42 -0
- data/config/locales/pt.yml +42 -0
- data/config/locales/ru.yml +50 -0
- data/config/locales/tr.yml +26 -1
- data/config/locales/uk.yml +50 -0
- data/config/locales/zh_CN.yml +42 -0
- data/config/locales/zh_TW.yml +42 -0
- data/lib/devise-security/controllers/helpers.rb +72 -51
- data/lib/devise-security/hooks/expirable.rb +3 -3
- data/lib/devise-security/hooks/paranoid_verification.rb +1 -3
- data/lib/devise-security/hooks/password_expirable.rb +3 -3
- data/lib/devise-security/hooks/session_limitable.rb +29 -14
- data/lib/devise-security/models/compatibility/{active_record.rb → active_record_patch.rb} +14 -2
- data/lib/devise-security/models/compatibility/{mongoid.rb → mongoid_patch.rb} +12 -1
- data/lib/devise-security/models/compatibility.rb +2 -2
- data/lib/devise-security/models/database_authenticatable_patch.rb +18 -10
- data/lib/devise-security/models/expirable.rb +6 -5
- data/lib/devise-security/models/paranoid_verification.rb +2 -2
- data/lib/devise-security/models/password_archivable.rb +3 -3
- data/lib/devise-security/models/password_expirable.rb +5 -1
- data/lib/devise-security/models/secure_validatable.rb +62 -11
- data/lib/devise-security/models/session_limitable.rb +17 -2
- data/lib/devise-security/orm/mongoid.rb +1 -1
- data/lib/devise-security/patches.rb +14 -8
- data/lib/devise-security/routes.rb +2 -3
- data/lib/devise-security/validators/password_complexity_validator.rb +53 -24
- data/lib/devise-security/version.rb +1 -1
- data/lib/devise-security.rb +15 -6
- data/lib/generators/devise_security/install_generator.rb +4 -6
- data/lib/generators/templates/{devise-security.rb → devise_security.rb} +9 -1
- data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
- data/test/controllers/test_password_expired_controller.rb +164 -0
- data/test/{test_security_question_controller.rb → controllers/test_security_question_controller.rb} +19 -37
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
- data/test/dummy/app/controllers/overrides/password_expired_controller.rb +17 -0
- data/test/dummy/app/controllers/widgets_controller.rb +9 -0
- data/test/dummy/app/models/application_user_record.rb +2 -1
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
- data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/mappings.rb +4 -2
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
- data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
- data/test/dummy/app/models/password_expired_user.rb +26 -0
- data/test/dummy/app/models/user.rb +10 -2
- data/test/dummy/app/models/widget.rb +1 -3
- data/test/dummy/app/mongoid/one_user.rb +5 -5
- data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
- data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
- data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
- data/test/dummy/app/mongoid/user_without_email.rb +7 -4
- data/test/dummy/config/application.rb +3 -7
- data/test/dummy/config/boot.rb +1 -1
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/test.rb +4 -13
- data/test/dummy/config/initializers/devise.rb +1 -5
- data/test/dummy/config/initializers/migration_class.rb +1 -8
- data/test/dummy/config/locales/en.yml +10 -0
- data/test/dummy/config/mongoid.yml +1 -1
- data/test/dummy/config/routes.rb +6 -3
- data/test/dummy/config.ru +1 -1
- data/test/dummy/db/migrate/20120508165529_create_tables.rb +15 -6
- data/test/dummy/lib/shared_expirable_columns.rb +1 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
- data/test/dummy/lib/shared_user.rb +17 -6
- data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
- data/test/dummy/lib/shared_verification_fields.rb +1 -0
- data/test/dummy/log/test.log +45240 -0
- data/test/i18n_test.rb +22 -0
- data/test/integration/test_paranoid_verification_code_workflow.rb +53 -0
- data/test/integration/test_password_expirable_workflow.rb +53 -0
- data/test/integration/test_session_limitable_workflow.rb +69 -0
- data/test/orm/active_record.rb +7 -4
- data/test/orm/mongoid.rb +2 -1
- data/test/support/integration_helpers.rb +35 -0
- data/test/support/mongoid.yml +1 -1
- data/test/test_compatibility.rb +15 -0
- data/test/test_complexity_validator.rb +251 -29
- data/test/test_database_authenticatable_patch.rb +146 -0
- data/test/test_helper.rb +23 -8
- data/test/test_install_generator.rb +12 -2
- data/test/test_paranoid_verification.rb +8 -9
- data/test/test_password_archivable.rb +34 -11
- data/test/test_password_expirable.rb +27 -27
- data/test/test_secure_validatable.rb +284 -50
- data/test/test_secure_validatable_overrides.rb +185 -0
- data/test/test_session_limitable.rb +57 -0
- data/test/tmp/config/initializers/devise_security.rb +52 -0
- data/test/tmp/config/locales/devise.security_extension.by.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.cs.yml +46 -0
- data/test/tmp/config/locales/devise.security_extension.de.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.en.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.es.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.fa.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.fr.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.hi.yml +43 -0
- data/test/tmp/config/locales/devise.security_extension.it.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.ja.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.nl.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.pt.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.ru.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.tr.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.uk.yml +50 -0
- data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +42 -0
- data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +42 -0
- metadata +202 -138
- data/.codeclimate.yml +0 -63
- data/.document +0 -5
- data/.gitignore +0 -43
- data/.mdlrc +0 -1
- data/.rubocop.yml +0 -64
- data/.ruby-version +0 -1
- data/.travis.yml +0 -39
- data/Appraisals +0 -35
- data/Gemfile +0 -10
- data/Rakefile +0 -27
- data/devise-security.gemspec +0 -50
- data/gemfiles/rails_4.2_stable.gemfile +0 -16
- data/gemfiles/rails_5.0_stable.gemfile +0 -15
- data/gemfiles/rails_5.1_stable.gemfile +0 -15
- data/gemfiles/rails_5.2_stable.gemfile +0 -15
- data/gemfiles/rails_6.0_beta.gemfile +0 -15
- data/lib/devise-security/orm/active_record.rb +0 -20
- data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -23
- data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -26
- data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -22
- data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -25
- data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -35
- data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -26
- data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -22
- data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -25
- data/lib/devise-security/schema.rb +0 -66
- data/test/dummy/app/controllers/foos_controller.rb +0 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/secure_user.rb +0 -9
- data/test/dummy/lib/shared_user_without_email.rb +0 -28
- data/test/test_password_expired_controller.rb +0 -46
- /data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +0 -0
data/test/i18n_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks'
|
4
|
+
|
5
|
+
class I18nTest < ActiveSupport::TestCase
|
6
|
+
def setup
|
7
|
+
@i18n = I18n::Tasks::BaseTask.new
|
8
|
+
@missing_keys = @i18n.missing_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_no_missing_keys
|
12
|
+
assert_empty @missing_keys,
|
13
|
+
"Missing #{@missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_no_inconsistent_interpolations
|
17
|
+
inconsistent_interpolations = @i18n.inconsistent_interpolations
|
18
|
+
error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
|
19
|
+
"Please run `i18n-tasks check-consistent-interpolations' to show them"
|
20
|
+
assert_empty inconsistent_interpolations, error_message
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestParanoidVerificationCodeWorkflow < ActionDispatch::IntegrationTest
|
6
|
+
include IntegrationHelpers
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@user = User.create!(
|
10
|
+
password: 'passWord1',
|
11
|
+
password_confirmation: 'passWord1',
|
12
|
+
email: 'bob@microsoft.com',
|
13
|
+
paranoid_verification_code: 'cookies'
|
14
|
+
) # the default verification code is nil
|
15
|
+
@user.confirm
|
16
|
+
|
17
|
+
assert @user.valid?
|
18
|
+
assert @user.need_paranoid_verification?
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'sign in and check paranoid verification code' do
|
22
|
+
sign_in(@user)
|
23
|
+
assert_redirected_to(root_path)
|
24
|
+
follow_redirect!
|
25
|
+
assert_redirected_to(user_paranoid_verification_code_path)
|
26
|
+
# @note This is not the same controller used by Devise for password changes
|
27
|
+
patch '/users/verification_code', params: {
|
28
|
+
user: {
|
29
|
+
paranoid_verification_code: 'cookies'
|
30
|
+
}
|
31
|
+
}
|
32
|
+
assert_redirected_to(root_path)
|
33
|
+
@user.reload
|
34
|
+
assert_not @user.need_paranoid_verification?
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'sign in and paranoid verification code is checked before redirect completes' do
|
38
|
+
sign_in(@user)
|
39
|
+
assert_redirected_to(root_path)
|
40
|
+
|
41
|
+
# simulates an external process verifying the paranoid verification code
|
42
|
+
@user.update(paranoid_verification_code: nil)
|
43
|
+
assert_not @user.need_paranoid_verification?
|
44
|
+
|
45
|
+
follow_redirect!
|
46
|
+
assert_response :success
|
47
|
+
|
48
|
+
# if the paranoid verification code is not empty/nil at this point they will be redirected to the
|
49
|
+
# paranoid verification code change controller.
|
50
|
+
get root_path
|
51
|
+
assert_response :success
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestPasswordExpirableWorkflow < ActionDispatch::IntegrationTest
|
6
|
+
include IntegrationHelpers
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@user = User.create!(password: 'passWord1',
|
10
|
+
password_confirmation: 'passWord1',
|
11
|
+
email: 'bob@microsoft.com',
|
12
|
+
password_changed_at: 4.months.ago) # the default expiration time is 3.months.ago
|
13
|
+
@user.confirm
|
14
|
+
|
15
|
+
assert @user.valid?
|
16
|
+
assert @user.need_change_password?
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'sign in and change expired password' do
|
20
|
+
sign_in(@user)
|
21
|
+
assert_redirected_to(root_path)
|
22
|
+
follow_redirect!
|
23
|
+
assert_redirected_to(user_password_expired_path)
|
24
|
+
# @note This is not the same controller used by Devise for password changes
|
25
|
+
put '/users/password_expired', params: {
|
26
|
+
user: {
|
27
|
+
current_password: 'passWord1',
|
28
|
+
password: 'Password12345!',
|
29
|
+
password_confirmation: 'Password12345!'
|
30
|
+
}
|
31
|
+
}
|
32
|
+
assert_redirected_to(root_path)
|
33
|
+
@user.reload
|
34
|
+
assert_not @user.need_change_password?
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'sign in and password is updated before redirect completes' do
|
38
|
+
sign_in(@user)
|
39
|
+
assert_redirected_to(root_path)
|
40
|
+
|
41
|
+
# simulates an external process updating the password
|
42
|
+
@user.update(password_changed_at: Time.zone.now)
|
43
|
+
assert_not @user.need_change_password?
|
44
|
+
|
45
|
+
follow_redirect!
|
46
|
+
assert_response :success
|
47
|
+
|
48
|
+
# if the password is expired at this point they will be redirected to the
|
49
|
+
# password change controller.
|
50
|
+
get root_path
|
51
|
+
assert_response :success
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
|
6
|
+
include IntegrationHelpers
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@user = User.create!(password: 'passWord1',
|
10
|
+
password_confirmation: 'passWord1',
|
11
|
+
email: 'bob@microsoft.com')
|
12
|
+
@user.confirm
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'failed login' do
|
16
|
+
assert_nil @user.unique_session_id
|
17
|
+
|
18
|
+
open_session do |session|
|
19
|
+
failed_sign_in(@user, session)
|
20
|
+
session.assert_response(:success)
|
21
|
+
assert_equal session.flash[:alert], I18n.t('devise.failure.invalid', authentication_keys: 'Email')
|
22
|
+
assert_nil @user.reload.unique_session_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
test 'successful login' do
|
27
|
+
assert_nil @user.unique_session_id
|
28
|
+
|
29
|
+
open_session do |session|
|
30
|
+
sign_in(@user, session)
|
31
|
+
session.assert_redirected_to '/'
|
32
|
+
session.get widgets_path
|
33
|
+
session.assert_response(:success)
|
34
|
+
assert_equal('success', session.response.body)
|
35
|
+
assert_not_nil @user.reload.unique_session_id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
test 'session is logged out when another session is created' do
|
40
|
+
first_session = open_session
|
41
|
+
second_session = open_session
|
42
|
+
unique_session_id = nil
|
43
|
+
|
44
|
+
first_session.tap do |session|
|
45
|
+
sign_in(@user, session)
|
46
|
+
session.assert_redirected_to '/'
|
47
|
+
session.get widgets_path
|
48
|
+
session.assert_response(:success)
|
49
|
+
assert_equal('success', session.response.body)
|
50
|
+
unique_session_id = @user.reload.unique_session_id
|
51
|
+
assert_not_nil unique_session_id
|
52
|
+
end
|
53
|
+
|
54
|
+
second_session.tap do |session|
|
55
|
+
sign_in(@user, session)
|
56
|
+
session.assert_redirected_to '/'
|
57
|
+
session.get widgets_path
|
58
|
+
session.assert_response(:success)
|
59
|
+
assert_equal('success', session.response.body)
|
60
|
+
assert_not_equal unique_session_id, @user.reload.unique_session_id
|
61
|
+
end
|
62
|
+
|
63
|
+
first_session.tap do |session|
|
64
|
+
session.get widgets_path
|
65
|
+
session.assert_redirected_to new_user_session_path
|
66
|
+
assert_equal session.flash[:alert], I18n.t('devise.failure.session_limited')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/test/orm/active_record.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record'
|
2
4
|
|
3
5
|
ActiveRecord::Migration.verbose = false
|
4
6
|
ActiveRecord::Base.logger = Logger.new(nil)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
|
8
|
+
if Rails.gem_version >= Gem::Version.new('6.0.0')
|
9
|
+
ActiveRecord::MigrationContext.new(File.expand_path('../dummy/db/migrate', __dir__), ActiveRecord::SchemaMigration).migrate
|
10
|
+
elsif Rails.gem_version >= Gem::Version.new('5.2.0')
|
11
|
+
ActiveRecord::MigrationContext.new(File.expand_path('../dummy/db/migrate', __dir__)).migrate
|
9
12
|
end
|
10
13
|
|
11
14
|
DatabaseCleaner[:active_record].strategy = :transaction
|
data/test/orm/mongoid.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'mongoid/version'
|
4
|
+
require 'database_cleaner-mongoid'
|
4
5
|
|
5
6
|
Mongoid.configure do |config|
|
6
7
|
config.load!('test/support/mongoid.yml', Rails.env)
|
@@ -8,5 +9,5 @@ Mongoid.configure do |config|
|
|
8
9
|
config.include_root_in_json = true
|
9
10
|
end
|
10
11
|
|
11
|
-
DatabaseCleaner[:mongoid].strategy = :
|
12
|
+
DatabaseCleaner[:mongoid].strategy = :deletion
|
12
13
|
ORMInvalidRecordException = Mongoid::Errors::Validations
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IntegrationHelpers
|
4
|
+
# login the user. This will exercise all the Warden Hooks
|
5
|
+
# @param user [User]
|
6
|
+
# @param session [ActionDispatch::Integration::Session]
|
7
|
+
# @return [void]
|
8
|
+
def sign_in(user, session = integration_session)
|
9
|
+
session.post(
|
10
|
+
new_user_session_path,
|
11
|
+
params: {
|
12
|
+
user: {
|
13
|
+
email: user.email,
|
14
|
+
password: user.password
|
15
|
+
}
|
16
|
+
}
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# attempt to login the user with a bad password. This will exercise all the Warden Hooks
|
21
|
+
# @param user [User]
|
22
|
+
# @param session [ActionDispatch::Integration::Session]
|
23
|
+
# @return [void]
|
24
|
+
def failed_sign_in(user, session)
|
25
|
+
session.post(
|
26
|
+
new_user_session_path,
|
27
|
+
params: {
|
28
|
+
user: {
|
29
|
+
email: user.email,
|
30
|
+
password: 'bad-password'
|
31
|
+
}
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
data/test/support/mongoid.yml
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class TestCompatibility < ActiveSupport::TestCase
|
6
|
+
test 'can access ActiveRecord namespace' do
|
7
|
+
skip unless DEVISE_ORM == :active_record
|
8
|
+
assert_nothing_raised { User.new.some_method_calling_active_record }
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'can access Mongoid namespace' do
|
12
|
+
skip unless DEVISE_ORM == :mongoid
|
13
|
+
assert_nothing_raised { User.new.some_method_calling_mongoid }
|
14
|
+
end
|
15
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'test_helper'
|
2
4
|
|
3
|
-
class PasswordComplexityValidatorTest <
|
5
|
+
class PasswordComplexityValidatorTest < ActiveSupport::TestCase
|
4
6
|
class ModelWithPassword
|
5
7
|
include ActiveModel::Validations
|
6
8
|
|
@@ -15,46 +17,266 @@ class PasswordComplexityValidatorTest < Minitest::Test
|
|
15
17
|
ModelWithPassword.clear_validators!
|
16
18
|
end
|
17
19
|
|
20
|
+
def create_model(password, opts = {})
|
21
|
+
ModelWithPassword.validates(
|
22
|
+
:password, 'devise_security/password_complexity': opts
|
23
|
+
)
|
24
|
+
ModelWithPassword.new(password)
|
25
|
+
end
|
26
|
+
|
18
27
|
def test_with_no_rules_anything_goes
|
19
|
-
assert(
|
28
|
+
assert(create_model('aaaa').valid?)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_allows_blank
|
32
|
+
assert(create_model('', { upper: 1 }).valid?)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_enforces_uppercase_invalid
|
36
|
+
model = create_model('aaaa', { upper: 1 })
|
37
|
+
|
38
|
+
assert_not(model.valid?)
|
39
|
+
assert_equal(
|
40
|
+
{ password: ['must contain at least one upper-case letter'] },
|
41
|
+
model.errors.messages
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_enforces_uppercase_valid
|
46
|
+
assert(create_model('Aaaa', { upper: 1 }).valid?)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_enforces_uppercase_count_invalid
|
50
|
+
model = create_model('Aaaa', { upper: 2 })
|
51
|
+
|
52
|
+
assert_not(model.valid?)
|
53
|
+
assert_equal(
|
54
|
+
{ password: ['must contain at least 2 upper-case letters'] },
|
55
|
+
model.errors.messages
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_enforces_uppercase_count_valid
|
60
|
+
assert(create_model('AAaa', { upper: 2 }).valid?)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_enforces_digit_invalid
|
64
|
+
model = create_model('aaaa', { digit: 1 })
|
65
|
+
|
66
|
+
assert_not(model.valid?)
|
67
|
+
assert_equal(
|
68
|
+
{ password: ['must contain at least one digit'] }, model.errors.messages
|
69
|
+
)
|
20
70
|
end
|
21
71
|
|
22
|
-
def
|
23
|
-
|
24
|
-
refute(ModelWithPassword.new('aaaa').valid?)
|
25
|
-
assert(ModelWithPassword.new('Aaaa').valid?)
|
72
|
+
def test_enforces_digit_valid
|
73
|
+
assert(create_model('1aaa', { digit: 1 }).valid?)
|
26
74
|
end
|
27
75
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
76
|
+
def test_enforces_digit_count_invalid
|
77
|
+
model = create_model('1aaa', { digit: 2 })
|
78
|
+
|
79
|
+
assert_not(model.valid?)
|
80
|
+
assert_equal(
|
81
|
+
{ password: ['must contain at least 2 digits'] }, model.errors.messages
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_enforces_digit_count_valid
|
86
|
+
assert(create_model('11aa', { digit: 2 }).valid?)
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_enforces_digits_invalid
|
90
|
+
model = create_model('aaaa', { digits: 1 })
|
91
|
+
|
92
|
+
assert_not(model.valid?)
|
93
|
+
assert_equal(
|
94
|
+
{ password: ['must contain at least one digit'] }, model.errors.messages
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_enforces_digits_valid
|
99
|
+
assert(create_model('1aaa', { digits: 1 }).valid?)
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_enforces_digits_count_invalid
|
103
|
+
model = create_model('1aaa', { digits: 2 })
|
104
|
+
|
105
|
+
assert_not(model.valid?)
|
106
|
+
assert_equal(
|
107
|
+
{ password: ['must contain at least 2 digits'] }, model.errors.messages
|
108
|
+
)
|
32
109
|
end
|
33
110
|
|
34
|
-
def
|
35
|
-
|
36
|
-
refute(ModelWithPassword.new('aaaa').valid?)
|
37
|
-
assert(ModelWithPassword.new('aaa1').valid?)
|
111
|
+
def test_enforces_digits_count_valid
|
112
|
+
assert(create_model('11aa', { digits: 2 }).valid?)
|
38
113
|
end
|
39
114
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
115
|
+
def test_enforces_lower_invalid
|
116
|
+
model = create_model('AAAA', { lower: 1 })
|
117
|
+
|
118
|
+
assert_not(model.valid?)
|
119
|
+
assert_equal(
|
120
|
+
{ password: ['must contain at least one lower-case letter'] },
|
121
|
+
model.errors.messages
|
122
|
+
)
|
44
123
|
end
|
45
124
|
|
46
|
-
def
|
47
|
-
|
48
|
-
refute(ModelWithPassword.new('aaaa').valid?)
|
49
|
-
assert(ModelWithPassword.new('aaa!').valid?)
|
125
|
+
def test_enforces_lower_valid
|
126
|
+
assert(create_model('aAAA', { lower: 1 }).valid?)
|
50
127
|
end
|
51
128
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
129
|
+
def test_enforces_lower_count_invalid
|
130
|
+
model = create_model('aAAA', { lower: 2 })
|
131
|
+
|
132
|
+
assert_not(model.valid?)
|
133
|
+
assert_equal(
|
134
|
+
{ password: ['must contain at least 2 lower-case letters'] },
|
135
|
+
model.errors.messages
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_enforces_lower_count_valid
|
140
|
+
assert(create_model('aaAA', { lower: 2 }).valid?)
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_enforces_symbol_invalid
|
144
|
+
model = create_model('aaaa', { symbol: 1 })
|
145
|
+
|
146
|
+
assert_not(model.valid?)
|
147
|
+
assert_equal(
|
148
|
+
{ password: ['must contain at least one punctuation mark or symbol'] },
|
149
|
+
model.errors.messages
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_enforces_symbol_valid
|
154
|
+
assert(create_model('!aaa', { symbol: 1 }).valid?)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_enforces_symbol_count_invalid
|
158
|
+
model = create_model('!aaa', { symbol: 2 })
|
159
|
+
|
160
|
+
assert_not(model.valid?)
|
161
|
+
assert_equal(
|
162
|
+
{ password: ['must contain at least 2 punctuation marks or symbols'] },
|
163
|
+
model.errors.messages
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_enforces_symbol_count_valid
|
168
|
+
assert(create_model('!!aa', { symbol: 2 }).valid?)
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_enforces_symbols_invalid
|
172
|
+
model = create_model('aaaa', { symbols: 1 })
|
173
|
+
|
174
|
+
assert_not(model.valid?)
|
175
|
+
assert_equal(
|
176
|
+
{ password: ['must contain at least one punctuation mark or symbol'] },
|
177
|
+
model.errors.messages
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_enforces_symbols_valid
|
182
|
+
assert(create_model('!aaa', { symbols: 1 }).valid?)
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_enforces_symbols_count_invalid
|
186
|
+
model = create_model('!aaa', { symbols: 2 })
|
187
|
+
|
188
|
+
assert_not(model.valid?)
|
189
|
+
assert_equal(
|
190
|
+
{ password: ['must contain at least 2 punctuation marks or symbols'] },
|
191
|
+
model.errors.messages
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_enforces_symbols_count_valid
|
196
|
+
assert(create_model('!!aa', { symbols: 2 }).valid?)
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_enforces_combination_only_lower_invalid
|
200
|
+
model = create_model('aaaa', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
201
|
+
|
202
|
+
assert_not(model.valid?)
|
203
|
+
assert_equal(
|
204
|
+
{
|
205
|
+
password:
|
206
|
+
[
|
207
|
+
'must contain at least one digit',
|
208
|
+
'must contain at least one punctuation mark or symbol',
|
209
|
+
'must contain at least one upper-case letter'
|
210
|
+
]
|
211
|
+
},
|
212
|
+
model.errors.messages
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
def test_enforces_combination_only_upper_invalid
|
217
|
+
model = create_model('AAAA', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
218
|
+
|
219
|
+
assert_not(model.valid?)
|
220
|
+
assert_equal(
|
221
|
+
{
|
222
|
+
password:
|
223
|
+
[
|
224
|
+
'must contain at least one digit',
|
225
|
+
'must contain at least one lower-case letter',
|
226
|
+
'must contain at least one punctuation mark or symbol'
|
227
|
+
]
|
228
|
+
},
|
229
|
+
model.errors.messages
|
230
|
+
)
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_enforces_combination_only_digit_invalid
|
234
|
+
model = create_model('1111', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
235
|
+
|
236
|
+
assert_not(model.valid?)
|
237
|
+
assert_equal(
|
238
|
+
{
|
239
|
+
password:
|
240
|
+
[
|
241
|
+
'must contain at least one lower-case letter',
|
242
|
+
'must contain at least one punctuation mark or symbol',
|
243
|
+
'must contain at least one upper-case letter'
|
244
|
+
]
|
245
|
+
},
|
246
|
+
model.errors.messages
|
247
|
+
)
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_enforces_combination_only_symbol_invalid
|
251
|
+
model = create_model('!!!!', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
252
|
+
|
253
|
+
assert_not(model.valid?)
|
254
|
+
assert_equal(
|
255
|
+
{
|
256
|
+
password:
|
257
|
+
[
|
258
|
+
'must contain at least one digit',
|
259
|
+
'must contain at least one lower-case letter',
|
260
|
+
'must contain at least one upper-case letter'
|
261
|
+
]
|
262
|
+
},
|
263
|
+
model.errors.messages
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_enforces_combination_some_but_not_all_invalid
|
268
|
+
model = create_model('aAa!', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
269
|
+
|
270
|
+
assert_not(model.valid?)
|
271
|
+
assert_equal(
|
272
|
+
{ password: ['must contain at least one digit'] },
|
273
|
+
model.errors.messages
|
274
|
+
)
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_enforces_combination_all_valid
|
278
|
+
model = create_model('aA1!', { lower: 1, upper: 1, digit: 1, symbol: 1 })
|
279
|
+
|
280
|
+
assert(model.valid?)
|
59
281
|
end
|
60
282
|
end
|