quo_vadis 2.0.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +59 -33
  4. data/app/controllers/quo_vadis/confirmations_controller.rb +47 -1
  5. data/app/controllers/quo_vadis/password_resets_controller.rb +11 -8
  6. data/app/controllers/quo_vadis/passwords_controller.rb +4 -2
  7. data/app/controllers/quo_vadis/recovery_codes_controller.rb +1 -1
  8. data/app/controllers/quo_vadis/sessions_controller.rb +3 -3
  9. data/app/controllers/quo_vadis/totps_controller.rb +1 -1
  10. data/app/models/quo_vadis/account.rb +14 -0
  11. data/app/models/quo_vadis/log.rb +4 -2
  12. data/app/models/quo_vadis/password.rb +16 -0
  13. data/{test/dummy/app → app}/views/quo_vadis/confirmations/edit.html.erb +0 -0
  14. data/app/views/quo_vadis/confirmations/edit_email.html.erb +14 -0
  15. data/app/views/quo_vadis/confirmations/index.html.erb +14 -0
  16. data/{test/dummy/app → app}/views/quo_vadis/confirmations/new.html.erb +0 -0
  17. data/{test/dummy/app → app}/views/quo_vadis/logs/index.html.erb +3 -1
  18. data/{test/dummy/app → app}/views/quo_vadis/mailer/account_confirmation.text.erb +0 -0
  19. data/{test/dummy/app → app}/views/quo_vadis/mailer/email_change_notification.text.erb +0 -0
  20. data/{test/dummy/app → app}/views/quo_vadis/mailer/identifier_change_notification.text.erb +0 -0
  21. data/{test/dummy/app → app}/views/quo_vadis/mailer/password_change_notification.text.erb +0 -0
  22. data/{test/dummy/app → app}/views/quo_vadis/mailer/password_reset_notification.text.erb +0 -0
  23. data/{test/dummy/app → app}/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb +0 -0
  24. data/{test/dummy/app → app}/views/quo_vadis/mailer/reset_password.text.erb +0 -0
  25. data/{test/dummy/app → app}/views/quo_vadis/mailer/totp_reuse_notification.text.erb +0 -0
  26. data/{test/dummy/app → app}/views/quo_vadis/mailer/totp_setup_notification.text.erb +0 -0
  27. data/{test/dummy/app → app}/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb +0 -0
  28. data/{test/dummy/app → app}/views/quo_vadis/password_resets/edit.html.erb +1 -9
  29. data/{test/dummy/app → app}/views/quo_vadis/password_resets/index.html.erb +0 -0
  30. data/{test/dummy/app → app}/views/quo_vadis/password_resets/new.html.erb +0 -0
  31. data/{test/dummy/app → app}/views/quo_vadis/passwords/edit.html.erb +1 -9
  32. data/{test/dummy/app → app}/views/quo_vadis/recovery_codes/challenge.html.erb +0 -0
  33. data/{test/dummy/app → app}/views/quo_vadis/recovery_codes/index.html.erb +0 -0
  34. data/{test/dummy/app → app}/views/quo_vadis/sessions/index.html.erb +0 -0
  35. data/{test/dummy/app → app}/views/quo_vadis/sessions/new.html.erb +0 -0
  36. data/{test/dummy/app → app}/views/quo_vadis/totps/challenge.html.erb +0 -0
  37. data/{test/dummy/app → app}/views/quo_vadis/totps/new.html.erb +0 -0
  38. data/{test/dummy/app → app}/views/quo_vadis/twofas/show.html.erb +0 -0
  39. data/config/locales/quo_vadis.en.yml +30 -1
  40. data/config/routes.rb +11 -5
  41. data/lib/generators/quo_vadis/install_generator.rb +1 -1
  42. data/lib/quo_vadis/controller.rb +3 -3
  43. data/lib/quo_vadis/defaults.rb +1 -1
  44. data/lib/quo_vadis/model.rb +8 -11
  45. data/lib/quo_vadis/version.rb +1 -1
  46. data/lib/quo_vadis.rb +5 -1
  47. data/test/dummy/app/controllers/sign_ups_controller.rb +1 -1
  48. data/test/dummy/config/initializers/quo_vadis.rb +0 -4
  49. data/test/integration/account_confirmation_test.rb +35 -2
  50. data/test/integration/logging_test.rb +14 -5
  51. data/test/integration/password_change_test.rb +16 -10
  52. data/test/integration/password_login_test.rb +14 -2
  53. data/test/integration/password_reset_test.rb +6 -6
  54. data/test/integration/totps_test.rb +1 -1
  55. data/test/models/account_test.rb +16 -0
  56. data/test/models/password_test.rb +16 -0
  57. data/test/models/recovery_code_test.rb +7 -1
  58. data/test/models/token_test.rb +1 -1
  59. metadata +29 -28
  60. data/test/dummy/app/views/quo_vadis/confirmations/index.html.erb +0 -5
@@ -1,14 +1,6 @@
1
1
  <h1>Change password</h1>
2
2
 
3
- <% if @password.errors.any? %>
4
- <ul>
5
- <% @password.errors.full_messages.each do |msg| %>
6
- <li><%= msg %></li>
7
- <% end %>
8
- </ul>
9
- <% end %>
10
-
11
- <%= form_with url: password_path, method: :put do |f| %>
3
+ <%= form_with model: @password, url: password_path, method: :put do |f| %>
12
4
  <p>
13
5
  <%= f.label :password %>
14
6
  <%= f.password_field :password, autocomplete: 'current-password' %>
@@ -31,7 +31,6 @@ en:
31
31
  other: You have %{count} recovery codes left.
32
32
  2fa:
33
33
  invalidated: You have invalidated your 2FA credentials and recovery codes.
34
-
35
34
  mailer:
36
35
  password_reset:
37
36
  subject: Change your password
@@ -46,6 +45,36 @@ en:
46
45
  totp_reuse: Your two-factor authentication code was reused just now
47
46
  twofa_deactivated: Two-factor authentication was deactivated just now
48
47
  recovery_codes_generation: Recovery codes have been generated for your account
48
+ log:
49
+ action:
50
+ login:
51
+ success: Logged in
52
+ failure: Failed login attempt (incorrect password)
53
+ unknown: Failed login attempt (unknown identifier)
54
+ totp:
55
+ setup: TOTP set up for 2FA
56
+ success: Authenticated via TOTP
57
+ failure: Failed authentication attempt via TOTP
58
+ reuse: Failed attempt to reuse TOTP code
59
+ recovery_code:
60
+ success: Authenticated via 2FA recovery code
61
+ failure: Failed authentication attempt via 2FA recovery code
62
+ generate: Generated new 2FA recovery codes
63
+ 2fa:
64
+ deactivated: Deactivated 2FA
65
+ identifier:
66
+ change: Changed identifier
67
+ email:
68
+ change: Changed email address
69
+ password:
70
+ change: Changed password
71
+ reset: Reset password
72
+ account:
73
+ confirmation: Confirmed account
74
+ logout:
75
+ self: Logged out
76
+ other: Logged out session remotely
77
+ revoke: Revoked access
49
78
  activerecord:
50
79
  errors:
51
80
  models:
data/config/routes.rb CHANGED
@@ -12,12 +12,18 @@ QuoVadis::Engine.routes.draw do
12
12
  resource :password, only: [:edit, :update]
13
13
 
14
14
  resources :password_resets, only: [:new, :create, :index]
15
- get '/pwd-reset/:token', to: 'password_resets#edit', as: 'edit_password_reset'
16
- put '/pwd-reset/:token', to: 'password_resets#update', as: 'password_reset'
15
+ get '/pwd-reset/:token', to: 'password_resets#edit', as: 'password_reset'
16
+ put '/pwd-reset/:token', to: 'password_resets#update'
17
17
 
18
- resources :confirmations, only: [:new, :create, :index]
19
- get '/confirm/:token', to: 'confirmations#edit', as: 'edit_confirmation'
20
- put '/confirm/:token', to: 'confirmations#update', as: 'confirmation'
18
+ resources :confirmations, only: [:new, :create, :index] do
19
+ collection do
20
+ get :edit_email
21
+ put :update_email
22
+ post :resend
23
+ end
24
+ end
25
+ get '/confirm/:token', to: 'confirmations#edit', as: 'confirmation'
26
+ put '/confirm/:token', to: 'confirmations#update'
21
27
 
22
28
  resources :totps, only: [:new, :create] do
23
29
  collection do
@@ -1,6 +1,6 @@
1
1
  module QuoVadis
2
2
  class InstallGenerator < Rails::Generators::Base
3
- source_root Pathname.new(__dir__) / '..' / '..' / '..' / 'test' / 'dummy' / 'app' / 'views' / 'quo_vadis'
3
+ source_root Pathname.new(__dir__) / '..' / '..' / '..' / 'app' / 'views' / 'quo_vadis'
4
4
 
5
5
  desc "Copy QuoVadis' views into your app."
6
6
  def copy_views
@@ -17,9 +17,8 @@ module QuoVadis
17
17
 
18
18
  def require_password_authentication
19
19
  return if logged_in?
20
- flash[:notice] = QuoVadis.translate 'flash.require_authentication'
21
20
  session[:qv_bookmark] = request.original_fullpath
22
- redirect_to quo_vadis.login_path
21
+ redirect_to quo_vadis.login_path, notice: QuoVadis.translate('flash.require_authentication')
23
22
  end
24
23
  alias_method :require_authentication, :require_password_authentication
25
24
 
@@ -88,7 +87,8 @@ module QuoVadis
88
87
 
89
88
  def request_confirmation(model)
90
89
  token = QuoVadis::AccountConfirmationToken.generate model.qv_account
91
- QuoVadis.deliver :account_confirmation, email: model.email, url: quo_vadis.edit_confirmation_url(token)
90
+ QuoVadis.deliver :account_confirmation, email: model.email, url: quo_vadis.confirmation_url(token)
91
+ session[:account_pending_confirmation] = model.qv_account.id
92
92
 
93
93
  flash[:notice] = QuoVadis.translate 'flash.confirmation.create'
94
94
  end
@@ -3,7 +3,7 @@ require 'active_support/core_ext'
3
3
  QuoVadis.configure do
4
4
  password_minimum_length 12
5
5
  mask_ips false
6
- cookie_name '__Host-qv'
6
+ cookie_name (Rails.env.production? ? '__Host-qv' : 'qv')
7
7
  session_lifetime :session
8
8
  session_lifetime_extend_to_end_of_day false
9
9
  session_idle_timeout :lifetime
@@ -14,11 +14,9 @@ module QuoVadis
14
14
 
15
15
  has_one :qv_account, as: :model, class_name: 'QuoVadis::Account', dependent: :destroy, autosave: true
16
16
 
17
- before_validation :qv_build_account_and_password, on: :create
18
- before_validation :qv_copy_identifier_to_account
17
+ before_validation :qv_copy_identifier_to_account, if: Proc.new { |m| m.qv_account }
19
18
 
20
- # Enable a new-user form to set a password.
21
- validate :qv_copy_password_errors, on: :create
19
+ validate :qv_copy_password_errors, if: Proc.new { |m| m.qv_account&.password }
22
20
 
23
21
  unless validators_on(identifier).any? { |v| ActiveRecord::Validations::UniquenessValidator === v }
24
22
  raise NotImplementedError, <<~END
@@ -44,24 +42,23 @@ module QuoVadis
44
42
 
45
43
  def password=(val)
46
44
  @password = val
47
- self.qv_account ||= build_qv_account
45
+ build_qv_account unless qv_account
48
46
  raise PasswordExistsError if qv_account.password&.persisted?
49
47
  (qv_account.password || qv_account.build_password).password = val
50
48
  end
51
49
 
52
50
  def password_confirmation=(val)
53
51
  @password_confirmation = val
54
- self.qv_account ||= build_qv_account
52
+ build_qv_account unless qv_account
55
53
  (qv_account.password || qv_account.build_password).password_confirmation = val
56
54
  end
57
55
 
58
- private
59
-
60
- def qv_build_account_and_password
61
- self.qv_account ||= build_qv_account
62
- qv_account.password || qv_account.build_password
56
+ def revoke_authentication_credentials
57
+ qv_account.revoke
63
58
  end
64
59
 
60
+ private
61
+
65
62
  def qv_copy_password_errors
66
63
  qv_account.password.valid? # force qv_account.password to validate
67
64
  qv_account.password.errors[:password ].each { |message| errors.add :password, message }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QuoVadis
4
- VERSION = '2.0.1'
4
+ VERSION = '2.1.2'
5
5
  end
data/lib/quo_vadis.rb CHANGED
@@ -45,8 +45,12 @@ module QuoVadis
45
45
  end
46
46
 
47
47
  def find_account_by_identifier_in_params(params)
48
+ Account.find_by identifier: identifier_value_in_params(params)
49
+ end
50
+
51
+ def identifier_value_in_params(params)
48
52
  identifier = detect_identifier params.keys
49
- Account.find_by identifier: params[identifier]
53
+ params[identifier]
50
54
  end
51
55
 
52
56
  # model - string class name, e.g. 'User'
@@ -13,7 +13,7 @@ class SignUpsController < ApplicationController
13
13
  if @user.save
14
14
  if QuoVadis.accounts_require_confirmation
15
15
  request_confirmation @user
16
- redirect_to sign_up_path(@user)
16
+ redirect_to quo_vadis.confirmations_path
17
17
  else
18
18
  redirect_to articles_path
19
19
  end
@@ -1,7 +1,3 @@
1
1
  QuoVadis.configure do
2
- # Cannot use the __Host- prefix in a non-SSL environment.
3
- # https://tools.ietf.org/html/draft-west-cookie-prefixes-05#section-3.2
4
- cookie_name 'qv'
5
-
6
2
  session_lifetime 1.week
7
3
  end
@@ -6,6 +6,10 @@ class AccountConfirmationTest < IntegrationTest
6
6
  QuoVadis.accounts_require_confirmation true
7
7
  end
8
8
 
9
+ teardown do
10
+ QuoVadis.accounts_require_confirmation false
11
+ end
12
+
9
13
 
10
14
  test 'new signup requiring confirmation' do
11
15
  assert_emails 1 do
@@ -36,8 +40,37 @@ class AccountConfirmationTest < IntegrationTest
36
40
  end
37
41
 
38
42
 
43
+ test 'new signup updates email' do
44
+ assert_emails 1 do
45
+ post sign_ups_path(user: {name: 'Bob', email: 'bob@example.com', password: '123456789abc'})
46
+ end
47
+
48
+ get quo_vadis.edit_email_confirmations_path
49
+ assert_response :success
50
+
51
+ # First email: changed-email notifier sent to original address
52
+ # Second email: confirmation email sent to new address
53
+ assert_emails 2 do
54
+ put quo_vadis.update_email_confirmations_path(email: 'bobby@example.com')
55
+ end
56
+ assert_equal ['bobby@example.com'], ActionMailer::Base.deliveries.last.to
57
+ assert_redirected_to quo_vadis.confirmations_path
58
+ end
59
+
60
+
61
+ test 'resend confirmation email in same session' do
62
+ assert_emails 1 do
63
+ post sign_ups_path(user: {name: 'Bob', email: 'bob@example.com', password: '123456789abc'})
64
+ end
65
+
66
+ assert_emails 1 do
67
+ post quo_vadis.resend_confirmations_path
68
+ end
69
+ end
70
+
71
+
39
72
  test 'resend confirmation email: valid identifier' do
40
- user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
73
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
41
74
 
42
75
  get quo_vadis.new_confirmation_path
43
76
  assert_response :success
@@ -91,7 +124,7 @@ class AccountConfirmationTest < IntegrationTest
91
124
 
92
125
 
93
126
  test 'accounts requiring confirmation cannot log in' do
94
- user = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
127
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
95
128
  post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
96
129
  assert_redirected_to quo_vadis.new_confirmation_path
97
130
  assert_equal 'Please confirm your account first.', flash[:notice]
@@ -15,13 +15,13 @@ class LoggingTest < IntegrationTest
15
15
  assert_response :success
16
16
 
17
17
  assert_select 'tbody tr' do
18
- assert_select 'td', 'password.change'
18
+ assert_select 'td', 'Changed password'
19
19
  assert_select 'td', '1.2.3.4'
20
20
  assert_select 'td', 'foo: bar, baz: qux'
21
21
  end
22
22
 
23
23
  assert_select 'tbody tr' do
24
- assert_select 'td', 'login.success'
24
+ assert_select 'td', 'Logged in'
25
25
  assert_select 'td', '127.0.0.1'
26
26
  assert_select 'td', ''
27
27
  end
@@ -43,7 +43,7 @@ class LoggingTest < IntegrationTest
43
43
 
44
44
 
45
45
  test 'login.unknown' do
46
- assert_log QuoVadis::Log::LOGIN_UNKNOWN, {}, nil do
46
+ assert_log QuoVadis::Log::LOGIN_UNKNOWN, {'identifier' => 'wrong'}, nil do
47
47
  post quo_vadis.login_path(email: 'wrong', password: 'wrong')
48
48
  end
49
49
  end
@@ -157,7 +157,7 @@ class LoggingTest < IntegrationTest
157
157
  test 'password.change' do
158
158
  login
159
159
  assert_log QuoVadis::Log::PASSWORD_CHANGE do
160
- put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
160
+ put quo_vadis.password_path(password: {password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx'})
161
161
  end
162
162
  end
163
163
 
@@ -165,7 +165,7 @@ class LoggingTest < IntegrationTest
165
165
  test 'password.reset' do
166
166
  assert_difference 'QuoVadis::Log.count', 2 do
167
167
  token = QuoVadis::PasswordResetToken.generate @account
168
- put quo_vadis.password_reset_path(token, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
168
+ put quo_vadis.password_reset_path(token, password: {password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx'})
169
169
  end
170
170
  assert_equal QuoVadis::Log::PASSWORD_RESET, QuoVadis::Log.first.action
171
171
  assert_equal QuoVadis::Log::LOGIN_SUCCESS, log.action
@@ -201,6 +201,15 @@ class LoggingTest < IntegrationTest
201
201
  end
202
202
 
203
203
 
204
+ test 'revoke' do
205
+ login_new_session
206
+ assert_log QuoVadis::Log::REVOKE do
207
+ QuoVadis::CurrentRequestDetails.ip = '127.0.0.1' # fake out the IP assertion
208
+ @account.revoke
209
+ end
210
+ end
211
+
212
+
204
213
  private
205
214
 
206
215
  def assert_log(action, metadata = {}, account = @account, &block)
@@ -18,29 +18,31 @@ class PasswordChangeTest < IntegrationTest
18
18
 
19
19
 
20
20
  test 'incorrect password' do
21
- put quo_vadis.password_path(password: 'x')
22
- assert_response :success
21
+ put quo_vadis.password_path(password: {password: 'x'})
22
+ assert_response :unprocessable_entity
23
23
  assert_equal ['is incorrect'], password_instance.errors[:password]
24
24
  end
25
25
 
26
26
 
27
27
  test 'new password empty' do
28
- put quo_vadis.password_path(password: '123456789abc', new_password: '')
29
- assert_response :success
28
+ put quo_vadis.password_path(password: {password: '123456789abc', new_password: ''})
29
+ assert_response :unprocessable_entity
30
30
  assert_equal ["can't be blank"], password_instance.errors[:new_password]
31
31
  end
32
32
 
33
33
 
34
34
  test 'new password too short' do
35
- put quo_vadis.password_path(password: '123456789abc', new_password: 'x')
36
- assert_response :success
35
+ put quo_vadis.password_path(password: {password: '123456789abc', new_password: 'x'})
36
+ assert_response :unprocessable_entity
37
37
  assert_equal ["is too short (minimum is #{QuoVadis.password_minimum_length} characters)"], password_instance.errors[:new_password]
38
38
  end
39
39
 
40
40
 
41
41
  test 'new password confirmation does not match' do
42
- put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'y')
43
- assert_response :success
42
+ put quo_vadis.password_path(password: {
43
+ password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'y'
44
+ })
45
+ assert_response :unprocessable_entity
44
46
  assert_equal ["doesn't match Password"], password_instance.errors[:new_password_confirmation]
45
47
  end
46
48
 
@@ -48,7 +50,9 @@ class PasswordChangeTest < IntegrationTest
48
50
  test 'success' do
49
51
  assert_emails 1 do
50
52
  assert_session_replaced do
51
- put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
53
+ put quo_vadis.password_path(password: {
54
+ password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx'
55
+ })
52
56
  assert_response :redirect
53
57
  assert_equal 'Your password has been changed.', flash[:notice]
54
58
  end
@@ -60,7 +64,9 @@ class PasswordChangeTest < IntegrationTest
60
64
  desktop = session_login
61
65
  phone = session_login
62
66
 
63
- desktop.put quo_vadis.password_path(password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx')
67
+ desktop.put quo_vadis.password_path(password: {
68
+ password: '123456789abc', new_password: 'xxxxxxxxxxxx', new_password_confirmation: 'xxxxxxxxxxxx'
69
+ })
64
70
  desktop.follow_redirect!
65
71
  assert desktop.controller.logged_in?
66
72
 
@@ -21,6 +21,7 @@ class PasswordLoginTest < IntegrationTest
21
21
 
22
22
  test 'successful login redirects to original path' do
23
23
  get also_secret_articles_path
24
+ refute_nil session[:qv_bookmark]
24
25
 
25
26
  User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
26
27
  post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
@@ -30,11 +31,22 @@ class PasswordLoginTest < IntegrationTest
30
31
  end
31
32
 
32
33
 
34
+ test 'successful login does not redirect to login path' do
35
+ get quo_vadis.login_path
36
+ assert_nil session[:qv_bookmark]
37
+
38
+ User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
39
+ post quo_vadis.login_path(email: 'bob@example.com', password: '123456789abc')
40
+
41
+ assert_redirected_to after_login_path
42
+ end
43
+
44
+
33
45
  test 'failed login' do
34
46
  User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
35
47
  post quo_vadis.login_path(email: 'bob@example.com', password: 'wrong')
36
48
 
37
- assert_response :success
49
+ assert_response :unprocessable_entity
38
50
  assert_equal quo_vadis.login_path, path
39
51
  end
40
52
 
@@ -42,7 +54,7 @@ class PasswordLoginTest < IntegrationTest
42
54
  test 'unknown login' do
43
55
  post quo_vadis.login_path(email: 'bob@example.com', password: 'wrong')
44
56
 
45
- assert_response :success
57
+ assert_response :unprocessable_entity
46
58
  assert_equal quo_vadis.login_path, path
47
59
  end
48
60
 
@@ -15,7 +15,7 @@ class PasswordResetTest < IntegrationTest
15
15
 
16
16
  test 'unknown identifier' do
17
17
  post quo_vadis.password_resets_path(email: 'foo@example.com')
18
- assert_response :success
18
+ assert_redirected_to quo_vadis.password_resets_path
19
19
  assert_equal 'A link to change your password has been emailed to you.', flash[:notice]
20
20
  end
21
21
 
@@ -53,10 +53,10 @@ class PasswordResetTest < IntegrationTest
53
53
  assert_emails 1 do
54
54
  post quo_vadis.password_resets_path(email: 'bob@example.com')
55
55
  end
56
- put quo_vadis.password_reset_path(extract_token_from_email, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
56
+ put quo_vadis.password_reset_path(extract_token_from_email, password: {password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx'})
57
57
  assert controller.logged_in?
58
58
 
59
- get quo_vadis.edit_password_reset_url(extract_token_from_email)
59
+ get quo_vadis.password_reset_url(extract_token_from_email)
60
60
  assert_redirected_to quo_vadis.new_password_reset_path
61
61
  assert_equal 'Either the link has expired or you have already reset your password.', flash[:alert]
62
62
  end
@@ -70,11 +70,11 @@ class PasswordResetTest < IntegrationTest
70
70
  end
71
71
 
72
72
  assert_no_difference 'QuoVadis::Session.count' do
73
- put quo_vadis.password_reset_path(extract_token_from_email, password: '', password_confirmation: '')
73
+ put quo_vadis.password_reset_path(extract_token_from_email, password: {password: '', password_confirmation: ''})
74
74
  end
75
75
 
76
76
  assert_equal digest, @user.qv_account.password.reload.password_digest
77
- assert_response :success
77
+ assert_response :unprocessable_entity
78
78
  assert_equal quo_vadis.password_reset_path(extract_token_from_email), path
79
79
  end
80
80
 
@@ -95,7 +95,7 @@ class PasswordResetTest < IntegrationTest
95
95
  end
96
96
 
97
97
  assert_difference 'QuoVadis::Session.count', (- 2 + 1) do
98
- put quo_vadis.password_reset_path(extract_token_from_email, password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx')
98
+ put quo_vadis.password_reset_path(extract_token_from_email, password: {password: 'xxxxxxxxxxxx', password_confirmation: 'xxxxxxxxxxxx'})
99
99
  end
100
100
 
101
101
  assert controller.logged_in?
@@ -74,7 +74,7 @@ class TotpsTest < IntegrationTest
74
74
 
75
75
  post quo_vadis.authenticate_totps_path(totp: '123456')
76
76
  refute QuoVadis::Session.last.second_factor_authenticated?
77
- assert_response :success
77
+ assert_response :unprocessable_entity
78
78
  assert_equal 'Sorry, the code was incorrect. Please check your system clock is correct and try again.', flash[:alert]
79
79
  end
80
80
 
@@ -31,4 +31,20 @@ class AccountTest < ActiveSupport::TestCase
31
31
  end
32
32
  end
33
33
 
34
+
35
+ test 'revoke' do
36
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
37
+ account = u.qv_account
38
+ account.create_totp last_used_at: 1.minute.ago
39
+ account.generate_recovery_codes
40
+
41
+ u.revoke_authentication_credentials
42
+ account.reload
43
+
44
+ assert_nil account.password
45
+ assert_nil account.totp
46
+ assert_empty account.recovery_codes
47
+ assert_empty account.sessions
48
+ end
49
+
34
50
  end
@@ -46,6 +46,9 @@ class PasswordTest < ActiveSupport::TestCase
46
46
 
47
47
  test 'model passes through password to quo_vadis' do
48
48
  user = User.new name: 'bob', email: 'bob@example.com'
49
+ assert user.valid?
50
+
51
+ user = User.new name: 'bob', email: 'bob@example.com', password: ''
49
52
  refute user.valid?
50
53
  refute_empty user.errors[:password]
51
54
 
@@ -117,6 +120,19 @@ class PasswordTest < ActiveSupport::TestCase
117
120
  end
118
121
 
119
122
 
123
+ test 'passwords can only be updated via #change and #reset' do
124
+ user = User.create! name: 'bob', email: 'bob@example.com', password: VALID_PASSWORD
125
+ pw = user.qv_account.password
126
+
127
+ refute pw.update password: 'secretsecret'
128
+ assert_equal ["must be updated via #change or #reset"], pw.errors[:password]
129
+
130
+ pw.password = VALID_PASSWORD
131
+ refute pw.save
132
+ assert_equal ["must be updated via #change or #reset"], pw.errors[:password]
133
+ end
134
+
135
+
120
136
  test 'cascade destroy' do
121
137
  user = User.create! name: 'bob', email: 'bob@example.com', password: VALID_PASSWORD
122
138
  assert user.qv_account.persisted?
@@ -42,13 +42,19 @@ class RecoveryCodeTest < ActiveSupport::TestCase
42
42
  test 'generate a fresh set of codes' do
43
43
  account = @user.qv_account
44
44
  codes = []
45
- assert_difference 'QuoVadis::RecoveryCode.count', 5 do
45
+ assert_difference 'QuoVadis::RecoveryCode.count', (-1 + 5) do
46
46
  codes = account.generate_recovery_codes
47
47
  end
48
48
  assert_equal 5, codes.length
49
+ assert_equal 5, account.recovery_codes.count
49
50
  codes.each do |code|
50
51
  assert_instance_of String, code
51
52
  end
53
+
54
+ new_codes = account.generate_recovery_codes
55
+ assert_equal 5, new_codes.length
56
+ assert_equal 5, account.recovery_codes.count
57
+ refute_equal new_codes, codes
52
58
  end
53
59
 
54
60
  end
@@ -52,7 +52,7 @@ class TokenTest < ActiveSupport::TestCase
52
52
 
53
53
  test 'password reset already done' do
54
54
  token = QuoVadis::PasswordResetToken.generate @account
55
- @account.password.update password: 'secretsecret'
55
+ @account.password.reset 'secretsecret', 'secretsecret'
56
56
  assert_nil QuoVadis::PasswordResetToken.find_account(token)
57
57
  end
58
58