devise-security 0.14.0.rc1 → 0.15.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -60
  3. data/app/controllers/devise/password_expired_controller.rb +10 -1
  4. data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
  5. data/app/views/devise/password_expired/show.html.erb +5 -5
  6. data/config/locales/by.yml +48 -0
  7. data/config/locales/cs.yml +40 -0
  8. data/config/locales/de.yml +12 -2
  9. data/config/locales/en.yml +12 -1
  10. data/config/locales/es.yml +9 -9
  11. data/config/locales/fa.yml +40 -0
  12. data/config/locales/hi.yml +41 -0
  13. data/config/locales/it.yml +34 -4
  14. data/config/locales/ja.yml +1 -1
  15. data/config/locales/nl.yml +40 -0
  16. data/config/locales/pt.yml +40 -0
  17. data/config/locales/ru.yml +48 -0
  18. data/config/locales/uk.yml +48 -0
  19. data/config/locales/zh_CN.yml +40 -0
  20. data/config/locales/zh_TW.yml +40 -0
  21. data/lib/devise-security.rb +1 -0
  22. data/lib/devise-security/controllers/helpers.rb +59 -50
  23. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  24. data/lib/devise-security/hooks/session_limitable.rb +21 -10
  25. data/lib/devise-security/models/compatibility.rb +2 -2
  26. data/lib/devise-security/models/compatibility/{active_record.rb → active_record_patch.rb} +12 -1
  27. data/lib/devise-security/models/compatibility/{mongoid.rb → mongoid_patch.rb} +11 -1
  28. data/lib/devise-security/models/mongoid/old_password.rb +1 -1
  29. data/lib/devise-security/models/password_expirable.rb +5 -1
  30. data/lib/devise-security/models/session_limitable.rb +17 -2
  31. data/lib/devise-security/schema.rb +1 -1
  32. data/lib/devise-security/validators/password_complexity_validator.rb +4 -2
  33. data/lib/devise-security/version.rb +1 -1
  34. data/lib/generators/devise_security/install_generator.rb +2 -2
  35. data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +0 -0
  36. data/test/controllers/test_password_expired_controller.rb +141 -0
  37. data/test/{test_security_question_controller.rb → controllers/test_security_question_controller.rb} +0 -0
  38. data/test/dummy/app/assets/config/manifest.js +3 -0
  39. data/test/dummy/app/controllers/widgets_controller.rb +6 -0
  40. data/test/dummy/app/models/user.rb +8 -0
  41. data/test/dummy/config/application.rb +1 -0
  42. data/test/dummy/config/routes.rb +4 -3
  43. data/test/dummy/db/migrate/20120508165529_create_tables.rb +11 -2
  44. data/test/dummy/log/test.log +1799 -0
  45. data/test/integration/test_password_expirable_workflow.rb +57 -0
  46. data/test/integration/test_session_limitable_workflow.rb +67 -0
  47. data/test/orm/active_record.rb +4 -1
  48. data/test/support/integration_helpers.rb +47 -0
  49. data/test/test_compatibility.rb +13 -0
  50. data/test/test_complexity_validator.rb +12 -0
  51. data/test/test_helper.rb +21 -6
  52. data/test/test_install_generator.rb +10 -0
  53. data/test/test_session_limitable.rb +57 -0
  54. data/test/tmp/config/initializers/devise-security.rb +44 -0
  55. data/test/tmp/config/locales/devise.security_extension.de.yml +38 -0
  56. data/test/tmp/config/locales/devise.security_extension.en.yml +40 -0
  57. data/test/tmp/config/locales/devise.security_extension.es.yml +29 -0
  58. data/test/tmp/config/locales/devise.security_extension.fa.yml +40 -0
  59. data/test/tmp/config/locales/devise.security_extension.fr.yml +29 -0
  60. data/test/tmp/config/locales/devise.security_extension.it.yml +40 -0
  61. data/test/tmp/config/locales/devise.security_extension.ja.yml +29 -0
  62. data/test/tmp/config/locales/devise.security_extension.nl.yml +40 -0
  63. data/test/tmp/config/locales/devise.security_extension.pt.yml +40 -0
  64. data/test/tmp/config/locales/devise.security_extension.ru.yml +48 -0
  65. data/test/tmp/config/locales/devise.security_extension.tr.yml +17 -0
  66. data/test/tmp/config/locales/devise.security_extension.uk.yml +48 -0
  67. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +40 -0
  68. metadata +165 -121
  69. data/.codeclimate.yml +0 -63
  70. data/.document +0 -5
  71. data/.gitignore +0 -43
  72. data/.mdlrc +0 -1
  73. data/.rubocop.yml +0 -64
  74. data/.ruby-version +0 -1
  75. data/.travis.yml +0 -41
  76. data/Appraisals +0 -35
  77. data/Gemfile +0 -10
  78. data/Rakefile +0 -28
  79. data/devise-security.gemspec +0 -50
  80. data/gemfiles/rails_4.2_stable.gemfile +0 -16
  81. data/gemfiles/rails_5.0_stable.gemfile +0 -15
  82. data/gemfiles/rails_5.1_stable.gemfile +0 -15
  83. data/gemfiles/rails_5.2_stable.gemfile +0 -15
  84. data/gemfiles/rails_6.0_beta.gemfile +0 -15
  85. data/test/dummy/app/models/.gitkeep +0 -0
  86. data/test/test_password_expired_controller.rb +0 -46
@@ -0,0 +1,40 @@
1
+ zh_TW:
2
+ errors:
3
+ messages:
4
+ taken_in_past: '曾被使用過。'
5
+ equal_to_current_password: '必須與目前密碼不同。'
6
+ password_complexity:
7
+ digit:
8
+ one: 必須包含至少一個數字
9
+ other: 必須包含至少 %{count} 個數字
10
+ lower:
11
+ one: 必須包含至少一個小寫字母
12
+ other: 必須包含至少 %{count} 個小寫字母
13
+ symbol:
14
+ one: 必須包含至少一個特殊符號
15
+ other: 必須包含至少 %{count} 個特殊符號
16
+ upper:
17
+ one: 必須包含至少一個大寫字母
18
+ other: 必須包含至少 %{count} 個大寫字母
19
+ devise:
20
+ invalid_captcha: '輸入的驗證碼無效。'
21
+ invalid_security_question: '安全問題答案無效。'
22
+ paranoid_verify:
23
+ code_required: '請輸入由我們客服團隊提供的代碼'
24
+ paranoid_verification_code:
25
+ show:
26
+ submit_verification_code: 送出驗證碼
27
+ verification_code: 驗證碼
28
+ submit: 送出
29
+ password_expired:
30
+ updated: '你的新密碼已儲存'
31
+ change_required: '你的密碼已經過期,請更新密碼。'
32
+ show:
33
+ renew_your_password: 更新你的密碼
34
+ current_password: 目前密碼
35
+ new_password: 新密碼
36
+ new_password_confirmation: 確認新密碼
37
+ change_my_password: 更改我的密碼
38
+ failure:
39
+ session_limited: '你的登入憑證已在另一個瀏覽器上被使用,請重新登入以在此瀏覽器繼續使用。'
40
+ expired: '你的帳號因過久沒使用而已經過期,請洽網站管理員。'
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ DEVISE_ORM = ENV.fetch('DEVISE_ORM', 'active_record').to_sym unless defined?(DEVISE_ORM)
2
3
 
3
4
  require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
4
5
  require 'active_support/core_ext/integer'
@@ -40,71 +40,80 @@ module DeviseSecurity
40
40
 
41
41
  # controller instance methods
42
42
 
43
- private
44
-
45
- # lookup if an password change needed
46
- def handle_password_change
47
- return if warden.nil?
48
-
49
- if !devise_controller? && !ignore_password_expire? && !request.format.nil? && request.format.html?
50
- Devise.mappings.keys.flatten.any? do |scope|
51
- if signed_in?(scope) && warden.session(scope)['password_expired']
52
- # re-check to avoid infinite loop if date changed after login attempt
53
- if send(:"current_#{scope}").try(:need_change_password?)
54
- store_location_for(scope, request.original_fullpath) if request.get?
55
- redirect_for_password_change scope
56
- return
57
- else
58
- warden.session(scope)[:password_expired] = false
59
- end
43
+ private
44
+
45
+ # Called as a `before_action` on all actions on any controller that uses
46
+ # this helper. If the user's session is marked as having an expired
47
+ # password we double check in case it has been changed by another process,
48
+ # then redirect to the password change url.
49
+ #
50
+ # @note `Warden::Manager.after_authentication` is run AFTER this method
51
+ #
52
+ # @note Once the warden session has `'password_expired'` set to `false`,
53
+ # it will **never** be checked again until the user re-logs in.
54
+ def handle_password_change
55
+ return if warden.nil?
56
+
57
+ if !devise_controller? &&
58
+ !ignore_password_expire? &&
59
+ !request.format.nil? &&
60
+ request.format.html?
61
+ Devise.mappings.keys.flatten.any? do |scope|
62
+ if signed_in?(scope) && warden.session(scope)['password_expired'] == true
63
+ if send(:"current_#{scope}").try(:need_change_password?)
64
+ store_location_for(scope, request.original_fullpath) if request.get?
65
+ redirect_for_password_change(scope)
66
+ else
67
+ warden.session(scope)['password_expired'] = false
60
68
  end
61
69
  end
62
70
  end
63
71
  end
72
+ end
64
73
 
65
- # lookup if extra (paranoid) code verification is needed
66
- def handle_paranoid_verification
67
- return if warden.nil?
74
+ # lookup if extra (paranoid) code verification is needed
75
+ def handle_paranoid_verification
76
+ return if warden.nil?
68
77
 
69
- if !devise_controller? && !request.format.nil? && request.format.html?
70
- Devise.mappings.keys.flatten.any? do |scope|
71
- if signed_in?(scope) && warden.session(scope)['paranoid_verify']
72
- store_location_for(scope, request.original_fullpath) if request.get?
73
- redirect_for_paranoid_verification scope
74
- return
75
- end
78
+ if !devise_controller? && !request.format.nil? && request.format.html?
79
+ Devise.mappings.keys.flatten.any? do |scope|
80
+ if signed_in?(scope) && warden.session(scope)['paranoid_verify']
81
+ store_location_for(scope, request.original_fullpath) if request.get?
82
+ redirect_for_paranoid_verification scope
83
+ return
76
84
  end
77
85
  end
78
86
  end
87
+ end
79
88
 
80
- # redirect for password update with alert message
81
- def redirect_for_password_change(scope)
82
- redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', {scope: 'devise.password_expired'})
83
- end
89
+ # redirect for password update with alert message
90
+ def redirect_for_password_change(scope)
91
+ redirect_to change_password_required_path_for(scope), alert: I18n.t('change_required', { scope: 'devise.password_expired' })
92
+ end
84
93
 
85
- def redirect_for_paranoid_verification(scope)
86
- redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', {scope: 'devise.paranoid_verify'})
87
- end
94
+ def redirect_for_paranoid_verification(scope)
95
+ redirect_to paranoid_verification_code_path_for(scope), alert: I18n.t('code_required', { scope: 'devise.paranoid_verify' })
96
+ end
88
97
 
89
- # path for change password
90
- def change_password_required_path_for(resource_or_scope = nil)
91
- scope = Devise::Mapping.find_scope!(resource_or_scope)
92
- change_path = "#{scope}_password_expired_path"
93
- send(change_path)
94
- end
98
+ # path for change password
99
+ def change_password_required_path_for(resource_or_scope = nil)
100
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
101
+ change_path = "#{scope}_password_expired_path"
102
+ send(change_path)
103
+ end
95
104
 
96
- def paranoid_verification_code_path_for(resource_or_scope = nil)
97
- scope = Devise::Mapping.find_scope!(resource_or_scope)
98
- change_path = "#{scope}_paranoid_verification_code_path"
99
- send(change_path)
100
- end
105
+ def paranoid_verification_code_path_for(resource_or_scope = nil)
106
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
107
+ change_path = "#{scope}_paranoid_verification_code_path"
108
+ send(change_path)
109
+ end
101
110
 
102
- protected
111
+ protected
103
112
 
104
- # allow to overwrite for some special handlings
105
- def ignore_password_expire?
106
- false
107
- end
113
+ # allow to overwrite for some special handlings
114
+ def ignore_password_expire?
115
+ false
116
+ end
108
117
  end
109
118
  end
110
119
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @note This happens after
4
+ # {DeviseSecurity::Controller::Helpers#handle_password_change}
3
5
  Warden::Manager.after_authentication do |record, warden, options|
4
6
  if record.respond_to?(:need_change_password?)
5
7
  warden.session(options[:scope])['password_expired'] = record.need_change_password?
@@ -1,26 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # After each sign in, update unique_session_id.
4
- # This is only triggered when the user is explicitly set (with set_user)
5
- # and on authentication. Retrieving the user from session (:fetch) does
6
- # not trigger it.
3
+ # After each sign in, update unique_session_id. This is only triggered when the
4
+ # user is explicitly set (with set_user) and on authentication. Retrieving the
5
+ # user from session (:fetch) does not trigger it.
7
6
  Warden::Manager.after_set_user except: :fetch do |record, warden, options|
8
- if record.respond_to?(:update_unique_session_id!) && warden.authenticated?(options[:scope])
7
+ if record.devise_modules.include?(:session_limitable) &&
8
+ warden.authenticated?(options[:scope]) &&
9
+ !record.skip_session_limitable?
9
10
  unique_session_id = Devise.friendly_token
10
11
  warden.session(options[:scope])['unique_session_id'] = unique_session_id
11
12
  record.update_unique_session_id!(unique_session_id)
12
13
  end
13
14
  end
14
15
 
15
- # Each time a record is fetched from session we check if a new session from another
16
- # browser was opened for the record or not, based on a unique session identifier.
17
- # If so, the old account is logged out and redirected to the sign in page on the next request.
16
+ # Each time a record is fetched from session we check if a new session from
17
+ # another browser was opened for the record or not, based on a unique session
18
+ # identifier. If so, the old account is logged out and redirected to the sign in
19
+ # page on the next request.
18
20
  Warden::Manager.after_set_user only: :fetch do |record, warden, options|
19
21
  scope = options[:scope]
20
22
  env = warden.request.env
21
23
 
22
- if record.respond_to?(:unique_session_id) && warden.authenticated?(scope) && options[:store] != false
23
- if record.unique_session_id != warden.session(scope)['unique_session_id'] && !env['devise.skip_session_limitable']
24
+ if record.devise_modules.include?(:session_limitable) &&
25
+ warden.authenticated?(scope) &&
26
+ options[:store] != false
27
+ if record.unique_session_id != warden.session(scope)['unique_session_id'] &&
28
+ !env['devise.skip_session_limitable'] &&
29
+ !record.skip_session_limitable?
30
+ Rails.logger.warn do
31
+ '[devise-security][session_limitable] session id mismatch: '\
32
+ "expected=#{record.unique_session_id.inspect} "\
33
+ "actual=#{warden.session(scope)['unique_session_id'].inspect}"
34
+ end
24
35
  warden.raw_session.clear
25
36
  warden.logout(scope)
26
37
  throw :warden, scope: scope, message: :session_limited
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "compatibility/#{DEVISE_ORM}"
3
+ require_relative "compatibility/#{DEVISE_ORM}_patch"
4
4
 
5
5
  module Devise
6
6
  module Models
@@ -9,7 +9,7 @@ module Devise
9
9
  # and/or older versions of ORMs.
10
10
  module Compatibility
11
11
  extend ActiveSupport::Concern
12
- include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
12
+ include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}Patch".constantize
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,10 @@
1
1
  module Devise
2
2
  module Models
3
3
  module Compatibility
4
- module ActiveRecord
4
+
5
+ class NotPersistedError < ActiveRecord::ActiveRecordError; end
6
+
7
+ module ActiveRecordPatch
5
8
  extend ActiveSupport::Concern
6
9
  unless Devise.activerecord51?
7
10
  # When the record was saved, was the +encrypted_password+ changed?
@@ -23,6 +26,14 @@ module Devise
23
26
  changed_attributes['encrypted_password'].present?
24
27
  end
25
28
  end
29
+
30
+ # Updates the record with the value and does not trigger validations or callbacks
31
+ # @param name [Symbol] attribute to update
32
+ # @param value [String] value to set
33
+ def update_attribute_without_validatons_or_callbacks(name, value)
34
+ update_column(name, value)
35
+ end
36
+
26
37
  end
27
38
  end
28
39
  end
@@ -1,7 +1,10 @@
1
1
  module Devise
2
2
  module Models
3
3
  module Compatibility
4
- module Mongoid
4
+
5
+ class NotPersistedError < Mongoid::Errors::MongoidError; end
6
+
7
+ module MongoidPatch
5
8
  extend ActiveSupport::Concern
6
9
 
7
10
  # Will saving this record change the +email+ attribute?
@@ -15,6 +18,13 @@ module Devise
15
18
  def will_save_change_to_encrypted_password?
16
19
  changed.include? 'encrypted_password'
17
20
  end
21
+
22
+ # Updates the document with the value and does not trigger validations or callbacks
23
+ # @param name [Symbol] attribute to update
24
+ # @param value [String] value to set
25
+ def update_attribute_without_validatons_or_callbacks(name, value)
26
+ set(Hash[*[name, value]])
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -13,7 +13,7 @@ class OldPassword
13
13
 
14
14
  field :password_archivable_id, type: String
15
15
  validates_presence_of :password_archivable_id
16
- index({ password_archivable_type: 1, password_archivable_id: 1 }, name: :index_password_archivable)
16
+ index({ password_archivable_type: 1, password_archivable_id: 1 }, name: 'index_password_archivable')
17
17
 
18
18
  include Mongoid::Timestamps
19
19
 
@@ -92,7 +92,11 @@ module Devise::Models
92
92
  # Update +password_changed_at+ for new records and changed passwords.
93
93
  # @note called as a +before_save+ hook
94
94
  def update_password_changed
95
- return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
95
+ if defined?(will_save_change_to_attribute?)
96
+ return unless (new_record? || will_save_change_to_encrypted_password?) && !will_save_change_to_password_changed_at?
97
+ else
98
+ return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
99
+ end
96
100
 
97
101
  self.password_changed_at = Time.zone.now
98
102
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'compatibility'
3
4
  require 'devise-security/hooks/session_limitable'
4
5
 
5
6
  module Devise
@@ -11,13 +12,27 @@ module Devise
11
12
  # someone used his credentials to sign in.
12
13
  module SessionLimitable
13
14
  extend ActiveSupport::Concern
15
+ include Devise::Models::Compatibility
14
16
 
17
+ # Update the unique_session_id on the model. This will be checked in
18
+ # the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
19
+ # @param unique_session_id [String]
20
+ # @return [void]
21
+ # @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
15
22
  def update_unique_session_id!(unique_session_id)
16
- self.unique_session_id = unique_session_id
23
+ raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
17
24
 
18
- save(validate: false)
25
+ update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
26
+ Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" }
27
+ end
19
28
  end
20
29
 
30
+ # Should session_limitable be skipped for this instance?
31
+ # @return [Boolean]
32
+ # @return [false] by default. This can be overridden by application logic as necessary.
33
+ def skip_session_limitable?
34
+ false
35
+ end
21
36
  end
22
37
  end
23
38
  end
@@ -29,7 +29,7 @@ module DeviseSecurity
29
29
  # create_table :old_passwords do
30
30
  # t.password_archivable
31
31
  # end
32
- # add_index :old_passwords, [:password_archivable_type, :password_archivable_id], name: :index_password_archivable
32
+ # add_index :old_passwords, [:password_archivable_type, :password_archivable_id], name: 'index_password_archivable'
33
33
  #
34
34
  def password_archivable
35
35
  apply_devise_schema :encrypted_password, String, limit: 128, null: false
@@ -3,17 +3,19 @@
3
3
  # Password complexity validator
4
4
  # Options:
5
5
  # - digit: minimum number of digits in the validated string
6
+ # - digits: minimum number of digits in the validated string
6
7
  # - lower: minimum number of lower-case letters in the validated string
7
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
8
10
  # - upper: minimum number of upper-case letters in the validated string
9
11
  class DeviseSecurity::PasswordComplexityValidator < ActiveModel::EachValidator
10
12
  PATTERNS = {
11
13
  digit: /\p{Digit}/,
12
14
  digits: /\p{Digit}/,
13
15
  lower: /\p{Lower}/,
14
- upper: /\p{Upper}/,
15
16
  symbol: /\p{Punct}|\p{S}/,
16
- symbols: /\p{Punct}|\p{S}/
17
+ symbols: /\p{Punct}|\p{S}/,
18
+ upper: /\p{Upper}/
17
19
  }.freeze
18
20
 
19
21
  def validate_each(record, attribute, value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.14.0.rc1'
4
+ VERSION = '0.15.0'
5
5
  end
@@ -3,8 +3,8 @@
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
7
- LOCALES = %w[en es de fr it ja tr].freeze
6
+ class InstallGenerator < Rails::Generators::Base
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
9
  source_root File.expand_path('../../templates', __FILE__)
10
10
  desc 'Install the devise security extension'
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class Devise::PasswordExpiredControllerTest < 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
+ password_changed_at: 4.months.ago,
16
+ confirmed_at: 5.months.ago
17
+ )
18
+ assert @user.valid?
19
+ assert @user.need_change_password?
20
+
21
+ sign_in(@user)
22
+ end
23
+
24
+ test 'should render show' do
25
+ get :show
26
+ assert_includes @response.body, 'Renew your password'
27
+ end
28
+
29
+ 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
49
+ assert_redirected_to root_path
50
+ assert_equal response.content_type, 'text/html'
51
+ end
52
+
53
+ 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
73
+ assert_response :success
74
+ assert_template :show
75
+ assert_equal response.content_type, 'text/html'
76
+ end
77
+
78
+ 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
108
+ end
109
+
110
+ 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
140
+ end
141
+ end