Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定

←[Rails 6で認証認可入り掲示板APIを構築する #15 pundit導入](https://qiita.com/rf_p/items/6ab38a354c8f31b164db)

# post_policyの編集

まずは`spec/spec_helper.rb`を以下のように変更します。
punditの公式にあるように、以下の通り追加すればpundit用のrspecメソッドが使えるようになります。

```diff:spec/spec_helper.rb
 RSpec.configure do |config|
...

+  require "pundit/rspec"
 end
```

次に`spec/policies/post_policy_spec.rb`にテストを組み込んでいきます。

```rb:spec/policies/post_policy_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe PostPolicy, type: :policy do
  let(:user) { create(:user) }
  let(:post) { create(:post) }

  subject { described_class }

  permissions :index?, :show? do
    it "未ログインの時に許可" do
      expect(subject).to permit(nil, post)
    end
  end

  permissions :create? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, post)
    end
    it "ログインしている時に許可" do
      expect(subject).to permit(user, post)
    end
  end

  permissions :update?, :destroy? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, post)
    end
    it "ログインしているが別ユーザーの時に不許可" do
      expect(subject).not_to permit(user, post)
    end
    it "ログインしていて同一ユーザーの時に許可" do
      post.user = user
      expect(subject).to permit(user, post)
    end
  end
end
```

なんとなく読み解けると思いますが、念の為解説を。

```rb
  permissions :index?, :show? do
    it "未ログインの時に許可" do
      expect(subject).to permit(nil, post)
    end
  end
```

index?とshow?は条件が同じのためまとめてテストをしています。
`permit(nil, post)`は第1引数にログインユーザーmodelを、第2引数に対象modelを指定します。
すると第1引数のユーザーが第2引数のオブジェクトのindex?やshow?の権限があるかテストをします。

```rb
  permissions :update?, :destroy? do
...
    it "ログインしているが別ユーザーの時に不許可" do
      expect(subject).not_to permit(user, post)
    end
    it "ログインしていて同一ユーザーの時に許可" do
      post.user = user
      expect(subject).to permit(user, post)
    end
  end
```

not_toは見たままですが、許可されていないことのテストですね。
そして、postの所有ユーザーを一致したことで最後のテストはパスします。

# request specの修正

一旦rspecを動かしてみます。
すると`spec/requests/v1/posts_request_spec.rb`が結構コケます。

原因は上記と同じく、#updateや#destoryが所属ユーザーのログインじゃないと403になるからです。
ですが、`posts_request_spec.rb`で使っている`authorized_user_headers`ヘルパは内部で`create(:user)`をしているため、ログインユーザーとpostユーザーを一致できません。

そのため、以下のように修正を加えます。

```diff:spec/support/authorization_spec_helper.rb
 module AuthorizationSpecHelper
-  def authorized_user_headers
-    user = create(:user)
+  def authorized_user_headers(user = nil)
+    user = create(:user) if user.nil?
     post v1_user_session_url, params: { email: user.email, password: "password" }
```

これで、`authorized_user_headers`に引数無しで渡した場合は内部でuserが作られ、引数でuserを渡した場合はそれを利用します。

ただし`authorized_user_headers`が少し複雑になってしまいrubocopのAbcSizeに引っかかるので、以下の対応をします。

```diff:.rubocop.yml

...
+
+# AbcSize デフォルト15はキツいので20に上げる
+Metrics/AbcSize:
+  Max: 20
```

さて、ようやくrequest specの修正です。

```diff:spec/requests/v1/posts_request_spec.rb
...
     it "正常レスポンスコードが返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: update_param
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user)
       expect(response.status).to eq 200
     end
     it "subject, bodyが正しく返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: update_param
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user)
       json = JSON.parse(response.body)
       expect(json["post"]["subject"]).to eq("update_subjectテスト")
       expect(json["post"]["body"]).to eq("update_bodyテスト")
     end
     it "不正パラメータの時にerrorsが返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: { subject: "" }
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: { subject: "" }, headers: authorized_user_headers(post.user)
       json = JSON.parse(response.body)
       expect(json.key?("errors")).to be true
     end
@@ -106,13 +109,13 @@ RSpec.describe "V1::Posts", type: :request do
       create(:post)
     end
     it "正常レスポンスコードが返ってくる" do
-      delete v1_post_url({ id: delete_post.id })
+      delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user)
       expect(response.status).to eq 200
     end
     it "1件減って返ってくる" do
       delete_post
       expect do
-        delete v1_post_url({ id: delete_post.id })
+        delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user)
       end.to change { Post.count }.by(-1)
     end
```

そこまで大きな変更は無いですね。
`authorized_user_headers`にpostの所有ユーザーを渡すことで、認可を通過します。

# 所有ユーザー一致判定メソッドの作成

ここをもう少し直感的に変えていきます。
自分自身のものか判定する処理はpostに限らず、今後もいろいろなmodelで流用しそうですよね。

```rb
  def update?
    @record.user == @user
  end
 
  def destroy?
    @record.user == @user
  end
```

そのため、application_policy.rbに自分自身のものか判定するプライベートメソッドを作ります。

```diff:app/policies/application_policy.rb
class ApplicationPolicy
...

+  private
+
+  def mine?
+    @record.user == @user
+  end

   #
   # scope
   #
  class Scope
...
```

post_policyに反映してみます。

```diff:app/policies/post_policy.rb
   def update?
-    @record.user == @user
+    mine?
   end
 
   def destroy?
-    @record.user == @user
+    mine?
   end
```

スッキリしましたね。
これで、今後はuserの関連を持つmodelが自身が所有している場合のみ実行するactionは、policyファイルを作って`mine?`メソッドを配置するだけ。超お手軽ですね。


# 続き

→[Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加](https://qiita.com/rf_p/items/bdaeab765d55ef924d02)
【[連載目次へ](https://qiita.com/rf_p/items/a846553c11571f4177eb#%E7%9B%AE%E6%AC%A1)】