Ruby on Rails チュートリアルで30歳までに人生を変える(第12章)
こんにちは。opiyoです。
今回は、第12章をやっていきます。
第12章はパスワードを忘れた時の再設定方法です。
どうやら難しそうですが、早速行ってみましょう。
railsチュートリアル12章の演習解説
12.1.1 演習 PasswordResetsコントローラ
12.1.1.1
<問題>この時点で、テストスイートが greenになっていることを確認してみましょう。
<回答> 問題なし
12.1.1.2
<問題>表 12.1の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: アカウント有効化で行った演習 (11.1.1.1) と同じ理由です。
<回答> メールの本文で使うリンクになるので、絶対パス=_urlを使わないとどのサイトなのか参照できないから
12.1.2 演習 新しいパスワードの設定
12.1.2.1
<問題>リスト 12.4のform_forメソッドでは、なぜ@password_resetではなく:password_resetを使っているのでしょうか? 考えてみてください。
<回答> @password_reset(オブジェクト)を使う必要がない。これを使うとオブジェクトの有り・無しによってRailsがよしなにやってくれる。 今回は、入力されたパスワードを使ってDB検索するだけなので一番シンプルな形にするために:password_rest(シンボル)を使ってる。
12.1.3 演習 createアクションでパスワード再設定
12.1.3.1
<問題>試しに有効なメールアドレスをフォームから送信してみましょう (図 12.6)。どんなエラーメッセージが表示されたでしょうか?
<回答> エラーにならない。
12.1.3.2
<問題>コンソールに移り、先ほどの演習課題で送信した結果、(エラーと表示されてはいるものの) 該当するuserオブジェクトにはreset_digestとreset_sent_atがあることを確認してみましょう。また、それぞれの値はどのようになっていますか?
<回答> エラーにならなかったので、再度確認します。
12.2.1 演習 新しいパスワードの設定
12.2.1.1
<問題>ブラウザから、送信メールのプレビューをしてみましょう。「Date」の欄にはどんな情報が表示されているでしょうか?
<回答>
Date:Mon, 28 Aug 2017 07:09:54 +0000
12.2.1.2
<問題>パスワード再設定フォームから有効なメールアドレスを送信してみましょう。また、Railsサーバーのログを見て、生成された送信メールの内容を確認してみてください。
<回答> メッセージが表示されエラーなく登録された(Password has been reset.)
Sent mail to haku@co.jp (18.8ms) Date: Mon, 28 Aug 2017 16:10:44 +0900 From: noreply@example.com To: haku@co.jp Message-ID: <59a3c1f4ad18e_8d93fdb09687134926e4@rh0257.rhizomedom.co.jp.mail> Subject: Password reset Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_59a3c1f4ac5a4_8d93fdb096871349250 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit To reset your password click the link below: http://localhost:3000/password_resets/6rzf5BbQ9uFRCrekZp3PtA/edit?email=haku%40co.jp 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_59a3c1f4ac5a4_8d93fdb096871349250 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> <a href="http://localhost:3000/password_resets/6rzf5BbQ9uFRCrekZp3PtA/edit?email=haku%40co.jp">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_59a3c1f4ac5a4_8d93fdb096871349250--
12.2.1.3
<問題>コンソールに移り、先ほどの演習課題でパスワード再設定をしたUserオブジェクトを探してください。オブジェクトを見つけたら、そのオブジェクトが持つreset_digestとreset_sent_atの値を確認してみましょう。
<回答>
> User.last User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] => #<User:0x007fc463209b10 id: 102, name: "haku", email: "haku@co.jp", created_at: Mon, 28 Aug 2017 03:26:44 UTC +00:00, updated_at: Mon, 28 Aug 2017 07:11:03 UTC +00:00, password_digest: "$2a$10$BGL4JzqH6XKZQIHcaZG1CeAXndlo25Gaj1z.74gDwTTHEwjlzuZ9a", remember_digest: nil, admin: false, activation_digest: "$2a$10$kSWicCWAUJWf8ip4/LKhVehAvTG.t56.W2PyXWfVykUkpM6wzTHTa", activated: true, activated_at: Mon, 28 Aug 2017 03:26:47 UTC +00:00, reset_digest: "$2a$10$SjkLM3hWhvqIGxWzmXHftOtINDC89Ti9OZQrJBpMq2jyFIB2.fzoG", reset_sent_at: Mon, 28 Aug 2017 07:10:44 UTC +00:00>
12.2.2 演習 送信メールのテスト
12.2.2.1
<問題>メイラーのテストだけを実行してみてください。このテストは greenになっているでしょうか?
<回答> なっている。
12.2.2.2
<問題>リスト 12.12にある2つ目のCGI.escapeを削除すると、テストが redになることを確認してみましょう。
<回答>
FAIL["test_password_reset", UserMailerTest, 2.998306857998614] test_password_reset#UserMailerTest (3.00s) Expected /michael@example\.com/ to match # encoding: US-ASCII "\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nTo reset your password click the link below:\r\n\r\nhttp://localhost:3000/password_resets/95ZsztFc2ioqiuI92qDcxQ/edit?email=michael%40example.com\r\n\r\nThis link will expire in two hours.\r\n\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n\r\n\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n <style>\r\n /* Email styles need to be inline */\r\n </style>\r\n </head>\r\n\r\n <body>\r\n <h1>Password reset</h1>\r\n\r\n<p>To reset your password click the link below:</p>\r\n\r\n<a href=\"http://localhost:3000/password_resets/95ZsztFc2ioqiuI92qDcxQ/edit?email=michael%40example.com\">Reset password</a>\r\n\r\n<p>This link will expire in two hours.</p>\r\n\r\n<p>\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n</p>\r\n\r\n </body>\r\n</html>\r\n\r\n----==_mimepart_59a3c6f85e58_f443ffb6443fa0465c8--\r\n". test/mailers/user_mailer_test.rb:26:in `block in <class:UserMailerTest>'
12.3.1 演習 editアクションで再設
12.3.1.1
<問題>12.2.1.1で示した手順に従って、Railsサーバーのログから送信メールを探し出し、そこに記されているリンクを見つけてください。そのリンクをブラウザから表示してみて、図 12.11のように表示されるか確かめてみましょう。
<回答>
http://localhost:3000/password_resets/-e_N9zdv0kKekwluwQXnzw/edit?email=haku%40co.jp
表示させることを確認。
12.3.1.2
<問題>先ほど表示したページから、実際に新しいパスワードを送信してみましょう。どのような結果になるでしょうか?
<回答> エラーにならないので、再度確認する
12.3.2 演習 パスワードを更新する
12.3.2.1
<問題>12.2.1.1で得られたリンク (Railsサーバーのログから取得) をブラウザで表示し、passwordとconfirmationの文字列をわざと間違えて送信してみましょう。どんなエラーメッセージが表示されるでしょうか?
<回答>
The form contains 1 error. Password confirmation doesn't match Password ["Password confirmation doesn't match Password"]
12.3.3.2
<問題>コンソールに移り、パスワード再設定を送信したユーザーオブジェクトを見つけてください。見つかったら、そのオブジェクトのpassword_digestの値を取得してみましょう。次に、パスワード再設定フォームから有効なパスワードを入力し、送信してみましょう (図 12.13)。パスワードの再設定は成功したら、再度password_digestの値を取得し、先ほど取得した値と異なっていることを確認してみましょう。ヒント: 新しい値はuser.reloadを通して取得する必要があります。
<回答>
> after = User.last # パスワード変更を実施 > before = User.last > before.password_digest == after.password_digest => false
12.3.3 演習 パスワードを更新する
12.3.3.1
<問題>リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.20に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。ちなみにリスト 12.20にあるコードには、前章の演習 (リスト 11.39) の解答も含まれています。
<回答>
# app/models/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
12.3.3.2
<問題>リスト 12.16のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐 (リスト 12.21) を統合テストで網羅してみましょう (12.21 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです)。期限切れをテストする方法はいくつかありますが、リスト 12.21でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます (なお、大文字と小文字は区別されません)。
12.3.3.3
<問題>2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。しかし、もっと良くする方法はまだあります。例えば、公共の (または共有された) コンピューターでパスワード再設定が行われた場合を考えてみてください。仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまいます (しかもそのままログイン機構まで突破されてしまいます!)。この問題を解決するために、リスト 12.22のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう5。
12.3.3.4
<問題>リスト 12.18に1行追加し、1つ前の演習課題に対するテストを書いてみましょう。ヒント: リスト 9.25のassert_nilメソッドとリスト 11.33のuser.reloadメソッドを組み合わせて、reset_digest属性を直接テストしてみましょう。