👋

sorcery パスワードリセット機能で使用するカラムreset_password_token

2022/08/03に公開

パスワードリセットメールが正常に送られているか reset_password_token の値を使用してテストを行う中でどのタイミングで値が生成されるか把握していなかったのでなかなかうまくいきませんでした。
今回は reset_password_token の生成されるタイミングを調べてみました。

テスト修正前

RSpec.describe UserMailer, type: :mailer do
  let(:user) { create(:user) }
  let(:mail) { UserMailer.reset_password_email(user) }
  let(:mail_body) do
    mail.body.encoded.split(/
/).map { |i| Base64.decode64(i) }.join
  end
  # Base64 encodeをデコードして比較できるようにする

context 'ActionMailerの送信' do
  it 'メール文が正しく表示されること' do
    # メール文内にトークンを変換したURLが存在するか確認する
    expect(mail_body).to match edit_password_reset_url(user.reset_password_token)
    expect(mail_body).to match(user.name)
  end

ユーザーを作成後 UserMailer.reset_password_email(user)を使用しメールを送信する処理を行なっていますがパスワードリセットトークンが nil なので失敗に終わります。

def reset_password_email(user)
  @user = User.find(user.id)
  @url = edit_password_reset_url(@user.reset_password_token) #@user.reset_password_tokenがnilなのでエラーになる
  mail(to: user.email, subject: 'パスワードリセット')
end


ActionController::UrlGenerationError:
No route matches {:action=>"edit", :controller=>"password_resets", :id=>nil}, possible unmatched constraints: [:id]
Did you mean?  edit_password_reset_path

reset_password_email メソッドが実行される前にトークンを作成する必要があった為
どこで作成されるのかを追ってみました。

reset_password_token

こちらの値は変更前は nil でユーザーがパスワードを変える時にエンコードされた値がカラムに追加されます
パスワードを変更する際は、reset_password_email の値は nil の為からのバリデーションに引っ掛かってしまうのでallow_nilオプションをつけることによりバリデーションをスキップすることができます。

validates :reset_password_token, presence: true, uniqueness: true, allow_nil: true

トークンが発行されるまでの流れになります。

1.ユーザーがパスワードを忘れてしまったのでパスワード申請ホームへ遷移する
2.ユーザーが登録済みのメールアドレスを入力する。
3.ユーザーが入力したメールアドレスを元にトークンを発行する
@user&.を使用している理由は@user の値が nil になってしまった時の対策です
&を使用せずに記載していると nil に対して deliver_reset_password_instructions!メソッドを呼んでエラーになります
そこで&.を使用すれば、もし nil であっても nil を返すだけになり、例外は発生しません

def create
  @user = User.find_by_email(params[:email]) #メールアドレスの検索
  @user&.deliver_reset_password_instructions!#トークンの発行と発行されたトークンを元にURLの生成とメールの送信処理も行われる。
  redirect_to root_url, success: 'パスワードリセットメールを送りました'
end

deliver_reset_password_instructions!メソッドの内部で mailers に記載をしているメソッドが実行されます。

  def reset_password_email(user)
    @user = User.find(user.id)
    @url = edit_password_reset_url(@user.reset_password_token)#deliver_reset_password_instructionsメソッドで作成されたトークンを元にリセとパスワード用のURLを作成する。
    mail(to: user.email, subject: 'パスワードリセット')
  end

テスト修正前は edit_password_reset_url(@user.reset_password_token)の個所でトークンが nil なのでエラーになっていました。
結果として let!で(:user_token_create) { user&.deliver_reset_password_instructions! }を reset_password_email メソッドの前に実行すればテストを通すことができました。

テスト修正後

RSpec.describe UserMailer, type: :mailer do
  let(:user) { create(:user) }
  let!(:user_token_create) { user&.deliver_reset_password_instructions! } #ここでトークンを作成する
  let(:mail) { UserMailer.reset_password_email(user) }
  let(:mail_body) do
    mail.body.encoded.split(/
/).map { |i| Base64.decode64(i) }.join
  end
  # Base64 encodeをデコードして比較できるようにする

  context 'ActionMailerの送信' do
    it 'メール文が正しく表示されること' do
      # メール文内にトークンを変換したURLが存在するか確認する
      expect(mail_body).to match edit_password_reset_url(user.reset_password_token)
      expect(mail_body).to match(user.name)

最終的にトークンの値は送られてきたメールからパスワード変更リンクへ遷移を行い、新しいパスワードを更新した後にリセットされるようになっています。

Discussion