quo_vadis 2.0.2 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +37 -30
  4. data/app/controllers/quo_vadis/password_resets_controller.rb +11 -8
  5. data/app/controllers/quo_vadis/passwords_controller.rb +4 -2
  6. data/app/controllers/quo_vadis/recovery_codes_controller.rb +1 -1
  7. data/app/controllers/quo_vadis/sessions_controller.rb +2 -2
  8. data/app/controllers/quo_vadis/totps_controller.rb +1 -1
  9. data/app/mailers/quo_vadis/mailer.rb +16 -16
  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/{test/dummy/app → app}/views/quo_vadis/confirmations/edit_email.html.erb +0 -0
  15. data/{test/dummy/app → app}/views/quo_vadis/confirmations/index.html.erb +0 -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 +4 -4
  41. data/lib/generators/quo_vadis/install_generator.rb +1 -1
  42. data/lib/quo_vadis/controller.rb +2 -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 +6 -4
  47. data/test/dummy/config/initializers/quo_vadis.rb +0 -4
  48. data/test/integration/logging_test.rb +13 -4
  49. data/test/integration/password_change_test.rb +16 -10
  50. data/test/integration/password_login_test.rb +14 -2
  51. data/test/integration/password_reset_test.rb +6 -6
  52. data/test/integration/totps_test.rb +1 -1
  53. data/test/mailers/mailer_test.rb +49 -32
  54. data/test/models/account_test.rb +24 -2
  55. data/test/models/model_test.rb +4 -1
  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 +28 -28
@@ -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,8 +12,8 @@ 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
18
  resources :confirmations, only: [:new, :create, :index] do
19
19
  collection do
@@ -22,8 +22,8 @@ QuoVadis::Engine.routes.draw do
22
22
  post :resend
23
23
  end
24
24
  end
25
- get '/confirm/:token', to: 'confirmations#edit', as: 'edit_confirmation'
26
- put '/confirm/:token', to: 'confirmations#update', as: 'confirmation'
25
+ get '/confirm/:token', to: 'confirmations#edit', as: 'confirmation'
26
+ put '/confirm/:token', to: 'confirmations#update'
27
27
 
28
28
  resources :totps, only: [:new, :create] do
29
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,7 @@ 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)}
92
91
  session[:account_pending_confirmation] = model.qv_account.id
93
92
 
94
93
  flash[:notice] = QuoVadis.translate 'flash.confirmation.create'
@@ -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.2'
4
+ VERSION = '2.1.3'
5
5
  end
data/lib/quo_vadis.rb CHANGED
@@ -73,12 +73,14 @@ module QuoVadis
73
73
  end
74
74
 
75
75
  def notify(action, params)
76
- QuoVadis::Mailer.with(params).send(action).deliver_later
76
+ deliver(action, params, later: true)
77
77
  end
78
78
 
79
- def deliver(action, params)
80
- mail = QuoVadis::Mailer.with(params).send(action)
81
- QuoVadis.enqueue_transactional_emails ?
79
+ def deliver(action, params, later: QuoVadis.enqueue_transactional_emails)
80
+ mail = QuoVadis::Mailer
81
+ .with(params.merge(ip: QuoVadis::CurrentRequestDetails.ip, timestamp: Time.now))
82
+ .send(action)
83
+ later ?
82
84
  mail.deliver_later :
83
85
  mail.deliver_now
84
86
  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
@@ -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
@@ -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
 
@@ -44,14 +44,16 @@ class MailerTest < ActionMailer::TestCase
44
44
 
45
45
 
46
46
  test 'email change notification' do
47
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').email_change_notification
47
+ email = QuoVadis::Mailer.with(
48
+ email: 'Foo <foo@example.com>',
49
+ ip: '1.2.3.4',
50
+ timestamp: Time.now
51
+ ).email_change_notification
48
52
 
49
53
  # freeze_time
50
54
 
51
55
  assert_emails 1 do
52
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
53
- email.deliver_now
54
- end
56
+ email.deliver_now
55
57
  end
56
58
 
57
59
  assert_equal ['foo@example.com'], email.to
@@ -62,14 +64,17 @@ class MailerTest < ActionMailer::TestCase
62
64
 
63
65
 
64
66
  test 'identifier change notification' do
65
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>', identifier: 'email').identifier_change_notification
67
+ email = QuoVadis::Mailer.with(
68
+ email: 'Foo <foo@example.com>',
69
+ identifier: 'email',
70
+ ip: '1.2.3.4',
71
+ timestamp: Time.now
72
+ ).identifier_change_notification
66
73
 
67
74
  # freeze_time
68
75
 
69
76
  assert_emails 1 do
70
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
71
- email.deliver_now
72
- end
77
+ email.deliver_now
73
78
  end
74
79
 
75
80
  assert_equal ['foo@example.com'], email.to
@@ -80,14 +85,16 @@ class MailerTest < ActionMailer::TestCase
80
85
 
81
86
 
82
87
  test 'password change notification' do
83
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').password_change_notification
88
+ email = QuoVadis::Mailer.with(
89
+ email: 'Foo <foo@example.com>',
90
+ ip: '1.2.3.4',
91
+ timestamp: Time.now
92
+ ).password_change_notification
84
93
 
85
94
  # freeze_time
86
95
 
87
96
  assert_emails 1 do
88
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
89
- email.deliver_now
90
- end
97
+ email.deliver_now
91
98
  end
92
99
 
93
100
  assert_equal ['foo@example.com'], email.to
@@ -98,14 +105,16 @@ class MailerTest < ActionMailer::TestCase
98
105
 
99
106
 
100
107
  test 'password reset notification' do
101
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').password_reset_notification
108
+ email = QuoVadis::Mailer.with(
109
+ email: 'Foo <foo@example.com>',
110
+ ip: '1.2.3.4',
111
+ timestamp: Time.now
112
+ ).password_reset_notification
102
113
 
103
114
  # freeze_time
104
115
 
105
116
  assert_emails 1 do
106
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
107
- email.deliver_now
108
- end
117
+ email.deliver_now
109
118
  end
110
119
 
111
120
  assert_equal ['foo@example.com'], email.to
@@ -116,14 +125,16 @@ class MailerTest < ActionMailer::TestCase
116
125
 
117
126
 
118
127
  test 'totp setup notification' do
119
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').totp_setup_notification
128
+ email = QuoVadis::Mailer.with(
129
+ email: 'Foo <foo@example.com>',
130
+ ip: '1.2.3.4',
131
+ timestamp: Time.now
132
+ ).totp_setup_notification
120
133
 
121
134
  # freeze_time
122
135
 
123
136
  assert_emails 1 do
124
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
125
- email.deliver_now
126
- end
137
+ email.deliver_now
127
138
  end
128
139
 
129
140
  assert_equal ['foo@example.com'], email.to
@@ -134,14 +145,16 @@ class MailerTest < ActionMailer::TestCase
134
145
 
135
146
 
136
147
  test 'totp reuse notification' do
137
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').totp_reuse_notification
148
+ email = QuoVadis::Mailer.with(
149
+ email: 'Foo <foo@example.com>',
150
+ ip: '1.2.3.4',
151
+ timestamp: Time.now
152
+ ).totp_reuse_notification
138
153
 
139
154
  # freeze_time
140
155
 
141
156
  assert_emails 1 do
142
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
143
- email.deliver_now
144
- end
157
+ email.deliver_now
145
158
  end
146
159
 
147
160
  assert_equal ['foo@example.com'], email.to
@@ -152,14 +165,16 @@ class MailerTest < ActionMailer::TestCase
152
165
 
153
166
 
154
167
  test '2fa deactivated notification' do
155
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').twofa_deactivated_notification
168
+ email = QuoVadis::Mailer.with(
169
+ email: 'Foo <foo@example.com>',
170
+ ip: '1.2.3.4',
171
+ timestamp: Time.now
172
+ ).twofa_deactivated_notification
156
173
 
157
174
  # freeze_time
158
175
 
159
176
  assert_emails 1 do
160
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
161
- email.deliver_now
162
- end
177
+ email.deliver_now
163
178
  end
164
179
 
165
180
  assert_equal ['foo@example.com'], email.to
@@ -170,14 +185,16 @@ class MailerTest < ActionMailer::TestCase
170
185
 
171
186
 
172
187
  test 'recovery codes generation notification' do
173
- email = QuoVadis::Mailer.with(email: 'Foo <foo@example.com>').recovery_codes_generation_notification
188
+ email = QuoVadis::Mailer.with(
189
+ email: 'Foo <foo@example.com>',
190
+ ip: '1.2.3.4',
191
+ timestamp: Time.now
192
+ ).recovery_codes_generation_notification
174
193
 
175
194
  # freeze_time
176
195
 
177
196
  assert_emails 1 do
178
- QuoVadis::CurrentRequestDetails.set(ip: '1.2.3.4') do
179
- email.deliver_now
180
- end
197
+ email.deliver_now
181
198
  end
182
199
 
183
200
  assert_equal ['foo@example.com'], email.to
@@ -13,8 +13,11 @@ class AccountTest < ActiveSupport::TestCase
13
13
 
14
14
 
15
15
  test 'notifies on identifier change when notifier is not email' do
16
+ freeze_time
16
17
  p = Person.create! username: 'bob', email: 'bob@example.com', password: 'secretsecret'
17
- assert_enqueued_email_with QuoVadis::Mailer, :identifier_change_notification, args: {email: 'bob@example.com', identifier: 'username'} do
18
+ assert_enqueued_email_with QuoVadis::Mailer,
19
+ :identifier_change_notification,
20
+ args: {email: 'bob@example.com', identifier: 'username', ip: nil, timestamp: Time.now} do
18
21
  assert_enqueued_emails 1 do
19
22
  p.update username: 'robert@example.com'
20
23
  end
@@ -23,12 +26,31 @@ class AccountTest < ActiveSupport::TestCase
23
26
 
24
27
 
25
28
  test 'does not notify on identifier change when notifier is email' do
29
+ freeze_time
26
30
  u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
27
- assert_enqueued_email_with QuoVadis::Mailer, :email_change_notification, args: {email: 'bob@example.com'} do
31
+ assert_enqueued_email_with QuoVadis::Mailer,
32
+ :email_change_notification,
33
+ args: {email: 'bob@example.com', ip: nil, timestamp: Time.now} do
28
34
  assert_enqueued_emails 1 do
29
35
  u.update email: 'robert@example.com'
30
36
  end
31
37
  end
32
38
  end
33
39
 
40
+
41
+ test 'revoke' do
42
+ u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
43
+ account = u.qv_account
44
+ account.create_totp last_used_at: 1.minute.ago
45
+ account.generate_recovery_codes
46
+
47
+ u.revoke_authentication_credentials
48
+ account.reload
49
+
50
+ assert_nil account.password
51
+ assert_nil account.totp
52
+ assert_empty account.recovery_codes
53
+ assert_empty account.sessions
54
+ end
55
+
34
56
  end
@@ -58,8 +58,11 @@ class ModelTest < ActiveSupport::TestCase
58
58
 
59
59
 
60
60
  test 'notifies on email change' do
61
+ freeze_time
61
62
  u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
62
- assert_enqueued_email_with QuoVadis::Mailer, :email_change_notification, args: {email: 'bob@example.com'} do
63
+ assert_enqueued_email_with QuoVadis::Mailer,
64
+ :email_change_notification,
65
+ args: {email: 'bob@example.com', ip: nil, timestamp: Time.now} do
63
66
  u.update email: 'robert@example.com'
64
67
  end
65
68
  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?