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時間以上前の時刻に行われた」という言明と一致します。