Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストのアクセス制御を、テスト駆動開発で実装する

# 前提条件

Micropostsコントローラーは、「関連付けられたユーザーを通してマイクロポストにアクセスする」というユースケースが前提となります。そのため、`create`アクションや`destroy`アクションを利用するユーザーは、ログイン済みでなければなりません。

# マイクロポストのアクセス制御に対するテストを記述する

このようなアクセス制御に関する実装は、バグがあった場合の被害が大きくなります。開発者として安心を得るためにも、テスト駆動で実装していきましょう。

テストの実装箇所は`test/controllers/microposts_controller_test.rb`となります。

```ruby:test/controllers/microposts_controller_test.rb
require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end
end
```

上記テストの要点は以下です。

* 「未ログインのユーザーによって投げられた正しいリクエストに対し、マイクロポストの数が変化しないこと」を確認している
* 「未ログインのユーザーが正しいリクエストを投げた場合、ログインページにリダイレクトされること」を確認している

# 最初にテストを記述した時点での`test/controllers/microposts_controller_test.rb`に対するテストの結果

この時点で、`test/controllers/microposts_controller_test.rb`に対するテストを行ってみましょう。

```
# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2236
Started with run options --seed 36069

ERROR["test_should_redirect_create_when_not_logged_in", MicropostsControllerTest, 1.9570221000176389]
 test_should_redirect_create_when_not_logged_in#MicropostsControllerTest (1.96s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'create' could not be found for MicropostsController
            test/controllers/microposts_controller_test.rb:11:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:10:in `block in <class:MicropostsControllerTest>'

ERROR["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 2.150527499994496]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (2.15s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'destroy' could not be found for MicropostsController
            test/controllers/microposts_controller_test.rb:18:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:17:in `block in <class:MicropostsControllerTest>'

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

Finished in 2.16834s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips
```

メッセージの内容からすると、「（Micropostsコントローラーに）`create`アクションも`destroy`アクションも存在しない」という趣旨のエラーということになるでしょう。

## 余談 - `delete`の引数に`micropost`ではなく`microposts`を使ってしまった場合

テスト「should redirect destroy when not logged in」のコードを、以下のように誤って記述してしまったら、テストの結果はどうなるでしょうか。

```ruby:
test "should redirect destroy when not logged in" do
  assert_no_difference 'Micropost.count' do
    delete microposts_path(@micropost) # <-- micropostではなくmicropostsになっている
  end
  assert_redirected_to login_url
end
```

結果は以下のようになります。

```
# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2223
Started with run options --seed 18530

...略

ERROR["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 1.9526230999908876]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (1.95s)
ActionController::RoutingError:         ActionController::RoutingError: No route matches [DELETE] "/microposts.499495288"
            test/controllers/microposts_controller_test.rb:18:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:17:in `block in <class:MicropostsControllerTest>'

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.95662s
2 tests, 2 assertions, 1 failures, 1 errors, 0 skips
```

以下のエラーメッセージが重要です。

```
ActionController::RoutingError: No route matches [DELETE]
```

テスト中にこのようなエラーメッセージが出現した場合は、「単数形と複数形の適用を間違えた」という可能性について検討してみるヒントといえます。

# Micropostsコントローラーに、`create`アクションと`destroy`アクションを実装する

ひとまずは、Micropostsコントローラーに`create`アクションと`destroy`アクションを実装するところから始めましょう。

```diff:app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
+
+   def create
+   end
+
+   def destroy
+   end
  end
```

# Micropostsコントローラーに`create`アクションと`destroy`アクションを実装した時点でのテストの結果

```
# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2250
Started with run options --seed 15761

 FAIL["test_should_redirect_create_when_not_logged_in", MicropostsControllerTest, 2.1255913999921177]
 test_should_redirect_create_when_not_logged_in#MicropostsControllerTest (2.13s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/controllers/microposts_controller_test.rb:13:in `block in <class:MicropostsControllerTest>'

 FAIL["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 2.910650000005262]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (2.91s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/controllers/microposts_controller_test.rb:20:in `block in <class:MicropostsControllerTest>'

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

Finished in 2.91299s
2 tests, 4 assertions, 2 failures, 0 errors, 0 skips
```

「リダイレクトされるべきところがリダイレクトされていない」という理由でテストが失敗しています。当該テストでリダイレクトされる条件は「ログインしていないとき」ということなので、「ログインしていなければ、ログインページにリダイレクトする」という処理の実装が必要となります。

…どこかで見たことがある実装ですね。

# 「ログインしていなければ、ログインページにリダイレクトする」という処理を、Micropostsコントローラーにも適用できるようにする

## Usersコントローラーには、既に同様の処理を行う`logged_in_user`メソッドが存在する

以下が`UsersController#logged_in_user`の実装となります。

```ruby:UsersController#logged_in_user
def logged_in_user
  unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url
  end
end
```

「ログインしていなければ、ログインページにリダイレクトする」という処理がバッチリ書かれていますね。これをMicropostsコントローラーで使えるようにしたいのです。

## `UsersController#logged_in_user`メソッドの定義を、Applicationコントローラーに移す

というわけで、`logged_in_user`メソッドの実装箇所を、`UsersController`ならびに`MicropostsController`双方のスーパークラスである`ApplicationController`に移していきます。

新たな`app/controllers/application_controller.rb`の実装は以下のようになります。

```diff:app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
+
+   private
+
+     # ユーザーのログインを確認する
+     def logged_in_user
+       unless logged_in?
+         store_location
+         flash[:danger] = "Please log in."
+         redirect_to login_url
+       end
+     end
  end
```

JavaやC#などの経験がある人であれば、「あれ？この場所で`private`呼び出して、`UsersController`や`MicropostsController`で`logged_in_user`メソッド呼べるの？」という疑問を持つのではないでしょうか。その点については、後述「`private`という言葉の意味について」という項目で解説しています。

### `private`という言葉の意味について

Rubyのクラス中で用いる`private`メソッドは、単に「以降で定義されるクラスメソッドを、レシーバ付きで呼び出せないようにする」という意味のメソッドです。逆に「レシーバさえつけなければ当該メソッドを呼び出すことが可能」ということをも意味します。

「レシーバをつけないで当該メソッドを呼び出せる場所」というのは、すなわち「当該クラス、もしくは当該クラスを継承したサブクラスのみ」を意味します。結果として、「`private`メソッド以降で定義されるクラスメソッドは、当該クラス、もしくは当該クラスを継承したサブクラスでしか呼び出すことができない」という動作になるのです。

**`private`メソッド以降で定義されたメソッドも、当該クラスを継承したサブクラスで使用することが可能**という事実は重要です。特にJavaやC#等から入ってきた人の場合、このあたりの部分での混乱に注意が必要かと思います。

## `MicropostsController`にbeforeフィルターを追加する

`app/controllers/microposts_controller.rb`には、beforeフィルターとして`logged_in_user`を使う旨を記述する必要があります。対象となるアクションは`create`および`destroy`です。

```diff:app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
+   before_action :logged_in_user, only: [:create, :destroy]

    def create
    end

    def destroy
    end
  end
```

「`MicropostsController`内には、`logged_in_user`の定義を書く必要がない」というのがポイントです。スーパークラスである`ApplicationController`から継承してくるためです。

## 既存の`UsersController#logged_in_user`の定義を削除する

`logged_in_user`の実装箇所を`ActionController`に移したので、既に存在する`UsersController#logged_in_user`の定義は削除する必要があります（続く演習でも、このテーマが取り上げられています）。

```diff:app/controllers/users_controller.rb
  class UsersController < ApplicationController
    before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
    ...略

    private

      ...略
 
      # beforeアクション
-
-     # ログイン済みユーザーかどうか確認
-     def logged_in_user
-       unless logged_in?
-         store_location
-         flash[:danger] = "Please log in."
-         redirect_to login_url
-       end
-     end

      ...略
  end
```

## test/controllers/microposts_controller_test.rb`に対するテストが成功するようになる

ここまでの実装が完了すれば、`test/controllers/microposts_controller_test.rb`に対するテストは成功するようになるはずです。

```
# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2263
Started with run options --seed 2211

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.17518s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが成功しましたね。これで「マイクロポストのアクセス制御を実装できた」といえます。