日別アーカイブ: 2021年12月2日

Rails-tutorialのまとめ12.3(パスワードの再設定をテストする 主に演習)

12章から続く

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”、
メールアドレスがあるかどうかを確認します。

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

12章のまとめ

1:パスワードの再設定は Active Recordオブジェクトではないが、
セッションやアカウント有効化の場合と同様に、リソースでモデル化できる

2:Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる

3:Action MailerではテキストメールとHTMLメールの両方を利用できる

4:メイラーアクションで定義したインスタンス変数は、
他のアクションやビューと同様、メイラーのビューから参照できる

5:パスワードを再設定させるために、生成したトークンを使って一意のURLを作る

6:より安全なパスワード再設定のために、ハッシュ化したトークン (ダイジェスト) を使う

7:メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用

8:SendGridを使うとproduction環境からメールを送信できる

その13に続く