diff --git a/NEWS.md b/NEWS.md index e8dc71f01..0f6581768 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,11 +9,21 @@ complete changelog, see the git history for each version via the version links. - Removed support for Ruby versions older than 2.2 - Removed support for Rails versions older than 4.2 - Removed all deprecated code from Clearance 1.x +- Removed `User#confirmation_token`, `User#forgot_password!`, and + `User#generate_confirmation_token` as part of the change to expiring, + databaseless password reset tokens. ### Changed +- Password resets now use expiring signed tokens that do not require persistence + to the `users` table. By default, the tokens are generated with + `ActiveSupport::MessageVerifier` and expire in 15 minutes. - Flash messages now use `flash[:alert]` rather than `flash[:notice]` as they were used as errors more than notices. +### Added +- `rails generate clearance:upgrade` generator to prepare your Clearance 1.x + project for Clearance 2.0 + [2.0.0]: https://github.com/thoughtbot/clearance/compare/v1.14.1...2.0 ## [1.14.1] - May 12, 2016 diff --git a/app/controllers/clearance/passwords_controller.rb b/app/controllers/clearance/passwords_controller.rb index c3176a100..0fca6207e 100644 --- a/app/controllers/clearance/passwords_controller.rb +++ b/app/controllers/clearance/passwords_controller.rb @@ -10,7 +10,6 @@ def new def create if user = find_user_for_create - user.forgot_password! deliver_email(user) end @@ -26,9 +25,9 @@ def edit def update @user = find_user_for_update - if @user.update_password password_reset_params - sign_in @user - redirect_to url_after_update + if @user.update_password(password_reset_params) + sign_in(@user) + redirect_to(url_after_update) else flash_failure_after_update render template: "passwords/edit" @@ -38,45 +37,50 @@ def update private def deliver_email(user) - mail = ::ClearanceMailer.change_password(user) - mail.deliver_later + ::ClearanceMailer.change_password(user).deliver_later end def password_reset_params params[:password_reset][:password] end - def find_user_by_id_and_confirmation_token - user_param = Clearance.configuration.user_id_parameter - - Clearance.configuration.user_model. - find_by_id_and_confirmation_token params[user_param], params[:token].to_s - end - def find_user_for_create Clearance.configuration.user_model. find_by_normalized_email params[:password][:email] end + def find_user_by_password_reset_token(token) + verifier = Clearance.configuration.message_verifier + user_model = Clearance.configuration.user_model + + begin + user_id, encrypted_password, expiration = verifier.verify(token) + rescue + expiration = 1.day.ago + end + + if expiration.future? + user_model.find_by(id: user_id, encrypted_password: encrypted_password) + end + end + def find_user_for_edit - find_user_by_id_and_confirmation_token + find_user_by_password_reset_token(params[:token]) end def find_user_for_update - find_user_by_id_and_confirmation_token + find_user_by_password_reset_token(params[:token]) end def ensure_existing_user - unless find_user_by_id_and_confirmation_token - flash_failure_when_forbidden + unless find_user_by_password_reset_token(params[:token]) + flash_failure_when_invalid render template: "passwords/new" end end - def flash_failure_when_forbidden - flash.now[:alert] = translate(:forbidden, - scope: [:clearance, :controllers, :passwords], - default: t("flashes.failure_when_forbidden")) + def flash_failure_when_invalid + flash.now[:alert] = translate("flashes.failure_when_password_reset_invalid") end def flash_failure_after_update diff --git a/app/mailers/clearance_mailer.rb b/app/mailers/clearance_mailer.rb index ffd91aa01..5749e4f6d 100644 --- a/app/mailers/clearance_mailer.rb +++ b/app/mailers/clearance_mailer.rb @@ -1,6 +1,8 @@ class ClearanceMailer < ActionMailer::Base def change_password(user) @user = user + @token = generate_password_reset_token(@user) + mail( from: Clearance.configuration.mailer_sender, to: @user.email, @@ -10,4 +12,14 @@ def change_password(user) ), ) end + + private + + def generate_password_reset_token(user) + Clearance.configuration.message_verifier.generate([ + user.id, + user.encrypted_password, + Clearance.configuration.password_reset_time_limit.from_now, + ]) + end end diff --git a/app/views/clearance_mailer/change_password.html.erb b/app/views/clearance_mailer/change_password.html.erb index bc315ecba..ed96e1b4c 100644 --- a/app/views/clearance_mailer/change_password.html.erb +++ b/app/views/clearance_mailer/change_password.html.erb @@ -2,7 +2,7 @@

<%= link_to t(".link_text", default: "Change my password"), - edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %> + edit_user_password_url(@user, token: @token) %>

<%= raw t(".closing") %>

diff --git a/app/views/clearance_mailer/change_password.text.erb b/app/views/clearance_mailer/change_password.text.erb index ed01f74b5..39231c289 100644 --- a/app/views/clearance_mailer/change_password.text.erb +++ b/app/views/clearance_mailer/change_password.text.erb @@ -1,5 +1,5 @@ <%= t(".opening") %> -<%= edit_user_password_url(@user, token: @user.confirmation_token.html_safe) %> +<%= edit_user_password_url(@user, token: @token) %> <%= raw t(".closing") %> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 6761ab321..d924a83ac 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -4,8 +4,8 @@

<%= t(".description") %>

<%= form_for :password_reset, - url: user_password_path(@user, token: @user.confirmation_token), - html: { method: :put } do |form| %> + url: user_password_path(@user, token: params[:token]), + html: { method: :patch } do |form| %>
<%= form.label :password %> <%= form.password_field :password %> diff --git a/config/locales/clearance.en.yml b/config/locales/clearance.en.yml index da8de0daa..2068c0c9d 100644 --- a/config/locales/clearance.en.yml +++ b/config/locales/clearance.en.yml @@ -17,6 +17,8 @@ en: failure_when_forbidden: Please double check the URL or try submitting the form again. failure_when_not_signed_in: Please sign in to continue. + failure_when_password_reset_invalid: Your password reset token has expired + or is invalid. helpers: label: password: diff --git a/db/migrate/20110111224543_create_clearance_users.rb b/db/migrate/20110111224543_create_clearance_users.rb index 75e22bfee..11106ec5c 100644 --- a/db/migrate/20110111224543_create_clearance_users.rb +++ b/db/migrate/20110111224543_create_clearance_users.rb @@ -4,7 +4,6 @@ def self.up t.timestamps null: false t.string :email, null: false t.string :encrypted_password, limit: 128, null: false - t.string :confirmation_token, limit: 128 t.string :remember_token, limit: 128, null: false end diff --git a/db/schema.rb b/db/schema.rb index bf7e2f009..ff34b8c90 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,12 +13,11 @@ ActiveRecord::Schema.define(version: 20110111224543) do - create_table "users", force: true do |t| + create_table "users", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "email", null: false t.string "encrypted_password", limit: 128, null: false - t.string "confirmation_token", limit: 128 t.string "remember_token", limit: 128, null: false end diff --git a/lib/clearance/configuration.rb b/lib/clearance/configuration.rb index 32c5bfbd2..05768a73a 100644 --- a/lib/clearance/configuration.rb +++ b/lib/clearance/configuration.rb @@ -47,6 +47,17 @@ class Configuration # @return [String] attr_accessor :mailer_sender + # Used to generate and verify password reset tokens + # Defaults to an `ActiveSupport::MessageVerifier` instance, initialized + # with `secret_key_base`. + # @return [#generate #verify] + attr_accessor :message_verifier + + # Determines how long password reset emails are valid for + # Defaults to 15 minutes + # @return [Integer] + attr_accessor :password_reset_time_limit + # The password strategy to use when authenticating and setting passwords. # Defaults to {Clearance::PasswordStrategies::BCrypt}. # @return [Module #authenticated? #password=] @@ -93,6 +104,10 @@ def initialize @cookie_name = "remember_token" @httponly = false @mailer_sender = 'reply@example.com' + @message_verifier = ActiveSupport::MessageVerifier.new( + Rails.application.secrets.secret_key_base, + ) + @password_reset_time_limit = 15.minutes @redirect_url = '/' @routes = true @secure_cookie = false diff --git a/lib/clearance/user.rb b/lib/clearance/user.rb index fbf6a6e88..efd3126ea 100644 --- a/lib/clearance/user.rb +++ b/lib/clearance/user.rb @@ -42,10 +42,6 @@ module Clearance # @return [String] The value used to identify this user in their {Session} # cookie. # - # @!attribute confirmation_token - # @return [String] The value used to identify this user in the password - # reset link. - # # @!attribute [r] password # @return [String] Transient (non-persisted) attribute that is set when # updating a user's password. Only the {#encrypted_password} is persisted. @@ -159,20 +155,6 @@ module Callbacks end end - # Generates a {#confirmation_token} for the user, which allows them to reset - # their password via an email link. - # - # Calling `forgot_password!` will cause the user model to be saved without - # validations. Any other changes you made to this user instance will also - # be persisted, without validation. It is inteded to be called on an - # instance with no changes (`dirty? == false`). - # - # @return [Boolean] Was the save successful? - def forgot_password! - generate_confirmation_token - save validate: false - end - # Generates a new {#remember_token} for the user, which will have the effect # of signing all of the user's current sessions out. This is called # internally by {Session#sign_out}. @@ -192,8 +174,7 @@ def reset_remember_token! # the configured password strategy. By default, this is # {PasswordStrategies::BCrypt#password=}. # - # This also has the side-effect of blanking the {#confirmation_token} and - # rotating the `#remember_token`. + # This also has the side-effect of rotating the `#remember_token`. # # Validations will be run as part of this update. If the user instance is # not valid, the password change will not be persisted, and this method will @@ -204,7 +185,6 @@ def update_password(new_password) self.password = new_password if valid? - self.confirmation_token = nil generate_remember_token end @@ -246,15 +226,6 @@ def skip_password_validation? (encrypted_password.present? && !encrypted_password_changed?) end - # Sets the {#confirmation_token} on the instance to a new value generated by - # {Token.new}. The change is not automatically persisted. If you would like - # to generate and save in a single method call, use {#forgot_password!}. - # - # @return [String] The new confirmation token - def generate_confirmation_token - self.confirmation_token = Clearance::Token.new - end - # Sets the {#remember_token} on the instance to a new value generated by # {Token.new}. The change is not automatically persisted. If you would like # to generate and save in a single method call, use diff --git a/lib/generators/clearance/install/install_generator.rb b/lib/generators/clearance/install/install_generator.rb index 92c1a6e99..ef3e186b4 100644 --- a/lib/generators/clearance/install/install_generator.rb +++ b/lib/generators/clearance/install/install_generator.rb @@ -75,7 +75,6 @@ def new_columns @new_columns ||= { email: 't.string :email', encrypted_password: 't.string :encrypted_password, limit: 128', - confirmation_token: 't.string :confirmation_token, limit: 128', remember_token: 't.string :remember_token, limit: 128' }.reject { |column| existing_users_columns.include?(column.to_s) } end diff --git a/lib/generators/clearance/specs/templates/features/clearance/visitor_resets_password_spec.rb.tt b/lib/generators/clearance/specs/templates/features/clearance/visitor_resets_password_spec.rb.tt index 80deeba13..224619eba 100644 --- a/lib/generators/clearance/specs/templates/features/clearance/visitor_resets_password_spec.rb.tt +++ b/lib/generators/clearance/specs/templates/features/clearance/visitor_resets_password_spec.rb.tt @@ -3,7 +3,6 @@ require "support/features/clearance_helpers" RSpec.feature "Visitor resets password" do before { ActionMailer::Base.deliveries.clear } -<% if defined?(ActiveJob) -%> around do |example| original_adapter = ActiveJob::Base.queue_adapter @@ -11,7 +10,6 @@ RSpec.feature "Visitor resets password" do example.run ActiveJob::Base.queue_adapter = original_adapter end -<% end -%> scenario "by navigating to the page" do visit sign_in_path @@ -22,7 +20,8 @@ RSpec.feature "Visitor resets password" do end scenario "with valid email" do - user = user_with_reset_password + user = FactoryGirl.create(:user) + reset_password_for(user.email) expect_page_to_display_change_password_message expect_reset_notification_to_be_sent_to user @@ -38,26 +37,18 @@ RSpec.feature "Visitor resets password" do private def expect_reset_notification_to_be_sent_to(user) - expect(user.confirmation_token).not_to be_blank - expect_mailer_to_have_delivery( - user.email, - "password", - user.confirmation_token - ) + expect_mailer_to_have_delivery(user.email, "password") end def expect_page_to_display_change_password_message expect(page).to have_content I18n.t("passwords.create.description") end - def expect_mailer_to_have_delivery(recipient, subject, body) + def expect_mailer_to_have_delivery(recipient, subject) expect(ActionMailer::Base.deliveries).not_to be_empty message = ActionMailer::Base.deliveries.any? do |email| - email.to == [recipient] && - email.subject =~ /#{subject}/i && - email.html_part.body =~ /#{body}/ && - email.text_part.body =~ /#{body}/ + email.to == [recipient] && email.subject =~ /#{subject}/i end expect(message).to be diff --git a/lib/generators/clearance/specs/templates/features/clearance/visitor_updates_password_spec.rb.tt b/lib/generators/clearance/specs/templates/features/clearance/visitor_updates_password_spec.rb.tt index 4c7504436..2895b9fbf 100644 --- a/lib/generators/clearance/specs/templates/features/clearance/visitor_updates_password_spec.rb.tt +++ b/lib/generators/clearance/specs/templates/features/clearance/visitor_updates_password_spec.rb.tt @@ -3,14 +3,14 @@ require "support/features/clearance_helpers" RSpec.feature "Visitor updates password" do scenario "with valid password" do - user = user_with_reset_password + user = FactoryGirl.create(:user) update_password user, "newpassword" expect_user_to_be_signed_in end scenario "signs in with new password" do - user = user_with_reset_password + user = FactoryGirl.create(:user) update_password user, "newpassword" sign_out sign_in_with user.email, "newpassword" @@ -19,7 +19,7 @@ RSpec.feature "Visitor updates password" do end scenario "tries with a blank password" do - user = user_with_reset_password + user = FactoryGirl.create(:user) visit_password_reset_page_for user change_password_to "" @@ -37,10 +37,18 @@ RSpec.feature "Visitor updates password" do def visit_password_reset_page_for(user) visit edit_user_password_path( user_id: user, - token: user.confirmation_token + token: token_for(user) ) end + def token_for(user) + Clearance.configuration.message_verifier.generate([ + user.id, + user.encrypted_password, + Clearance.configuration.password_reset_time_limit.from_now, + ]) + end + def change_password_to(password) fill_in "password_reset_password", with: password click_button I18n.t("helpers.submit.password_reset.submit") diff --git a/lib/generators/clearance/specs/templates/support/features/clearance_helpers.rb b/lib/generators/clearance/specs/templates/support/features/clearance_helpers.rb index 13540614c..5b1a2714a 100644 --- a/lib/generators/clearance/specs/templates/support/features/clearance_helpers.rb +++ b/lib/generators/clearance/specs/templates/support/features/clearance_helpers.rb @@ -38,12 +38,6 @@ def expect_user_to_be_signed_in def expect_user_to_be_signed_out expect(page).to have_content I18n.t("layouts.application.sign_in") end - - def user_with_reset_password - user = FactoryGirl.create(:user) - reset_password_for user.email - user.reload - end end end diff --git a/lib/generators/clearance/upgrade/USAGE b/lib/generators/clearance/upgrade/USAGE new file mode 100644 index 000000000..82497fca8 --- /dev/null +++ b/lib/generators/clearance/upgrade/USAGE @@ -0,0 +1,2 @@ +Description: + Prepare database for upgrade to Clearance 2.0 diff --git a/lib/generators/clearance/upgrade/templates/db/migrate/remove_confirmation_token_from_users.rb.tt b/lib/generators/clearance/upgrade/templates/db/migrate/remove_confirmation_token_from_users.rb.tt new file mode 100644 index 000000000..19675856c --- /dev/null +++ b/lib/generators/clearance/upgrade/templates/db/migrate/remove_confirmation_token_from_users.rb.tt @@ -0,0 +1,5 @@ +class RemoveConfirmationTokenFromUsers < ActiveRecord::Migration<%= migration_version %> + def change + remove_column :<%= Clearance.configuration.user_model.table_name %>, :confirmation_token, type: :string, limit: 128 + end +end diff --git a/lib/generators/clearance/upgrade/upgrade_generator.rb b/lib/generators/clearance/upgrade/upgrade_generator.rb new file mode 100644 index 000000000..6c2ec88bb --- /dev/null +++ b/lib/generators/clearance/upgrade/upgrade_generator.rb @@ -0,0 +1,34 @@ +require "rails/generators/base" +require "rails/generators/active_record" + +module Clearance + module Generators + class UpgradeGenerator < Rails::Generators::Base + include Rails::Generators::Migration + source_root File.expand_path("../templates", __FILE__) + + # for generating a timestamp when using `create_migration` + def self.next_migration_number(dir) + ActiveRecord::Generators::Base.next_migration_number(dir) + end + + def change_users_table + migration_name = "remove_confirmation_token_from_users.rb" + + migration_template( + "db/migrate/#{migration_name}.tt", + "db/migrate/#{migration_name}", + migration_version: migration_version, + ) + end + + private + + def migration_version + if Rails.version >= "5.0.0" + "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" + end + end + end + end +end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 79ad9d89e..3026943f2 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -146,4 +146,35 @@ expect(Clearance.configuration.reload_user_model).to be_nil end end + + describe "#message_verifier" do + it "returns the configured verifier if one has been configured" do + verifier = Class.new.new + Clearance.configure { |config| config.message_verifier = verifier } + + expect(Clearance.configuration.message_verifier).to be verifier + end + + it "returns an active support message verifier instance by default" do + Clearance.configuration = Clearance::Configuration.new + + expect(Clearance.configuration.message_verifier).to be_an_instance_of( + ActiveSupport::MessageVerifier, + ) + end + end + + describe "#password_reset_time_limit" do + it "is the configured amount if overridden" do + Clearance.configure { |config| config.password_reset_time_limit = 1.hour } + + expect(Clearance.configuration.password_reset_time_limit).to eq 1.hour + end + + it "is 15 minutes by default" do + Clearance.configuration = Clearance::Configuration.new + + expect(Clearance.configuration.password_reset_time_limit).to eq 15.minutes + end + end end diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb index 9d1fe112a..da44fac6d 100644 --- a/spec/controllers/passwords_controller_spec.rb +++ b/spec/controllers/passwords_controller_spec.rb @@ -16,14 +16,6 @@ describe "#create" do context "email corresponds to an existing user" do - it "generates a password change token" do - user = create(:user) - - post :create, password: { email: user.email.upcase } - - expect(user.reload.confirmation_token).not_to be_nil - end - it "sends the password reset email" do ActionMailer::Base.deliveries.clear user = create(:user) @@ -59,9 +51,9 @@ describe "#edit" do context "valid id and token are supplied" do it "renders the password form for the user" do - user = create(:user, :with_forgotten_password) + user = create(:user) - get :edit, user_id: user, token: user.confirmation_token + get :edit, user_id: user, token: token_for(user) expect(response).to be_success expect(response).to render_template(:edit) @@ -74,18 +66,43 @@ get :edit, user_id: 1, token: "" expect(response).to render_template(:new) - expect(flash.now[:alert]).to match(/double check the URL/i) + expect(flash.now[:alert]).to match(/expired or is invalid/i) end end context "invalid token is supplied" do it "renders the new password reset form with a flash alert" do - user = create(:user, :with_forgotten_password) + get :edit, user_id: 1, token: "a" + + expect(response).to render_template(:new) + expect(flash.now[:alert]).to match(/expired or is invalid/i) + end + end + + context "expired token is supplied" do + it "does not allow password reset to continue" do + user = create(:user) + token = token_for(user) - get :edit, user_id: 1, token: user.confirmation_token + "a" + Timecop.freeze(1.day.from_now) do + get :edit, user_id: 1, token: token + + expect(response).to render_template(:new) + expect(flash.now[:alert]).to match(/expired or is invalid/i) + end + end + end + + context "password is changed before reset is complete" do + it "does not allow password reset to continue" do + user = create(:user) + token = token_for(user) + user.update_password("foobar") + + get :edit, user_id: 1, token: token expect(response).to render_template(:new) - expect(flash.now[:alert]).to match(/double check the URL/i) + expect(flash.now[:alert]).to match(/expired or is invalid/i) end end end @@ -93,7 +110,7 @@ describe "#update" do context "valid id, token, and new password provided" do it "updates the user's password" do - user = create(:user, :with_forgotten_password) + user = create(:user) old_encrypted_password = user.encrypted_password put :update, update_parameters(user, new_password: "my_new_password") @@ -102,7 +119,7 @@ end it "signs the user in and redirects" do - user = create(:user, :with_forgotten_password) + user = create(:user) put :update, update_parameters(user, new_password: "my_new_password") @@ -113,18 +130,17 @@ context "password update fails" do it "does not update the password" do - user = create(:user, :with_forgotten_password) + user = create(:user) old_encrypted_password = user.encrypted_password put :update, update_parameters(user, new_password: "") user.reload expect(user.encrypted_password).to eq old_encrypted_password - expect(user.confirmation_token).to be_present end it "re-renders the password edit form" do - user = create(:user, :with_forgotten_password) + user = create(:user) put :update, update_parameters(user, new_password: "") @@ -140,8 +156,16 @@ def update_parameters(user, options = {}) { user_id: user, - token: user.confirmation_token, - password_reset: { password: new_password } + token: token_for(user), + password_reset: { password: new_password }, } end + + def token_for(user) + Clearance.configuration.message_verifier.generate([ + user.id, + user.encrypted_password, + 15.minutes.from_now, + ]) + end end diff --git a/spec/factories.rb b/spec/factories.rb index 204620e6a..9ccab6cc1 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -7,10 +7,6 @@ email password 'password' - trait :with_forgotten_password do - confirmation_token Clearance::Token.new - end - factory :user_with_optional_password, class: 'UserWithOptionalPassword' do password nil encrypted_password '' diff --git a/spec/generators/clearance/upgrade/upgrade_generator_spec.rb b/spec/generators/clearance/upgrade/upgrade_generator_spec.rb new file mode 100644 index 000000000..674092779 --- /dev/null +++ b/spec/generators/clearance/upgrade/upgrade_generator_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" +require "generators/clearance/upgrade/upgrade_generator" + +describe Clearance::Generators::UpgradeGenerator, :generator do + it "copies a database migration to the host application" do + run_generator + + migration = migration_file( + "db/migrate/remove_confirmation_token_from_users.rb", + ) + + expect(migration).to exist + expect(migration).to have_correct_syntax + expect(migration).to contain("remove_column :users, :confirmation_token") + end +end diff --git a/spec/mailers/clearance_mailer_spec.rb b/spec/mailers/clearance_mailer_spec.rb index 81fae81fd..6d511efcd 100644 --- a/spec/mailers/clearance_mailer_spec.rb +++ b/spec/mailers/clearance_mailer_spec.rb @@ -3,7 +3,6 @@ describe ClearanceMailer do it "is from DO_NOT_REPLY" do user = create(:user) - user.forgot_password! email = ClearanceMailer.change_password(user) @@ -12,7 +11,6 @@ it "is sent to user" do user = create(:user) - user.forgot_password! email = ClearanceMailer.change_password(user) @@ -21,7 +19,6 @@ it "sets its subject" do user = create(:user) - user.forgot_password! email = ClearanceMailer.change_password(user) @@ -30,7 +27,6 @@ it "has html and plain text parts" do user = create(:user) - user.forgot_password! email = ClearanceMailer.change_password(user) @@ -40,19 +36,23 @@ end it "contains a link to edit the password" do - user = create(:user) - user.forgot_password! - host = ActionMailer::Base.default_url_options[:host] - link = "http://#{host}/users/#{user.id}/password/edit" \ - "?token=#{user.confirmation_token}" - - email = ClearanceMailer.change_password(user) - - expect(email.text_part.body).to include(link) - expect(email.html_part.body).to include(link) - expect(email.html_part.body).to have_css( - "a", - text: I18n.t("clearance_mailer.change_password.link_text") - ) + Timecop.freeze do + user = create(:user) + host = ActionMailer::Base.default_url_options[:host] + allow(Clearance.configuration.message_verifier).to receive(:generate). + with([user.id, user.encrypted_password, 15.minutes.from_now]). + and_return("THIS_IS_THE_TOKEN") + link = "http://#{host}/users/#{user.id}/password/edit" \ + "?token=THIS_IS_THE_TOKEN" + + email = ClearanceMailer.change_password(user) + + expect(email.text_part.body).to include(link) + expect(email.html_part.body).to include(link) + expect(email.html_part.body).to have_css( + "a", + text: I18n.t("clearance_mailer.change_password.link_text"), + ) + end end end diff --git a/spec/user_spec.rb b/spec/user_spec.rb index e3a1238e7..07bae51dc 100644 --- a/spec/user_spec.rb +++ b/spec/user_spec.rb @@ -70,7 +70,7 @@ describe "#update_password" do context "with a valid password" do it "changes the encrypted password" do - user = create(:user, :with_forgotten_password) + user = create(:user) old_encrypted_password = user.encrypted_password user.update_password("new_password") @@ -78,16 +78,8 @@ expect(user.encrypted_password).not_to eq old_encrypted_password end - it "clears the confirmation token" do - user = create(:user, :with_forgotten_password) - - user.update_password("new_password") - - expect(user.confirmation_token).to be_nil - end - it "sets the remember token" do - user = create(:user, :with_forgotten_password) + user = create(:user) user.update_password("my_new_password") @@ -98,21 +90,13 @@ context "with blank password" do it "does not change the encrypted password" do - user = create(:user, :with_forgotten_password) + user = create(:user) old_encrypted_password = user.encrypted_password user.update_password("") expect(user.encrypted_password.to_s).to eq old_encrypted_password end - - it "does not clear the confirmation token" do - user = create(:user, :with_forgotten_password) - - user.update_password("") - - expect(user.confirmation_token).not_to be_nil - end end end @@ -129,16 +113,6 @@ end end - describe "#forgot_password!" do - it "generates the confirmation token" do - user = create(:user, confirmation_token: nil) - - user.forgot_password! - - expect(user.confirmation_token).not_to be_nil - end - end - describe "a user with an optional email" do subject { user }