Railsチュートリアル 第12章 パスワードの再設定 - PasswordResets#editの動作を、テスト駆動で実装していく

# PasswordResetsコントローラーの、ここまでに実装してきた内容に対するテスト

PasswordResetsコントローラーの`new`アクションおよび`create`アクションに対するテストは、Railsチュートリアル本文の内容に合わせていくと、以下のような内容になります。

```ruby:test/integration/password_resets_test.rb
require 'test_helper'

class PasswordResetsTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
    @user = users(:rhakurei)
  end

  test "password resets" do
    get new_password_reset_path
    assert_template 'password_resets/new'
    # メールアドレスが無効
    post password_resets_path, params: { password_reset: { email: "" } }
    assert_not flash.empty?
    assert_template 'password_resets/new'
    get new_password_reset_path
    assert flash.empty?
    # メールアドレスが有効
    post password_resets_path, params: { password_reset: { email: @user.email} }
    assert_not_equal @user.reset_digest, @user.reload.reset_digest
    assert_equal 1, ActionMailer::Base.deliveries.size
    assert_not flash.empty?
    assert_redirected_to root_url
  end
end
```

# テスト実装の前提となる、PasswordResetsコントローラーの`edit`アクションの動作

* 有効なメールアドレスと有効なパスワード再設定用トークンの組み合わせで`edit`アクションが呼び出された場合、type属性が`hidden`であるinput要素に当該メールアドレスを格納する
    * 後続の`update`アクションで使用するため
* 無効な`edit`アクションの呼び出しがあった場合、/ にリダイレクトする
    * 無効なメールアドレス
    * 有効化されていないユーザー
    * 無効なパスワード再設定用トークン

# PasswordResetsコントローラーの`edit`アクションに対するテスト

上記前提を踏まえた上で、PasswordResetsコントローラーの`edit`アクションに対するテストを追加していきましょう。

```diff:test/integration/password_resets_test.rb
  require 'test_helper'

  class PasswordResetsTest < ActionDispatch::IntegrationTest

    def setup
      ActionMailer::Base.deliveries.clear
      @user = users(:rhakurei)
    end

    test "password resets" do
      get new_password_reset_path
      assert_template 'password_resets/new'
      # メールアドレスが無効
      post password_resets_path, params: { password_reset: { email: "" } }
      assert_not flash.empty?
      assert_template 'password_resets/new'
      get new_password_reset_path
      assert flash.empty?
      # メールアドレスが有効
      post password_resets_path, params: { password_reset: { email: @user.email} }
      assert_not_equal @user.reset_digest, @user.reload.reset_digest
      assert_equal 1, ActionMailer::Base.deliveries.size
      assert_not flash.empty?
      assert_redirected_to root_url
+     # パスワード再設定フォームのテスト
+     user = assigns(:user)
+     # メールアドレスが無効
+     get edit_password_reset_path(user.reset_token, email: "")
+     assert_redirected_to root_url
+     # 無効なユーザー
+     user.toggle!(:activated)
+     get edit_password_reset_path(user.reset_token, email: user.email)
+     assert_redirected_to root_url
+     user.toggle!(:activated)
+     # メールアドレスが有効で、トークンが無効
+     get edit_password_reset_path('wrong token', email: user.email)
+     assert_redirected_to root_url
+     # メールアドレスもトークンも有効
+     get edit_password_reset_path(user.reset_token, email: user.email)
+     assert_template 'password_resets/edit'
+     assert_select "input[name=email][type=hidden][value=?]", user.email
    end
  end
```

# PasswordResetsコントローラーの`edit`アクションに対するテストの実装が完了した時点でテストを実行するとどうなるか

PasswordResetsコントローラーの`edit`アクションに対するテストの実装が完了した時点で、一度テストを実行してみます。

```
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 671
Started with run options --seed 36691

 FAIL["test_password_resets", PasswordResetsTest, 2.968936000001122]
 test_password_resets#PasswordResetsTest (2.97s)
        Expected response to be a <3XX: redirect>, but was a <200: OK>
        test/integration/password_resets_test.rb:29:in `block in <class:PasswordResetsTest>'

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.97318s
1 tests, 9 assertions, 1 failures, 0 errors, 0 skips
```

以下のようなメッセージが出力されてテストが失敗しています。

```
Expected response to be a <3XX: redirect>, but was a <200: OK>
```

「リダイレクトされるべきところ、リダイレクトされていない」という趣旨のメッセージですね。

# 無効な`edit`アクションの呼び出しがあった場合、/ にリダイレクトする

* 無効なメールアドレス
* 有効化されていないユーザー
* 無効なパスワード再設定用トークン

以上のようなパラメータで`edit`アクションの呼び出しが行われた場合、/ にリダイレクトされなければならない、という仕様でしたね。このような実装を実現するためには、PasswordResetsコントローラーのbeforeフィルターを使用します。

```diff:app/controllers/password_resets_controller.rb
  class PasswordResetsController < ApplicationController
+   before_action :get_user, only:   [:edit, :update]
+   before_action :valid_user, only: [:edit, :update]

    ...略
+
+   private
+
+     def get_user
+       @user = User.find_by(email: params[:email])
+     end
+
+     # 正しいユーザーかどうか確認する
+     def valid_user
+       unless (@user && @user.activated? && 
+               @user.authenticated?(:reset, params[id]))
+         redirect_to root_url
+       end
+     end
  end
```

「実際の判定と、/ へのリダイレクト」という処理の実体は、`valid_user`メソッドで行われています。`get_user`メソッドは、`valid_user`メソッドで処理を行う前提として、`@user`変数の内容を定義しています。

なお、`@user`という変数名が`create`アクション内でも使われていますが、`create`アクションの`@user`変数の内容と今回の`@user`変数の内容が混ざり合うことはありません。`get_user`メソッド・`valid_user`メソッドともに、`edit`アクションと`update`アクションにしか関係しないからです。

# 無効な`edit`アクションの呼び出しに対する処理を実装した時点でテストを実行するとどうなるか

```
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 684
Started with run options --seed 51259

ERROR["test_password_resets", PasswordResetsTest, 3.9712250999982643]
 test_password_resets#PasswordResetsTest (3.97s)
NameError:         NameError: undefined local variable or method `email' for #<PasswordResetsController:0x00005629bcfa2850>
            app/controllers/password_resets_controller.rb:27:in `get_user'
            test/integration/password_resets_test.rb:28:in `block in <class:PasswordResetsTest>'

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.97519s
1 tests, 8 assertions, 0 failures, 1 errors, 0 skips
```

以下のようなエラーが出てテストが失敗しています。

```
NameError: undefined local variable or method `email'
```

「`email`という未定義の変数またはメソッドが呼び出されている」というエラーです。「`edit`アクションの呼び出しの際、クエリパラメータに`email`が含まれるようにする」必要がありますね。

# `edit`アクションの呼び出しの際、クエリパラメータに`email`が含まれるようにする

この段階で、PasswordResetsリソースの`edit`アクションに対するビューを実装していきます。「クエリパラメータに`email`が含まれるようにすること」も含める必要がありますね。実装場所は`app/views/password_resets/edit.html.erb`です。

```erb:app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages'%>

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

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```

## `hidden_field_tag`というメソッドの意味

```ruby:
f.hidden_field :email, @user.email
```

`f.hidden_field`を使った以上のような呼び出しの場合、メールアドレスは`params[:user][:email]`に保存されます。

```ruby:
hidden_field_tag :email, @user.email
```

一方、`hidden_field_tag`を使った以上のような呼び出しの場合、メールアドレスは`params[:email]`に保存されます。

今回は、`params[:email]`を使った以下のような呼び出しが行われるのが前提です。「正当なクエリパラメータが構築されるためには、受け側には`f.hidden_field`ではなく`hidden_field_tag`が必要になる」ということですね。

```ruby:
@user = User.find_by(email: params[:email])
```

# PasswordResetsコントローラーの`edit`アクションの実装が完了

ここまでの実装が完了した時点で、PasswordResetsコントローラーの`edit`アクションに対するテストを実行してみます。

```
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 749
Started with run options --seed 30899

  1/1: [===================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.38865s
1 tests, 13 assertions, 0 failures, 0 errors, 0 skips
```

テストが成功しました。これで「PasswordResetsコントローラーの`edit`アクションの実装は完了した」といえますね。

# PasswordResetsコントローラーの`edit`アクションに対するテストは、逆にどのような実装抜けがあったらテストが失敗するのか

別記事にて解説しています。

* [Railsチュートリアル 第12章 パスワードの再設定 - PasswordResetsコントローラーのeditアクションに対するテストは、どのような実装抜けがあったらテストが失敗するのか](https://qiita.com/rapidliner00/items/e5d37d8f775841d8e110)