devise-security 0.12.0 → 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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +3 -1
  3. data/README.md +199 -65
  4. data/app/controllers/devise/paranoid_verification_code_controller.rb +28 -12
  5. data/app/controllers/devise/password_expired_controller.rb +34 -10
  6. data/app/views/devise/paranoid_verification_code/show.html.erb +4 -4
  7. data/app/views/devise/password_expired/show.html.erb +6 -6
  8. data/config/locales/bg.yml +42 -0
  9. data/config/locales/by.yml +50 -0
  10. data/config/locales/cs.yml +46 -0
  11. data/config/locales/de.yml +33 -7
  12. data/config/locales/en.yml +26 -1
  13. data/config/locales/es.yml +31 -6
  14. data/config/locales/fa.yml +42 -0
  15. data/config/locales/fr.yml +42 -0
  16. data/config/locales/hi.yml +43 -0
  17. data/config/locales/it.yml +36 -4
  18. data/config/locales/ja.yml +42 -0
  19. data/config/locales/nl.yml +42 -0
  20. data/config/locales/pt.yml +42 -0
  21. data/config/locales/ru.yml +50 -0
  22. data/config/locales/tr.yml +42 -0
  23. data/config/locales/uk.yml +50 -0
  24. data/config/locales/zh_CN.yml +42 -0
  25. data/config/locales/zh_TW.yml +42 -0
  26. data/lib/devise-security/controllers/helpers.rb +74 -51
  27. data/lib/devise-security/hooks/expirable.rb +6 -4
  28. data/lib/devise-security/hooks/paranoid_verification.rb +3 -3
  29. data/lib/devise-security/hooks/password_expirable.rb +5 -3
  30. data/lib/devise-security/hooks/session_limitable.rb +31 -14
  31. data/lib/devise-security/models/active_record/old_password.rb +5 -0
  32. data/lib/devise-security/models/compatibility/active_record_patch.rb +41 -0
  33. data/lib/devise-security/models/compatibility/mongoid_patch.rb +32 -0
  34. data/lib/devise-security/models/compatibility.rb +8 -15
  35. data/lib/devise-security/models/database_authenticatable_patch.rb +20 -10
  36. data/lib/devise-security/models/expirable.rb +14 -7
  37. data/lib/devise-security/models/mongoid/old_password.rb +21 -0
  38. data/lib/devise-security/models/paranoid_verification.rb +4 -2
  39. data/lib/devise-security/models/password_archivable.rb +19 -8
  40. data/lib/devise-security/models/password_expirable.rb +103 -48
  41. data/lib/devise-security/models/secure_validatable.rb +69 -12
  42. data/lib/devise-security/models/security_questionable.rb +2 -0
  43. data/lib/devise-security/models/session_limitable.rb +19 -2
  44. data/lib/devise-security/orm/mongoid.rb +7 -0
  45. data/lib/devise-security/patches/controller_captcha.rb +2 -0
  46. data/lib/devise-security/patches/controller_security_question.rb +3 -1
  47. data/lib/devise-security/patches.rb +16 -8
  48. data/lib/devise-security/rails.rb +2 -0
  49. data/lib/devise-security/routes.rb +4 -3
  50. data/lib/devise-security/validators/password_complexity_validator.rb +62 -0
  51. data/lib/devise-security/version.rb +3 -1
  52. data/lib/devise-security.rb +23 -11
  53. data/lib/generators/devise_security/install_generator.rb +6 -6
  54. data/lib/generators/templates/devise_security.rb +52 -0
  55. data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +2 -0
  56. data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
  57. data/test/controllers/test_password_expired_controller.rb +164 -0
  58. data/test/controllers/test_security_question_controller.rb +66 -0
  59. data/test/dummy/Rakefile +3 -1
  60. data/test/dummy/app/assets/config/manifest.js +3 -0
  61. data/test/dummy/app/controllers/application_controller.rb +2 -0
  62. data/test/dummy/app/controllers/captcha/sessions_controller.rb +2 -0
  63. data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
  64. data/test/dummy/app/controllers/overrides/password_expired_controller.rb +17 -0
  65. data/test/dummy/app/controllers/security_question/unlocks_controller.rb +2 -0
  66. data/test/dummy/app/controllers/widgets_controller.rb +9 -0
  67. data/test/dummy/app/models/application_record.rb +10 -2
  68. data/test/dummy/app/models/application_user_record.rb +12 -0
  69. data/test/dummy/app/models/captcha_user.rb +7 -2
  70. data/test/dummy/app/models/mongoid/confirmable_fields.rb +15 -0
  71. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +18 -0
  72. data/test/dummy/app/models/mongoid/expirable_fields.rb +13 -0
  73. data/test/dummy/app/models/mongoid/lockable_fields.rb +15 -0
  74. data/test/dummy/app/models/mongoid/mappings.rb +15 -0
  75. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +13 -0
  76. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +12 -0
  77. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +11 -0
  78. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +12 -0
  79. data/test/dummy/app/models/mongoid/recoverable_fields.rb +13 -0
  80. data/test/dummy/app/models/mongoid/registerable_fields.rb +21 -0
  81. data/test/dummy/app/models/mongoid/rememberable_fields.rb +12 -0
  82. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +13 -0
  83. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +15 -0
  84. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +12 -0
  85. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +11 -0
  86. data/test/dummy/app/models/mongoid/trackable_fields.rb +16 -0
  87. data/test/dummy/app/models/mongoid/validatable_fields.rb +9 -0
  88. data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
  89. data/test/dummy/app/models/password_expired_user.rb +26 -0
  90. data/test/dummy/app/models/security_question_user.rb +9 -4
  91. data/test/dummy/app/models/user.rb +16 -1
  92. data/test/dummy/app/models/widget.rb +4 -0
  93. data/test/dummy/app/mongoid/admin.rb +31 -0
  94. data/test/dummy/app/mongoid/one_user.rb +58 -0
  95. data/test/dummy/app/mongoid/shim.rb +25 -0
  96. data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
  97. data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
  98. data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
  99. data/test/dummy/app/mongoid/user_without_email.rb +38 -0
  100. data/test/dummy/config/application.rb +13 -11
  101. data/test/dummy/config/boot.rb +3 -1
  102. data/test/dummy/config/environment.rb +3 -1
  103. data/test/dummy/config/environments/test.rb +6 -13
  104. data/test/dummy/config/initializers/devise.rb +6 -3
  105. data/test/dummy/config/initializers/migration_class.rb +3 -6
  106. data/test/dummy/config/locales/en.yml +10 -0
  107. data/test/dummy/config/mongoid.yml +6 -0
  108. data/test/dummy/config/routes.rb +8 -3
  109. data/test/dummy/config.ru +3 -1
  110. data/test/dummy/db/migrate/20120508165529_create_tables.rb +17 -6
  111. data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +2 -0
  112. data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +2 -0
  113. data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +2 -0
  114. data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +2 -0
  115. data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +2 -0
  116. data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +2 -0
  117. data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +2 -0
  118. data/test/dummy/db/migrate/20180319114023_add_widget.rb +2 -0
  119. data/test/dummy/lib/shared_expirable_columns.rb +15 -0
  120. data/test/dummy/lib/shared_security_questions_fields.rb +17 -0
  121. data/test/dummy/lib/shared_user.rb +43 -0
  122. data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
  123. data/test/dummy/lib/shared_user_without_omniauth.rb +24 -0
  124. data/test/dummy/lib/shared_verification_fields.rb +16 -0
  125. data/test/dummy/log/test.log +45240 -0
  126. data/test/i18n_test.rb +22 -0
  127. data/test/integration/test_paranoid_verification_code_workflow.rb +53 -0
  128. data/test/integration/test_password_expirable_workflow.rb +53 -0
  129. data/test/integration/test_session_limitable_workflow.rb +69 -0
  130. data/test/orm/active_record.rb +15 -0
  131. data/test/orm/mongoid.rb +13 -0
  132. data/test/support/integration_helpers.rb +35 -0
  133. data/test/support/mongoid.yml +6 -0
  134. data/test/test_compatibility.rb +15 -0
  135. data/test/test_complexity_validator.rb +282 -0
  136. data/test/test_database_authenticatable_patch.rb +146 -0
  137. data/test/test_helper.rb +41 -9
  138. data/test/test_install_generator.rb +20 -3
  139. data/test/test_paranoid_verification.rb +10 -9
  140. data/test/test_password_archivable.rb +37 -13
  141. data/test/test_password_expirable.rb +72 -9
  142. data/test/test_secure_validatable.rb +289 -55
  143. data/test/test_secure_validatable_overrides.rb +185 -0
  144. data/test/test_session_limitable.rb +57 -0
  145. data/test/tmp/config/initializers/devise_security.rb +52 -0
  146. data/test/tmp/config/locales/devise.security_extension.by.yml +50 -0
  147. data/test/tmp/config/locales/devise.security_extension.cs.yml +46 -0
  148. data/test/tmp/config/locales/devise.security_extension.de.yml +42 -0
  149. data/test/tmp/config/locales/devise.security_extension.en.yml +42 -0
  150. data/test/tmp/config/locales/devise.security_extension.es.yml +42 -0
  151. data/test/tmp/config/locales/devise.security_extension.fa.yml +42 -0
  152. data/test/tmp/config/locales/devise.security_extension.fr.yml +42 -0
  153. data/test/tmp/config/locales/devise.security_extension.hi.yml +43 -0
  154. data/test/tmp/config/locales/devise.security_extension.it.yml +42 -0
  155. data/test/tmp/config/locales/devise.security_extension.ja.yml +42 -0
  156. data/test/tmp/config/locales/devise.security_extension.nl.yml +42 -0
  157. data/test/tmp/config/locales/devise.security_extension.pt.yml +42 -0
  158. data/test/tmp/config/locales/devise.security_extension.ru.yml +50 -0
  159. data/test/tmp/config/locales/devise.security_extension.tr.yml +42 -0
  160. data/test/tmp/config/locales/devise.security_extension.uk.yml +50 -0
  161. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +42 -0
  162. data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +42 -0
  163. metadata +290 -124
  164. data/.circleci/config.yml +0 -41
  165. data/.document +0 -5
  166. data/.gitignore +0 -40
  167. data/.rubocop.yml +0 -63
  168. data/.ruby-version +0 -1
  169. data/.travis.yml +0 -25
  170. data/Appraisals +0 -19
  171. data/Gemfile +0 -3
  172. data/Rakefile +0 -28
  173. data/devise-security.gemspec +0 -44
  174. data/gemfiles/rails_4.1_stable.gemfile +0 -8
  175. data/gemfiles/rails_4.2_stable.gemfile +0 -8
  176. data/gemfiles/rails_5.0_stable.gemfile +0 -8
  177. data/gemfiles/rails_5.1_stable.gemfile +0 -8
  178. data/gemfiles/rails_5.2_rc1.gemfile +0 -8
  179. data/lib/devise-security/models/old_password.rb +0 -4
  180. data/lib/devise-security/orm/active_record.rb +0 -18
  181. data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -21
  182. data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -24
  183. data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -20
  184. data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -23
  185. data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -33
  186. data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -24
  187. data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -20
  188. data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -23
  189. data/lib/devise-security/schema.rb +0 -64
  190. data/lib/generators/templates/devise-security.rb +0 -38
  191. data/test/dummy/app/controllers/foos_controller.rb +0 -0
  192. data/test/dummy/app/models/.gitkeep +0 -0
  193. data/test/dummy/app/models/secure_user.rb +0 -3
  194. data/test/test_password_expired_controller.rb +0 -44
  195. data/test/test_security_question_controller.rb +0 -84
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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ ActiveRecord::Migration.verbose = false
6
+ ActiveRecord::Base.logger = Logger.new(nil)
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
12
+ end
13
+
14
+ DatabaseCleaner[:active_record].strategy = :transaction
15
+ ORMInvalidRecordException = ActiveRecord::RecordInvalid
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongoid/version'
4
+ require 'database_cleaner-mongoid'
5
+
6
+ Mongoid.configure do |config|
7
+ config.load!('test/support/mongoid.yml', Rails.env)
8
+ config.use_utc = true
9
+ config.include_root_in_json = true
10
+ end
11
+
12
+ DatabaseCleaner[:mongoid].strategy = :deletion
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
@@ -0,0 +1,6 @@
1
+ test:
2
+ clients:
3
+ default:
4
+ database: devise-test-suite
5
+ hosts:
6
+ - localhost:<%= ENV.fetch('MONGODB_PORT', '27017') %>
@@ -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
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class PasswordComplexityValidatorTest < ActiveSupport::TestCase
6
+ class ModelWithPassword
7
+ include ActiveModel::Validations
8
+
9
+ attr_reader :password
10
+
11
+ def initialize(password)
12
+ @password = password
13
+ end
14
+ end
15
+
16
+ def setup
17
+ ModelWithPassword.clear_validators!
18
+ end
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
+
27
+ def test_with_no_rules_anything_goes
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
+ )
70
+ end
71
+
72
+ def test_enforces_digit_valid
73
+ assert(create_model('1aaa', { digit: 1 }).valid?)
74
+ end
75
+
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
+ )
109
+ end
110
+
111
+ def test_enforces_digits_count_valid
112
+ assert(create_model('11aa', { digits: 2 }).valid?)
113
+ end
114
+
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
+ )
123
+ end
124
+
125
+ def test_enforces_lower_valid
126
+ assert(create_model('aAAA', { lower: 1 }).valid?)
127
+ end
128
+
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?)
281
+ end
282
+ end