railsチュートリアル12章備忘録

##概要
チュートリアル２周目の自分へ向けてさらっと
なにやったか要点だけおさえておく。

##パスワードの再設定

``$ git checkout -b password-reset``

この章でやること

- ログインページのformに「forgot password」リンクを追加
- 「forgot password」formのページ作成
- 「Reset password」パスワード再設定の確認を求めるformのページ作成

全体の流れ

1. ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてDBからユーザーを見つける

2. 該当のメールアドレスがDBにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成する

3. 再設定用ダイジェストはDBに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく

4. ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、DB内に保存しておいた再設定用ダイジェストと比較する（トークンを認証する）

5. 認証に成功したら、パスワード変更用のformをユーザーに表示する


##PasswordResetsリソース
新たなモデルは作らずに、代わりに必要なデータ（再設定用のダイジェストなど）をUserモデルに追加していく。

###PasswordResetsコントローラ
最初のステップとしてパスワード再設定用のコントローラを作成
``$ rails generate controller PasswordResets new edit --no-test-framework``

``no-test``　-> テストを生成しないオプション
コントローラの単体テストをする代わりに、統合テストでカバーしていくため。

今回の実装では、新しいパスワードを再設定するためのフォームと、
Userモデル内のパスワードを変更するためのフォームが必要になる。
``new、create、edit、update``のルーティングも用意する。

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

```app/views/sessions/new.html.erb
      <%= link_to "(forgot password)", new_password_reset_path %>
```

| HTTPリクエスト | URL | Action | 名前付きルート |
|:-:|:-:|:-:|:-:|:-:|
|GET| /password_resets/new | new |  new_password_reset_path |   
|POST| /password_resets | create | password_resets_path  |   
|GET| /password_resets/トークン/edit | edit | edit_password_reset_url(token)  |   
|PATCH| /password_resets/トークン | update |  password_reset_url(token) |   

``new_password_reset_path``は、RESTfulのルーティングに従っている。


###新しいパスワードの設定
Userモデルにパスワード再設定用のカラムを追加

| users | 型 |
|:-:|:-:|
|  reset_digest | string  |
|  reset_sent_at | datetime |
マイグレーションに属性を追加
``$ rails generate migration add_reset_to_users reset_digest:string \``
``> reset_sent_at:datetime``

``$ rails db:migrate``

```app/views/sessions/new.html.erb(再掲載)
      <%= link_to "(forgot password)", new_password_reset_path %>
```

###createアクションでパスワード再設定

- formから送信を行なった後、メールアドレスをキーとしてユーザーをDBから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでDBの属性を更新する必要がある。

- その後ルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示。送信が無効の場合は、ログインと同様にnewページを出力してflash.nowメッセージを表示する。

##パスワード再設定のメール送信
Userメーラー（``app/mailers/user_mailer.rb``）を生成したときに、
デフォルトのpassword_resetメソッドもまとめて生成されている。

###パスワード再設定のメールとテンプレート
UserメイラーにあるコードをUserモデルに移すリファクタリング
``UserMailer.password_reset(self).deliver_now``
パスワード再設定に対しても行う

####パスワード再設定のリンクをメール送信

```app/mailers/user_mailer.rb
  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password reset"
  end
```

####パスワード再設定のテンプレート（テキスト）作成
``app/views/user_mailer/password_reset.text.erb``

####パスワード再設定のテンプレート（HTML）作成
``app/views/user_mailer/password_reset.text.erb``



####Railsのメールプレビュー機能でパスワード再設定のメールをプレビューするメソッド

```test/mailers/previews/user_mailer_preview.rb
  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end
```
railsサーバーのlogでパスワード再設定メールを確認。

###送信メールのテスト
メーラーメソッドのテストを書く

```test/mailers/user_mailer_test.rb
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase

  test "password_reset" do
    user = users(:michael)
    user.reset_token = User.new_token
    mail = UserMailer.password_reset(user)
    assert_equal "Password reset", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.reset_token,        mail.body.encoded
    assert_match CGI.escape(user.email),  mail.body.encoded
  end
end
```

##パスワードを再設定する
PasswordResetsコントローラのeditアクションの実装を進める。

###editアクションで再設定
パスワード再設定の送信メールには、次のようなリンクが含まれている
``https://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=fu%40bar.com``

このリンクを機能させるには、パスワード再設定フォームを表示するビュー
(パスワード入力フィールドと確認用フィールド)が必要。
面倒な点
-> メールアドレスをキーとしてユーザーを検索するためには、editアクションとupdateアクションの両方でメールアドレスが必要。

editアクションでメールアドレスを取り出すことは問題ないが、フォームを一度送信してしまうと、この情報は消えてしまう。
-> ページ内に隠しフィールドとして保存する手法をとれば、フォームから送信したときに他の情報と一緒にメールアドレスが送信される。

```app/views/password_resets/edit.html.erb
      <%= hidden_field_tag :email, @user.email %>
```


###パスワードを更新する
formから新しいパスワードを送信するには、formからの送信に対応するupdateアクションが必要。
このupdateアクションでは、次の４つのケースを考慮する必要がある。

1. パスワード再設定の有効期限が切れていないか
2. 無効なパスワードであれば失敗させる（失敗した理由も表示する）
3. 新しいパスワードが空文字列になっていないか（ユーザー情報の編集ではOKだった）
4. 新しいパスワードが正しければ、更新する

(1)はeditとupdateアクションに次のようなメソッドとbeforeフィルターを用意することで対応する。
``before_action :check_expiration, only: [:edit, :update]``


```ruby
#期限切れかどうかを確認する
def check_expiration
  if @user.password_reset_expired?
    flash[:danger] = "Password reset has expired."
    redirect_to new_password_reset_url
  end
end
```
``password_reset_expired?`` 
-> 期限切れかどうかを確認するインスタンスメソッド。Userモデルで定義する。
2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。

```app/models/user.rb
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end
```



(2)更新が失敗したときにeditのビューを再描画させ、
``<%= hidden_field_tag :email, @user.email %>``にエラーメッセージが表示させ解決。
(4)更新が成功したときにパスワードを再設定し、あとはログインに成功したときと同様の処理を進めて解決。

(3)今回の問題点は、パスワードが空文字だった場合の処理。
理由は以前Userモデルを作った時に、パスワードが空でも良い（``allow_nil``)実装をしたため。
今回は@userオブジェクトに``errors.add``を使ってエラーメッセージを追加する。
``@user.errors.add(:password, :blank)``
-> パスワードが空だった時に空の文字列に対するデフォルトのメッセージを表示してくれる。

``app/controllers/password_resets_controller.rb``にあてはめる。

##パスワードの再設定をテストする
送信に成功した場合と失敗した場合の統合テストを作成する。
``$ rails generate integration_test password_resets``

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

class PasswordResetsTest < ActionDispatch::IntegrationTest

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

  test "password resets" do
    get new_password_reset_path
    assert_template 'password_resets/new'
    assert_select 'input[name=?]', 'password_reset[email]'
    # メールアドレスが無効
    post password_resets_path, params: { password_reset: { email: "" } }
    assert_not flash.empty?
    assert_template 'password_resets/new'
    # メールアドレスが有効
    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
    # 無効なパスワードとパスワード確認
    patch password_reset_path(user.reset_token),
          params: { email: user.email,
                    user: { password:              "foobaz",
                            password_confirmation: "barquux" } }
    assert_select 'div#error_explanation'
    # パスワードが空
    patch password_reset_path(user.reset_token),
          params: { email: user.email,
                    user: { password:              "",
                            password_confirmation: "" } }
    assert_select 'div#error_explanation'
    # 有効なパスワードとパスワード確認
    patch password_reset_path(user.reset_token),
          params: { email: user.email,
                    user: { password:              "foobaz",
                            password_confirmation: "foobaz" } }
    assert is_logged_in?
    assert_not flash.empty?
    assert_redirected_to user
  end
end
```