Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #13 パスワード再設定編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#12 ActionMailer, アクティベーション編](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)
次回：[#14 ユーザ投稿表示, ページネーション編](https://qiita.com/aokyo17/items/cb2ca3f320462a91c93b)

この記事は、**動画を観た時間を記録するアプリのポートフォリオ**です。
今回は**メールでのパスワードを再設定**をできるようにします。

# 今回の流れ
1. パスワード再設定のイメージをつかむ
* ビューを作る
* トークンとダイジェストを生成しURL入りメールを送信する
* URLの情報が正しいか確認し再設定する

# パスワード再設定のイメージ
パスワード再設定は[#12](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)で紹介したアクティベーションと似ています。
ぜひ比較しながらご覧ください。
<img width="411" alt="lantern_lantern_reset_password_sitemap.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/4cb37a02-4385-34f0-207f-581516009828.png">
アクティベーションの時はビューが必要ありませんでした。
しかし今回は__2つのビュー__と__4つのアクション__が必要です。

今回も先にコントローラとリソースを生成します。

```bash:bash
$ rails g controller PasswordResets new create edit update
```

```rb:config/routes.rb
# 中略
resources :password_resets, only: [:new, :create, :edit, :update]
```

# パスワード再設定までをつくる手順
以下はパスワード再設定をつくるときの手順です。

1. パスワード再設定用のビューを作る（new）
* トークンとダイジェストを生成し、メールを送信する（create）
* メール内のURLにトークンとメールアドレスを忍ばせる
* URLをクリックしたらURL内の情報が有効か確認する（edit） 
* 確認した情報や入力したパスワードが正しければ再設定が完了する（update）

# パスワード再設定用のビューを生成（new）
それでは始めていきましょう。
始めに先にビューを完成させてしまいます。
整えるビューは3つです。

* ログイン画面にパスワード再設定リンク（メール入力）を追加する
* パスワード再設定画面（メール入力）を作る
* パスワード再設定画面（パスワード入力）を作る

## ログイン画面にリンクを追加
ログイン画面のビューからパスワード再設定にアクセスできるように編集します。

```erb:app/views/sessions/new.html.erb
<% provide(:title, "ログイン") %>
<div class="container form-container login-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">ログイン</h1>
      <%= form_with(scope: :session, url: login_path, local: true) do |form| %>

        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>
        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>
        <div class="form-group form-check">
          <%= form.check_box :remember_me, class: 'form-check-input' %>
          <%= form.label :remember_me, '次から保存（ログイン省略）', class: 'form-check-label' %>
        </div>
        <div class="form-group">
          <%= form.submit "ログイン", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
      <p class="form-go-to-signup-or-login">新しくはじめる方は<%= link_to "こちら", signup_path %></p>
      <p class="form-go-to-password-reset">パスワードを忘れた方は<%= link_to "こちら", new_password_reset_path %></p>
    </div>
  </div>
</div>
```

<img width="414" alt="lantern_lantern_login_with_reset_password.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/f87a3e45-54b8-9ab5-b803-be5fa5351c52.png">

## パスワード再設定画面（メール入力）を作る
続いてメールを送るまでのパスワード再設定画面を作ります。

```erb:app/views/password_resets/new.html.erb
<% provide(:title, "パスワード再設定依頼") %>
<div class="container form-container password_reset-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">パスワード再設定</h1>
      <%= form_with(scope: :password_reset, url: password_resets_path, local: true) do |form| %>
        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>
        <div class="form-group">
          <%= form.submit "送信", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
      <p class="form-go-to-signup-or-login">思い出した方は<%= link_to "こちら", login_path %></p>
    </div>
  </div>
</div>
```

<img width="414" alt="lantern_lantern_reset_password_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/33ad1779-b9f5-cd10-4baf-67e135075e0d.png">

## パスワード再設定画面（パスワード入力）を作る
ここはちょっとクセがあります。
なぜならPATCHであるapdateアクションは、何をキーにしてユーザを判別すれば良いのか考える必要があるからです。

メール送信時（GET / #edit）はURL内のメールアドレスをキーにしました。
同じようにするにはフォームでメールアドレスを送信してほしいものです。
しかしユーザ側からすると手間になります。

そこで隠しフィールドを使います。

```erb:
<%= hidden_field_tag :email, @user.email %>
```

こんな風に記述すると見えない形でパラメータを送ってくれます。
（params[:email]の形で取得できます）
これを用いつつ、ビューを完成させましょう。

```erb:app/views/password_resets/edit.html.erb
<% provide(:title, "パスワード再設定") %>
<div class="container form-container password_reset-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">パスワード再設定</h1>
      <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |form| %>
        <%= render 'shared/error_messages' %>
        
        <%= hidden_field_tag :email, @user.email %>
        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>
        <div class="form-group">
          <%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード（再入力）" %>
        </div>
        <div class="form-group">
          <%= form.submit "送信", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
    </div>
  </div>
</div>
```

![lantern_lantern_reset_password_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/406427ce-d166-596e-8618-ed1dff75e4c8.png)
これでビューは完了しました。

# トークンとダイジェストを生成しメールを送信する（create）
続いてはトークンとダイジェストの生成からメール送信までを記述しましょう。
手順は以下の通りです。

* ダイジェスト用の属性を与える
* トークンとダイジェストを生成、認証するメソッドを確認する
* 再設定用トークンとダイジェストを生成、メールを送信するメソッドをつくる

## ダイジェスト用の属性を与える
その前にマイグレーションで属性を与えます。
以前の記事と異なるのは、再設定の有効期限を記す属性も加えるという点です。

```bash:bash
$ rails g migration add_reset_to_users reset_digest:string reset_sent_at:datetime
$ rails db:migrate
```

## トークンとダイジェストを生成、認証するメソッドを確認する
トークンやダイジェストの生成、認証するメソッドは[#9](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)ですでに紹介しています。
すでに__#9をお読みの方は飛ばしてください。__
それ以外の方は以下のメソッドを追加してください。

```rb:app/models/user.rb
class User < ApplicationRecord
  # 中略
  class << self
    # ダイジェストを生成する
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end

    # トークンを生成する    
    def new_token
      SecureRandom.urlsafe_base64
    end
  end

  # トークンとダイジェストを認証する
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
```

## 再設定用トークンとダイジェストを生成、メールを送信するメソッドをつくる
次はメール送信までに必要なメソッドを用意しましょう。
必要なメソッドを列挙します。

* create_reset_digest → トークンとダイジェストをまとめて生成
* send_password_reset_email → メールを送信

トークンは仮属性なのでattr_accessorによる記述もお忘れないように。

```rb:
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  # 中略
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
  end
  
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end
  
  private
  # 中略
end
```

ついcreate_reset_digestをcreate_activation_digestのすぐ下に記述したくなります。
しかしここはprivateメソッドです。
なぜprivateを使い分けているのでしょうか。

理由はこうです。

* create_activation_digestは__このUserクラス内でしか使いません__
* よって__余計なスコープを広げないために、privateメソッドを使用__します
* でもcreate_reset_digestは__コントローラで使用__します
* よってcreate_reset_digestは__Userクラスを超える__のでprivateには置けません

使い分けられるよう、心がけましょう。

## createアクションからメール送信などを行う
それではcreate_reset_digestなどをコントローラで使いましょう。
メール送信に関する部分はcreateアクションに記述します。

```rb:app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  def new
  end
  
  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "再設定用のURLを入力したメールに送信しました"
      redirect_to root_url
    else
      flash[:danger] = "お使いのメールアドレスは登録されていません"
      render 'new'
    end
  end

  def edit
  end
  
  def update
  end
end
```

# メールの設定を行う
createアクションでメール送信を行いました。
続いてはメール内容を整えます。
 
メーラーの生成に関しては[#12](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)でご確認ください。

お読みでない方用に、必要なコードのみ紹介します。
__#12をお読みの方はこのコードのみ飛ばしてください__

```bash:bash
$ rails g mailer UserMailer account_activation password_reset
```

```rb:app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'noreply@example.com'
  layout 'mailer'
end
```

本番環境（〇〇は本番環境で自分のアプリを開いたときのURL）

```rb:config/environments/production.rb
# 中略
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '〇〇.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
  }
```

__この後からはお読みの方も必要な手順です。__
間違えないよう注意していただけたら幸いです。

## メールの送信先とタイトルをつくる
送信先とタイトルはUserMailerクラスの仕事です。
記述しましょう。

```rb:mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  # 中略
  def password_reset(user)
    @user = user
    mail to: user.email, subject: "【重要】Lantern Lanternよりパスワード再設定のためのメールを届けました"
  end
end
```

## メール内のURLにトークンとメールアドレスを忍ばせる
メールの本文はViewの仕事です。
Viewはhtml版とtext版で2つあるので注意しましょう。
こんな風に記述するとURLにトークンとメールアドレスを仕込むことができます。

```erb:app/views/user_mailer/password_reset.html.erb
<h1>Lantern Lantern</h1>
<p><%= @user.name %>さんへ</p>
<p>下記の『パスワードを再設定する』をクリックしてパスワードを再設定してください。</p>
<%= link_to "パスワードを再設定する", edit_password_reset_url(@user.reset_token, email: @user.email) %>
```

```erb:app/views/user_mailer/password_reset.text.erb
<%= @user.name %>さんへ

Lantern Lanternです。下記のリンクからパスワードを再設定してください。
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
```

どうやってURLを指定したのか。
[#12](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)でも説明しましたがこちらでも解説します。
URLの指定にはこのようなコードを使用しました。

```rb:
edit_password_reset_url(@user.reset_token, email: @user.email)
```

まずはルートを調べてURLのパターンを確認しましょう。

```bash:bash
$ rails routes | grep edit_password_reset
edit_password_reset GET /password_resets/:id/edit(.:format)
```

『:id』と書かれたこの部分。
ここがedit_password_reset_urlの第1引数に対応します。
だからここにトークンを記述しています。

では第2引数は何をしているのでしょう。
第2引数はハッシュを使用するとクエリパラメータを付与します。
具体的には最後（/editの直後）にemailキーとメールアドレスを組み込みます。

結果としてこんなURLが生成されます。

```
http://www.example.com/password_resets/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
```

以上がURLのからくりです。

## メールのプレビューをつくる
先ほど作ったメールがどんな感じに作られたか。
確認するためにプレビューが欲しいところです。
まずはspec内にこのような記述をしましょう。

```rb:spec/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
  # 中略
  # https://〇〇/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end
end
```

加えてテスト環境の設定が必要です。
__（\#12を済ませた方は必要ありません）__
[#12](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)にも書いていますが、一応ここでも簡易的に紹介します。

```rb:config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = '〇〇'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  config.action_mailer.perform_caching = false
```

'〇〇'のところは自分の環境によって変更する必要があります。
具体的にはdevelopment環境のURLを挿入します。
（わからない方は[#12](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)をご覧ください）

これが終わると以下のURLにアクセスしましょう。
するとプレビューが表示されます。

```
https://〇〇/rails/mailers/user_mailer/password_reset
```

![lantern_lantern_passowrd_reset_preview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/feeb0f94-73e9-13fc-23bd-379f43d1298e.png)
以上でプレビューは完了です。


# 情報が正しいか確認し再設定する（edit, update）
URLがクリックされたら__editアクション__を呼び出します。
ここで行うことは3つあります。

* __Userモデルを特定する__
* __存在するか、トークンは正しいのかを確かめる__
* __URLが期限切れでないかを確かめる__

この3つに関してはupdateアクションでも同じことを行います。
ということはこうした方がスッキリします。

* __上記3つをメソッド化する__
* __editとupdate時に呼び出す__

アクションの直前にメソッドを呼び出すにはbefore_actionを使います。

一方__updateアクション__はフォームにパスワードが入力された時の処理を書きます。
今回は3つのケースに対応します。

1. __入力されていない時__
* __無効なパスワードの時__
* __正しい時__

こちらはupdateアクションのみの振る舞いなので、直接書き込みます。
では、これらを踏まえて実装しましょう。

```rb:app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  before_action :get_user, only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]
  
  def new
  end
  
  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "再設定用のURLを入力したメールに送信しました"
      redirect_to root_url
    else
      flash[:danger] = "お使いのメールアドレスは登録されていません"
      render 'new'
    end
  end

  def edit
  end
  
  def update
    if params[:user][:password].empty?
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      @user.update_attribute(:reset_digest, nil)
      flash[:success] = "パスワードの再設定が完了しました"
      redirect_to @user
    else
      render 'edit'
    end
  end
  
  private
  
    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end
    
    def get_user
      @user = User.find_by(email: params[:email])
    end
    
    def valid_user
      unless @user && @user.activated? && @user.authenticated?(:reset, params[:id])
        flash[:danger] = "無効なURLです。再度メールアドレスを入力してください"
        redirect_to new_password_reset_url
      end
    end
    
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "パスワード再設定URLの有効期限が過ぎています。再度メールアドレスを入力してください"
        redirect_to new_password_reset_url
      end
    end
end
```

１つ言い忘れたことがあります。
それはcheck_expiration内にあるpassword_reset_expired?メソッドです。

期限切れかどうかを判別する処理に関しては、__別途Userモデルにメソッドを用意した上で実装__しています。

そちらのメソッドを紹介します。

```rb:app/models/user.rb
class User < ApplicationRecord
  # 中略
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  private
    # 中略
```

これでパスワード再設定に関する全ての実装が完了しました。

# テストを書く
最後にパスワード再設定に関するテストを完成させます。
いくつかあるので順に見ていきます。

## メーラーテストを書く
まずはメーラーのテストです。
メールの内容についてテストします。
なおメール本文に関してはデコードを行って検証しています。

```rb:spec/mailers/user_mailer_spec.rb
require "rails_helper"

RSpec.describe UserMailer, type: :mailer do
  
  let(:user) { create(:user) }
  
  # 中略
  describe "password_reset" do
    it "renders mails" do
      user.reset_token = User.new_token
      mail = UserMailer.password_reset(user)
      expect(mail.subject).to eq "【重要】Lantern Lanternよりパスワード再設定のためのメールを届けました"
      expect(mail.to).to eq ["michael@example.com"]
      expect(mail.from).to eq ["noreply@example.com"]
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include "Michael Example"
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include user.reset_token
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include CGI.escape(user.email)
    end
  end
end
```

## パスワード変更のテストを書く
メーラー以外のテストを書きます。
まずはRequest specを生成するところから始めましょう。

```bash:bash
$ rails g rspec:request password_resets
```

今回テストする内容は以下の通りです。

__createアクション__

* 無効なメールアドレス
* 有効なメールアドレス

__editアクション__

* 無効なメールアドレス
* 無効なユーザ
* 無効なトークン
* 有効なメールアドレス、ユーザ、トークン

__updateアクション__

* 無効なパスワード
* 空のパスワード
* 期限切れのトークン
* 有効なパスワード

では記述します。

```rb:spec/requests/password_resets_spec.rb
require 'rails_helper'

RSpec.describe "PasswordResets", type: :request do

  let(:user) { create(:user) }
  
  describe "POST /password_resets" do
    it "is invalid email address" do
      get new_password_reset_path
      expect(request.fullpath).to eq '/password_resets/new'
      post password_resets_path, params: { password_reset: { email: "" } }
      expect(flash[:danger]).to be_truthy
      expect(request.fullpath).to eq '/password_resets'
    end
      
    it "is valid email address" do
      get new_password_reset_path
      expect(request.fullpath).to eq '/password_resets/new'
      post password_resets_path, params: { password_reset: { email: user.email } }
      expect(flash[:info]).to be_truthy
      expect(flash[:danger]).to be_falsey
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end
  end
  
  describe "GET /password_resets/:id/edit" do
    it "is invalid email address" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: "")
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end
      
      
    it "is invalid user" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      user.toggle!(:activated)
      get edit_password_reset_path(user.reset_token, email: user.email)
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
      user.toggle!(:activated)
    end
      
    it "is invalid token" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path('wrong token', email: user.email)
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end
      
    it "is valid information" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      expect(flash[:danger]).to be_falsey
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}/edit?email=#{CGI.escape(user.email)}"
    end
  end
  
  describe "PATCH /password_resets/:id" do
    it "is invalid password" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "barquux"
        }
      }
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}"
    end
    
    it "is empty password" do
    post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "",
          password_confirmation: ""
        }
      }
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}"
    end

    it "has expired token" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      user.update_attribute(:reset_sent_at, 3.hours.ago)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "foobaz"
        }
      }
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end

    it "is valid information" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "foobaz"
        }
      }
      expect(flash[:success]).to be_truthy
      expect(is_logged_in?).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq "/users/1"
    end
  end
end
```

このテストではassignsを使用するところがあります。

```rb:
# 該当箇所
user = assigns(:user)
get edit_password_reset_path(user.reset_token, email: "")
```

これにより__FactoryBotのuser__ではなく、__コントローラ内インスタンス変数のuser__を__userとして取得__し直しています。なぜでしょう。

理由は以下の通りです。

* edit_password_reset_pathの引数に当たる__reset_token__は、attr_accessorによって生成された__仮属性__です。
* 仮属性なのでletで生成した__FactoryBotのuserにはreset_tokenが存在しません__。よって__エラー__になります。
* したがってreset_tokenが代入された__PasswordResetsコントローラのインスタンス変数user__を使用する必要があります。

というわけで、コントローラのインスタンス変数のuserが必要です。
そこでassignsを使用します。
assignsはコントローラのインスタンス変数を取得します。

そのためにはgem 'rails-controller-testing'が必要です。
（とてもためらいましたが）導入しましょう。

```diff:Gemfile
group :development, :test do
+ gem 'rails-controller-testing'
end
```

```bash:bash
$ bundle install
```

これで問題なく動作します。
テストを走らせてみましょう。

```bash:bash
$ rails spec
```

問題なければ、以上でテストは終了です。
次回はユーザ投稿の表示を行います。

# 追記：ですます調に統一しました
今回から語尾を「ですます」に統一しました。
情報が統一されて見やすいかなと。
分かりやすい記事になるよう努めます。

やさしい記事の書き方↓
[これであなたのQiita記事もランキング入り！？@jnchitoによる編集リクエスト解説（解説動画付き）](https://qiita.com/jnchito/items/a939906de64bf34700a9)

---
前回：[#12 ActionMailer, アクティベーション編](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)
次回：[#14 ユーザ投稿表示, ページネーション編](https://qiita.com/aokyo17/items/cb2ca3f320462a91c93b)