←Rails 6で認証認可入り掲示板APIを構築する #15 pundit導入
post_policyの編集
まずはspec/spec_helper.rb
を以下のように変更します。
punditの公式にあるように、以下の通り追加すればpundit用のrspecメソッドが使えるようになります。
RSpec.configure do |config|
...
+ require "pundit/rspec"
end
次に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
なんとなく読み解けると思いますが、念の為解説を。
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?の権限があるかテストをします。
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ユーザーを一致できません。
そのため、以下のように修正を加えます。
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に引っかかるので、以下の対応をします。
...
+
+# AbcSize デフォルト15はキツいので20に上げる
+Metrics/AbcSize:
+ Max: 20
さて、ようやくrequest specの修正です。
...
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で流用しそうですよね。
def update?
@record.user == @user
end
def destroy?
@record.user == @user
end
そのため、application_policy.rbに自分自身のものか判定するプライベートメソッドを作ります。
class ApplicationPolicy
...
+ private
+
+ def mine?
+ @record.user == @user
+ end
#
# scope
#
class Scope
...
post_policyに反映してみます。
def update?
- @record.user == @user
+ mine?
end
def destroy?
- @record.user == @user
+ mine?
end
スッキリしましたね。
これで、今後はuserの関連を持つmodelが自身が所有している場合のみ実行するactionは、policyファイルを作ってmine?
メソッドを配置するだけ。超お手軽ですね。