devise-security 0.14.2 → 0.17.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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -59
  3. data/app/controllers/devise/paranoid_verification_code_controller.rb +13 -1
  4. data/app/controllers/devise/password_expired_controller.rb +24 -6
  5. data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
  6. data/app/views/devise/password_expired/show.html.erb +5 -5
  7. data/config/locales/bg.yml +41 -0
  8. data/config/locales/by.yml +49 -0
  9. data/config/locales/cs.yml +41 -0
  10. data/config/locales/de.yml +15 -2
  11. data/config/locales/en.yml +15 -2
  12. data/config/locales/es.yml +10 -9
  13. data/config/locales/fa.yml +41 -0
  14. data/config/locales/fr.yml +1 -0
  15. data/config/locales/hi.yml +42 -0
  16. data/config/locales/it.yml +35 -4
  17. data/config/locales/ja.yml +2 -1
  18. data/config/locales/nl.yml +41 -0
  19. data/config/locales/pt.yml +41 -0
  20. data/config/locales/ru.yml +49 -0
  21. data/config/locales/tr.yml +1 -0
  22. data/config/locales/uk.yml +49 -0
  23. data/config/locales/zh_CN.yml +41 -0
  24. data/config/locales/zh_TW.yml +41 -0
  25. data/lib/devise-security/controllers/helpers.rb +59 -50
  26. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  27. data/lib/devise-security/hooks/session_limitable.rb +21 -11
  28. data/lib/devise-security/models/database_authenticatable_patch.rb +15 -5
  29. data/lib/devise-security/models/password_archivable.rb +2 -2
  30. data/lib/devise-security/models/password_expirable.rb +5 -1
  31. data/lib/devise-security/models/secure_validatable.rb +56 -6
  32. data/lib/devise-security/models/session_limitable.rb +10 -1
  33. data/lib/devise-security/validators/password_complexity_validator.rb +53 -24
  34. data/lib/devise-security/version.rb +1 -1
  35. data/lib/devise-security.rb +13 -5
  36. data/lib/generators/devise_security/install_generator.rb +3 -3
  37. data/lib/generators/templates/{devise-security.rb → devise_security.rb} +6 -1
  38. data/test/controllers/test_paranoid_verification_code_controller.rb +68 -0
  39. data/test/controllers/test_password_expired_controller.rb +121 -19
  40. data/test/controllers/test_security_question_controller.rb +16 -40
  41. data/test/dummy/app/assets/config/manifest.js +3 -0
  42. data/test/dummy/app/controllers/overrides/paranoid_verification_code_controller.rb +7 -0
  43. data/test/dummy/app/controllers/overrides/password_expired_controller.rb +7 -0
  44. data/test/dummy/app/controllers/widgets_controller.rb +3 -0
  45. data/test/dummy/app/models/application_user_record.rb +2 -1
  46. data/test/dummy/app/models/mongoid/confirmable_fields.rb +2 -0
  47. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +4 -3
  48. data/test/dummy/app/models/mongoid/expirable_fields.rb +2 -0
  49. data/test/dummy/app/models/mongoid/lockable_fields.rb +2 -0
  50. data/test/dummy/app/models/mongoid/mappings.rb +4 -2
  51. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +2 -0
  52. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +2 -0
  53. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +2 -0
  54. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +2 -0
  55. data/test/dummy/app/models/mongoid/recoverable_fields.rb +2 -0
  56. data/test/dummy/app/models/mongoid/registerable_fields.rb +4 -2
  57. data/test/dummy/app/models/mongoid/rememberable_fields.rb +2 -0
  58. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +2 -0
  59. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +2 -0
  60. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +2 -0
  61. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +2 -0
  62. data/test/dummy/app/models/mongoid/trackable_fields.rb +2 -0
  63. data/test/dummy/app/models/mongoid/validatable_fields.rb +2 -0
  64. data/test/dummy/app/models/paranoid_verification_user.rb +26 -0
  65. data/test/dummy/app/models/password_expired_user.rb +26 -0
  66. data/test/dummy/app/models/user.rb +1 -2
  67. data/test/dummy/app/models/widget.rb +1 -3
  68. data/test/dummy/app/mongoid/one_user.rb +5 -5
  69. data/test/dummy/app/mongoid/user_on_engine.rb +2 -2
  70. data/test/dummy/app/mongoid/user_on_main_app.rb +2 -2
  71. data/test/dummy/app/mongoid/user_with_validations.rb +3 -3
  72. data/test/dummy/app/mongoid/user_without_email.rb +3 -3
  73. data/test/dummy/config/application.rb +4 -4
  74. data/test/dummy/config/boot.rb +1 -1
  75. data/test/dummy/config/environment.rb +1 -1
  76. data/test/dummy/config/environments/test.rb +3 -13
  77. data/test/dummy/config/initializers/migration_class.rb +1 -8
  78. data/test/dummy/config/locales/en.yml +10 -0
  79. data/test/dummy/config/mongoid.yml +1 -1
  80. data/test/dummy/config/routes.rb +5 -3
  81. data/test/dummy/db/migrate/20120508165529_create_tables.rb +3 -3
  82. data/test/dummy/lib/shared_expirable_columns.rb +1 -0
  83. data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
  84. data/test/dummy/lib/shared_user.rb +17 -6
  85. data/test/dummy/lib/shared_user_without_email.rb +2 -1
  86. data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
  87. data/test/dummy/lib/shared_verification_fields.rb +1 -0
  88. data/test/dummy/{app/models/.gitkeep → log/development.log} +0 -0
  89. data/test/dummy/log/test.log +101533 -0
  90. data/test/integration/test_password_expirable_workflow.rb +53 -0
  91. data/test/integration/test_session_limitable_workflow.rb +2 -0
  92. data/test/orm/active_record.rb +7 -4
  93. data/test/orm/mongoid.rb +2 -1
  94. data/test/support/integration_helpers.rb +15 -33
  95. data/test/support/mongoid.yml +1 -1
  96. data/test/test_compatibility.rb +2 -0
  97. data/test/test_complexity_validator.rb +250 -29
  98. data/test/test_database_authenticatable_patch.rb +146 -0
  99. data/test/test_helper.rb +12 -6
  100. data/test/test_install_generator.rb +12 -2
  101. data/test/test_paranoid_verification.rb +0 -1
  102. data/test/test_password_archivable.rb +34 -11
  103. data/test/test_password_expirable.rb +26 -26
  104. data/test/test_secure_validatable.rb +292 -50
  105. data/test/test_secure_validatable_overrides.rb +185 -0
  106. data/test/test_session_limitable.rb +27 -1
  107. data/test/tmp/config/initializers/devise_security.rb +49 -0
  108. data/test/tmp/config/locales/devise.security_extension.by.yml +49 -0
  109. data/test/tmp/config/locales/devise.security_extension.cs.yml +41 -0
  110. data/test/tmp/config/locales/devise.security_extension.de.yml +41 -0
  111. data/test/tmp/config/locales/devise.security_extension.en.yml +42 -0
  112. data/test/tmp/config/locales/devise.security_extension.es.yml +30 -0
  113. data/test/tmp/config/locales/devise.security_extension.fa.yml +41 -0
  114. data/test/tmp/config/locales/devise.security_extension.fr.yml +30 -0
  115. data/test/tmp/config/locales/devise.security_extension.hi.yml +42 -0
  116. data/test/tmp/config/locales/devise.security_extension.it.yml +41 -0
  117. data/test/tmp/config/locales/devise.security_extension.ja.yml +30 -0
  118. data/test/tmp/config/locales/devise.security_extension.nl.yml +41 -0
  119. data/test/tmp/config/locales/devise.security_extension.pt.yml +41 -0
  120. data/test/tmp/config/locales/devise.security_extension.ru.yml +49 -0
  121. data/test/tmp/config/locales/devise.security_extension.tr.yml +18 -0
  122. data/test/tmp/config/locales/devise.security_extension.uk.yml +49 -0
  123. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +41 -0
  124. data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +41 -0
  125. metadata +168 -132
  126. data/.codeclimate.yml +0 -63
  127. data/.document +0 -5
  128. data/.gitignore +0 -43
  129. data/.mdlrc +0 -1
  130. data/.rubocop.yml +0 -64
  131. data/.ruby-version +0 -1
  132. data/.travis.yml +0 -39
  133. data/Appraisals +0 -35
  134. data/Gemfile +0 -10
  135. data/Rakefile +0 -27
  136. data/devise-security.gemspec +0 -50
  137. data/gemfiles/rails_4.2_stable.gemfile +0 -16
  138. data/gemfiles/rails_5.0_stable.gemfile +0 -15
  139. data/gemfiles/rails_5.1_stable.gemfile +0 -15
  140. data/gemfiles/rails_5.2_stable.gemfile +0 -15
  141. data/gemfiles/rails_6.0_beta.gemfile +0 -15
  142. data/lib/devise-security/orm/active_record.rb +0 -20
  143. data/lib/devise-security/schema.rb +0 -66
  144. data/test/dummy/app/models/secure_user.rb +0 -9
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
@@ -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
- if Rails.gem_version >= Gem::Version.new('5.2.0')
6
- ActiveRecord::MigrationContext.new(File.expand_path('../../dummy/db/migrate', __FILE__)).migrate
7
- else
8
- 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
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 = :truncation
12
+ DatabaseCleaner[:mongoid].strategy = :deletion
12
13
  ORMInvalidRecordException = Mongoid::Errors::Validations
@@ -1,47 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module IntegrationHelpers
2
4
  # login the user. This will exercise all the Warden Hooks
3
5
  # @param user [User]
4
6
  # @param session [ActionDispatch::Integration::Session]
5
7
  # @return [void]
6
- # @note accounts for differences in the integration test API between rails versions
7
- def sign_in(user, session)
8
- if Rails.gem_version > Gem::Version.new('5.0')
9
- session.post new_user_session_path, params: {
10
- user: {
11
- email: user.email,
12
- password: user.password
13
- }
14
- }
15
- else
16
- session.post new_user_session_path, {
17
- user: {
18
- email: user.email,
19
- password: user.password
20
- }
21
- }
22
- end
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
+ }
23
15
  end
24
16
 
25
17
  # attempt to login the user with a bad password. This will exercise all the Warden Hooks
26
18
  # @param user [User]
27
19
  # @param session [ActionDispatch::Integration::Session]
28
20
  # @return [void]
29
- # @note accounts for differences in the integration test API between rails versions
30
21
  def failed_sign_in(user, session)
31
- if Rails.gem_version > Gem::Version.new('5.0')
32
- session.post new_user_session_path, params: {
33
- user: {
34
- email: user.email,
35
- password: 'bad-password'
36
- }
37
- }
38
- else
39
- session.post new_user_session_path, {
40
- user: {
41
- email: user.email,
42
- password: 'bad-password'
43
- }
44
- }
45
- end
22
+ session.post new_user_session_path, params: {
23
+ user: {
24
+ email: user.email,
25
+ password: 'bad-password',
26
+ },
27
+ }
46
28
  end
47
29
  end
@@ -1,5 +1,5 @@
1
1
  test:
2
- <%= Mongoid::VERSION.to_i > 4 ? 'clients' : 'sessions' %>:
2
+ clients:
3
3
  default:
4
4
  database: devise-test-suite
5
5
  hosts:
@@ -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,46 +17,265 @@ 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
+ model.errors.messages,
41
+ { password: ["must contain at least one upper-case letter"] }
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
+ model.errors.messages,
55
+ { password: ["must contain at least 2 upper-case letters"] }
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
+ model.errors.messages, { password: ["must contain at least one digit"] }
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
+ model.errors.messages, { password: ["must contain at least 2 digits"] }
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
+ model.errors.messages, { password: ["must contain at least one digit"] }
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
+ model.errors.messages, { password: ["must contain at least 2 digits"] }
108
+ )
32
109
  end
33
110
 
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?)
111
+ def test_enforces_digits_count_valid
112
+ assert(create_model('11aa', { digits: 2 }).valid?)
38
113
  end
39
114
 
40
- def test_enforces_lower
41
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1 }
42
- refute(ModelWithPassword.new('AAAA').valid?)
43
- assert(ModelWithPassword.new('AAAa').valid?)
115
+ def test_enforces_lower_invalid
116
+ model = create_model('AAAA', { lower: 1 })
117
+
118
+ assert_not(model.valid?)
119
+ assert_equal(
120
+ model.errors.messages,
121
+ { password: ["must contain at least one lower-case letter"] }
122
+ )
44
123
  end
45
124
 
46
- def test_enforces_symbol
47
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { symbol: 1 }
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 test_enforces_combination
53
- ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1, upper: 1, digit: 1, symbol: 1 }
54
- refute(ModelWithPassword.new('abcd').valid?)
55
- refute(ModelWithPassword.new('ABCD').valid?)
56
- refute(ModelWithPassword.new('1234').valid?)
57
- refute(ModelWithPassword.new('$!,*').valid?)
58
- assert(ModelWithPassword.new('aB3*').valid?)
129
+ def test_enforces_lower_count_invalid
130
+ model = create_model('aAAA', { lower: 2 })
131
+
132
+ assert_not(model.valid?)
133
+ assert_equal(
134
+ model.errors.messages,
135
+ { password: ["must contain at least 2 lower-case letters"] }
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
+ model.errors.messages,
149
+ { password: ["must contain at least one punctuation mark or symbol"] }
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
+ model.errors.messages,
163
+ { password: ["must contain at least 2 punctuation marks or symbols"] }
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
+ model.errors.messages,
177
+ { password: ["must contain at least one punctuation mark or symbol"] }
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
+ model.errors.messages,
191
+ { password: ["must contain at least 2 punctuation marks or symbols"] }
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
+ model.errors.messages,
205
+ {
206
+ password:
207
+ [
208
+ "must contain at least one digit",
209
+ "must contain at least one punctuation mark or symbol",
210
+ "must contain at least one upper-case letter"
211
+ ]
212
+ }
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
+ model.errors.messages,
222
+ {
223
+ password:
224
+ [
225
+ "must contain at least one digit",
226
+ "must contain at least one lower-case letter",
227
+ "must contain at least one punctuation mark or symbol"
228
+ ]
229
+ }
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
+ model.errors.messages,
239
+ {
240
+ password:
241
+ [
242
+ "must contain at least one lower-case letter",
243
+ "must contain at least one punctuation mark or symbol",
244
+ "must contain at least one upper-case letter"
245
+ ]
246
+ }
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
+ model.errors.messages,
256
+ {
257
+ password:
258
+ [
259
+ "must contain at least one digit",
260
+ "must contain at least one lower-case letter",
261
+ "must contain at least one upper-case letter"
262
+ ]
263
+ }
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
+ model.errors.messages, { password: ["must contain at least one digit"] }
273
+ )
274
+ end
275
+
276
+ def test_enforces_combination_all_valid
277
+ model = create_model('aA1!', { lower: 1, upper: 1, digit: 1, symbol: 1 })
278
+
279
+ assert(model.valid?)
59
280
  end
60
281
  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
+ result = 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
+ result = 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,12 +25,6 @@ SimpleCov.start do
18
25
  add_group 'Tests', 'test'
19
26
  end
20
27
 
21
- if ENV['CI']
22
- require 'coveralls'
23
- SimpleCov.formatter = Coveralls::SimpleCov::Formatter
24
- Coveralls.wear!
25
- end
26
-
27
28
  require 'pry'
28
29
  require 'dummy/config/environment'
29
30
  require 'minitest/autorun'
@@ -31,6 +32,11 @@ require 'rails/test_help'
31
32
  require 'devise-security'
32
33
  require 'database_cleaner'
33
34
  require "orm/#{DEVISE_ORM}"
35
+
36
+ # Controller testing is the way that Devise itself tests the functionality of
37
+ # controller, even though it has been deprecated in favor of request tests.
38
+ require 'rails-controller-testing'
39
+ Rails::Controller::Testing.install
34
40
  require 'support/integration_helpers'
35
41
 
36
42
  class Minitest::Test
@@ -6,18 +6,28 @@ 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
13
13
  run_generator
14
- assert_file 'config/initializers/devise-security.rb'
14
+ assert_file 'config/initializers/devise_security.rb'
15
+ assert_file 'config/locales/devise.security_extension.by.yml'
16
+ assert_file 'config/locales/devise.security_extension.cs.yml'
15
17
  assert_file 'config/locales/devise.security_extension.de.yml'
16
18
  assert_file 'config/locales/devise.security_extension.en.yml'
17
19
  assert_file 'config/locales/devise.security_extension.es.yml'
20
+ assert_file 'config/locales/devise.security_extension.fa.yml'
18
21
  assert_file 'config/locales/devise.security_extension.fr.yml'
22
+ assert_file 'config/locales/devise.security_extension.hi.yml'
19
23
  assert_file 'config/locales/devise.security_extension.it.yml'
20
24
  assert_file 'config/locales/devise.security_extension.ja.yml'
25
+ assert_file 'config/locales/devise.security_extension.nl.yml'
26
+ assert_file 'config/locales/devise.security_extension.pt.yml'
27
+ assert_file 'config/locales/devise.security_extension.ru.yml'
21
28
  assert_file 'config/locales/devise.security_extension.tr.yml'
29
+ assert_file 'config/locales/devise.security_extension.uk.yml'
30
+ assert_file 'config/locales/devise.security_extension.zh_CN.yml'
31
+ assert_file 'config/locales/devise.security_extension.zh_TW.yml'
22
32
  end
23
33
  end