【Rails】ユーザーの削除と権限【Rails Tutorial 10章まとめ】

##管理ユーザー
###admin属性
ユーザーを削除する機能を追加するが、この機能は特別な権限を持ったユーザーのみが行えるようにする。
まずはUserモデルにadmin属性を追加する。

```console
$ rails generate migration add_admin_to_users admin:boolean
```
マイグレーションファイルのadd_columnにdefault: falseオプションを与えて、admin属性のデフォルト値をfalseにする。

```db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
```

サンプルユーザーの一人のadmin属性値をtrueにする。

```db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end
```

###admin属性のテスト
admin属性を変更されるとセキュリティ上問題になるので、ユーザーの新規登録と編集ではユーザー情報を送信する場合にStrong Parametersという形式を用いていた。

```rb
 def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
 end
```

これが機能しているかどうかのテストを書く。

```test/controllers/users_controller_test.rb
  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: { user: { password:              '',
                                                    password_confirmation: '',
                                                    admin: true } }
    assert_not @other_user.admin?
  end
```

admin?メソッドはadmin属性を追加したことで生成されるメソッドで、Userオブジェクトに使うとadminの値に応じて論理値を返す。

##ユーザーの削除
###削除用リンク
ユーザー一覧ページに、管理者にのみ表示される削除用リンクを表示する。

```erb:app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>
```
リンク表示の条件式に!current_user?(user)を付けることで、管理ユーザー自身は削除できないようになっている。
link_toにmethod: :deleteを追加することで、DELETEリクエストを送信できるようになっている。
また、data: { confirm: "You sure?" }を付けると削除前に確認用のポップアップが表示される。

###destroyアクション
ユーザーを削除するdestroyアクションを書く。

```app/controllers/users_controller.rb
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end
```

destroyメソッドでUserオブジェクトを削除する。
その後、フラッシュメッセージを表示してユーザー一覧ページにリダイレクトする。
また、logged_in_userを使ってログインユーザー以外はアクセスできないようにしておく。

###admin_userフィルター
管理者以外に削除用リンクを表示しないようにするだけでは、セキュリティ上の問題がある。
そこで、destroyアクションには管理者のみがリクエストを送れるようにするadmin_userメソッドを用意する。

```app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end
```
destroyアクションにDELETEリクエストを送っても、現在のユーザーが管理者でなければリダイレクトする。

##ユーザー削除のテスト
###管理者権限によるユーザー削除の制限のテスト
テスト用ユーザーの一人を管理者にしておく。

```test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true
```

ユーザー削除に関するテストは次の２点である。
ユーザーを削除しようとした時、
①ログインしていないユーザーの場合はログイン画面にリダイレクトする。
②ログインしているが管理者でないユーザーの場合はホーム画面にリダイレクトする。

```test/controllers/users_controller_test.rb
  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end
```

ユーザーが削除されていないことをassert_no_differenceで確認している。

###ユーザー削除のテスト
ユーザー一覧画面のテストに管理者によるユーザー削除を含める。

```test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end
```

管理者であれば、ユーザー一覧画面にはユーザー削除用リンクが表示されていることをassert_selectで確認している。
二つ目のテストではこれの逆である。
また、unless user == @adminとすることで、管理者以外の削除リンクだけが表示されていることを確認している。