12.3.3 パスワードの再設定をテストする
パスワード再設定をテストする手順は、アカウント有効化のテストと多くの共通点がありますが、
テストの冒頭部分には次のような違いがあります。
最初に「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, 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
assert_select “input[name=email][type=hidden][value=?]”
, user.email
上のコードは、inputタグに正しい名前、type=”hidden”、
メールアドレスがあるかどうかを確認します。
rails test
演習
1:create_reset_digestメソッドはupdate_attributeを
2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。
テンプレートを使って、update_attributeの呼び出しを
1回のupdate_columns呼び出しにまとめてみましょう。
(これでデータベースへの問い合わせが1回で済むようになります)。
また、変更後にテストを実行し、 greenになることも確認してください。
user.rb (前略) # パスワード再設定の属性を設定する 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
2:テンプレートを埋めて、期限切れのパスワード再設定で発生する分岐を統合テストで網羅してみましょう(response.bodyは、
そのページのHTML本文をすべて返すメソッドです)。
期限切れをテストする方法はいくつかありますが、
オススメした手法を使えば、
レスポンスの本文に「expired」という語があるかどうかでチェックできます(なお、大文字と小文字は区別されません)。
assert_match /FILL_IN/i, response.body expired
password_resets_test.rb (前略) test "expired token" do get new_password_reset_path post password_resets_path, params: { password_reset: { email: @user.email } } @user = assigns(:user) @user.update_attribute(:reset_sent_at, 3.hours.ago) patch password_reset_path(@user.reset_token), params: { email: @user.email, user: { password: "foobar", password_confirmation: "foobar" } } assert_response :redirect follow_redirect! #追加 assert_match "expired", response.body end
3:2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。
しかし、もっと良くする方法はまだあります。
例えば、公共の (または共有された) コンピューターでパスワード再設定が行われた場合を考えてみてください。
仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを
表示させ、パスワードを更新してしまうことができてしまいます
(しかもそのままログイン機構まで突破されてしまいます!)。
この問題を解決するために、コードを追加し、
パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょ。
パスワード再設定が成功したらダイジェストをnilにする
app/controllers/password_resets_controller.rb
@user.update_attribute(:reset_digest, nil)
4:1行追加し、1つ前の演習課題に対するテストを書いてみましょう。assert_nilメソッドとuser.reloadメソッドを組み合わせて、
reset_digest属性を直接テストしてみましょう。
password_resets_test.rb require 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest (中略) test "password resets" do (中略) assert_redirected_to user assert_nil user.reload['reset_digest'] end
12.4 本番環境でのメール送信 (再掲)
もし既に前章でセットアップを終わらせていたら 演習までスキップしてしまっても大丈夫です。
演習
1:production環境でユーザー登録を試してみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
動作確認するだけ
2:メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。
ヒント: ターミナルからheroku logsコマンドを実行してみましょう。
動作確認するだけ: ターミナルからheroku logsコマンドを実行。
3:アカウントを有効化できたら、今度はパスワードの再設定を試してみましょう。正しくパスワードの再設定ができたでしょうか?
動作確認するだけ
12.6 証明: 期限切れの比較
12.3では、パスワードの期限が切れたかどうかを調べるために、次の比較を行いました。
reset_sent_at < 2.hours.ago
リスト 12.17で説明したように、この式を「少ない」と解釈すると逆の意味になってしまいますので、「早い」と解釈してみてください 。
最初に、期間を2つ定義します。Δtrをパスワード再設定メールを送信してからの期間、Δteをパスワード再設定の有効な期間 (例: 2時間) と定めます。
パスワードの再設定は、メールが送信された時刻から経過した期間が、有効期間よりも長くなった場合に「期限切れ」となります。これを次のように表します。
ここで、現在時刻 (訳注: 比較を行った時刻) をtN、パスワード再設定メールの送信時刻をtr、有効期間が切れる時刻 (例: 2時間経過後) をteと表すと、次の2つの関係式を得ることができます。
式 (12.2)と式 (12.3)を式 (12.1)に代入すると、次の結果が得られます。
両辺に−1をかけると、次の式が得られます。
式 (12.4)をRailsのコードに置き換え、値をte=2 時間前とすると、
password_reset_expired?メソッドと同じコードになります。
def password_reset_expired?
  reset_sent_at < 2.hours.ago
end
<記号を「〜より少ない」ではなく「〜より早い時刻」と解釈すれば、「パスワードの再設定は、現在より2時間以上前の時刻に行われた」という言明と一致します。
