Rails 6で認証認可入り掲示板APIを構築する #18･終 user controllerの実装

←[Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加](https://qiita.com/rf_p/items/bdaeab765d55ef924d02)

全18回に渡る連載も今回で終了です。

今回はuser controllerを作ります。
今までの集大成なので、例示するコードを見ずに作ってみることをオススメします。

# 仕様

主に管理者がユーザー管理をする用途と、ユーザーが自分自身の更新削除をするための機能群として想定しています。

* #index 管理者のみ閲覧可能
* #show 自分自身か管理者のみ閲覧可能
* #create 管理者のみ作成可能
* #update 自分自身か管理者のみ更新可能
* #destroy 管理者のみ削除可能

# 手順

1. users_controllerの作成
2. user_policyの作成
3. user_policyテストの実装
4. user_policyの実装
5. users_controllerテストの実装
6. users_controllerの実装

という手順で進めてみます。

# 1. users_controllerの作成

<details><summary>実装例</summary><div>
コマンド叩くだけなので簡単ですね。

```powershell
$ rails g controller v1/users
```

rubocopに怒られないように微修正したファイル群がこちら。

```rb:app/controllers/v1/users_controller.rb
# frozen_string_literal: true

module V1
  #
  #  users controller
  #
  class UsersController < ApplicationController
  end
end

```

```rb:spec/requests/v1/users_request_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "V1::Users", type: :request do
end
```
</div></details>

# 2. user_policyの作成

<details><summary>実装例</summary><div>

コマンド叩いてファイル作ります。

```powershell
$ rails g pundit:policy user
```

rubocopに怒られないように微修正を加えたらとりあえず完成。

```rb:app/policies/user_policy.rb
# frozen_string_literal: true

#
# userのポリシークラス
#
class UserPolicy < ApplicationPolicy
  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end
```

```rb:spec/policies/user_policy_spec.rb
# 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
```

</div></details>

# 3. user_policyテストの実装

<details><summary>実装例</summary><div>

仕様のおさらい。

* #index 管理者のみ閲覧可能
* #show 自分自身か管理者のみ閲覧可能
* #create 管理者のみ作成可能
* #update 自分自身か管理者のみ更新可能
* #destroy 管理者のみ削除可能

#index, #create, #destroyで共通化、#show, #updateで共通化できそうですね。

それを念頭に実装したのがこちら。

```rb:spec/policies/user_policy_spec.rb
# 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を設定していないのでテストがいくつかコケることを確認してください。
</div></details>

# 4. user_policyの実装

<details><summary>実装例</summary><div>

```rb:app/policies/user_policy.rb
# 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に提議しました。

あとは特筆することもなく、テストが通過するようになったはずです。</div></details>

# 5. users_controllerテストの実装

<details><summary>実装例</summary><div>

```rb:spec/requests/v1/users_request_spec.rb
# 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の修正も必要ですね。

```diff:app/serializers/user_serializer.rb
 # user serializer
 #
 class UserSerializer < ActiveModel::Serializer
-  attributes :id, :name, :email
+  attributes :id, :name, :email, :admin
 end
```

以上で準備完了。次にcontroller実装に入ります。</div></details>

# 6. users_controllerの実装

<details><summary>実装例</summary><div>

controllerにアクセスできるようにするため、routesの修正をします。

```diff:config/routes.rb
 Rails.application.routes.draw do
   namespace "v1" do
     resources :posts
+    resources :users
     mount_devise_token_auth_for "User", at: "auth"
   end
```

続いてcontrollerです。
こちらはpostのほぼ流用でいけますね。

```rb:app/controllers/v1/users_controller.rb
# 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の時のみ付与できるようにしています。</div></details>

以上です。
全18回に渡りご覧いただきありがとうございました。

ぜひこれを元に、コメント機能やソーシャルログイン等の機能拡張していってください。


# 目次

【[連載目次へ](https://qiita.com/rf_p/items/a846553c11571f4177eb#%E7%9B%AE%E6%AC%A1)】