←Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加
全18回に渡る連載も今回で終了です。
今回はuser controllerを作ります。
今までの集大成なので、例示するコードを見ずに作ってみることをオススメします。
仕様
主に管理者がユーザー管理をする用途と、ユーザーが自分自身の更新削除をするための機能群として想定しています。
- #index 管理者のみ閲覧可能
- #show 自分自身か管理者のみ閲覧可能
- #create 管理者のみ作成可能
- #update 自分自身か管理者のみ更新可能
- #destroy 管理者のみ削除可能
手順
- users_controllerの作成
- user_policyの作成
- user_policyテストの実装
- user_policyの実装
- users_controllerテストの実装
- users_controllerの実装
という手順で進めてみます。
1. users_controllerの作成
実装例
$ rails g controller v1/users
rubocopに怒られないように微修正したファイル群がこちら。
# frozen_string_literal: true
module V1
#
# users controller
#
class UsersController < ApplicationController
end
end
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "V1::Users", type: :request do
end
2. user_policyの作成
実装例
コマンド叩いてファイル作ります。
$ rails g pundit:policy user
rubocopに怒られないように微修正を加えたらとりあえず完成。
# frozen_string_literal: true
#
# userのポリシークラス
#
class UserPolicy < ApplicationPolicy
#
# scope
#
class Scope < Scope
def resolve
scope.all
end
end
end
# frozen_string_literal: true
require "rails_helper"
RSpec.describe UserPolicy, type: :policy do
let(:user) { User.new }
subject { described_class }
permissions ".scope" do
pending "add some examples to (or delete) #{__FILE__}"
end
permissions :show? do
pending "add some examples to (or delete) #{__FILE__}"
end
permissions :create? do
pending "add some examples to (or delete) #{__FILE__}"
end
permissions :update? do
pending "add some examples to (or delete) #{__FILE__}"
end
permissions :destroy? do
pending "add some examples to (or delete) #{__FILE__}"
end
end
3. user_policyテストの実装
実装例
仕様のおさらい。
- #index 管理者のみ閲覧可能
- #show 自分自身か管理者のみ閲覧可能
- #create 管理者のみ作成可能
- #update 自分自身か管理者のみ更新可能
- #destroy 管理者のみ削除可能
#index, #create, #destroyで共通化、#show, #updateで共通化できそうですね。
それを念頭に実装したのがこちら。
# frozen_string_literal: true
require "rails_helper"
RSpec.describe UserPolicy, type: :policy do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let(:admin_user) { create(:user, :admin) }
subject { described_class }
permissions :index?, :create?, :destroy? do
it "未ログインの時に不許可" do
expect(subject).not_to permit(nil, user)
end
it "adminユーザー以外でログインしている時に不許可" do
expect(subject).not_to permit(user, user)
end
it "adminユーザーでログインしている時に許可" do
expect(subject).to permit(admin_user, user)
end
end
permissions :show?, :update? do
it "未ログインの時に不許可" do
expect(subject).not_to permit(nil, user)
end
it "ログインしているが別ユーザーの時に不許可" do
expect(subject).not_to permit(user, another_user)
end
it "adminユーザーでログインしている時に許可" do
expect(subject).to permit(admin_user, user)
end
it "ログインしていて同一ユーザーの時に許可" do
expect(subject).to permit(user, user)
end
end
end
another_user
という別ユーザーを定義しているのがポイント。
さて、この時点ではpolicyを設定していないのでテストがいくつかコケることを確認してください。
4. user_policyの実装
実装例
# frozen_string_literal: true
#
# userのポリシークラス
#
class UserPolicy < ApplicationPolicy
def index?
admin?
end
def show?
me? || admin?
end
def create?
admin?
end
def update?
me? || admin?
end
def destroy?
admin?
end
private
def me?
@record == @user
end
#
# scope
#
class Scope < Scope
def resolve
scope.all
end
end
end
me?
というprivateメソッドをuser_policy.rbに定義しているのがポイントです。
application_policy.rbにあるmine?
は、@record.user == @user
と、@record
の持つuser
が自分のものか比較していました。
ですが今回は@record
と@user
が一致するか確認するので、英語の文法的にmine?
ではなくme?
になります。その上、user
自身と比較するのは現時点ではusers_controllerでしかなさそうなので、application_policyではなくuser_policyに提議しました。
あとは特筆することもなく、テストが通過するようになったはずです。
5. users_controllerテストの実装
実装例
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "V1::Users", type: :request do
before do
@user = create(:user, name: "userテスト")
@authorized_headers = authorized_user_headers @user
admin = create(:user, :admin)
@authorized_admin_headers = authorized_user_headers admin
end
describe "GET /v1/users#index" do
before do
create_list(:user, 3)
end
it "正常レスポンスコードが返ってくる" do
get v1_users_url, headers: @authorized_admin_headers
expect(response.status).to eq 200
end
it "件数が正しく返ってくる" do
get v1_users_url, headers: @authorized_admin_headers
json = JSON.parse(response.body)
expect(json["users"].length).to eq(3 + 2) # headers用2件を含む
end
it "id降順にレスポンスが返ってくる" do
get v1_users_url, headers: @authorized_admin_headers
json = JSON.parse(response.body)
first_id = json["users"][0]["id"]
expect(json["users"][1]["id"]).to eq(first_id - 1)
expect(json["users"][2]["id"]).to eq(first_id - 2)
expect(json["users"][3]["id"]).to eq(first_id - 3)
expect(json["users"][4]["id"]).to eq(first_id - 4)
end
end
describe "GET /v1/users#show" do
it "正常レスポンスコードが返ってくる" do
get v1_user_url({ id: @user.id }), headers: @authorized_headers
expect(response.status).to eq 200
end
it "nameが正しく返ってくる" do
get v1_user_url({ id: @user.id }), headers: @authorized_headers
json = JSON.parse(response.body)
expect(json["user"]["name"]).to eq("userテスト")
end
it "存在しないidの時に404レスポンスが返ってくる" do
last_user = User.last
get v1_user_url({ id: last_user.id + 1 }), headers: @authorized_headers
expect(response.status).to eq 404
end
end
describe "POST /v1/users#create" do
let(:new_user) do
attributes_for(:user, name: "create_nameテスト", email: "email+create_test@example.com", admin: true)
end
it "正常レスポンスコードが返ってくる" do
post v1_users_url, params: new_user, headers: @authorized_admin_headers
expect(response.status).to eq 200
end
it "1件増えて返ってくる" do
expect do
post v1_users_url, params: new_user, headers: @authorized_admin_headers
end.to change { User.count }.by(1)
end
it "name, email, adminが正しく返ってくる" do
post v1_users_url, params: new_user, headers: @authorized_admin_headers
json = JSON.parse(response.body)
expect(json["user"]["name"]).to eq("create_nameテスト")
expect(json["user"]["email"]).to eq("email+create_test@example.com")
expect(json["user"]["admin"]).to be true
end
it "不正パラメータの時にerrorsが返ってくる" do
post v1_users_url, params: {}, headers: @authorized_admin_headers
json = JSON.parse(response.body)
expect(json.key?("errors")).to be true
end
end
describe "PUT /v1/users#update" do
let(:update_param) do
update_param = attributes_for(:user, name: "update_nameテスト", email: "email+update_test@example.com", admin: true)
update_param[:id] = @user.id
update_param
end
it "正常レスポンスコードが返ってくる" do
put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
expect(response.status).to eq 200
end
it "name, email, adminが正しく返ってくる" do
put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
json = JSON.parse(response.body)
expect(json["user"]["name"]).to eq("update_nameテスト")
expect(json["user"]["email"]).to eq("email+update_test@example.com")
expect(json["user"]["admin"]).to be false # admin権限は書き換えできると困るのでfalseのまま
end
it "不正パラメータの時にerrorsが返ってくる" do
put v1_user_url({ id: update_param[:id] }), params: { name: "" }, headers: @authorized_headers
json = JSON.parse(response.body)
expect(json.key?("errors")).to be true
end
it "存在しないidの時に404レスポンスが返ってくる" do
last_user = User.last
put v1_user_url({ id: last_user.id + 1 }), params: update_param, headers: @authorized_admin_headers
expect(response.status).to eq 404
end
end
describe "DELETE /v1/users#destroy" do
it "正常レスポンスコードが返ってくる" do
delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
expect(response.status).to eq 200
end
it "1件減って返ってくる" do
expect do
delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
end.to change { User.count }.by(-1)
end
it "存在しないidの時に404レスポンスが返ってくる" do
last_user = User.last
delete v1_user_url({ id: last_user.id + 1 }), headers: @authorized_admin_headers
expect(response.status).to eq 404
end
end
end
いくつかpostの時とは違う考慮点があります。
- headerを作る際に
create(:user)
をしているので、その2件分を考慮してテストを書くこと。- 存在しないid確認の際も同様、last_userを取得して+1
- adminフラグは自由に書き換えれてしまうと困るので、createでadminフラグは反映されるがupdateでは反映されないこと
あたりを取り入れています。
また、レスポンスにadmin判定があるので、serializerの修正も必要ですね。
# user serializer
#
class UserSerializer < ActiveModel::Serializer
- attributes :id, :name, :email
+ attributes :id, :name, :email, :admin
end
以上で準備完了。次にcontroller実装に入ります。
6. users_controllerの実装
実装例
controllerにアクセスできるようにするため、routesの修正をします。
Rails.application.routes.draw do
namespace "v1" do
resources :posts
+ resources :users
mount_devise_token_auth_for "User", at: "auth"
end
続いてcontrollerです。
こちらはpostのほぼ流用でいけますね。
# frozen_string_literal: true
module V1
#
# users controller
#
class UsersController < ApplicationController
before_action :set_user, only: %i[show update destroy]
def index
users = User.order(created_at: :desc).limit(20)
authorize users
render json: users
end
def show
authorize @user
render json: @user
end
def create
user = User.new(user_create_params)
user[:provider] = :email
authorize user
if user.save
render json: user
else
render json: { errors: user.errors }
end
end
def update
authorize @user
if @user.update(user_params)
render json: @user
else
render json: { errors: @user.errors }
end
end
def destroy
authorize @user
@user.destroy
render json: @user
end
private
def set_user
@user = User.find(params[:id])
end
def user_create_params
# createの時のみadmin権限を設定できるようにする
params.permit(:name, :email, :admin, :password)
end
def user_params
params.permit(:name, :email, :password)
end
end
end
テストのセクションで書いたように、adminはcreateの時のみ付与できるようにしています。
以上です。
全18回に渡りご覧いただきありがとうございました。
ぜひこれを元に、コメント機能やソーシャルログイン等の機能拡張していってください。
目次
【連載目次へ】