quo_vadis 1.4.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +11 -8
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +14 -1
  5. data/Gemfile.lock +178 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +435 -127
  8. data/Rakefile +15 -9
  9. data/app/controllers/quo_vadis/confirmations_controller.rb +56 -0
  10. data/app/controllers/quo_vadis/logs_controller.rb +20 -0
  11. data/app/controllers/quo_vadis/password_resets_controller.rb +65 -0
  12. data/app/controllers/quo_vadis/passwords_controller.rb +26 -0
  13. data/app/controllers/quo_vadis/recovery_codes_controller.rb +54 -0
  14. data/app/controllers/quo_vadis/sessions_controller.rb +50 -132
  15. data/app/controllers/quo_vadis/totps_controller.rb +72 -0
  16. data/app/controllers/quo_vadis/twofas_controller.rb +26 -0
  17. data/app/mailers/quo_vadis/mailer.rb +73 -0
  18. data/app/models/quo_vadis/account.rb +59 -0
  19. data/app/models/quo_vadis/account_confirmation_token.rb +17 -0
  20. data/app/models/quo_vadis/log.rb +57 -0
  21. data/app/models/quo_vadis/password.rb +52 -0
  22. data/app/models/quo_vadis/password_reset_token.rb +17 -0
  23. data/app/models/quo_vadis/recovery_code.rb +26 -0
  24. data/app/models/quo_vadis/session.rb +55 -0
  25. data/app/models/quo_vadis/token.rb +42 -0
  26. data/app/models/quo_vadis/totp.rb +56 -0
  27. data/bin/console +15 -0
  28. data/bin/rails +21 -0
  29. data/bin/setup +8 -0
  30. data/config/locales/quo_vadis.en.yml +50 -23
  31. data/config/routes.rb +40 -12
  32. data/db/migrate/202102150904_setup.rb +48 -0
  33. data/lib/generators/quo_vadis/install_generator.rb +4 -23
  34. data/lib/quo_vadis.rb +100 -98
  35. data/lib/quo_vadis/controller.rb +227 -0
  36. data/lib/quo_vadis/crypt.rb +43 -0
  37. data/lib/quo_vadis/current_request_details.rb +11 -0
  38. data/lib/quo_vadis/defaults.rb +18 -0
  39. data/lib/quo_vadis/encrypted_type.rb +17 -0
  40. data/lib/quo_vadis/engine.rb +9 -11
  41. data/lib/quo_vadis/hmacable.rb +26 -0
  42. data/lib/quo_vadis/ip_masking.rb +31 -0
  43. data/lib/quo_vadis/model.rb +86 -0
  44. data/lib/quo_vadis/version.rb +3 -1
  45. data/quo_vadis.gemspec +18 -25
  46. metadata +46 -246
  47. data/app/controllers/controller_mixin.rb +0 -109
  48. data/app/mailers/quo_vadis/notifier.rb +0 -30
  49. data/app/models/model_mixin.rb +0 -128
  50. data/lib/generators/quo_vadis/templates/migration.rb.erb +0 -18
  51. data/lib/generators/quo_vadis/templates/quo_vadis.rb.erb +0 -96
  52. data/test/dummy/.gitignore +0 -2
  53. data/test/dummy/Rakefile +0 -7
  54. data/test/dummy/app/controllers/application_controller.rb +0 -3
  55. data/test/dummy/app/controllers/articles_controller.rb +0 -20
  56. data/test/dummy/app/controllers/users_controller.rb +0 -17
  57. data/test/dummy/app/helpers/application_helper.rb +0 -2
  58. data/test/dummy/app/helpers/articles_helper.rb +0 -2
  59. data/test/dummy/app/models/article.rb +0 -2
  60. data/test/dummy/app/models/person.rb +0 -3
  61. data/test/dummy/app/models/user.rb +0 -3
  62. data/test/dummy/app/views/articles/index.html.erb +0 -1
  63. data/test/dummy/app/views/articles/new.html.erb +0 -11
  64. data/test/dummy/app/views/layouts/application.html.erb +0 -30
  65. data/test/dummy/app/views/layouts/sessions.html.erb +0 -3
  66. data/test/dummy/app/views/quo_vadis/notifier/change_password.text.erb +0 -9
  67. data/test/dummy/app/views/quo_vadis/notifier/invite.text.erb +0 -8
  68. data/test/dummy/app/views/sessions/edit.html.erb +0 -11
  69. data/test/dummy/app/views/sessions/forgotten.html.erb +0 -13
  70. data/test/dummy/app/views/sessions/invite.html.erb +0 -31
  71. data/test/dummy/app/views/sessions/new.html.erb +0 -15
  72. data/test/dummy/app/views/users/new.html.erb +0 -14
  73. data/test/dummy/config.ru +0 -4
  74. data/test/dummy/config/application.rb +0 -21
  75. data/test/dummy/config/boot.rb +0 -10
  76. data/test/dummy/config/database.yml +0 -22
  77. data/test/dummy/config/environment.rb +0 -5
  78. data/test/dummy/config/environments/development.rb +0 -26
  79. data/test/dummy/config/environments/production.rb +0 -49
  80. data/test/dummy/config/environments/test.rb +0 -37
  81. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  82. data/test/dummy/config/initializers/inflections.rb +0 -10
  83. data/test/dummy/config/initializers/mime_types.rb +0 -5
  84. data/test/dummy/config/initializers/quo_vadis.rb +0 -77
  85. data/test/dummy/config/initializers/rack_patch.rb +0 -16
  86. data/test/dummy/config/initializers/secret_token.rb +0 -7
  87. data/test/dummy/config/initializers/session_store.rb +0 -8
  88. data/test/dummy/config/locales/en.yml +0 -5
  89. data/test/dummy/config/locales/quo_vadis.en.yml +0 -21
  90. data/test/dummy/config/routes.rb +0 -5
  91. data/test/dummy/db/migrate/20110124125037_create_users.rb +0 -13
  92. data/test/dummy/db/migrate/20110124131535_create_articles.rb +0 -14
  93. data/test/dummy/db/migrate/20110127094709_add_authentication_to_users.rb +0 -18
  94. data/test/dummy/db/migrate/20111004112209_create_people.rb +0 -13
  95. data/test/dummy/db/migrate/20111004132342_add_authentication_to_people.rb +0 -18
  96. data/test/dummy/db/schema.rb +0 -33
  97. data/test/dummy/public/404.html +0 -26
  98. data/test/dummy/public/422.html +0 -26
  99. data/test/dummy/public/500.html +0 -26
  100. data/test/dummy/public/favicon.ico +0 -0
  101. data/test/dummy/public/javascripts/application.js +0 -2
  102. data/test/dummy/public/javascripts/controls.js +0 -965
  103. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  104. data/test/dummy/public/javascripts/effects.js +0 -1123
  105. data/test/dummy/public/javascripts/prototype.js +0 -6001
  106. data/test/dummy/public/javascripts/rails.js +0 -175
  107. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  108. data/test/dummy/script/rails +0 -6
  109. data/test/integration/activation_test.rb +0 -108
  110. data/test/integration/authenticate_test.rb +0 -39
  111. data/test/integration/blocked_test.rb +0 -23
  112. data/test/integration/config_test.rb +0 -118
  113. data/test/integration/cookie_test.rb +0 -67
  114. data/test/integration/csrf_test.rb +0 -41
  115. data/test/integration/forgotten_test.rb +0 -93
  116. data/test/integration/helper_test.rb +0 -18
  117. data/test/integration/locale_test.rb +0 -197
  118. data/test/integration/navigation_test.rb +0 -7
  119. data/test/integration/sign_in_person_test.rb +0 -26
  120. data/test/integration/sign_in_test.rb +0 -24
  121. data/test/integration/sign_out_test.rb +0 -20
  122. data/test/integration/sign_up_test.rb +0 -21
  123. data/test/quo_vadis_test.rb +0 -7
  124. data/test/support/integration_case.rb +0 -11
  125. data/test/test_helper.rb +0 -86
  126. data/test/unit/user_test.rb +0 -75
@@ -1,175 +0,0 @@
1
- (function() {
2
- // Technique from Juriy Zaytsev
3
- // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
- function isEventSupported(eventName) {
5
- var el = document.createElement('div');
6
- eventName = 'on' + eventName;
7
- var isSupported = (eventName in el);
8
- if (!isSupported) {
9
- el.setAttribute(eventName, 'return;');
10
- isSupported = typeof el[eventName] == 'function';
11
- }
12
- el = null;
13
- return isSupported;
14
- }
15
-
16
- function isForm(element) {
17
- return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
- }
19
-
20
- function isInput(element) {
21
- if (Object.isElement(element)) {
22
- var name = element.nodeName.toUpperCase()
23
- return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
- }
25
- else return false
26
- }
27
-
28
- var submitBubbles = isEventSupported('submit'),
29
- changeBubbles = isEventSupported('change')
30
-
31
- if (!submitBubbles || !changeBubbles) {
32
- // augment the Event.Handler class to observe custom events when needed
33
- Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
- function(init, element, eventName, selector, callback) {
35
- init(element, eventName, selector, callback)
36
- // is the handler being attached to an element that doesn't support this event?
37
- if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
- (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
- // "submit" => "emulated:submit"
40
- this.eventName = 'emulated:' + this.eventName
41
- }
42
- }
43
- )
44
- }
45
-
46
- if (!submitBubbles) {
47
- // discover forms on the page by observing focus events which always bubble
48
- document.on('focusin', 'form', function(focusEvent, form) {
49
- // special handler for the real "submit" event (one-time operation)
50
- if (!form.retrieve('emulated:submit')) {
51
- form.on('submit', function(submitEvent) {
52
- var emulated = form.fire('emulated:submit', submitEvent, true)
53
- // if custom event received preventDefault, cancel the real one too
54
- if (emulated.returnValue === false) submitEvent.preventDefault()
55
- })
56
- form.store('emulated:submit', true)
57
- }
58
- })
59
- }
60
-
61
- if (!changeBubbles) {
62
- // discover form inputs on the page
63
- document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
- // special handler for real "change" events
65
- if (!input.retrieve('emulated:change')) {
66
- input.on('change', function(changeEvent) {
67
- input.fire('emulated:change', changeEvent, true)
68
- })
69
- input.store('emulated:change', true)
70
- }
71
- })
72
- }
73
-
74
- function handleRemote(element) {
75
- var method, url, params;
76
-
77
- var event = element.fire("ajax:before");
78
- if (event.stopped) return false;
79
-
80
- if (element.tagName.toLowerCase() === 'form') {
81
- method = element.readAttribute('method') || 'post';
82
- url = element.readAttribute('action');
83
- params = element.serialize();
84
- } else {
85
- method = element.readAttribute('data-method') || 'get';
86
- url = element.readAttribute('href');
87
- params = {};
88
- }
89
-
90
- new Ajax.Request(url, {
91
- method: method,
92
- parameters: params,
93
- evalScripts: true,
94
-
95
- onComplete: function(request) { element.fire("ajax:complete", request); },
96
- onSuccess: function(request) { element.fire("ajax:success", request); },
97
- onFailure: function(request) { element.fire("ajax:failure", request); }
98
- });
99
-
100
- element.fire("ajax:after");
101
- }
102
-
103
- function handleMethod(element) {
104
- var method = element.readAttribute('data-method'),
105
- url = element.readAttribute('href'),
106
- csrf_param = $$('meta[name=csrf-param]')[0],
107
- csrf_token = $$('meta[name=csrf-token]')[0];
108
-
109
- var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
- element.parentNode.insert(form);
111
-
112
- if (method !== 'post') {
113
- var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
- form.insert(field);
115
- }
116
-
117
- if (csrf_param) {
118
- var param = csrf_param.readAttribute('content'),
119
- token = csrf_token.readAttribute('content'),
120
- field = new Element('input', { type: 'hidden', name: param, value: token });
121
- form.insert(field);
122
- }
123
-
124
- form.submit();
125
- }
126
-
127
-
128
- document.on("click", "*[data-confirm]", function(event, element) {
129
- var message = element.readAttribute('data-confirm');
130
- if (!confirm(message)) event.stop();
131
- });
132
-
133
- document.on("click", "a[data-remote]", function(event, element) {
134
- if (event.stopped) return;
135
- handleRemote(element);
136
- event.stop();
137
- });
138
-
139
- document.on("click", "a[data-method]", function(event, element) {
140
- if (event.stopped) return;
141
- handleMethod(element);
142
- event.stop();
143
- });
144
-
145
- document.on("submit", function(event) {
146
- var element = event.findElement(),
147
- message = element.readAttribute('data-confirm');
148
- if (message && !confirm(message)) {
149
- event.stop();
150
- return false;
151
- }
152
-
153
- var inputs = element.select("input[type=submit][data-disable-with]");
154
- inputs.each(function(input) {
155
- input.disabled = true;
156
- input.writeAttribute('data-original-value', input.value);
157
- input.value = input.readAttribute('data-disable-with');
158
- });
159
-
160
- var element = event.findElement("form[data-remote]");
161
- if (element) {
162
- handleRemote(element);
163
- event.stop();
164
- }
165
- });
166
-
167
- document.on("ajax:after", "form", function(event, element) {
168
- var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
- inputs.each(function(input) {
170
- input.value = input.readAttribute('data-original-value');
171
- input.removeAttribute('data-original-value');
172
- input.disabled = false;
173
- });
174
- });
175
- })();
File without changes
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
-
4
- APP_PATH = File.expand_path('../../config/application', __FILE__)
5
- require File.expand_path('../../config/boot', __FILE__)
6
- require 'rails/commands'
@@ -1,108 +0,0 @@
1
- require 'test_helper'
2
-
3
- class ActivationTest < ActiveSupport::IntegrationCase
4
-
5
- teardown do
6
- Capybara.reset_sessions!
7
- end
8
-
9
- test 'a user can be invited' do
10
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
11
- assert user.save
12
- assert QuoVadis::SessionsController.new.invite_to_activate user
13
-
14
- assert ActionMailer::Base.deliveries.last
15
- assert !ActionMailer::Base.deliveries.empty?
16
- email = ActionMailer::Base.deliveries.last
17
-
18
- assert_equal ['bob@example.com'], email.to
19
- assert_equal ['noreply@example.com'], email.from
20
- assert_equal 'Activate your account', email.subject
21
- # Why doesn't this use the default url option set up in test/test_helper.rb#9?
22
- assert_match Regexp.new(Regexp.escape(invitation_url user.token, :host => 'www.example.com')), email.encoded
23
- end
24
-
25
- test 'a user without email cannot be invited' do
26
- user = User.new_for_activation :name => 'Bob'
27
- assert user.save
28
- assert ! QuoVadis::SessionsController.new.invite_to_activate(user)
29
- end
30
-
31
- test 'user can accept a valid invitation and set valid credentials' do
32
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
33
- user.generate_token
34
- assert user.save
35
-
36
- visit invitation_url(user.token, :host => 'www.example.com')
37
- fill_in 'Username', :with => 'bob'
38
- fill_in 'Password', :with => 'secret'
39
- click_button 'Save my details'
40
- assert_equal root_path, current_path
41
- within '.flash.notice' do
42
- assert page.has_content?("Your account is active and you're now signed in.")
43
- end
44
- assert_nil user.reload.token
45
- assert_nil user.token_created_at
46
- end
47
-
48
- test 'user can accept a valid invitation but not set invalid credentials' do
49
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
50
- user.generate_token
51
- assert user.save
52
-
53
- visit invitation_url(user.token, :host => 'www.example.com')
54
- fill_in 'Username', :with => 'bob'
55
- fill_in 'Password', :with => ''
56
- click_button 'Save my details'
57
- assert_equal activation_path(user.token), current_path
58
- assert user.reload.token
59
- assert user.token_created_at
60
- end
61
-
62
- test 'user cannot view an expired invitation' do
63
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
64
- user.generate_token
65
- assert user.save
66
- user.update_attributes :token_created_at => 1.day.ago
67
-
68
- visit invitation_url(user.token, :host => 'www.example.com')
69
- assert_equal root_path, current_path
70
- within '.flash.alert' do
71
- assert page.has_content?("Sorry, this link isn't valid anymore.")
72
- end
73
- end
74
-
75
- test 'user cannot accept an expired invitation' do
76
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
77
- user.generate_token
78
- assert user.save
79
-
80
- visit invitation_url(user.token, :host => 'www.example.com')
81
- user.update_attributes :token_created_at => 1.day.ago
82
- fill_in 'Username', :with => 'bob'
83
- fill_in 'Password', :with => 'secret'
84
- click_button 'Save my details'
85
- assert_equal root_path, current_path
86
- within '.flash.alert' do
87
- assert page.has_content?("Sorry, this link isn't valid anymore.")
88
- end
89
- end
90
-
91
- test 'data can be passed to invitation email' do
92
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
93
- assert user.save
94
- assert QuoVadis::SessionsController.new.invite_to_activate user, :foo => 'Barbaz'
95
- email = ActionMailer::Base.deliveries.last
96
- assert_match Regexp.new('Barbaz'), email.encoded
97
- end
98
-
99
- test 'data can be passed to invitation email to override :from and :subject' do
100
- user = User.new_for_activation :name => 'Bob', :email => 'bob@example.com'
101
- assert user.save
102
- assert QuoVadis::SessionsController.new.invite_to_activate user,
103
- :subject => 'Foo', :from => 'Bar <bar@example.com>'
104
- email = ActionMailer::Base.deliveries.last
105
- assert_equal ['bar@example.com'], email.from
106
- assert_equal 'Foo', email.subject
107
- end
108
- end
@@ -1,39 +0,0 @@
1
- require 'test_helper'
2
-
3
- class AuthenticationTest < ActiveSupport::IntegrationCase
4
-
5
- teardown do
6
- Capybara.reset_sessions!
7
- end
8
-
9
- test 'action not requiring authentication' do
10
- visit articles_path
11
-
12
- assert_equal articles_path, current_path
13
- within 'h1' do
14
- assert page.has_content?('Articles')
15
- end
16
- end
17
-
18
- test 'action requiring authentication' do
19
- # try to see page
20
- visit new_article_path
21
-
22
- # test we need to authenticate
23
- assert_equal sign_in_path, current_path
24
- within '.flash.notice' do
25
- assert page.has_content? 'Please sign in first.'
26
- end
27
-
28
- # sign in
29
- user_factory 'Bob', 'bob', 'secret'
30
- sign_in_as 'bob', 'secret'
31
- visit new_article_path
32
-
33
- # test we can now see page
34
- assert_equal new_article_path, current_path
35
- within 'h1' do
36
- assert page.has_content?('New Article')
37
- end
38
- end
39
- end
@@ -1,23 +0,0 @@
1
- require 'test_helper'
2
-
3
- class BlockedTest < ActiveSupport::IntegrationCase
4
-
5
- test 'sign-in process can be blocked' do
6
- user_factory 'Bob', 'bob', 'secret'
7
- user_factory 'Jim', 'jim', 'secret'
8
-
9
- QuoVadis.blocked = Proc.new do |controller|
10
- controller.params[:username] == 'bob'
11
- end
12
-
13
- sign_in_as 'bob', 'secret'
14
- within '.flash' do
15
- assert page.has_content?('Sorry, your account is blocked.')
16
- end
17
-
18
- sign_in_as 'jim', 'secret'
19
- within '.flash' do
20
- assert page.has_content?('You have successfully signed in.')
21
- end
22
- end
23
- end
@@ -1,118 +0,0 @@
1
- require 'test_helper'
2
-
3
- class ConfigTest < ActiveSupport::IntegrationCase
4
-
5
- setup do
6
- user_factory 'Bob', 'bob', 'secret'
7
- end
8
-
9
- test 'signed_in_url config' do
10
- sign_in_as 'bob', 'secret'
11
- assert_equal root_path, current_path
12
- visit sign_out_path
13
-
14
- QuoVadis.signed_in_url = :articles
15
-
16
- sign_in_as 'bob', 'secret'
17
- assert_equal articles_path, current_path
18
- end
19
-
20
- test 'signed_in_url proc config' do
21
- QuoVadis.signed_in_url = Proc.new do |user, controller|
22
- user.name == 'Bob' ? :articles : :root
23
- end
24
- sign_in_as 'bob', 'secret'
25
- assert_equal articles_path, current_path
26
-
27
- QuoVadis.signed_in_url = Proc.new do |user, controller|
28
- user.name != 'Bob' ? :articles : :root
29
- end
30
- sign_in_as 'bob', 'secret'
31
- assert_equal root_path, current_path
32
- end
33
-
34
- test 'override_original_url config' do
35
- visit new_article_path
36
- assert_equal sign_in_path, current_path
37
- sign_in_as 'bob', 'secret'
38
- assert_equal new_article_path, current_path
39
- visit sign_out_path
40
-
41
- QuoVadis.override_original_url = true
42
-
43
- visit new_article_path
44
- assert_equal sign_in_path, current_path
45
- sign_in_as 'bob', 'secret'
46
- assert_equal root_path, current_path
47
- end
48
-
49
- test 'blocked config' do
50
- QuoVadis.blocked = Proc.new do |controller|
51
- controller.params[:username] == 'bob'
52
- end
53
- sign_in_as 'bob', 'secret'
54
- within '.flash' do
55
- assert page.has_content?('Sorry, your account is blocked.')
56
- end
57
- sign_in_as 'jim', 'secret'
58
- within '.flash' do
59
- assert page.has_no_content?('Sorry, your account is blocked.')
60
- end
61
- end
62
-
63
- test 'signed_out_url config' do
64
- visit sign_out_path
65
- assert_equal root_path, current_path
66
-
67
- QuoVadis.signed_out_url = :articles
68
-
69
- visit sign_out_path
70
- assert_equal articles_path, current_path
71
- end
72
-
73
- test 'signed_in_hook' do
74
- QuoVadis.signed_in_hook = Proc.new do |user, request|
75
- user.update_attributes :name => 'Robert'
76
- end
77
- sign_in_as 'bob', 'secret'
78
- within '#topnav' do
79
- assert page.has_content?('You are signed in as Robert.')
80
- end
81
- end
82
-
83
- test 'failed_sign_in hook' do
84
- QuoVadis.failed_sign_in_hook = Proc.new do |request|
85
- request.flash[:muppet] = request.params[:username]
86
- end
87
- sign_in_as 'bob', 'wrong'
88
- within '.flash.muppet' do
89
- assert page.has_content?('bob')
90
- end
91
- end
92
-
93
- test 'signed_out hook' do
94
- QuoVadis.signed_out_hook = Proc.new do |user, request|
95
- request.flash[:fyi] = user.name
96
- end
97
- sign_in_as 'bob', 'secret'
98
- visit sign_out_path
99
-
100
- within '.flash.fyi' do
101
- assert page.has_content?('Bob')
102
- end
103
- end
104
-
105
- test 'layout config' do
106
- QuoVadis.layout = 'sessions'
107
- visit sign_in_path
108
- assert page.has_content?('Sessions layout')
109
- end
110
-
111
- test 'change-password mailer from config' do
112
- QuoVadis.from = 'jim@example.com'
113
- (user = User.last).generate_token
114
- email = QuoVadis::Notifier.change_password(user)
115
- assert_equal ['jim@example.com'], email.from
116
- end
117
-
118
- end