Railsチュートリアル 第11章 アカウントの有効化 - authenticated?に関する想定外のテスト失敗と、その解決

# 前提

[Railsチュートリアル 第11章 11.3 アカウントを有効化する](https://railstutorial.jp/chapters/account_activation?version=5.1#sec-activating_the_account)の最後のほうに、「抽象化された`authenticated?`メソッドに対するテスト」という項目がありました。

変更が必要な実装は以下2つです。

* `SessionsHelper#current_user`メソッド
* `UserTest`の「authenticated? should return false for a new user with nil digest」テスト

# 発生した問題

しかしながら、私の環境では、当該2つのメソッドの実装を変更しても、まだテストが成功しませんでした。内容は以下です。

```
# 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
```

「有効なユーザー情報を返すべきところ、`nil`を返してしまっている」という失敗内容ですね。

私の環境では、`test/helpers/sessions_helper_test.rb`の11行目対応するテストは以下です。

```
test "current user returns right user when session is nil" do
  assert_equal @user, current_user
  assert is_logged_in?
end
```

# どこでバグを埋め込んだのか調べる

## `current_user`の実装

まずは`current_user`の実装を追いかけていきましょう。

```ruby: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?(:remember , cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end
```

## 最初の`if`文では、どのコードが実行されるか

「session is nil」という前提なので、以下の文が実行されます。

```ruby:
user_id = cookies.signed[:user_id]
```

`test/helpers/sessions_helper_test.rb`の`setup`で、テスト環境の永続cookiesに有効なユーザーIDは書き込まれているはずなので、この文は真になります。以下は`setup`の内容です。

```ruby:SessionsHelperTest#setup
def setup
  @user = users(:rhakurei)
  remember(@user)
end
```

## `user_id`に有効なユーザーIDが格納された後に実行されるコード

`user_id`に有効なユーザーIDが格納されたならば、続いて実行されるのは以下のコードです。

```ruby:
if user && user.authenticated?(:remember , cookies[:remember_token])
  log_in user
  @current_user = user
end
```

## `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
```

私はここで、「`user && user.authenticated?(:remember , cookies[:remember_token])`が`false`となるという状況は、`user.authenticated?`が`false`を返してしまっているための結果ではないか」と想定しました。`digest`が`nil`でないことを確認する必要があります。

というわけで、問題となりそうな部分に`debugger`メソッドを挿入してみます。

```diff:User#authenticated?（現状）
  def authenticated?(attribute, token)
    digest = send("#{attribute}_token")
+   debugger
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
```

## `debugger`の実行結果

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

  /0: [=---=---=---=---=---=---=---=---=---=-] 0% Time: 00:00:00,  ETA: ??:??:??
[29, 38] in /var/www/sample_app/app/models/user.rb
   29: 
   30:   # 渡されたトークンがダイジェストと一致したらtrueを返す
   31:     def authenticated?(attribute, token)
   32:       digest = send("#{attribute}_token")
   33:       debugger
=> 34:       return false if digest.nil?
   35:       BCrypt::Password.new(digest).is_password?(token)
   36:     end
   37: 
   38:   # ユーザーのログイン情報を破棄する
(byebug) digest
nil
(byebug) attribute
:remember
(byebug) "#{attribute}_token"
"remember_token"
(byebug) send("#{attribute}_token")
nil
```

見事に`digest`が`nil`を返しています。

また、以下の事柄もわかりました。

* `"#{attribute}_token"`が返す文字列は`"remember_token"`であること
* `send("#{attribute}_token")`の結果が`nil`であること

# 間違いは何か

正しい`digest`を得るためには、`send`に渡す引数は以下である必要があります。

```diff:
- `"#{attribute}_token"`
+ `"#{attribute}_digest"`
```

# `user.authenticated?`の修正

`user.authenticated?`を以下のように修正します。

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

# 改めてテストを実行

```
# 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
```

今度こそテストが成功しました。