devise-security 0.15.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +3 -1
  3. data/README.md +43 -24
  4. data/app/controllers/devise/paranoid_verification_code_controller.rb +26 -12
  5. data/app/controllers/devise/password_expired_controller.rb +23 -10
  6. data/config/locales/bg.yml +42 -0
  7. data/config/locales/by.yml +2 -0
  8. data/config/locales/cs.yml +6 -0
  9. data/config/locales/de.yml +4 -0
  10. data/config/locales/en.yml +3 -1
  11. data/config/locales/es.yml +13 -0
  12. data/config/locales/fa.yml +2 -0
  13. data/config/locales/fr.yml +15 -2
  14. data/config/locales/hi.yml +22 -20
  15. data/config/locales/it.yml +2 -0
  16. data/config/locales/ja.yml +13 -0
  17. data/config/locales/nl.yml +2 -0
  18. data/config/locales/pt.yml +2 -0
  19. data/config/locales/ru.yml +2 -0
  20. data/config/locales/tr.yml +26 -1
  21. data/config/locales/uk.yml +2 -0
  22. data/config/locales/zh_CN.yml +2 -0
  23. data/config/locales/zh_TW.yml +2 -0
  24. data/lib/devise-security/controllers/helpers.rb +25 -13
  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 +10 -6
  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 +62 -11
  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 +15 -6
  42. data/lib/generators/devise_security/install_generator.rb +4 -6
  43. data/{test/tmp/config/initializers/devise-security.rb → lib/generators/templates/devise_security.rb} +9 -1
  44. data/test/controllers/test_paranoid_verification_code_controller.rb +133 -0
  45. data/test/controllers/test_password_expired_controller.rb +122 -99
  46. data/test/controllers/test_security_question_controller.rb +19 -37
  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 +4 -13
  82. data/test/dummy/config/initializers/devise.rb +1 -5
  83. data/test/dummy/config/initializers/migration_class.rb +1 -8
  84. data/test/dummy/config/locales/en.yml +10 -0
  85. data/test/dummy/config/mongoid.yml +1 -1
  86. data/test/dummy/config/routes.rb +3 -1
  87. data/test/dummy/config.ru +1 -1
  88. data/test/dummy/db/migrate/20120508165529_create_tables.rb +5 -5
  89. data/test/dummy/lib/shared_expirable_columns.rb +1 -0
  90. data/test/dummy/lib/shared_security_questions_fields.rb +1 -0
  91. data/test/dummy/lib/shared_user.rb +17 -6
  92. data/test/dummy/lib/shared_user_without_omniauth.rb +12 -3
  93. data/test/dummy/lib/shared_verification_fields.rb +1 -0
  94. data/test/dummy/log/test.log +44592 -1151
  95. data/test/i18n_test.rb +22 -0
  96. data/test/integration/test_paranoid_verification_code_workflow.rb +53 -0
  97. data/test/integration/test_password_expirable_workflow.rb +2 -6
  98. data/test/integration/test_session_limitable_workflow.rb +5 -3
  99. data/test/orm/active_record.rb +7 -7
  100. data/test/orm/mongoid.rb +2 -1
  101. data/test/support/integration_helpers.rb +10 -22
  102. data/test/support/mongoid.yml +1 -1
  103. data/test/test_compatibility.rb +2 -0
  104. data/test/test_complexity_validator.rb +247 -37
  105. data/test/test_database_authenticatable_patch.rb +146 -0
  106. data/test/test_helper.rb +11 -12
  107. data/test/test_install_generator.rb +2 -2
  108. data/test/test_paranoid_verification.rb +8 -9
  109. data/test/test_password_archivable.rb +34 -11
  110. data/test/test_password_expirable.rb +27 -27
  111. data/test/test_secure_validatable.rb +284 -50
  112. data/test/test_secure_validatable_overrides.rb +185 -0
  113. data/test/test_session_limitable.rb +9 -9
  114. data/{lib/generators/templates/devise-security.rb → test/tmp/config/initializers/devise_security.rb} +9 -1
  115. data/test/tmp/config/locales/devise.security_extension.by.yml +50 -0
  116. data/test/tmp/config/locales/devise.security_extension.cs.yml +46 -0
  117. data/test/tmp/config/locales/devise.security_extension.de.yml +4 -0
  118. data/test/tmp/config/locales/devise.security_extension.en.yml +3 -1
  119. data/test/tmp/config/locales/devise.security_extension.es.yml +22 -9
  120. data/test/tmp/config/locales/devise.security_extension.fa.yml +2 -0
  121. data/test/tmp/config/locales/devise.security_extension.fr.yml +15 -2
  122. data/test/tmp/config/locales/devise.security_extension.hi.yml +43 -0
  123. data/test/tmp/config/locales/devise.security_extension.it.yml +2 -0
  124. data/test/tmp/config/locales/devise.security_extension.ja.yml +13 -0
  125. data/test/tmp/config/locales/devise.security_extension.nl.yml +2 -0
  126. data/test/tmp/config/locales/devise.security_extension.pt.yml +2 -0
  127. data/test/tmp/config/locales/devise.security_extension.ru.yml +2 -0
  128. data/test/tmp/config/locales/devise.security_extension.tr.yml +26 -1
  129. data/test/tmp/config/locales/devise.security_extension.uk.yml +2 -0
  130. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +2 -0
  131. data/test/tmp/config/locales/devise.security_extension.zh_TW.yml +42 -0
  132. metadata +65 -45
  133. data/lib/devise-security/orm/active_record.rb +0 -20
  134. data/lib/devise-security/patches/confirmations_controller_captcha.rb +0 -23
  135. data/lib/devise-security/patches/confirmations_controller_security_question.rb +0 -26
  136. data/lib/devise-security/patches/passwords_controller_captcha.rb +0 -22
  137. data/lib/devise-security/patches/passwords_controller_security_question.rb +0 -25
  138. data/lib/devise-security/patches/registrations_controller_captcha.rb +0 -35
  139. data/lib/devise-security/patches/sessions_controller_captcha.rb +0 -26
  140. data/lib/devise-security/patches/unlocks_controller_captcha.rb +0 -22
  141. data/lib/devise-security/patches/unlocks_controller_security_question.rb +0 -25
  142. data/lib/devise-security/schema.rb +0 -66
  143. data/test/dummy/app/controllers/foos_controller.rb +0 -0
  144. data/test/dummy/app/models/secure_user.rb +0 -9
  145. data/test/dummy/lib/shared_user_without_email.rb +0 -28
@@ -1,35 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Password complexity validator
3
+ # (NIST)[https://pages.nist.gov/800-63-3/sp800-63b.html#appA] does not recommend
4
+ # the use of a password complexity checks because...
5
+ #
6
+ # > Length and complexity requirements beyond those recommended here
7
+ # > significantly increase the difficulty of memorized secrets and increase user
8
+ # > frustration. As a result, users often work around these restrictions in a
9
+ # > way that is counterproductive. Furthermore, other mitigations such as
10
+ # > blacklists, secure hashed storage, and rate limiting are more effective at
11
+ # > preventing modern brute-force attacks. Therefore, no additional complexity
12
+ # > requirements are imposed.
13
+ #
4
14
  # Options:
5
- # - digit: minimum number of digits in the validated string
6
- # - digits: minimum number of digits in the validated string
7
- # - lower: minimum number of lower-case letters in the validated string
8
- # - symbol: minimum number of punctuation characters or symbols in the validated string
9
- # - symbols: minimum number of punctuation characters or symbols in the validated string
10
- # - upper: minimum number of upper-case letters in the validated string
15
+ # - `digit | digits`: minimum number of digits in the validated string. Uses
16
+ # the `digit` localization key.
17
+ # - `lower`: minimum number of lower-case letters in the validated string
18
+ # - `symbol | symbols`: minimum number of punctuation characters or symbols in
19
+ # the validated string. Uses the `symbol` localization key.
20
+ # - `upper`: minimum number of upper-case letters in the validated string
11
21
  class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
12
- PATTERNS = {
13
- digit: /\p{Digit}/,
14
- digits: /\p{Digit}/,
15
- lower: /\p{Lower}/,
16
- symbol: /\p{Punct}|\p{S}/,
17
- symbols: /\p{Punct}|\p{S}/,
18
- upper: /\p{Upper}/
19
- }.freeze
22
+ # A Hash of the possible valid patterns that can be checked against. The keys
23
+ # for this Hash are singular symbols corresponding to entries in the
24
+ # localization files. Override or redefine this method if you want to include
25
+ # custom patterns (e.g., `letter: /\p{Alpha}/` for all letters).
26
+ #
27
+ # @return [Hash<Symbol,Regexp>]
28
+ def patterns
29
+ {
30
+ digit: /\p{Digit}/,
31
+ lower: /\p{Lower}/,
32
+ symbol: /\p{Punct}|\p{S}/,
33
+ upper: /\p{Upper}/
34
+ }
35
+ end
20
36
 
21
- def validate_each(record, attribute, value)
22
- active_pattern_keys.each do |key|
23
- minimum = [0, options[key].to_i].max
24
- pattern = Regexp.new PATTERNS[key]
37
+ # Validate the complexity of the password. This validation does not check to
38
+ # ensure the password is not blank. That is the responsibility of other
39
+ # validations. This validator will also ignore any patterns that are not
40
+ # explicitly configured to be used or whose minimum limits are less than 1.
41
+ #
42
+ # @param record [ActiveModel::Model]
43
+ # @param attribute [Symbol]
44
+ # @param password [String]
45
+ def validate_each(record, attribute, password)
46
+ return if password.blank?
25
47
 
26
- unless (value || '').scan(pattern).size >= minimum
27
- record.errors.add attribute, :"password_complexity.#{key}", count: minimum
28
- end
29
- end
30
- end
48
+ options.sort.each do |pattern_name, minimum|
49
+ normalized_option = pattern_name.to_s.singularize.to_sym
31
50
 
32
- def active_pattern_keys
33
- options.keys & PATTERNS.keys
51
+ next unless patterns.key?(normalized_option)
52
+ next unless minimum.positive?
53
+ next if password.scan(patterns[normalized_option]).size >= minimum
54
+
55
+ record.errors.add(
56
+ attribute,
57
+ :"password_complexity.#{normalized_option}",
58
+ count: minimum
59
+ )
60
+ end
34
61
  end
35
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.15.0'
4
+ VERSION = '0.18.0'
5
5
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  DEVISE_ORM = ENV.fetch('DEVISE_ORM', 'active_record').to_sym unless defined?(DEVISE_ORM)
3
4
 
4
- require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
5
+ require DEVISE_ORM.to_s if DEVISE_ORM.in? %i[active_record mongoid]
5
6
  require 'active_support/core_ext/integer'
6
7
  require 'active_support/ordered_hash'
7
8
  require 'active_support/concern'
@@ -9,15 +10,20 @@ require 'devise'
9
10
 
10
11
  module Devise
11
12
  # Number of seconds that passwords are valid (e.g 3.months)
12
- # Disable pasword expiration with +false+
13
+ # Disable password expiration with +false+
13
14
  # Expire only on demand with +true+
14
15
  mattr_accessor :expire_password_after
15
16
  @@expire_password_after = 3.months
16
17
 
17
- # Validate password for strongness
18
+ # Validate password complexity
18
19
  mattr_accessor :password_complexity
19
20
  @@password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
20
21
 
22
+ # Define the class used to validate password complexity. Set to a Class or a
23
+ # string which will be used to determine which class to use.
24
+ mattr_accessor :password_complexity_validator
25
+ @@password_complexity_validator = 'devise_security/password_complexity_validator'
26
+
21
27
  # Number of old passwords in archive
22
28
  mattr_accessor :password_archiving_count
23
29
  @@password_archiving_count = 5
@@ -79,11 +85,14 @@ module Devise
79
85
  # paranoid_verification will regenerate verifacation code after faild attempt
80
86
  mattr_accessor :paranoid_code_regenerate_after_attempt
81
87
  @@paranoid_code_regenerate_after_attempt = 10
88
+
89
+ # Whether to allow passwords that are equal (case insensitive) to the email
90
+ mattr_accessor :allow_passwords_equal_to_email
91
+ @@allow_passwords_equal_to_email = false
82
92
  end
83
93
 
84
- # an security extension for devise
94
+ # a security extension for devise
85
95
  module DeviseSecurity
86
- autoload :Schema, 'devise-security/schema'
87
96
  autoload :Patches, 'devise-security/patches'
88
97
 
89
98
  module Controllers
@@ -104,6 +113,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
104
113
  # requires
105
114
  require 'devise-security/routes'
106
115
  require 'devise-security/rails'
107
- require "devise-security/orm/#{DEVISE_ORM}"
116
+ require "devise-security/orm/#{DEVISE_ORM}" if DEVISE_ORM == :mongoid
108
117
  require 'devise-security/models/database_authenticatable_patch'
109
118
  require 'devise-security/models/paranoid_verification'
@@ -3,23 +3,21 @@
3
3
  module DeviseSecurity
4
4
  module Generators
5
5
  # Generator for Rails to create or append to a Devise initializer.
6
- class InstallGenerator < Rails::Generators::Base
6
+ class InstallGenerator < Rails::Generators::Base
7
7
  LOCALES = %w[by cs de en es fa fr hi it ja nl pt ru tr uk zh_CN zh_TW].freeze
8
8
 
9
- source_root File.expand_path('../../templates', __FILE__)
9
+ source_root File.expand_path('../templates', __dir__)
10
10
  desc 'Install the devise security extension'
11
11
 
12
12
  def copy_initializer
13
- template('devise-security.rb',
14
- 'config/initializers/devise-security.rb',
15
- )
13
+ template('devise_security.rb', 'config/initializers/devise_security.rb')
16
14
  end
17
15
 
18
16
  def copy_locales
19
17
  LOCALES.each do |locale|
20
18
  copy_file(
21
19
  "../../../config/locales/#{locale}.yml",
22
- "config/locales/devise.security_extension.#{locale}.yml",
20
+ "config/locales/devise.security_extension.#{locale}.yml"
23
21
  )
24
22
  end
25
23
  end
@@ -7,7 +7,9 @@ Devise.setup do |config|
7
7
  # Should the password expire (e.g 3.months)
8
8
  # config.expire_password_after = false
9
9
 
10
- # Need 1 char of A-Z, a-z and 0-9
10
+ # Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
11
+ # You may use "digits" in place of "digit" and "symbols" in place of
12
+ # "symbol" based on your preference
11
13
  # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
12
14
 
13
15
  # How many passwords to keep in archive
@@ -41,4 +43,10 @@ Devise.setup do |config|
41
43
 
42
44
  # Time period for account expiry from last_activity_at
43
45
  # config.expire_after = 90.days
46
+
47
+ # Allow password to equal the email
48
+ # config.allow_passwords_equal_to_email = false
49
+
50
+ # paranoid_verification will regenerate verification code after failed attempt
51
+ # config.paranoid_code_regenerate_after_attempt = 10
44
52
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class Devise::ParanoidVerificationCodeControllerTest < ActionController::TestCase
6
+ include Devise::Test::ControllerHelpers
7
+
8
+ setup do
9
+ @controller.class.respond_to :json, :xml
10
+ @request.env['devise.mapping'] = Devise.mappings[:user]
11
+ @user = User.create!(
12
+ username: 'hello',
13
+ email: 'hello@path.travel',
14
+ password: 'Password4',
15
+ confirmed_at: 5.months.ago,
16
+ paranoid_verification_code: 'cookies'
17
+ )
18
+ assert @user.valid?
19
+ assert @user.need_paranoid_verification?
20
+
21
+ sign_in(@user)
22
+ end
23
+
24
+ test 'redirects to root on show if user not logged in' do
25
+ sign_out(@user)
26
+ get :show
27
+ assert_redirected_to :root
28
+ end
29
+
30
+ test "redirects to root on show if user doesn't need paranoid verification" do
31
+ @user.update(paranoid_verification_code: nil)
32
+ get :show
33
+ assert_redirected_to :root
34
+ end
35
+
36
+ test 'renders show on show if user needs paranoid verification' do
37
+ @user.update(paranoid_verification_code: 'cookies')
38
+ get :show
39
+ assert_template :show
40
+ end
41
+
42
+ test 'redirects on update if user not logged in' do
43
+ sign_out(@user)
44
+ patch :update
45
+ assert_redirected_to :root
46
+ end
47
+
48
+ test 'redirects on update if user does not need paranoid verification' do
49
+ @user.update(paranoid_verification_code: nil)
50
+ patch :update
51
+ assert_redirected_to :root
52
+ end
53
+
54
+ test 'update paranoid_verification_code with default format' do
55
+ patch(
56
+ :update,
57
+ params: {
58
+ user: {
59
+ paranoid_verification_code: 'cookies'
60
+ }
61
+ }
62
+ )
63
+ assert_redirected_to root_path
64
+ assert_equal 'Verification code accepted', flash[:notice]
65
+ assert_equal('text/html', response.media_type)
66
+ end
67
+
68
+ test 'update paranoid_verification_code using JSON format' do
69
+ patch(
70
+ :update,
71
+ format: :json,
72
+ params: {
73
+ user: {
74
+ paranoid_verification_code: 'cookies'
75
+ }
76
+ }
77
+ )
78
+
79
+ assert_response 204
80
+ assert_equal root_url, response.location
81
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
82
+ end
83
+
84
+ test 'update paranoid_verification_code using XML format' do
85
+ patch(
86
+ :update,
87
+ format: :xml,
88
+ params: {
89
+ user: {
90
+ paranoid_verification_code: 'cookies'
91
+ }
92
+ }
93
+ )
94
+ assert_response 204
95
+ assert_equal root_url, response.location
96
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
97
+ end
98
+ end
99
+
100
+ class ParanoidVerificationCodeCustomRedirectTest < ActionController::TestCase
101
+ include Devise::Test::ControllerHelpers
102
+ tests Overrides::ParanoidVerificationCodeController
103
+
104
+ setup do
105
+ @controller.class.respond_to :json, :xml
106
+ @request.env['devise.mapping'] = Devise.mappings[:paranoid_verification_user]
107
+ @user = ParanoidVerificationUser.create!(
108
+ username: 'hello',
109
+ email: 'hello@path.travel',
110
+ password: 'Password4',
111
+ confirmed_at: 5.months.ago,
112
+ paranoid_verification_code: 'cookies'
113
+ )
114
+ assert @user.valid?
115
+ assert @user.need_paranoid_verification?
116
+
117
+ sign_in(@user)
118
+ end
119
+
120
+ test 'redirects to custom redirect route on update' do
121
+ patch(
122
+ :update,
123
+ params: {
124
+ paranoid_verification_user: {
125
+ paranoid_verification_code: 'cookies'
126
+ }
127
+ }
128
+ )
129
+
130
+ assert_redirected_to '/cats'
131
+ assert_equal 'Verification code accepted', flash[:notice]
132
+ end
133
+ end
@@ -7,7 +7,7 @@ class Devise::PasswordExpiredControllerTest < ActionController::TestCase
7
7
 
8
8
  setup do
9
9
  @controller.class.respond_to :json, :xml
10
- @request.env["devise.mapping"] = Devise.mappings[:user]
10
+ @request.env['devise.mapping'] = Devise.mappings[:user]
11
11
  @user = User.create!(
12
12
  username: 'hello',
13
13
  email: 'hello@path.travel',
@@ -21,121 +21,144 @@ class Devise::PasswordExpiredControllerTest < ActionController::TestCase
21
21
  sign_in(@user)
22
22
  end
23
23
 
24
+ test 'redirects on show if user not logged in' do
25
+ sign_out(@user)
26
+ get :show
27
+ assert_redirected_to :root
28
+ end
29
+
30
+ test 'redirects on show if user does not need password change' do
31
+ @user.update(password_changed_at: Time.zone.now)
32
+ get :show
33
+ assert_redirected_to :root
34
+ end
35
+
24
36
  test 'should render show' do
25
37
  get :show
26
38
  assert_includes @response.body, 'Renew your password'
27
39
  end
28
40
 
41
+ test 'redirects on update if user not logged in' do
42
+ sign_out(@user)
43
+ put :update
44
+ assert_redirected_to :root
45
+ end
46
+
47
+ test 'redirects on update if user does not need password change' do
48
+ @user.update(password_changed_at: Time.zone.now)
49
+ put :update
50
+ assert_redirected_to :root
51
+ end
52
+
29
53
  test 'update password with default format' do
30
- if Rails.gem_version < Gem::Version.new('5.0')
31
- put :update,
32
- {
33
- user: {
34
- current_password: 'Password4',
35
- password: 'Password5',
36
- password_confirmation: 'Password5'
37
- }
38
- }
39
- else
40
- put :update,
41
- params: {
42
- user: {
43
- current_password: 'Password4',
44
- password: 'Password5',
45
- password_confirmation: 'Password5'
46
- }
47
- }
48
- end
54
+ put(
55
+ :update,
56
+ params: {
57
+ user: {
58
+ current_password: 'Password4',
59
+ password: 'Password5',
60
+ password_confirmation: 'Password5'
61
+ }
62
+ }
63
+ )
49
64
  assert_redirected_to root_path
50
- assert_equal response.content_type, 'text/html'
65
+ assert_equal('text/html', response.media_type)
51
66
  end
52
67
 
53
68
  test 'password confirmation does not match' do
54
- if Rails.gem_version < Gem::Version.new('5.0')
55
- put :update,
56
- {
57
- user: {
58
- current_password: 'Password4',
59
- password: 'Password5',
60
- password_confirmation: 'Password6'
61
- }
62
- }
63
- else
64
- put :update,
65
- params: {
66
- user: {
67
- current_password: 'Password4',
68
- password: 'Password5',
69
- password_confirmation: 'Password6'
70
- }
71
- }
72
- end
69
+ put(
70
+ :update,
71
+ params: {
72
+ user: {
73
+ current_password: 'Password4',
74
+ password: 'Password5',
75
+ password_confirmation: 'Password6'
76
+ }
77
+ }
78
+ )
79
+
73
80
  assert_response :success
74
81
  assert_template :show
75
- assert_equal response.content_type, 'text/html'
82
+ assert_equal('text/html', response.media_type)
83
+ assert_includes(
84
+ response.body,
85
+ 'Password confirmation doesn&#39;t match Password'
86
+ )
76
87
  end
77
88
 
78
89
  test 'update password using JSON format' do
79
- if Rails.gem_version < Gem::Version.new('5.0')
80
- # The responders gem that is compatible with Rails 4.2
81
- # does not return a 204 No Content for common data formats
82
- # This is the previously existing behavior so it is allowed
83
- put :update,
84
- {
85
- user: {
86
- current_password: 'Password4',
87
- password: 'Password5',
88
- password_confirmation: 'Password5'
89
- }
90
- },
91
- format: :json
92
- assert_redirected_to root_path
93
- assert_equal response.content_type, 'text/html'
94
- else
95
- put :update,
96
- format: :json,
97
- params: {
98
- user: {
99
- current_password: 'Password4',
100
- password: 'Password5',
101
- password_confirmation: 'Password5'
102
- }
103
- }
104
- assert_response 204
105
- assert_equal root_url, response.location
106
- assert_nil response.content_type, 'No Content-Type header should be set for No Content response'
107
- end
90
+ put(
91
+ :update,
92
+ format: :json,
93
+ params: {
94
+ user: {
95
+ current_password: 'Password4',
96
+ password: 'Password5',
97
+ password_confirmation: 'Password5'
98
+ }
99
+ }
100
+ )
101
+
102
+ assert_response 204
103
+ assert_equal root_url, response.location
104
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
108
105
  end
109
106
 
110
107
  test 'update password using XML format' do
111
- if Rails.gem_version < Gem::Version.new('5.0')
112
- # The responders gem that is compatible with Rails 4.2
113
- # does not return a 204 No Content for common data formats
114
- # This is the previously existing behavior so it is allowed
115
- put :update,
116
- {
117
- user: {
118
- current_password: 'Password4',
119
- password: 'Password5',
120
- password_confirmation: 'Password5'
121
- },
122
- },
123
- format: :xml
124
- assert_redirected_to root_path
125
- assert_equal response.content_type, 'text/html'
126
- else
127
- put :update,
128
- format: :xml,
129
- params: {
130
- user: {
131
- current_password: 'Password4',
132
- password: 'Password5',
133
- password_confirmation: 'Password5'
134
- }
135
- }
136
- assert_response 204
137
- assert_equal root_url, response.location
138
- assert_nil response.content_type, 'No Content-Type header should be set for No Content response'
139
- end
108
+ put(
109
+ :update,
110
+ format: :xml,
111
+ params: {
112
+ user: {
113
+ current_password: 'Password4',
114
+ password: 'Password5',
115
+ password_confirmation: 'Password5'
116
+ }
117
+ }
118
+ )
119
+ assert_response 204
120
+ assert_equal root_url, response.location
121
+ assert_nil response.media_type, 'No Content-Type header should be set for No Content response'
122
+ end
123
+ end
124
+
125
+ class PasswordExpiredCustomRedirectTest < ActionController::TestCase
126
+ include Devise::Test::ControllerHelpers
127
+ tests Overrides::PasswordExpiredController
128
+
129
+ setup do
130
+ @controller.class.respond_to :json, :xml
131
+ @request.env['devise.mapping'] = Devise.mappings[:password_expired_user]
132
+ @user = PasswordExpiredUser.create!(
133
+ username: 'hello',
134
+ email: 'hello@path.travel',
135
+ password: 'Password4',
136
+ password_changed_at: 4.months.ago,
137
+ confirmed_at: 5.months.ago
138
+ )
139
+ assert @user.valid?
140
+ assert @user.need_change_password?
141
+
142
+ sign_in(@user)
143
+ end
144
+
145
+ test 'update password with custom redirect route' do
146
+ put(
147
+ :update,
148
+ params: {
149
+ password_expired_user: {
150
+ current_password: 'Password4',
151
+ password: 'Password5',
152
+ password_confirmation: 'Password5'
153
+ }
154
+ }
155
+ )
156
+
157
+ assert_redirected_to '/cookies'
158
+ end
159
+
160
+ test 'yield resource to block on update' do
161
+ put(:update, params: { password_expired_user: { current_password: '123' } })
162
+ assert @controller.update_block_called?, 'Update failed to yield resource to provided block'
140
163
  end
141
164
  end