quo_vadis 2.0.2 → 2.1.3

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 (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?