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

# これから行っていくもの

* AccountActivationsコントローラーの`edit`アクションの実装
* 当該アクションに対するテストの実装
* AccountActivationsコントローラーに関係するリファクタリング
    * AccountActivationsコントローラーからUserモデルにコードを移していく

# `authenticated?`メソッドの抽象化

## 前提

### アカウント有効化リンクにおける`params`ハッシュ

```
http://www.example.com/account_activations/C477rjyLBojP4v5nfKcoqQ/edit?email=foo%40example.com
```

例えば上述のようなURLで`AccountActivations#edit`アクションを呼び出した場合、アカウントの有効化に必要な属性は、`params`ハッシュを使って以下のように参照できます。

* 有効化トークンは、`params[:id]`として参照できる
* メールアドレスは、`params[:email]`として参照できる

### 既存の`authenticated?`メソッド

今回製作中のサンプルアプリケーションには、すでに「与えられたトークンとRDBに記録されたダイジェストの両者を照合し、合致すれば認証する」という実装は存在します。`User#authenticated?`というメソッドがそれですね。

```ruby:User#authenticated?
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```

なお、`User#authenticated?`の実装については、大枠はそのままながら引数のとり方を変更していきます。

## アカウント有効化リンクからユーザーを検索・認証する仕組み

上記前提を踏まえ、「アカウント有効化リンクからユーザーを検索・認証する仕組み」は以下のように実装していくことになります。

```ruby:
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
```

## `User#authenticated?`の実装の変更

`remember_digest`はUserモデルの属性です。Userモデル内においては、以下のように使うことができます。

```ruby:
self.remember_digest
```

では、上記`remember_digest`の`remenber`という部分を可変にし、「状況に応じて呼び出すメソッドを変える」という実装を実現するためにはどうすればいいか。そこが`User#authenticated?`の実装の変更ポイントとなります。すなわち、以下のような呼び出しができるようにしたい、ということです。

```ruby:
self.FOOBAR_digest
```

### メタプログラミング

「プログラムそのものをプログラムによって作成する」というプログラミング手法のことです。「Rubyの言語体系に含まれるメタプログラミング手法は、Smalltalkの影響[^Smalltalkの影響]が強いものである」とはよく言及されますね。とりわけRailsにおいては、メタプログラミング手法の多用により、「素のRubyとは別物」と言われるほどの言語体系が構築されるに至っています。

Railsチュートリアルの第11章においては、`method_missing`[^method_missing]までは言及されておらず、`send`についてのみ言及されています。

[^Smalltalkの影響]: [RegionalRubyKaigi レポート (13) 札幌 Ruby 会議 02](https://magazine.rubyist.net/articles/0030/0030-SapporoRubyKaigi02Report.html)の、項目「知らないと損をする Smalltalk」あたりが、「Smalltalkの影響」についての概説的な言及となっています。

[^method_missing]: 定義されていないメソッドの呼び出しに対する動作を定義するメソッドです。

#### `send`メソッドの動作

```
# rails console --sandbox

>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3
```

上記Railsコンソールの例では、「`send`メソッドにシンボル`:length`や文字列`"length"`を渡した結果は、`length`メソッドを直接実行した結果と同じになる」という点が重要です。

```
# rails console --sandbox

>> user = User.first

>> user.send(:activation_digest)
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"
>> user.send("activation_digest")
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"

>> attribute = :activation
=> :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"
```

続いては、「`send`メソッドを用いて、RDB上に存在する最初のユーザーの`activation_digest`属性にアクセスする」という例です。`activation_digest`メソッドも、`length`メソッドと同様、「メソッド自身の名前を、`send`の引数にシンボルや文字列として渡すことにより実行する」ことが可能です。

特に重要なのは、最後の「式展開により、呼び出すメソッドの名前の一部を変数としている」という実行例です。

##### 「式展開により、呼び出すメソッドの名前の一部を変数とする」その動作の解説

まず、`:activation`というシンボル[^シンボルか文字列か]を値とする変数`attribute`を定義します。

```ruby:
attribute = :activation
```

その上で、文字列の展開により引数を組み立てて…

```ruby:
"#{attribute}_digest"
```

`send`メソッドに渡します。

```ruby:
send("#{attribute}_digest")
```

[^シンボルか文字列か]: `attribute`の値として、文字列`'activation'`を与えても、最終的な動作は変わりません。但し、「実行効率・使用メモリ量における優位性」等の理由により、Rubyにおける当該用法ではシンボルを使うことが一般的です。

##### 余談 - `send`に類似するメソッドとか概念とか

そういえば、Excelのワークシート関数である`INDIRECT`も、Rubyの`send`と似たような使い方をしますよね。

`send`に類似するメソッドとして、`eval`というメソッドも存在します。`send`が「メソッド名」を渡して用いるのに対し、`eval`は「Rubyコードそのもの」を渡して用いるメソッドです。

### 実際に`User#authenticated?`の実装を変更する

ここまでのポイントを踏まえた上で、実際に`User#authenticated?`の実装内容に手を加えていきましょう。

```ruby:User#authenticated?の新しい実装
def authenticated?(attribute, token)
  digest = send("#{attribute}_token")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end
```

```diff:app/models/user.rb
  class User < ApplicationRecord
  ...略

-   def authenticated?(remember_token)
+   def authenticated?(attribute, token)
+     digest = send("#{attribute}_digest")
-     return false if remember_digest.nil?
+     return false if digest.nil?
-     BCrypt::Password.new(remember_digest).is_password?(remember_token)
+      BCrypt::Password.new(digest).is_password?(token)
    end

    ...略
  end
```

### 既存の`authenticated?`メソッド使用箇所の実装を変更する

#### 現状ではテストが成功しない

現状では、アプリケーション全体に対するテストは成功しません。

```
# rails test
Running via Spring preloader in process 13778
Started with run options --seed 51138

ERROR["test_current_user_returns_nil_when_remember_digest_is_wrong", SessionsHelperTest, 3.314027799991891]
 test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (3.31s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            app/helpers/sessions_helper.rb:26:in `current_user'
            test/helpers/sessions_helper_test.rb:17:in `block in <class:SessionsHelperTest>'

ERROR["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 3.3577567999600433]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (3.36s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            app/helpers/sessions_helper.rb:26:in `current_user'
            test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 5.586914699990302]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (5.59s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

  44/44: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.20436s
44 tests, 185 assertions, 0 failures, 3 errors, 0 skips
```

「ArgumentError: wrong number of arguments (given 1, expected 2)」というメッセージが見受けられますね。これは、「`authenticated?`メソッドに渡す引数の数が不足している」という趣旨のメッセージです。

まずは、3つのエラー共通で報告されている`app/models/user.rb:31`を修正していきましょう。

#### 修正箇所1 - `SessionsHelper#current_user`メソッド

まずは`app/helpers/sessions_helper.rb`の31行目を修正していきましょう。私の環境では、`SessionsHelper#current_user`に問題の行があります。

```diff:SessionsHelper#current_user
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
-     if user && user.authenticated?(cookies[:remember_token])
+     if user && user.authenticated?(:remember , cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
```

#### まだテストは成功しない

```
# rails test
Running via Spring preloader in process 13791
Started with run options --seed 46675

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 3.5224237999645993]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (3.53s)
ArgumentError:         ArgumentError: wrong number of arguments (given 1, expected 2)
            app/models/user.rb:31:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

 FAIL["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 4.250324499967974]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (4.25s)
        --- expected
        +++ actual
        @@ -1 +1 @@
        -#<User id: 959740715, name: "Reimu Hakurei", email: "rhakurei@example.com", created_at: "2019-12-06 09:01:01", updated_at: "2019-12-06 09:01:05", password_digest: "$2a$04$HVRWhbBA8amg3yoSSEKV9.vf6jg/T01/QCKfSI.w6ML...", remember_digest: "$2a$04$W7PA7NZAg5QbFQDgfqZQo.bmSUdr6OBKJ01JZKkI0gj...", admin: true, activation_digest: nil, activated: true, activated_at: nil>
        +nil
        test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

  44/44: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.53206s
44 tests, 187 assertions, 1 failures, 1 errors, 0 skips
```

「ArgumentError: wrong number of arguments (given 1, expected 2)」というメッセージがまだ見受けられます。今度は`test/models/user_test.rb:75`とあります。修正していきましょう。

#### 修正箇所2 - `UserTest`の「authenticated? should return false for a new user with nil digest」テスト

```
  test "authenticated? should return false for a new user with nil digest" do
-   assert_not @user.authenticated?('')
+   assert_not @user.authenticated?(:remember, '')
  end
```

#### まだ何かがおかしい

これで改めてテストを実行してみます。しかし、まだ成功しません。

```
# rails test
Running via Spring preloader in process 13807
Started with run options --seed 22372

 FAIL["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 2.9952282999875024]
 test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (3.00s)
        --- expected
        +++ actual
        @@ -1 +1 @@
        -#<User id: 959740715, name: "Reimu Hakurei", email: "rhakurei@example.com", created_at: "2019-12-06 09:11:02", updated_at: "2019-12-06 09:11:05", password_digest: "$2a$04$lRWR5q0KpF3UiW2r.hyD4e19FtV4Na6gh7iwkvdiely...", remember_digest: "$2a$04$26R0Y3nwF/Wh68N94ShIze7j6dLAT03Hva5oDt4PhP5...", admin: true, activation_digest: nil, activated: true, activated_at: nil>
        +nil
        test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'

  44/44: [=================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.43956s
44 tests, 188 assertions, 1 failures, 0 errors, 0 skips
```

Railsチュートリアル本文に言及されていない何かがおかしい、ということですね。

#### 修正箇所3 - `authenticated?`に関する想定外のテスト失敗と、その解決

長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第11章 アカウントの有効化 - authenticated?に関する想定外のテスト失敗と、その解決](https://qiita.com/rapidliner00/items/27c4b461254965e8953f)

#### これでようやくテストが成功

```
# rails test
Running via Spring preloader in process 13904
Started with run options --seed 21760

  44/44: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.01140s
44 tests, 189 assertions, 0 failures, 0 errors, 0 skips
```

ようやくテストが成功しました。正直ここまで長かったです。

# 演習 - `authenticated?`メソッドの抽象化

長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第11章 アカウントの有効化 - 演習 - authenticated?メソッドの抽象化](https://qiita.com/rapidliner00/items/b3743d72525f99b8ddf0)

# `edit`アクションで有効化

## ユーザーが有効化対象であることの確認

以下の`if`文が核となります。

```ruby:
if user && !user.activated? && user.authenticated?(:activation, params[:id])
```

### `!user.activated?`なる文の必要性

「既に有効化済みのユーザーに対し、再度有効化処理を実行することはしない」という意味の文です。このような文が必要となる理由は以下です。

* 「有効化が行われたら、その時点で当該ユーザーはログイン状態となる」という実装を行う
    * ユーザー体験を考えると、ユーザーの有効化とログインはセットで行われるのが好ましいため
* 上記のような実装の場合、何も対策をしていないと、「有効化リンクが盗み出されたら、そのリンクを使って自由にログインできてしまう」という脆弱性が発生する
* 以上の脆弱性への対策として、「既に有効化済みのユーザーに対し、再度有効化処理を実行することはしない」という実装を行う

### その他の部分

以下の事柄について確認を行っています。

* `user`が存在するユーザーであること
* 与えられたトークンが正しい有効化トークンであること

今回定義するアカウント有効化リンクにおいては、「有効化トークンは、`params[:id]`として参照できる」のでしたよね。

## アカウント有効化と、付随する処理の実体

コードは以下のようになります。

```ruby:
user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
```

処理の流れは以下のようになります。

1. 有効化されるユーザーの`:activated`属性の値に`true`を格納する
2. 有効化されるユーザーの`:actiated_at`属性の値に現在時刻を格納する
3. 有効化されたユーザーとしてログイン処理を行う
4. 正常に処理が行われた旨のフラッシュメッセージを定義する
5. 有効化されたユーザーのプロフィールページにリダイレクトする

ポイントは以下です。

* RDBへの変更の反映は、`update_attribute`メソッドによって行っている
    * バリデーションをバイパスする必要があるため
* 有効化されたユーザーとしてログインする処理を行っている
* 有効化されたユーザーのプロフィールページにリダイレクトされている

## 有効化URLによりユーザーを有効化する

ここまでの実装が完了すれば、メールで送信されたURLを使ってユーザーを有効化することができるようになります。

```:メール本文
Hi Foo Bar Baz,

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

https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org
```

### ポート番号の変更

私の環境では、Docker仮想環境側のTCP3000番ポートをローカル側のTCP8080番ポートと紐付けているため、`localhost:3000`という記述を`localhost:8080`に変更する必要があります。

### スキームを`https://`から`http://`に変更する

また、スキームが`https://`だと以下のエラーで有効化URLにアクセスできませんでした。

```
HTTP parse error, malformed request (): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>
```

スキームが`http://`だとアクセスできます。

### 最終的な有効化URL

最終的には、ローカル側でアクセスする有効化URLは以下のようになります。

```diff:
- https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org
+ http://localhost:8080/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org
```

### 有効化が成功する

有効化が成功すると、Webブラウザで以下のような画面が返ってきます。

<img width="1174" alt="スクリーンショット 2019-12-08 14.28.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/577cfd60-bb5e-f5c9-ebff-95f8ddb7b22b.png">

上述ユーザーのIDが102であることを踏まえて、Railsコンソールで`activated?`と`activated_at`の状態を確認してみます。

```
# rails console --sandbox

>> user = User.find(102)
>> user.activated?
=> true
>> user.activated_at
=> Sun, 08 Dec 2019 05:20:45 UTC +00:00
```

正常に有効化処理が行われたようですね。

### 同じ有効化URLを再度使うことはできない

同じ有効化URLで再度アクセスを試みると、以下のような画面が返ってきます。

<img width="1174" alt="スクリーンショット 2019-12-08 14.34.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/606e0900-256b-b696-a4f8-302016e05964.png">

「Invalid activation link」のフラッシュメッセージと、/ へのリダイレクト。こちらも想定どおりの挙動ですね。

## 「ユーザーが有効化されていなければログインできない」という動作の実装

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    ...略

    def create
      @user = User.find_by(email: params[:session][:email].downcase)
      if @user && @user.authenticate(params[:session][:password])
-       log_in @user
-       params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
-       redirect_back_or @user
+       if user.activated?
+         log_in @user
+         params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
+         redirect_back_or @user
+       else
+         message  = "Account not activated."
+         message += "Check your email for activation link."
+         flash[:warning] = message
+         redirect_to root_url
+       end
      else
        ...略
      end
    end

    ...略
  end
```

ログインしようとしたユーザーが有効化されていない場合は、「送付されたメールの確認を促すフラッシュメッセージを表示するようにした上で、/ にリダイレクト」という処理が行われます。

# 演習 - `edit`アクションで有効化

## 1. コンソールから、[11.2.4](https://railstutorial.jp/chapters/account_activation?version=5.1#sec-updating_create)で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

```
https://localhost:3000/account_activations/8I_nvDPPSA3TIqvNT4n9qA/edit?email=foobar%2Bfoobar%40example.org
```

上記URLを前提とすると、 /edit の前にある以下の文字列が有効化トークンとなります。

```
8I_nvDPPSA3TIqvNT4n9qA
```

Railsアプリケーション側では、`params[:id]`として参照できます。

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

上述「有効化URLによりユーザーを有効化する」の通りです。

* ポート番号が違うという問題に対する解決
* Docker環境において、`https://`スキームで有効化URLを扱えるようにする方法

以上の課題は今後に持ち越しでしょうか。

# 有効化のテストとリファクタリング

## 有効化のテスト

### テストコードの全体像

`test/integration/users_signup_test.rb`の全体像は以下のようになります。

```ruby:test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end

  test "invalid signup information" do
    get signup_path
    assert_select 'form[action="/signup"]'
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name: "",
                                        email: "user@invalid",
                                        password:              "foo",
                                        password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information with account activation" 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
    assert_equal 1, ActionMailer::Base.deliveries.size
    user = assigns(:user)
    assert_not user.activated?
    # 有効化していない状態でログインしてみる
    log_in_as(user)
    assert_not is_logged_in?
    # 有効化トークンが不正な場合
    get edit_account_activation_path("invalid token", email: user.email)
    assert_not is_logged_in?
    # トークンは正しいがメールアドレスが無効な場合
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # 有効化トークンが正しい場合
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!
    assert_template 'users/show'
    assert_not flash.empty?
    assert is_logged_in?
  end
end
```

#### 配信されたメッセージがきっかり1通であることを確認する

以下のコードにより、「メイラーによって配信されたメッセージが1通であること」を確認できます。

```ruby:
assert_equal 1, ActionMailer::Base.deliveries.size
```

但し、`ActionMailer::Base.delivaries`は変数であり、メイラーを使う他のテストで上書きされる可能性があることに注意しなければなりません。このテストが期待通りに行われるためには、`setup`メソッドで`delivaries`を初期化しておく必要があります。

```ruby:UsersSignupTest#setuop
def setup
  ActionMailer::Base.deriveries.clear
end
```

### テストが成功することの確認

コードにtypo等がなければ、この時点でテストは成功するはずです。

```
# rails test
Running via Spring preloader in process 14065
Started with run options --seed 9265

  44/44: [=================================] 100% Time: 00:00:10, Time: 00:00:10

Finished in 10.72402s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips
```

#### 私がやってしまったtypo

私は多数のtypoをやってしまったため、テストが成功するまでに数回エラーを出してしまいました。私がやってしまったtypoの例は以下です。

```diff:
- if user.activated?
+ if @user.activated?
```

```diff:
- deriveries
+ deliveries
```

```diff:
- delivaries
+ deliveries
```

```diff:
- user = assigns(:user)
+ user = assigns(@user)
```

```diff:
- assert is logged_in?
+ assert is_logged_in?
```

皆さんもtypoには気をつけましょう。

## リファクタリング - Userモデルにユーザー有効化メソッドを追加する

ここまでのテストが実装できれば、「ユーザー操作の一部をコントローラーからモデルに移動する」というリファクタリングが可能になります。具体的には、以下の処理をコントローラーからモデルに移していくことになります。

* ユーザーの有効化属性の更新
* 有効化メールの送信

### ユーザーの有効化属性の更新

ユーザーの有効化属性を更新する`activate`メソッドの実装は、以下のようになります。

```ruby:User#activate
def activate
  update_attribute(:activated,    true)
  update_attribute(:activated_at, Time.zone.now)
end
```

「`update_attribute`の呼び出しに際し、`user.`を頭につけなくなった」というのは大きなポイントです。`user.`がなくなるに至る経緯の説明は以下です。

* Userモデルに`user`という変数は存在しない
    * この点AccountActivationsコントローラーとは異なる
* AccountActivationsコントローラーにおける`user.`に相当するのは、Userモデルにおいては`self.`である
* セッターメソッド[^セッターメソッドの例]でない場合、レシーバーとして用いる`self.`は省略できる
    * `update_attribute`はセッターメソッドではない

[^セッターメソッドの例]: 現在開発中のサンプルアプリケーションで用いているセッターメソッドの例としては、`User#remember_token`や`User#activation_token`があります。

### 有効化メールの送信

有効化メールを送信する`send_activation_email`メソッドの実装は、以下のようになります。

```ruby:User#send_activation_email
def send_activation_email
  UserMailer.account_activation(self).deliver_now
end
```

こちらも「`@user`が`self`に変更されている」というのがポイントです。実装箇所がUsersコントローラーからUserモデルに変わったことにより、このような変更が必要となりました。

「実装箇所が変更されたことに伴う変数名の変更」について、Railsチュートリアル本文には、以下のような記述があります。

> どんなに簡単なリファクタリングであっても、この手の変更はつい忘れてしまうものです。テストをきちんと書いておけば、この種の見落としを検出できます。

### Userモデル全体の変更

`app/models/user.rb`に、`activate`メソッドと`send_activation_email`メソッドを追加していきます。

```diff:app/models/user.rb
  class User < ApplicationRecord
    ...略
+
+   # アカウントを有効にする
+   def activate
+     update_attribute(:activated,    true)
+     update_attribute(:activated_at, Time.zone.now)
+   end
+
+   # 有効化用のメールを送信する
+   def send_activation_email
+     UserMailer.account_activation(self).deliver_now
+   end

    private

      ...略
  end
```

### Usersコントローラーの`create`アクションにおける、有効化メール送信処理の実装変更

有効化メールを送信する処理が行われるのは、Usersコントローラーの`create`アクション内です。Userモデルに`send_activation_email`メソッドを実装したので、Usersコントローラーの`create`アクションでも当該メソッドを呼び出すように実装を変更していきます。

```diff:UsersController#create
  def create
    @user = User.new(user_params)
    if @user.save
-     UserMailer.account_activation(@user).deliver_now
+     @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
```

### AccoutActivationsコントローラーの`edit`アクションにおける、ユーザーの有効化処理の実装変更

ユーザーの有効化処理が行われるのは、AccountActivationsコントローラーのの`edit`アクション内です。Userモデルに`activate`メソッドを実装したので、AccoutActivationsコントローラーの`edit`アクションでも当該メソッドを呼び出すように実装を変更していきます。

```diff:AccountActivationsController#edit
  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
-     user.update_attribute(:activated,    true)
-     user.update_attribute(:activated_at, Time.zone.now)
+     user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
```

### テストが成功することを確認する

テストの成功を確認するところまでがリファクタリングです。実装の変更が完了したならば、すぐにテストに取り掛かりましょう。

```
# rails test
Running via Spring preloader in process 14117
Started with run options --seed 49116

  44/44: [=================================] 100% Time: 00:00:09, Time: 00:00:09

Finished in 9.66480s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips
```

テストは無事成功しました。

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

## 1.1. [リスト 11.39](https://railstutorial.jp/chapters/account_activation?version=5.1#code-update_columns)に記したテンプレートを使って、`update_attribute`の呼び出しを１回の`update_columns`呼び出しにまとめてみましょう (これでデータベースへの問い合わせが１回で済むようになります)。

> [リスト 11.35](https://railstutorial.jp/chapters/account_activation?version=5.1#code-user_activation_methods)にある`activate`メソッドはupdate_attributeを２回呼び出していますが、これは各行で１回ずつデータベースへ問い合わせしていることになります。

```diff:User#activate
  def activate
-   update_attribute(:activated,    true)
-   update_attribute(:activated_at, Time.zone.now)
+   update_columns(activated: true, activated_at: Time.zone.now)
  end
```

`update_attribute`と同様、`update_columns`においてもバリデーションは行われません。この点は`save`や`update_attributes`とは異なります。

ActiveRecordにおけるattributeの更新方法に関しては、@tyamagu2さんによる以下の記事が参考になります（ただし2016年時点の記事であることに留意）。

* [ActiveRecord の attribute 更新方法まとめ](https://qiita.com/tyamagu2/items/8abd93bb7ab0424cf084)

## 1.2. また、変更後にテストを実行し、`green`になることも確認してください。

```
# rails test
Running via Spring preloader in process 14130
Started with run options --seed 41078

  44/44: [=================================] 100% Time: 00:00:07, Time: 00:00:07

Finished in 7.69770s
44 tests, 195 assertions, 0 failures, 0 errors, 0 skips
```

無事テストは成功しました。

## 2. [リスト 11.40](https://railstutorial.jp/chapters/account_activation?version=5.1#code-show_only_active_users_exercise)のテンプレートを使って、後述引用の動作を変更してみましょう。なお、ここで使っているActive Recordの`where`メソッドについては、[13.3.3](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-a_proto_feed)でもう少し詳しく説明します。

> 現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。

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

長くなりましたので、別記事で解説します。

* [Railsチュートリアル 第11章 アカウントの有効化 - /users と /users/:id に対する統合テスト](https://qiita.com/rapidliner00/items/920f078eab0d44119790)