前提条件
Micropostsコントローラーは、「関連付けられたユーザーを通してマイクロポストにアクセスする」というユースケースが前提となります。そのため、create
アクションやdestroy
アクションを利用するユーザーは、ログイン済みでなければなりません。
マイクロポストのアクセス制御に対するテストを記述する
このようなアクセス制御に関する実装は、バグがあった場合の被害が大きくなります。開発者として安心を得るためにも、テスト駆動で実装していきましょう。
テストの実装箇所は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」のコードを、以下のように誤って記述してしまったら、テストの結果はどうなるでしょうか。
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
アクションを実装するところから始めましょう。
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
の実装となります。
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
の実装は以下のようになります。
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
です。
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
の定義は削除する必要があります(続く演習でも、このテーマが取り上げられています)。
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
無事テストが成功しましたね。これで「マイクロポストのアクセス制御を実装できた」といえます。