ぞえの技術めも

Ruby on Rails勉強中

【138日目】【1日20分のRailsチュートリアル】【第10章】パスワード再設定のテストを作成する

Ruby on Railsチュートリアル(第3版)

今日は「10.2.5 パスワードの再設定をテストする」から。

10.2.5 パスワードの再設定をテストする

まずはパスワード再設定のテストファイルを生成しましょう。

生成しました。

$ rails generate integration_test password_resets
      invoke  test_unit
      create    test/integration/password_resets_test.rb

テストの冒頭部分には次のような違いがあります: 最初に「forgot password」フォームを表示して無効なメールアドレスを送信し、次はそのフォームで有効なメールアドレスを送信します。
後者ではパスワード再設定用トークンが作成され、再設定用メールが送信されます。続いて、メールのリンクを開いて無効な情報を送信し、次にそのリンクから有効な情報を送信して、それぞれが期待どおりに動作することを確認します。

長い…!うっかりどれかのテスト書くの忘れそう。。

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

今回の新しい要素はinputタグぐらいでしょう
assert_select "input[name=email][type=hidden][value=?]", user.email

こんな書き方でinputタグのテストができるのかー。

user.toggle!(:activated) って出てきたことあったっけ、と思ってたらあったわ。

引数に:activatedを渡すことでアカウント有効化の切り替えができるんだね。

テストで問題ないことを確認。

$ bundle exec rake test
45 tests, 210 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【23分】

次は「10.3 本番環境でのメール」から。

【137日目】【1日20分のRailsチュートリアル】【第10章】パスワードリセットのupdateアクションを実装する

Ruby on Railsチュートリアル(第3版)

今日は「10.2.4 パスワードを再設定する」のupdateアクションを定義するところから。

10.2.4 パスワードを再設定する

リスト10.51のeditアクションに対応するupdateアクションを定義するには、4通りの場合分けに対応する必要があります:
パスワード再設定の期限が切れている場合、更新に成功した場合、更新が失敗した場合 (パスワードが正しくないなど)、更新が失敗した場合 (一見更新が成功したように見えるがパスワードが2つとも空欄) です。

いっぱい確認しなきゃいけないことあるんだな…。

app/controllers/password_resets_controller.rb

  :
  before_action :check_expiration, only: [:edit, :update]
  :
  def update
    if params[:user][:password].empty?
      @user.errors.add(:password,  "can't be empty")
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end
  :

    # 再設定用トークンが期限切れかどうかを確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end

上のコードが動作するには、このpassword_reset_expired?メソッドを定義する必要があります。
(中略)
reset_sent_at < 2.hours.ago
この「<」記号を「〜より少ない」と読んでしまうと、「パスワード再設定メール送信時から経過した時間が、2時間より少ない場合」となってしまい、ここで行おうとしていることと反対の意味になってしまいます。

えー!難しいな…。

2.hours.agoは「現在時刻より2時間前の時間」を取得できるので、2時間前の時間がreset_sent_at(パスワード再設定メールを送信した時間)より大きい場合は2時間経過していることになり、trueを返す、ってことか。
こう考えると別におかしくないな。

app/models/user.rb

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

リスト10.53のコードを使用すると、リスト10.52のupdateアクションが動作するようになります。

動作見てみよう。

前回起動できなかったサーバーは何もしてないのに起動できるようになってた。

何事もなかったかのようにサーバーを起動して、

$ rails server -b $IP -p $PORT

試しに以前パスワードリセットしてみたときのURLにアクセスしてみる。

f:id:kt_zoe:20170424124202p:plain

当然パスワード再設定の期限は切れているのでエラーメッセージが表示されました。

パスワード再設定メールを再度送信して、今度は空のパスワードを再設定しようとしてみる。

f:id:kt_zoe:20170424124240p:plain

エラーメッセージが表示されました。

エラーにならないパスワードを再設定するとパスワード再設定に成功した旨のメッセージが表示され、プロフィール画面が表示されました。

f:id:kt_zoe:20170424124257p:plain

問題なさそう。

今日の学習時間は【25分】

次は「10.2.5 パスワードの再設定をテストする」から。

【136日目】【1日20分のRailsチュートリアル】【第10章】パスワード再設定フォームを実装する

Ruby on Railsチュートリアル(第3版)

今日は「10.2.4 パスワードを再設定する」から。

10.2.4 パスワードを再設定する

フォームリンクが動作するためには、パスワード再設定のフォームが必要です。
この作業はユーザーのeditビューでユーザーを更新する (リスト9.2) のと似ていますが、今回はパスワード入力フィールドと確認用フィールドだけを使います。 (中略) このメールアドレスの最適な保存方法は、隠しフィールドとしてページ内に保存することです。

画面にはパスワード入力フィールドとパスワード確認用フィールドのみが表示されるけど、隠しフィールドでメールアドレスも送るようにするのか…!

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, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

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

再設定用のリンクをクリックすると、前者ではメールアドレスがparams[:email]に保存されますが、後者を使用するとparams[:user][:email]に保存されてしまうからです。

どっちでも良くない?なんて思ってしまったけど、取り出すときに後者だとできないのかな…。

このフォームを出力 (レンダリング) するためにPasswordResetsコントローラのeditアクション内で@userインスタンス変数を定義する必要があります。

before_actionで@userインスタンス変数を定義して正当なアカウントか確認する。

app/controllers/password_resets_controller.rb

  before_action :get_user,   only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
    :
  def edit
  end

  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

サーバー起動して画面見てみようとしたらサーバー起動しない…!

$ rails server -b $IP -p $PORT
  :
A server is already running. Check /home/ubuntu/workspace/sample_app/tmp/pids/server.pid.
Exiting

次のとき調べよう…psコマンドでそれらしいプロセスないんだけどな…。

今日の学習時間は【20分】

次は「10.2.4 パスワードを再設定する」のupdateアクションを定義するところから。
でもその前にサーバー起動できるようにするところから。。

【135日目】【1日20分のRailsチュートリアル】【第10章】パスワード再設定用メイラーメソッドのテストを書く

Ruby on Railsチュートリアル(第3版)

今日は「10.2.3 PasswordResetsメイラーメソッド」のテストを書くところから。

10.2.3 PasswordResetsメイラーメソッド

アカウント有効化メイラーメソッドのテスト (リスト10.18) の場合と同様、パスワード再設定用メイラーメソッドのテストを書くことにします (リスト10.47)。

アカウント有効化のときと同じようなテストを作成する。

test/mailers/user_mailer_test.rb

  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

テスト実行して問題ないことを確認。

$ bundle exec rake test
44 tests, 192 assertions, 0 failures, 0 errors, 0 skips

リスト10.43、リスト10.44、リスト10.45のコードを使用すると、正しいメールアドレスを送信したときの画面は図10.16のようになります。このメールはサーバーログではリスト10.49のように表示されます。

正しいメールアドレス送信してみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

パスワード再設定画面から有効なメールアドレスを送信。

f:id:kt_zoe:20170419123705p:plain

リダイレクト後の画面にflashメッセージが表示されることを確認。

パスワード再設定メールもサーバーログに出力されてました。

Sent mail to <メールアドレス> (16.9ms)
Date: Wed, 19 Apr 2017 01:58:18 +0000
From: noreply@example.com
To: <メールアドレス>
Message-ID: <58f6c43a2f077_6c13f9569dca45481827@kt-zoe-rails-tutorial-3478208.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_58f6c43a2b791_6c13f9569dca4548172a";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


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

To reset your password click the link below:

http://rails-tutorial-kt-zoe.c9users.io//password_resets/DQVXIK-zO7OtQwQfF7VaWQ/edit?email=<メールアドレス>

This link will expire in two hours. 

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

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

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

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

<a href="http://rails-tutorial-kt-zoe.c9users.io//password_resets/DQVXIK-zO7OtQwQfF7VaWQ/edit?email=<メールアドレス>">Reset password</a>

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

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

----==_mimepart_58f6c43a2b791_6c13f9569dca4548172a--

(<メールアドレス>の部分は実際にはフォームに入れたメールアドレスが表示される)

メールに記載されているURLが動作するにはまだ実装が必要らしい。もうちょいかな。

今日の学習時間は【17分】

次は「10.2.4 パスワードを再設定する」から。

【134日目】【1日20分のRailsチュートリアル】【第10章】パスワードリセットのメールプレビュー機能を実装する

Ruby on Railsチュートリアル(第3版)

今日は「10.2.3 PasswordResetsメイラーメソッド」から。

10.2.3 PasswordResetsメイラーメソッド

最初にユーザーメイラーにpassword_resetメソッドを作成し (リスト10.43)、続いてテキストメールのビューテンプレート (リスト10.44) と HTMLメールのビューテンプレート (リスト10.45) をそれぞれ定義します。

アカウント有効化のときと同じようにメソッドとビュー追加。

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

To reset your password click the link below:

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

This link will expire in two hours. 

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

app/views/user_mailer/password_reset.html.erb

<h1>Password reset</h1>

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

<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
                                                      email: @user.email) %>

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

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

アカウント有効化メールの場合 (10.1.2) と同様、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 server -b $IP -p $PORT

<ローカルアドレス>/rails/mailers/user_mailer/password_resetにアクセス。

f:id:kt_zoe:20170417123833p:plain

f:id:kt_zoe:20170417123845p:plain

パスワードリセットのメールプレビューが確認できました。

今日の学習時間は【17分】

次は「10.2.3 PasswordResetsメイラーメソッド」のテストを書くところから。

【133日目】【1日20分のRailsチュートリアル】【第10章】パスワード再設定画面でメールアドレスを送信したときの処理

Ruby on Railsチュートリアル(第3版)

今日は「10.2.2 PasswordResetsコントローラとフォーム」のフォームからメールアドレスを送信するところから。

10.2.2 PasswordResetsコントローラとフォーム

図10.12のフォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要があります。
(中略) 送信が無効の場合は、ログイン (リスト8.9) と同様にnewページを出力してflash.nowメッセージを表示します。

パスワード再設定フォームからメールアドレスが送信された後の処理をcreateアクションとして追加する。

app/controllers/password_resets_controller.rb

  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

Userモデル内のコードは、before_createコールバック (リスト10.3) 内で使用されるcreate_activation_digestメソッドと似ています(リスト10.42)。

上で追加したcreate_reset_digestメソッドとかあったっけ、と思ってたらここで追加するのか。
アカウント有効化の処理と似てますね。

app/models/user.rb

  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

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end
    :

図10.13に示すように、この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作します。

サーバーを起動して

$ rails server -b $IP -p $PORT

パスワード再設定画面で適当なメールアドレス(test01@example.com)を入れて送信してみる。

f:id:kt_zoe:20170413122631p:plain

エラーのfalshメッセージ表示されました。

正しいメールアドレス送信が正常に動作するにはメイラーメソッドの定義が必要らしい。まだか。

今日の学習時間は【26分】

次は「10.2.3 PasswordResetsメイラーメソッド」から。

【132日目】【1日20分のRailsチュートリアル】【第10章】パスワード再設定フォームを追加する

Ruby on Railsチュートリアル(第3版)

今日は「10.2.2 PasswordResetsコントローラとフォーム」から。

10.2.2 PasswordResetsコントローラとフォーム

ログインフォームを参考に、パスワード再設定フォームのビューを実装する。

app/views/password_resets/new.html.erb

<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

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

サーバーを起動して

$ rails server -b $IP -p $PORT

ログインフォームの「forgot password」から
<ローカルアドレス>/password_resets/newにアクセス。

f:id:kt_zoe:20170412124235p:plain

パスワード再設定フォームが表示されました。

今日はかなり短いけどここまで。。。

今日の学習時間は【7分】

次は「10.2.2 PasswordResetsコントローラとフォーム」のフォームからメールアドレスを送信するところから。