0
0

More than 3 years have passed since last update.

Railsチュートリアル 第12章 パスワードの再設定 - PasswordResets#createで、メールアドレスが有効な場合の処理をテスト駆動で実装していく

Posted at

メールアドレスが有効な場合の処理に対するテストの実装

Railsチュートリアル本文の通りに実装していくとすれば、以下のテストが「メールアドレスが有効な場合の処理に対するテスト」に該当することになります。

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.deriveries.size
+     assert_not flash.empty?
+     assert_redirected_to root_url
    end
  end

メールアドレスが有効な場合の処理に対するテストを実装した時点で、テストの結果はどうなるか

「メールアドレスが無効な場合の処理」が完了した時点における、app/controllers/password_resets_controller.rbのソースコードは以下です。

app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  def new
  end

  def create
    if false #TODO: 有効なユーザー情報を与えられるようにする
      #TODO: 有効なメールアドレスが与えられた場合の処理を実装する
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

  def edit
  end
end

テストの結果は以下のようになります。

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

 FAIL["test_password_resets", PasswordResetsTest, 2.7478716999994504]
 test_password_resets#PasswordResetsTest (2.75s)
        Expected nil to not be equal to nil.
        test/integration/password_resets_test.rb:19:in `block in <class:PasswordResetsTest>'

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

Finished in 2.74939s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips

以下のテストが、「Expected nil to not be equal to nil」というメッセージを返して失敗しています。

test/integration/password_resets_test.rb(19行目)
assert_not_equal @user.reset_digest, @user.reload.reset_digest

debuggerで確認したところ、@user.reset_digest@user.reload.reset_digestのいずれもnilとなっていました。「現在のところreset_digestを実装していない」からでしょうか。

ひとまずこの時点で確実なのは、「Userモデルにreset_digest属性に対する正しい実装が必要である」ということです。

User#create_reset_digestメソッドの実装

「Userモデルのreset_digest属性に対する正しい実装」は、パスワード再設定全体の中では、以下の手順に関連しています。

  • パスワード再設定用のトークンとダイジェストの組を生成する
  • 生成されたパスワード再設定用ダイジェストをRDBに保存する

Railsチュートリアル本文においては、これら一連の処理について、create_reset_digestメソッドとして定義されています。

早速、当該メソッドと関連する実装を追加していきましょう。

Userモデルにcreate_reset_digestメソッドと、関連する実装を追加する

app/models/user.rb
  class User < ApplicationRecord
-   attr_accessor :remember_token, :activation_token
+   attr_accessor :remember_token, :activation_token, :reset_token
    ...略
+
+   # パスワード再設定の属性を設定する
+   def create_reset_digest
+     self.reset_token = User.new_token
+     update_attribute(:reset_digest, User.digest(reset_token))
+     update_attribute(:reset_sent_at, Time.zone.now)
+   end

    private

      # メールアドレスをすべて小文字にする
      def downcase_email
        self.email.downcase!
      end

      # 有効化トークンとダイジェストを作成および代入する
      def create_activation_digest
        self.activation_token = User.new_token
        self.activation_digest = User.digest(activation_token)
      end
  end

Userモデルに新たに追加した実装は以下です。

  • create_reset_digestメソッド
    • パスワード再設定用トークンの生成
    • 生成されたトークンに対するダイジェストのRDBへの保存
    • パスワード再設定用トークンの生成日時のRDBへの保存
  • 仮想属性:reset_tokenに対するゲッターとセッターの追加

PasswordResetsコントローラーで、create_reset_digestメソッドを使うようにする

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 false #TODO: 有効なユーザー情報を与えられるようにする
+     if @user
-       #TODO: 有効なメールアドレスが与えられた場合の処理を実装する
+       @user.create_reset_digest
+       # TODO:メール送信処理を実装する
      else
        flash.now[:danger] = "Email address not found"
        render 'new'
      end
    end

    def edit
    end
  end

PasswordResetsコントローラーに新たに追加した実装は以下です。

  • @userを使わない仮実装を、@userを使う正式な実装に変更する
  • @user.create_reset_digestにより、パスワード再設定用トークン・ダイジェストの生成が正しく行われるようにする

create_reset_digestメソッド、および、関連する実装を追加した時点でのテストの結果

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

ERROR["test_password_resets", PasswordResetsTest, 3.3415248000001156]
 test_password_resets#PasswordResetsTest (3.34s)
NameError:         NameError: uninitialized constant PasswordResetsTest::Actionmailer
            test/integration/password_resets_test.rb:20:in `block in <class:PasswordResetsTest>'

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

Finished in 3.34828s
1 tests, 4 assertions, 0 failures, 1 errors, 0 skips

今度は以下のテストがエラーを返して失敗するようになりました。

test/integration/password_resets_test.rb(20行目)
assert_equal 1, Actionmailer::Base.deriveries.size

メールの送信処理が実装されていないことが原因のようです。

なお、Railsチュートリアル本文中の演習 - createアクションでパスワード再設定を行う場合、この時点で一旦実装を中断した上で演習を行っていきます。

メールの送信処理の実装

「アプリケーションは、パスワード再設定用メールを作成し、フォームで指定されたメールアドレスに送信する」という処理の実装部分です。

Usersモデルにsend_password_reset_emailメソッドを追加する

  class User < ApplicationRecord
    ...略    

    # 有効化用のメールを送信する
    def send_activation_email
      UserMailer.account_activation(self).deliver_now
    end

    # パスワード再設定の属性を設定する
    def create_reset_digest
      self.reset_token = User.new_token
      update_attribute(:reset_digest, User.digest(reset_token))
      update_attribute(:reset_sent_at, Time.zone.now)
    end
+
+   # パスワード再設定のメールを送信する
+   def send_password_reset_email
+     UserMailer.password_reset(self).deliver_now
+   end

    private

      # メールアドレスをすべて小文字にする
      def downcase_email
        self.email.downcase!
      end

      # 有効化トークンとダイジェストを作成および代入する
      def create_activation_digest
        self.activation_token = User.new_token
        self.activation_digest = User.digest(activation_token)
      end
  end

パスワード再設定用のメールを送信するメソッドは、send_password_reset_emailという名前で定義しています。

…よくよく見ると、send_activation_emailsend_password_reset_emailのコードって似てますよね。後々リファクタリングの対象として出てくるかもしれません。

PasswordResetsコントローラーで、send_password_reset_emailメソッドを使うようにする

  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
+       #TODO: フラッシュメッセージの定義とルートへのリダイレクト
      else
        flash.now[:danger] = "Email address not found"
        render 'new'
      end
    end

    def edit
    end
  end
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 191
Started with run options --seed 20579

ERROR["test_password_resets", PasswordResetsTest, 2.324982799999816]
 test_password_resets#PasswordResetsTest (2.33s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 0)
            app/mailers/user_mailer.rb:8:in `password_reset'
            app/models/user.rb:61:in `send_password_reset_email'
            app/controllers/password_resets_controller.rb:9:in `create'
            test/integration/password_resets_test.rb:18:in `block in <class:PasswordResetsTest>'

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

Finished in 2.32883s
1 tests, 3 assertions, 0 failures, 1 errors, 0 skips

app/mailers/user_mailer.rbの8行目で、「引数の数が0でなければならないのに、実際には1つの引数が渡されている」というエラーでテストが失敗しています。

app/mailers/user_mailer.rb(8行目)
def password_reset

今度はパスワード再設定用のメイラーメソッドを定義する必要がありそうですね。

パスワード再設定用のメイラーメソッドpassword_resetにおいて、引数の定義を変更する

まずは、現在のテストの失敗原因として指摘されている「引数の数が足りない」という問題を解決します。

  class UserMailer < ApplicationMailer

    def account_activation(user)
      @user = user
      mail to: user.email, subject: "Account activation"
    end

-   def password_reset
+   def password_reset(user)
-     @greeting = "Hi"
-     
-     mail to: "to@example.org"
    end
  end

password_resetの引数の定義を変更した時点でのテストの結果

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

ERROR["test_password_resets", PasswordResetsTest, 2.472823599999174]
 test_password_resets#PasswordResetsTest (2.47s)
NameError:         NameError: uninitialized constant PasswordResetsTest::Actionmailer
            test/integration/password_resets_test.rb:20:in `block in <class:PasswordResetsTest>'

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

Finished in 2.47449s
1 tests, 4 assertions, 0 failures, 1 errors, 0 skips
(byebug) Actionmailer::Base.deriveries.size
*** NameError Exception: uninitialized constant PasswordResetsTest::Actionmailer

nil

debuggerで調べたところ、「uninitialized constant PasswordResetsTest::Actionmailer」というのは、「Actionmailer::Base.deriveries.sizeが初期化されていないこと」が原因で発生しているようです。「password_resetメソッドの実効的な定義と、テキストメール・HTMLメールそれぞれのテンプレートが必要になる」ということでしょうか。

password_resetメソッドの実効的な定義と、テキストメール・HTMLメールそれぞれのテンプレート

password_resetメソッドの実効的な定義

app/mailers/user_mailer.rb
  class UserMailer < ApplicationMailer

    def account_activation(user)
      @user = user
      mail to: user.email, subject: "Account activation"
    end

    def password_reset(user)
+     @user = user
+     mail to: user.email, subject: "Password reset"
    end
  end

password_resetメソッドの実効的な定義には、以下の内容が含まれます。

  • メール本文中で使用する@userの内容
  • メールの宛先と題名

テキストメールのテンプレート

app/views/user_mailer/password_reset.text.erb
To reset your password click the link below:

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

This will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password stay as it is.

HTMLメールのテンプレート

test/mailers/previews/user_mailer_preview.rb
<h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

<p>This will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
</p>

パスワード再設定メールのプレビュー

パスワード再設定メールをプレビューできるようにする

パスワード再設定メールをプレビューできるようにするために、test/mailers/previews/user_mailer_preview.rbの内容を書き換えていきます。

test/mailers/previews/user_mailer_preview.rb
  # Preview all emails at http://localhost:8080/rails/mailers/user_mailer
  class UserMailerPreview < ActionMailer::Preview

    # Preview this email at http://localhost:8080/rails/mailers/user_mailer/account_activation
    def account_activation
      user = User.first
      user.activation_token = User.new_token
      UserMailer.account_activation(user)
    end

    # Preview this email at http://localhost:8080/rails/mailers/user_mailer/password_reset
    def password_reset
-     UserMailer.password_reset
+     user = User.first
+     user.reset_token = User.new_token
+     UserMailer.password_reset(user)
    end

  end

パスワード再設定メールをプレビューする

ここまでの実装が完了すれば、test/mailers/previews/user_mailer_preview.rbのコメント中に書かれたURLから、パスワード再設定用メールをプレビューすることができるようになります。

なお、事前にrails serverでサーバーを起動しておく必要があります。

下記はHTMLメールのプレビューです。

スクリーンショット 2019-12-14 8.35.45.png

下記はテキストメールのプレビューです。

スクリーンショット 2019-12-14 8.35.54.png

メール送信処理を実装した時点でのテストの結果

メール送信処理を実装した時点でのテストの結果は、以下のようになります。

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

 FAIL["test_password_resets", PasswordResetsTest, 4.604623200000788]
 test_password_resets#PasswordResetsTest (4.60s)
        Expected true to be nil or false
        test/integration/password_resets_test.rb:23:in `block in <class:PasswordResetsTest>'

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

Finished in 4.61010s
1 tests, 7 assertions, 1 failures, 0 errors, 0 skips

私の環境では、test/integration/password_resets_test.rbの23行目には以下の記述があります。

test/integration/password_resets_test.rb(23行目)
assert_not flash.empty?

のフラッシュメッセージが定義されていないことに起因する失敗ですね。

メール送信成功時のフラッシュメッセージを追加する

メール送信成功時のフラッシュメッセージの実装を、app/controllers/password_resets_controller.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] = "Email sent with password reset instructions"
      else
        flash.now[:danger] = "Email address not found"
        render 'new'
      end
    end

    def edit
    end
  end

メール送信成功時のフラッシュメッセージを追加した時点でのテストの結果

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

 FAIL["test_password_resets", PasswordResetsTest, 4.58620990000054]
 test_password_resets#PasswordResetsTest (4.59s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/integration/password_resets_test.rb:24:in `block in <class:PasswordResetsTest>'

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

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

私の環境では、test/integration/password_resets_test.rbの24行目には以下の記述があります。

test/integration/password_resets_test.rb(24行目)
assert_redirected_to root_url

「/ へリダイレクトされるべきところ、リダイレクトされていない」という失敗ですね。

メール送信成功時の処理に、/ へのリダイレクトを追加する

パスワード再設定用メールの送信が成功した場合、PasswordResetsController#createアクションの最後は / へのリダイレクトで終了します。ここで / へのリダイレクトを追加します。

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] = "Email sent with password reset instructions"
+       redirect_to root_url
      else
        flash.now[:danger] = "Email address not found"
        render 'new'
      end
    end

    def edit
    end
  end

/ へのリダイレクトを追加した時点でのテストの結果

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

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

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

ついにテストが成功しました。これにて、「有効なメールアドレスが与えられた際における、PasswordResetsController#createの実装」ならびに「PasswordResetsController#createの実装全体」が完了となりました。

現在までのテストが成功した時点における、パスワード再設定メールの送信用フォームに有効なメールアドレスを入力してSubmitボタンを押したときの挙動

まず、/password_resets に対してPOSTリクエストが送出され、PasswordResetsコントローラーのcreateメソッドが開始されます。

Started POST "/password_resets" for 172.17.0.1 at 2019-12-14 06:09:11 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by PasswordResetsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"0tty+a/E7Mp+BqIOZDaOGJQ2fH43di2VEeR3RaA9VC9MpoIb9a3BwgBNauyHYe4nBN1XC5M1TVglcsCUFNdAEQ==", "password_reset"=>"[FILTERED]", "commit"=>"Submit"}
  User Load (8.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "example-2@railstutorial.org"], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (14.0ms)  UPDATE "users" SET "reset_digest" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["reset_digest", "$2a$10$vD1xJCRoVswPj2qHsJD81OLCO3e/aviIRNCamE6OWi8TUkkG6HytS"], ["updated_at", "2019-12-14 06:09:11.878637"], ["id", 3]]
   (11.2ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (15.4ms)  UPDATE "users" SET "updated_at" = ?, "reset_sent_at" = ? WHERE "users"."id" = ?  [["updated_at", "2019-12-14 06:09:11.916190"], ["reset_sent_at", "2019-12-14 06:09:11.912111"], ["id", 3]]
   (14.7ms)  commit transaction
  Rendering user_mailer/password_reset.html.erb within layouts/mailer
  Rendered user_mailer/password_reset.html.erb within layouts/mailer (1.0ms)
  Rendering user_mailer/password_reset.text.erb within layouts/mailer
  Rendered user_mailer/password_reset.text.erb within layouts/mailer (0.7ms)
UserMailer#password_reset: processed outbound mail in 294.4ms

以下のはパスワード再設定用メールのヘッダー部分です。

Sent mail to example-2@railstutorial.org (3.5ms)
Date: Sat, 14 Dec 2019 06:09:12 +0000
From: noreply@example.com
To: example-2@railstutorial.org
Message-ID: <5df47c883f6f2_1c62ac8ce76084c8706b@705320d4d96d.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f";
 charset=UTF-8
Content-Transfer-Encoding: 7bit

以下のログはテキスト形式のパスワード再設定用メールの内容です。

----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

To reset your password click the link below:

https://localhost:3000/password_resets/bbZxxIr2r21HmbfMzsKevA/edit?email=example-2%40railstutorial.org

This will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password stay as it is.

以下のログはHTML形式のパスワード再設定用メールの内容です。

----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <h1>Password reset</h1>

<p>To reset your password click the link below:</p>

https://localhost:3000/password_resets/bbZxxIr2r21HmbfMzsKevA/edit?email=example-2%40railstutorial.org

<p>This will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
</p>

  </body>
</html>

----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f--

パスワード再設定用メールの内容についてのログは以上です。

Redirected to http://localhost:8080/
Completed 302 Found in 477ms (ActiveRecord: 63.7ms)

/password_resets に対するPOSTリクエスト(すなわちPasswordResetsコントローラーのcreateアクション)が、「302 FOUND」というステータスコードを返し、/ に対するリダイレクトによって完了しました。

以降は、リダイレクト後の、/ に対するGETリクエストのログです。

Started GET "/" for 172.17.0.1 at 2019-12-14 06:09:12 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by StaticPagesController#home as HTML
  Rendering static_pages/home.html.erb within layouts/application
  Rendered static_pages/home.html.erb within layouts/application (18.3ms)
  Rendered layouts/_rails_default.erb (220.1ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  Rendered layouts/_header.html.erb (1.0ms)
  Rendered layouts/_footer.html.erb (0.7ms)
Completed 200 OK in 377ms (Views: 359.5ms | ActiveRecord: 0.0ms)

/ の描画が、「200 OK」というステータスコードとともに正常に完了していますね。

実際にWebブラウザでパスワード再設定メールの送信用フォームにSubmitしてみる

まずは、パスワード再設定メールの送信用フォームを表示します。

スクリーンショット 2019-12-14 15.09.08.png

有効なメールアドレスを入力し、「Submit」ボタンを押すと、以下の画面が表示されます。

スクリーンショット 2019-12-14 15.09.18.png

確かに「Email sent with password reset instructions」というフラッシュメッセージが表示されていますね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0