第11章Railsチュートリアル演習問題と解答まとめ

Ruby on Railsチュートリアルの演習問題と解答をまとめる。


第11章 アカウントの有効化 - Railsチュートリアル

f:id:yukitoku_sw:20191019165508p:plain


アウトプットすることで、より自分の理解を深めることを目的としています。 自分なりに調べて考えた回答のため、記載内容に誤りがある場合はコメントいただけると幸いです。


演習11.1.1 AccountActivationsコントローラ

問題1

現時点でテストスイートを実行すると greenになることを確認してみましょう。

解答

テストを実行し、GREENになることを確認します。


問題2

表 11.2の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

解答

メール(外部)からアクセスしてもらうのには完全なURLが必要なため。


演習11.1.2 AccountActivationのデータモデル

問題1

本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。

解答

テスト結果はGREENのままです!


問題2

コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

解答

# Userクラスのインスタンス生成
>> user = User.first
  User Load (1.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-10-29 10:47:45"
, updated_at: "2019-10-29 10:47:45", password_digest: "$2a$10$lAeSzwDSMQ922cNr1tSR6uAvuvp1M7mN7UTSc3k.ngN...
", remember_digest: nil, admin: true, activation_digest: "$2a$10$EWa/9y8qndhN1V1EoG2Zj.rbf8QYO62qcXNJvuQWu3h
...", activated: true, activated_at: "2019-10-29 10:47:45">

# create_activation_digestメソッド呼び出し
>> user.create_activation_digest
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x00007f90e94d5518>)
Did you mean?  restore_activation_digest!

# ダイジェストの値を確認
>> user.activation_digest
=> "$2a$10$EWa/9y8qndhN1V1EoG2Zj.rbf8QYO62qcXNJvuQWu3hRrQZoUy3Pm"


問題3

リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

解答

  private

    # メールアドレスを全て小文字にする
    def downcase_email
      email.downcase!
    end

テストはGREENのままです!


演習11.2.1 送信メールのテンプレート

問題1

コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?

解答

>> CGI.escape('foo@example.com')
=> "foo%40example.com"
>> CGI.escape("Don't panic!")
=> "Don%27t+panic%21"


演習11.2.2 送信メールのプレビュー

問題1

Railsプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?

解答

[test/mailer/previews/user_mailer_preview.rb]

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation  # ココ
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset    
  def password_reset
    UserMailer.password_reset
  end

end

上記URLにアクセスするとプレビューを表示できる。
Date欄にはUTC時間が表示されているため日本時間とはずれている

f:id:yukitoku_sw:20191029221539p:plain


演習11.2.3 送信メールのテスト

問題1

この時点で、テストスイートが greenになっていることを確認してみましょう。

解答

テストはGREEN


問題2

リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。

解答

[test/mailers/user_mailer_test.rb]
require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  test "account_activation" do
    user = users(:michael)
    user.activation_token = User.new_token
    mail = UserMailer.account_activation(user)
    assert_equal "Account activation", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.name, mail.body.encoded
    assert_match user.activation_token, mail.body.encoded
    # assert_match CGI.escape(user.email), mail.body.encoded
    assert_match user.email, mail.body.encoded
  end

end

テストがREDになる


演習11.2.4 ユーザーのcreateアクションを更新

問題1

新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?

解答

リダイレクト先がroot_url

f:id:yukitoku_sw:20191029223601p:plain

有効化トークン:"authenticity_token"=>"no3cQwkVSAvi5HBUee/8YBJJ8odHce/7VlcJQfvwQ/nosOB8eHSsSRULLRQTx5oeBc6r068aMS0ihDzKI0+bgQ=="

Started POST "/signup" for 127.0.0.1 at 2019-10-29 22:29:51 +0900
Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"no3cQwkVSAvi5HBUee/8YBJJ8odHce/7VlcJQfvwQ/nosOB8eHSsSRULLRQTx5oeBc6r068aMS0ihDzKI0+bgQ==", "user"=>{"name"=>"hanzou", "email"=>"hhattori@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
   (0.1ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "hhattori@example.com"], ["LIMIT", 1]]
  SQL (0.6ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "hanzou"], ["email", "hhattori@example.com"], ["created_at", "2019-10-29 13:29:52.036498"], ["updated_at", "2019-10-29 13:29:52.036498"], ["password_digest", "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/34jmo1onx8W"], ["activation_digest", "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoVWkJR3TGuy."]]
   (1.5ms)  commit transaction
  Rendering user_mailer/account_activation.html.erb within layouts/mailer
  Rendered user_mailer/account_activation.html.erb within layouts/mailer (1.4ms)
  Rendering user_mailer/account_activation.text.erb within layouts/mailer
  Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.5ms)
UserMailer#account_activation: processed outbound mail in 29.5ms
Sent mail to hhattori@example.com (6.4ms)
Date: Tue, 29 Oct 2019 22:29:52 +0900
From: noreply@example.com
To: hhattori@example.com
Message-ID: <5db83ed025055_32483ffdb6c3a96c39ea@mba.mail>
Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


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

Hi hanzou,

Welcome to the Sample App! Click on the link below to activate your account:

http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com

----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840
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>Sample App</h1>

<p>Hi hanzou,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<a href="http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com">Activate</a>

  </body>
</html>

----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840--

Redirected to http://localhost:3000/
Completed 302 Found in 201ms (ActiveRecord: 2.4ms)


問題2

コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。

解答

ユーザーは作成されているが、activatedはfalse

>> user = User.find_by(name: "hanzou")
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "hanzou"], ["LIMIT", 1]]
=> #<User id: 101, name: "hanzou", email: "hhattori@example.com", created_at: "2019-10-29 13:29:52", updated_at: "2019-10-29 13:29:52", password_digest: "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/3...", remember_digest: nil, admin: false, activation_digest: "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoV...", activated: false, activated_at: nil>


演習11.3.1 authenticated? メソッドの抽象化

問題1

コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

解答

>> user = User.create(name: "kotaro", email: "kfuma@example.com", password: "password", password_confirmatio
n: "password") 
   (0.1ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["ema
il", "kfuma@example.com"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activat
ion_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "kotaro"], ["email", "kfuma@example.com"], ["created_at", 
"2019-10-29 13:54:15.560395"], ["updated_at", "2019-10-29 13:54:15.560395"], ["password_digest", "$2a$10$VUZ
oyTovYxJzZxNA7S16YuuDPVzHA7jhYK.RvHrPuMxf3CwZTu3my"], ["activation_digest", "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc
62x2JDDhpfNZQFTFvbiWe4UaugiJO"]]
   (2.4ms)  commit transaction
=> #<User id: 102, name: "kotaro", email: "kfuma@example.com", created_at: "2019-10-29 13:54:15", updated_at
: "2019-10-29 13:54:15", password_digest: "$2a$10$VUZoyTovYxJzZxNA7S16YuuDPVzHA7jhYK.RvHrPuMx...", remember_
digest: nil, admin: false, activation_digest: "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc62x2JDDhpfNZQFTFvbi...", activ
ated: false, activated_at: nil>

>> user.remember_token
=> nil
>> user.remember_digest
=> nil
>> user.activation_token
=> "D3tmp09iJ8lqbjdeqao4Kg"
>> user.activation_digest
=> "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc62x2JDDhpfNZQFTFvbiWe4UaugiJO"


演習11.3.2 editアクションで有効か

問題1

コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

解答

Sent mail to hhattori@example.com (6.4ms)
Date: Tue, 29 Oct 2019 22:29:52 +0900
From: noreply@example.com
To: hhattori@example.com
Message-ID: <5db83ed025055_32483ffdb6c3a96c39ea@mba.mail>
Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


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

Hi hanzou,

Welcome to the Sample App! Click on the link below to activate your account:

http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com

----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840
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>Sample App</h1>

<p>Hi hanzou,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<a href="http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com">Activate</a>

  </body>
</html>

----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840--

この部分

account_activations/_XTGFh5X4A3-5B9dryba3Q/


問題2

先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。

解答

先ほどのURLをブラウザに貼り付けるとユーザー詳細ページにとび、「Account activated!」というフラッシュが表示される

コンソールでactivatedを確認するとtrueに変わっている

>> user = User.find_by(name: "hanzou")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "hanzou"], ["LIMIT", 1]]
=> #<User id: 101, name: "hanzou", email: "hhattori@example.com", created_at: "2019-10-29 13:29:52", updated_at: "2019-10-30 12:35:42", password_digest: "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/3...", remember_digest: nil, admin: false, activation_digest: "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoV...", activated: true, activated_at: "2019-10-30 12:35:42">
>> user.activated
=> true


演習11.3.3 有効化のテストとリファクタリング

問題1

リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。

解答

  # アカウントを有効にする
  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end

テストもGREEN!


問題2

現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。

解答

  def index
    @users = User.where(activated: true).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?
  end


問題3

ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

解答

まずintegrationテストを作成する

$ rails g integration_test users_activation

テスト用のactivated: falseユーザーを新しく作る

[test/ fixtures/users.yml]


red:
  name: Red Marlboro
  email: red@example.com
  password_digest: <%= User.digest('password') %>
  activated: false
  activated_at: <%= Time.zone.now %>

テストの流れ

setupでactivatedユーザーとnon_activatedユーザーを定義する

テスト indexページはactivatedユーザーのみ
・ログインする
・usersにアクセスする
・@userへのリンクがあることを確認
・@non_activated_userへのリンクがないことを確認

テスト showページはactivatedユーザーのみ
・ログインする
・@userの詳細ページにアクセスする
・@non_activated_userの詳細ページにアクセスする
 ・root_urlにリダイレクトされる

require 'test_helper'

class UsersActivationTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
    @non_activated_user = users(:red)
  end

  test "index only activated user" do
    log_in_as(@user)
    get users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", user_path(@non_activated_user), count: 0
  end

  test "show only activated user" do
    log_in_as(@user)
    get user_path(@user)
    get user_path(@non_activated_user)
    assert_redirected_to root_url
  end

end

テストの名前何にするかすごい迷う、、、


演習11.4 本番環境でのメール送信

問題1

実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?

解答

f:id:yukitoku_sw:20191031004528p:plain


問題2

メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。ヒント: ターミナルからheroku logsコマンドを実行してみましょう。

解答

heroku logsコマンドをみると、有効化リンクをクリックした後にactivatedがアップデートされているのがわかる。


おしまい


yukitoku-sw.hatenablog.com