紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、Ruby on Railsチュートリアルの解答やAmazon Web Servicesについて学んだことをこつこつ情報発信するブログです。

【11章】Ruby on Railsチュートリアル演習まとめ&解答例【11.2 アカウント有効化のメール送信】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 11章 11.2 アカウント有効化のメール送信の演習まとめ&解答例です。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

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

本章での学び

アカウント有効化メールを送信するために、Action Mailerライブラリを使用して、メイラーを作成する。

【mailer】Userメイラーの作成

rails generateで自動生成する。 viewのテンプレートが2つ生成される。

  • テキストメール用
  • HTMLメール用
yokoyan:~/workspace/sample_app (account-activation) $ rails generate mailer UserMailer account_activation password_reset
Running via Spring preloader in process 1731
Expected string default value for '--jbuilder'; got true (boolean)
      create  app/mailers/user_mailer.rb
      invoke  erb
      create    app/views/user_mailer
      create    app/views/user_mailer/account_activation.text.erb
      create    app/views/user_mailer/account_activation.html.erb
      create    app/views/user_mailer/password_reset.text.erb
      create    app/views/user_mailer/password_reset.html.erb
      invoke  test_unit
      create    test/mailers/user_mailer_test.rb
      create    test/mailers/previews/user_mailer_preview.rb

【mailer】メールテンプレートのカスタマイズ

デフォルトのfromアドレスを編集する。 なお、この値はアプリケーション全体で共通となる。

  default from: 'noreply@example.com'

【mailer】メール送信処理の追加

自動生成された、account_activationメソッドを編集する。 ユーザー情報のインスタンス変数を作成し、user.email宛にメールを送信する。

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

【view】テキストメールとHTMLメールビューのカスタマイズ

ユーザーの有効化URLには、有効化トークンを含める。 q5lt38hQDc_959PVoo6b7Aの文字列は、実装済みのnew_tokenメソッドで生成した値。 Base64でエンコードされている。

なお、AccountActivationsコントローラのeditアクションでは、paramsハッシュでparams[:id]として参照することができる。

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

また、URLからユーザーを特定するために、メールアドレスをクエリパラメータで追加する。 URLの末尾に?を付与して、キーと値のペアを記載する。

なお、URLに記載するメールアドレスの@は、URLで使えない文字列であるため %40でエスケープする必要がある。 Railsでは自動的にエスケープしてくれる。

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

上記を踏まえ、実装する。

テキストメールのテンプレート。

Hi <%= @user.name %>

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

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>

HTMLメールのテンプレート。

<h1>Sample App</h1>

<p>Hi <%= @user.name %></p>,

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

<%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>

テストコードの作成

演習1

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

実行結果は以下の通り。 @や、'、!がエスケープされて表示される。

yokoyan:~/workspace/sample_app (account-activation) $ rails console
Running via Spring preloader in process 1755
Loading development environment (Rails 5.0.0.1)
>> CGI.escape('foo@example.com')
=> "foo%40example.com"
>> 
>> CGI.escape("Don't panic!")                                                                                                                                                                                                   
=> "Don%27t+panic%21"
>> 

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

本章での学び

特殊なURLにアクセスして、メールの内容をその場でプレビューする。

【environment】develop環境のメール設定変更

ホスト名を各自の環境に合わせて設定する。

  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'xxxxxxxxxxxx.c9users.io'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }

設定が完了したら、develop環境のサーバを再起動する。

【mailer】Userメイラーのプレビューファイルの修正

自動生成されたプレビューファイルは、そのままでは動かないため、ユーザー情報を追加する。

  • DBの最初のユーザー
  • 有効化トークン(メールテンプレート内で使用しているため省略不可)
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

動作確認

ブラウザから、下記URLにアクセスする。

https://xxxxxxxxx.c9users.io/rails/mailers/user_mailer/account_activation.html

メイラーのプレビュー画面が表示される。

image

演習1

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

上記の動作確認の通り。 「Date」には、UTC時間が表示されているため、日本と9時間ずれてる。

11.2.3 送信メールのテスト

本章での学び

前項で実装した、メールプレビューのテストコードを作成する。

【test】Userメイラーのテストコードの作成

  • テストユーザーとしてfixtureからmichaelを取得する
  • テストユーザーの有効化トークンを生成する
  • Userメイラーのアカウント有効化処理にテストユーザー情報を渡して、メールオブジェクトを作成する
  • メールオブジェクトの件名をチェックする
  • メールオブジェクトの送信先メールアドレスをチェックする
  • メールオブジェクトの送信元メールアドレスをチェックする
  • テストユーザーの名前が、メールの本文に含まれているかチェックする
  • テストユーザーの有効化トークンが、メールの本文に含まれているかチェックする
  • テストユーザーのアドレスがエスケープされて、メールの本文に含まれているかチェックする

上記を踏まえ実装する。 自動的に生成されるtest "password_reset"については、12章で実装するためコメントアウトする。 (残しておくとテストがredになってしまう)

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
  end

【config】テストファイル内のドメイン名を設定する

テスト内でのドメインを設定する。

  config.action_mailer.default_url_options = { host: 'example.com' }

動作確認

mailerのテストがgreenになることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test:mailers
Started with run options --seed 17823

  1/1: [========================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.63635s
1 tests, 9 assertions, 0 failures, 0 errors, 0 skips

演習1

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

テストスイートがgreenであることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 2015
Started with run options --seed 24199

  43/43: [======================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.04996s
43 tests, 188 assertions, 0 failures, 0 errors, 0 skips

演習2

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

CGI.escape部分をコメントアウトする。

    # assert_match CGI.escape(user.email), mail.body.encoded
    assert_match user.email, mail.body.encoded

テストスイートがredになることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 2134
Started with run options --seed 58681

 FAIL["test_account_activation", UserMailerTest, 1.9940800210024463]
 test_account_activation#UserMailerTest (1.99s)
        Expected /michael@example\.com/ to match # encoding: US-ASCII
        "\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example\r\n\r\nWelcome to the Sample App! Click on the link below to active your account:\r\n\r\nhttp://example.com/account_activations/pFlo06HSkw3Yug79-uZaQg/edit?email=michael%40example.com\r\n\r\n\r\n\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0\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>Sample App</h1>\r\n\r\n<p>Hi Michael Example</p>,\r\n\r\n<p>\r\n  Welcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://example.com/account_activations/pFlo06HSkw3Yug79-uZaQg/edit?email=michael%40example.com\">Activate</a>\r\n\r\n  </body>\r\n</html>\r\n\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0--\r\n".
        test/mailers/user_mailer_test.rb:14:in `block in <class:UserMailerTest>'

  43/43: [======================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.15642s
43 tests, 188 assertions, 1 failures, 0 errors, 0 skips

動作確認後は、コメントアウト部分を元に戻しておく。

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

本章での学び

作成したメイラーを使うために、createアクションを修正する。

【controller】ユーザー登録時に、アカウント有効化メールの送信処理を追加する

以下の処理を、

    if @user.save
      log_in(@user)
      #保存の成功をここで扱う
      flash[:success] = "Welcome to the Sample App!"
      redirect_to user_url @user
    else

以下のように修正する。 deliver_nowは、メールを送信するメソッド。 アカウント有効化メールを送信後、ログインするのではなく、TOPページにリダイレクトさせる

    if @user.save
      UserMailer.account_activation(@user).deliver_now
      flash[:success] ="Please check your email to activate your account"
      redirect_to root_url
    else

【test】統合テストの修正

現時点では、テストがredになってしまう。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 1859
Started with run options --seed 60543

 FAIL["test_valid_signup_information", UsersSignupTest, 1.6497548810002627]
 test_valid_signup_information#UsersSignupTest (1.65s)
        expecting <"users/show"> but rendering with <["user_mailer/account_activation", "layouts/mailer", "static_pages/home", "layouts/_rails_default", "layouts/_shim", "layouts/_header", "layouts/_footer", "layouts/application"]>
        test/integration/users_signup_test.rb:37:in `block in <class:UsersSignupTest>'

  43/43: [========================================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.49370s
43 tests, 186 assertions, 1 failures, 0 errors, 0 skips

失敗するテストを一時的にコメントアウトする。

  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name: "Example User",
                                          email: "user@example.com",
                                          password: "password",
                                          password_confirmation: "password" } }
    end
    follow_redirect!
    # assert_template 'users/show'
    # assert_not flash.empty?
    # assert is_logged_in?
  end

テストがgreenになることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 1971
Started with run options --seed 16030

  43/43: [========================================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.11845s
43 tests, 185 assertions, 0 failures, 0 errors, 0 skips

動作確認

ブラウザから、ユーザー登録を行う。 (test2ユーザーで登録を実施)

image.png

コンソールにメール送信ログが表示されていることを確認。

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

Hi test2

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

https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com



----==_mimepart_594c956b2e8ac_82a276bab859751
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 test2</p>,

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

<a href="https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com">Activate</a>

  </body>
</html>

演習1

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

コンソールログをより確認。 リダイレクト先のURLが下記の通りルートURLに302で送信されていることを確認。

Redirected to https://rails-tutorial-yokoyan.c9users.io/
Completed 302 Found in 747ms (ActiveRecord: 21.9ms)

有効化トークンの値は、authenticity_token"=>"BChjYdix9aSf0+wv9gPXrz3WoPJzZPKLVPE/bSqMMskkQkQ0cwXPbKmyr583JHM+0APDwxH37qT6+YbHNKWxwg==

Started GET "/" for 125.199.218.217 at 2017-06-23 04:13:13 +0000
Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
  ActiveRecord::SchemaMigration Load (0.3ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by StaticPagesController#home as HTML
  Rendering static_pages/home.html.erb within layouts/application
  Rendered static_pages/home.html.erb within layouts/application (320.5ms)
  Rendered layouts/_rails_default.html.erb (100.5ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  Rendered layouts/_header.html.erb (3.9ms)
  Rendered layouts/_footer.html.erb (0.4ms)
Completed 200 OK in 447ms (Views: 436.8ms | ActiveRecord: 0.0ms)


Started GET "/signup" for 125.199.218.217 at 2017-06-23 04:13:17 +0000
Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#new as HTML
  Rendering users/new.html.erb within layouts/application
  Rendered shared/_error_messages.html.erb (0.4ms)
  Rendered users/_form.html.erb (421.1ms)
  Rendered users/new.html.erb within layouts/application (422.4ms)
  Rendered layouts/_rails_default.html.erb (30.7ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  Rendered layouts/_header.html.erb (0.7ms)
  Rendered layouts/_footer.html.erb (0.5ms)
Completed 200 OK in 473ms (Views: 460.4ms | ActiveRecord: 0.7ms)


Started POST "/signup" for 125.199.218.217 at 2017-06-23 04:13:30 +0000
Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
  ActiveRecord::SchemaMigration Load (0.1ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"BChjYdix9aSf0+wv9gPXrz3WoPJzZPKLVPE/bSqMMskkQkQ0cwXPbKmyr583JHM+0APDwxH37qT6+YbHNKWxwg==", "user"=>{"name"=>"test2", "email"=>"test2@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
   (0.1ms)  begin transaction
  User Exists (5.9ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "test2@example.com"], ["LIMIT", 1]]
  SQL (0.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "test2"], ["email", "test2@example.com"], ["created_at", 2017-06-23 04:13:30 UTC], ["updated_at", 2017-06-23 04:13:30 UTC], ["password_digest", "$2a$10$ox4DtTzrIjUOsCTFUe3dg.aqbZXJ1taP0HF.v6cTr6C4DUs3zImTK"], ["activation_digest", "$2a$10$OhYHKhyL8LqTJ/r2livKRuRTVhWSfXROncbmcmHmmWVmpINb9xE.S"]]
   (14.9ms)  commit transaction
  Rendering user_mailer/account_activation.html.erb within layouts/mailer
  Rendered user_mailer/account_activation.html.erb within layouts/mailer (6.1ms)
  Rendering user_mailer/account_activation.text.erb within layouts/mailer
  Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.4ms)
UserMailer#account_activation: processed outbound mail in 150.2ms
Sent mail to test2@example.com (8.9ms)
Date: Fri, 23 Jun 2017 04:13:31 +0000
From: noreply@example.com
To: test2@example.com
Message-ID: <594c956b30451_82a276bab859864@yokoyan-rails-tutorial-4550834.mail>
Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_594c956b2e8ac_82a276bab859751";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


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

Hi test2

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

https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com



----==_mimepart_594c956b2e8ac_82a276bab859751
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 test2</p>,

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

<a href="https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com">Activate</a>

  </body>
</html>

----==_mimepart_594c956b2e8ac_82a276bab859751--

Redirected to https://rails-tutorial-yokoyan.c9users.io/
Completed 302 Found in 747ms (ActiveRecord: 21.9ms)


Started GET "/" for 125.199.218.217 at 2017-06-23 04:13:31 +0000
Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by StaticPagesController#home as HTML
  Rendering static_pages/home.html.erb within layouts/application
  Rendered static_pages/home.html.erb within layouts/application (165.7ms)
  Rendered layouts/_rails_default.html.erb (50.5ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  Rendered layouts/_header.html.erb (3.6ms)
  Rendered layouts/_footer.html.erb (0.4ms)
Completed 200 OK in 229ms (Views: 227.6ms | ActiveRecord: 0.0ms)

演習2

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

コンソールから確認。 追加されたユーザーの有効化ステータスが、falseであることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails console
Running via Spring preloader in process 2627
Loading development environment (Rails 5.0.0.1)
>> test2 = User.find_by(email: "test2@example.com")
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "test2@example.com"], ["LIMIT", 1]]
=> #<User id: 101, name: "test2", email: "test2@example.com", created_at: "2017-06-23 04:13:30", updated_at: "2017-06-23 04:13:30", password_digest: "$2a$10$ox4DtTzrIjUOsCTFUe3dg.aqbZXJ1taP0HF.v6cTr6C...", remember_digest: nil, admin: false, activation_digest: "$2a$10$OhYHKhyL8LqTJ/r2livKRuRTVhWSfXROncbmcmHmmWV...", activated: nil, activated_at: nil>
>> 
?> test2.activated?
=> false
>> 

おわりに

メールの送信処理の組み込みが完了しました。 メールテンプレートの作成や、メール送信処理そのものも、 Railsだと簡単に実装できます。

有効化トークンを埋め込んだURLによる認証は、ECサイト等でも当たり前にある機能であるため、 仕組みが非常に勉強になりました。