devise-security 0.16.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +3 -1
  3. data/README.md +18 -7
  4. data/app/controllers/devise/paranoid_verification_code_controller.rb +26 -12
  5. data/app/controllers/devise/password_expired_controller.rb +22 -5
  6. data/config/locales/bg.yml +42 -0
  7. data/config/locales/by.yml +1 -0
  8. data/config/locales/cs.yml +5 -0
  9. data/config/locales/de.yml +3 -0
  10. data/config/locales/en.yml +2 -1
  11. data/config/locales/es.yml +12 -0
  12. data/config/locales/fa.yml +1 -0
  13. data/config/locales/fr.yml +14 -2
  14. data/config/locales/hi.yml +1 -0
  15. data/config/locales/it.yml +1 -0
  16. data/config/locales/ja.yml +12 -0
  17. data/config/locales/nl.yml +1 -0
  18. data/config/locales/pt.yml +1 -0
  19. data/config/locales/ru.yml +1 -0
  20. data/config/locales/tr.yml +25 -1
  21. data/config/locales/uk.yml +1 -0
  22. data/config/locales/zh_CN.yml +1 -0
  23. data/config/locales/zh_TW.yml +1 -0
  24. data/lib/devise-security/controllers/helpers.rb +23 -11
  25. data/lib/devise-security/hooks/expirable.rb +3 -3
  26. data/lib/devise-security/hooks/paranoid_verification.rb +1 -3
  27. data/lib/devise-security/hooks/password_expirable.rb +1 -3
  28. data/lib/devise-security/hooks/session_limitable.rb +4 -4
  29. data/lib/devise-security/models/compatibility/active_record_patch.rb +4 -3
  30. data/lib/devise-security/models/compatibility/mongoid_patch.rb +3 -2
  31. data/lib/devise-security/models/database_authenticatable_patch.rb +18 -10
  32. data/lib/devise-security/models/expirable.rb +6 -5
  33. data/lib/devise-security/models/paranoid_verification.rb +2 -2
  34. data/lib/devise-security/models/password_archivable.rb +3 -3
  35. data/lib/devise-security/models/secure_validatable.rb +57 -20
  36. data/lib/devise-security/orm/mongoid.rb +1 -1
  37. data/lib/devise-security/patches.rb +14 -8
  38. data/lib/devise-security/routes.rb +2 -3
  39. data/lib/devise-security/validators/password_complexity_validator.rb +53 -26
  40. data/lib/devise-security/version.rb +1 -1
  41. data/lib/devise-security.rb +9 -3
  42. data/lib/generators/devise_security/install_generator.rb +3 -5
  43. data/lib/generators/templates/devise_security.rb +6 -1
  44. data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
  45. data/test/controllers/test_password_expired_controller.rb +87 -33
  46. data/test/controllers/test_security_question_controller.rb +25 -19
  47. data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
  48. data/test/dummy/app/controllers/overrides/password_expired_controller.rb +17 -0
  49. data/test/dummy/app/controllers/widgets_controller.rb +3 -0
  50. data/test/dummy/app/models/application_user_record.rb +2 -1
  51. data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
  52. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
  53. data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
  54. data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
  55. data/test/dummy/app/models/mongoid/mappings.rb +4 -2
  56. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
  57. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
  58. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
  59. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
  60. data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
  61. data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
  62. data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
  63. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
  64. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
  65. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
  66. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
  67. data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
  68. data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
  69. data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
  70. data/test/dummy/app/models/password_expired_user.rb +26 -0
  71. data/test/dummy/app/models/user.rb +5 -5
  72. data/test/dummy/app/models/widget.rb +1 -3
  73. data/test/dummy/app/mongoid/one_user.rb +5 -5
  74. data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
  75. data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
  76. data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
  77. data/test/dummy/app/mongoid/user_without_email.rb +7 -4
  78. data/test/dummy/config/application.rb +3 -7
  79. data/test/dummy/config/boot.rb +1 -1
  80. data/test/dummy/config/environment.rb +1 -1
  81. data/test/dummy/config/environments/test.rb +1 -0
  82. data/test/dummy/config/initializers/devise.rb +1 -5
  83. data/test/dummy/config/locales/en.yml +10 -0
  84. data/test/dummy/config/routes.rb +3 -1
  85. data/test/dummy/config.ru +1 -1
  86. data/test/dummy/db/migrate/20120508165529_create_tables.rb +5 -5
  87. data/test/dummy/lib/shared_expirable_columns.rb +1 -0
  88. data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
  89. data/test/dummy/lib/shared_user.rb +17 -6
  90. data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
  91. data/test/dummy/lib/shared_verification_fields.rb +1 -0
  92. data/test/dummy/log/test.log +39637 -16086
  93. data/test/i18n_test.rb +22 -0
  94. data/test/integration/test_paranoid_verification_code_workflow.rb +53 -0
  95. data/test/integration/test_password_expirable_workflow.rb +2 -2
  96. data/test/integration/test_session_limitable_workflow.rb +5 -3
  97. data/test/orm/active_record.rb +7 -7
  98. data/test/support/integration_helpers.rb +18 -12
  99. data/test/test_compatibility.rb +2 -0
  100. data/test/test_complexity_validator.rb +247 -37
  101. data/test/test_database_authenticatable_patch.rb +146 -0
  102. data/test/test_helper.rb +7 -8
  103. data/test/test_install_generator.rb +1 -1
  104. data/test/test_paranoid_verification.rb +8 -9
  105. data/test/test_password_archivable.rb +34 -11
  106. data/test/test_password_expirable.rb +27 -27
  107. data/test/test_secure_validatable.rb +265 -107
  108. data/test/test_secure_validatable_overrides.rb +185 -0
  109. data/test/test_session_limitable.rb +9 -9
  110. data/test/tmp/config/initializers/{devise-security.rb → devise_security.rb} +6 -1
  111. data/test/tmp/config/locales/devise.security_extension.by.yml +1 -0
  112. data/test/tmp/config/locales/devise.security_extension.cs.yml +5 -0
  113. data/test/tmp/config/locales/devise.security_extension.de.yml +3 -0
  114. data/test/tmp/config/locales/devise.security_extension.en.yml +2 -1
  115. data/test/tmp/config/locales/devise.security_extension.es.yml +12 -0
  116. data/test/tmp/config/locales/devise.security_extension.fa.yml +1 -0
  117. data/test/tmp/config/locales/devise.security_extension.fr.yml +14 -2
  118. data/test/tmp/config/locales/devise.security_extension.hi.yml +21 -20
  119. data/test/tmp/config/locales/devise.security_extension.it.yml +1 -0
  120. data/test/tmp/config/locales/devise.security_extension.ja.yml +12 -0
  121. data/test/tmp/config/locales/devise.security_extension.nl.yml +1 -0
  122. data/test/tmp/config/locales/devise.security_extension.pt.yml +1 -0
  123. data/test/tmp/config/locales/devise.security_extension.ru.yml +1 -0
  124. data/test/tmp/config/locales/devise.security_extension.tr.yml +25 -1
  125. data/test/tmp/config/locales/devise.security_extension.uk.yml +1 -0
  126. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +1 -0
  127. data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +1 -0
  128. metadata +82 -41
  129. data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -23
  130. data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -26
  131. data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -22
  132. data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -25
  133. data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -35
  134. data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -26
  135. data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -22
  136. data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -25
  137. data/test/dummy/app/controllers/foos_controller.rb +0 -0
  138. data/test/dummy/app/models/secure_user.rb +0 -9
  139. data/test/dummy/lib/shared_user_without_email.rb +0 -28
  140. data/test/dummy/log/development.log +0 -883
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
@@ -26,8 +26,8 @@ class TestPasswordExpirableWorkflow < ActionDispatch::IntegrationTest
26
26
  user: {
27
27
  current_password: 'passWord1',
28
28
  password: 'Password12345!',
29
- password_confirmation: 'Password12345!',
30
- },
29
+ password_confirmation: 'Password12345!'
30
+ }
31
31
  }
32
32
  assert_redirected_to(root_path)
33
33
  @user.reload
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
@@ -29,7 +31,7 @@ class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
29
31
  session.assert_redirected_to '/'
30
32
  session.get widgets_path
31
33
  session.assert_response(:success)
32
- assert_equal session.response.body, 'success'
34
+ assert_equal('success', session.response.body)
33
35
  assert_not_nil @user.reload.unique_session_id
34
36
  end
35
37
  end
@@ -44,7 +46,7 @@ class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
44
46
  session.assert_redirected_to '/'
45
47
  session.get widgets_path
46
48
  session.assert_response(:success)
47
- assert_equal session.response.body, 'success'
49
+ assert_equal('success', session.response.body)
48
50
  unique_session_id = @user.reload.unique_session_id
49
51
  assert_not_nil unique_session_id
50
52
  end
@@ -54,7 +56,7 @@ class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
54
56
  session.assert_redirected_to '/'
55
57
  session.get widgets_path
56
58
  session.assert_response(:success)
57
- assert_equal session.response.body, 'success'
59
+ assert_equal('success', session.response.body)
58
60
  assert_not_equal unique_session_id, @user.reload.unique_session_id
59
61
  end
60
62
 
@@ -1,14 +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
- case
6
- when Rails.gem_version >= Gem::Version.new('6.0.0')
7
- ActiveRecord::MigrationContext.new(File.expand_path('../../dummy/db/migrate', __FILE__), ActiveRecord::SchemaMigration).migrate
8
- when Rails.gem_version >= Gem::Version.new('5.2.0')
9
- ActiveRecord::MigrationContext.new(File.expand_path('../../dummy/db/migrate', __FILE__)).migrate
10
- else
11
- ActiveRecord::Migrator.migrate(File.expand_path('../../dummy/db/migrate', __FILE__))
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
12
  end
13
13
 
14
14
  DatabaseCleaner[:active_record].strategy = :transaction
@@ -6,12 +6,15 @@ module IntegrationHelpers
6
6
  # @param session [ActionDispatch::Integration::Session]
7
7
  # @return [void]
8
8
  def sign_in(user, session = integration_session)
9
- session.post new_user_session_path, params: {
10
- user: {
11
- email: user.email,
12
- password: user.password,
13
- },
14
- }
9
+ session.post(
10
+ new_user_session_path,
11
+ params: {
12
+ user: {
13
+ email: user.email,
14
+ password: user.password
15
+ }
16
+ }
17
+ )
15
18
  end
16
19
 
17
20
  # attempt to login the user with a bad password. This will exercise all the Warden Hooks
@@ -19,11 +22,14 @@ module IntegrationHelpers
19
22
  # @param session [ActionDispatch::Integration::Session]
20
23
  # @return [void]
21
24
  def failed_sign_in(user, session)
22
- session.post new_user_session_path, params: {
23
- user: {
24
- email: user.email,
25
- password: 'bad-password',
26
- },
27
- }
25
+ session.post(
26
+ new_user_session_path,
27
+ params: {
28
+ user: {
29
+ email: user.email,
30
+ password: 'bad-password'
31
+ }
32
+ }
33
+ )
28
34
  end
29
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class TestCompatibility < ActiveSupport::TestCase
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
- class PasswordComplexityValidatorTest < Minitest::Test
5
+ class PasswordComplexityValidatorTest < ActiveSupport::TestCase
4
6
  class ModelWithPassword
5
7
  include ActiveModel::Validations
6
8
 
@@ -15,58 +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(ModelWithPassword.new('aaaa').valid?)
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 test_enforces_uppercase
23
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { upper: 1 }
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 test_enforces_count
29
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { upper: 2 }
30
- refute(ModelWithPassword.new('Aaaa').valid?)
31
- assert(ModelWithPassword.new('AAaa').valid?)
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
+ )
32
83
  end
33
84
 
34
- def test_enforces_digit
35
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { digit: 1 }
36
- refute(ModelWithPassword.new('aaaa').valid?)
37
- assert(ModelWithPassword.new('aaa1').valid?)
85
+ def test_enforces_digit_count_valid
86
+ assert(create_model('11aa', { digit: 2 }).valid?)
38
87
  end
39
88
 
40
- def test_enforces_digits
41
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { digits: 2 }
42
- refute(ModelWithPassword.new('aaa1').valid?)
43
- assert(ModelWithPassword.new('aa12').valid?)
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
+ )
44
96
  end
45
97
 
46
- def test_enforces_lower
47
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1 }
48
- refute(ModelWithPassword.new('AAAA').valid?)
49
- assert(ModelWithPassword.new('AAAa').valid?)
98
+ def test_enforces_digits_valid
99
+ assert(create_model('1aaa', { digits: 1 }).valid?)
50
100
  end
51
101
 
52
- def test_enforces_symbol
53
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { symbol: 1 }
54
- refute(ModelWithPassword.new('aaaa').valid?)
55
- assert(ModelWithPassword.new('aaa!').valid?)
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
+ )
56
123
  end
57
124
 
58
- def test_enforces_symbols
59
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { symbols: 2 }
60
- refute(ModelWithPassword.new('aaa!').valid?)
61
- assert(ModelWithPassword.new('aa!?').valid?)
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
+ )
62
151
  end
63
152
 
64
- def test_enforces_combination
65
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1, upper: 1, digit: 1, symbol: 1 }
66
- refute(ModelWithPassword.new('abcd').valid?)
67
- refute(ModelWithPassword.new('ABCD').valid?)
68
- refute(ModelWithPassword.new('1234').valid?)
69
- refute(ModelWithPassword.new('$!,*').valid?)
70
- assert(ModelWithPassword.new('aB3*').valid?)
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?)
71
281
  end
72
282
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestDatabaseAuthenticatablePatch < ActiveSupport::TestCase
6
+ def create_user
7
+ User.create(
8
+ email: 'bob@microsoft.com',
9
+ password: 'Password1!',
10
+ password_confirmation: 'Password1!'
11
+ ) do |user|
12
+ user.extend(Devise::Models::DatabaseAuthenticatablePatch)
13
+ end
14
+ end
15
+
16
+ test 'updates if all params are present and valid' do
17
+ user = create_user
18
+
19
+ assert(
20
+ user.update_with_password(
21
+ {
22
+ current_password: 'Password1!',
23
+ password: 'Password2!',
24
+ password_confirmation: 'Password2!'
25
+ }
26
+ )
27
+ )
28
+ end
29
+
30
+ test 'does not update if current_password is missing' do
31
+ user = create_user
32
+
33
+ user.update_with_password(
34
+ {
35
+ password: 'Password2!',
36
+ password_confirmation: 'Password2!'
37
+ }
38
+ )
39
+
40
+ assert_equal(["Current password can't be blank"], user.errors.full_messages)
41
+ end
42
+
43
+ test 'does not update if current_password is incorrect' do
44
+ user = create_user
45
+
46
+ user.update_with_password(
47
+ {
48
+ current_password: 'Password2!',
49
+ password: 'Password2!',
50
+ password_confirmation: 'Password2!'
51
+ }
52
+ )
53
+
54
+ assert_equal(['Current password is invalid'], user.errors.full_messages)
55
+ end
56
+
57
+ test 'does not update if password is missing' do
58
+ user = create_user
59
+
60
+ user.update_with_password(
61
+ {
62
+ current_password: 'Password1!',
63
+ password: '',
64
+ password_confirmation: 'Password2!'
65
+ }
66
+ )
67
+
68
+ assert_equal(["Password can't be blank"], user.errors.full_messages)
69
+ end
70
+
71
+ test 'does not update if password is invalid and mismatches confirmation' do
72
+ user = create_user
73
+
74
+ user.update_with_password(
75
+ {
76
+ current_password: 'Password1!',
77
+ password: 'f',
78
+ password_confirmation: 'Password2!'
79
+ }
80
+ )
81
+
82
+ assert_equal(
83
+ [
84
+ "Password confirmation doesn't match Password",
85
+ 'Password is too short (minimum is 7 characters)',
86
+ 'Password must contain at least one digit',
87
+ 'Password must contain at least one upper-case letter'
88
+ ],
89
+ user.errors.full_messages
90
+ )
91
+ end
92
+
93
+ test 'does not update if password is invalid and matches confirmation' do
94
+ user = create_user
95
+
96
+ user.update_with_password(
97
+ {
98
+ current_password: 'Password1!',
99
+ password: 'f',
100
+ password_confirmation: 'f'
101
+ }
102
+ )
103
+
104
+ assert_equal(
105
+ [
106
+ 'Password is too short (minimum is 7 characters)',
107
+ 'Password must contain at least one digit',
108
+ 'Password must contain at least one upper-case letter'
109
+ ],
110
+ user.errors.full_messages
111
+ )
112
+ end
113
+
114
+ test 'does not update if password_confirmation is missing' do
115
+ user = create_user
116
+
117
+ user.update_with_password(
118
+ {
119
+ current_password: 'Password1!',
120
+ password: 'Password2!',
121
+ password_confirmation: ''
122
+ }
123
+ )
124
+
125
+ assert_equal(
126
+ ["Password confirmation can't be blank"], user.errors.full_messages
127
+ )
128
+ end
129
+
130
+ test 'does not update if password_confirmation is mismatched' do
131
+ user = create_user
132
+
133
+ user.update_with_password(
134
+ {
135
+ current_password: 'Password1!',
136
+ password: 'Password2!',
137
+ password_confirmation: 'Password3!'
138
+ }
139
+ )
140
+
141
+ assert_equal(
142
+ ["Password confirmation doesn't match Password"],
143
+ user.errors.full_messages
144
+ )
145
+ end
146
+ end
data/test/test_helper.rb CHANGED
@@ -3,6 +3,13 @@
3
3
  ENV['RAILS_ENV'] ||= 'test'
4
4
 
5
5
  require 'simplecov'
6
+
7
+ if ENV['CI']
8
+ require 'simplecov-lcov'
9
+ SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
10
+ SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
11
+ end
12
+
6
13
  SimpleCov.start do
7
14
  add_filter 'gemfiles'
8
15
  add_filter 'test/dummy/db'
@@ -18,14 +25,6 @@ SimpleCov.start do
18
25
  add_group 'Tests', 'test'
19
26
  end
20
27
 
21
- if ENV['CI']
22
- require 'simplecov'
23
- require 'simplecov-lcov'
24
- SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
25
- SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
26
- SimpleCov.start
27
- end
28
-
29
28
  require 'pry'
30
29
  require 'dummy/config/environment'
31
30
  require 'minitest/autorun'
@@ -6,7 +6,7 @@ require 'generators/devise_security/install_generator'
6
6
 
7
7
  class TestInstallGenerator < Rails::Generators::TestCase
8
8
  tests DeviseSecurity::Generators::InstallGenerator
9
- destination File.expand_path('../tmp', __FILE__)
9
+ destination File.expand_path('tmp', __dir__)
10
10
  setup :prepare_destination
11
11
 
12
12
  test 'Assert all files are properly created' do