0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsチュートリアル10章まとめ

Posted at

この章でやること ユーザーの更新・表示・削除

  • Usersリソースのうち未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる
  • まず、ユーザーが自分のプロフィールを自分で更新できるようにする
  • 次に、すべてのユーザーを一覧できるようにする。サンプルデータとページネーション(pagination)も学ぶ
  • 最後に、ユーザーを削除し、データベースから完全に消去する機能を追加。ユーザーの削除はどのユーザーにも許可できるものではないので、管理ユーザーという特権クラスを作成し、このユーザーにのみ削除を許可するようにする

10.1 ユーザーを更新する

導入
ユーザー情報を編集するパターンは、新規ユーザーの作成と似ている
新規ユーザー用のビューを出力するnewアクションと同じようにして、ユーザーを編集するためのeditアクションを作成すればよい。
同様に、POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成する
最大の違いは、ユーザー登録は誰でも実行できるが、ユーザー情報を更新できるのはそのユーザー自身に限られる
beforeフィルターを使ってこのアクセス制御を実現していく

updating-usersトピックブランチを作成

$ git checkout -b updating-users

10.1.1 編集フォーム

Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装
editアクションはデータベースからユーザーデータを読み込む

ユーザー編集ページの正しいURLが/users/1/edit。
ユーザーのidはparams[:id]変数で取り出すことができるため@user変数を定義

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def edit
    @user = User.find(params[:id])
  end

end

対応するユーザーのeditビューを作成

$ touch app/views/users/edit.html.erb


```app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

@user変数を使ったため、自動で既に編集フォームに名前やメールが入った状態になる

検証ツールでHTMLをみると

<form accept-charset="UTF-8" action="/users/1" class="edit_user"
      id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
  .
  .
  .
</form>

次の入力フィールドに隠し属性がある

<input name="_method" type="hidden" value="patch" />

WebブラウザはネイティブではPATCHリクエストを送信できない。
RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを偽造している。(hiddenを使っている)

railsのform_with(@user)のコードは新規作成newとeditで文章が変わらないが、Railsはどうやって新規ユーザー用のPOSTリクエストとユーザー編集用のPATCHリクエストを区別するのか?
その答えは、Railsは新規ユーザーか、既存のDBにいるユーザーか、
Active Recordのnew_record?論理値メソッドを使って区別できるから。

Rails Consoleで確認してみる。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

※もう少し調べてみた

調査 
new_record?理論値メソッド
new_record?はDBにレコードが登録されて「いなければ」true 登録されていればfalse
→レコードが登録されていなければPOSTで送り、レコードが登録されていればPATCHを使う

Railsは、form_with(@user)を使ってフォームを構成すると、@user.new_record?がtrueの時にはPOSTリクエスト、falseのときにはPATCHリクエストを使う。

仕上げに、ナビゲーションバーにあるユーザー設定へのリンクを更新する。

Usersリソースの名前付きルートである、edit_user_pathと、current_userというヘルパーメソッドを使うと、実装が簡単。

<%= link_to "Settings", edit_user_path(current_user) %>

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", edit_user_path(current_user) %></li> #passを定義
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

調査 target_blankの調査 なぜ脆弱性があるのか

https://webegins.com/target-blank/
復習 provide メソッド
viewでprovideヘルパーを利用することで、ここのテンプレートからレイアウト側にタイトルを引き渡すことができる。

			view/home.html.erb
			<% provide :title, 'タイトルの例' %>
			レイアウト側からは、yield メソッドを利用することで呼び出せる。
			
			layouts/application.html.erb
			<title><%= yield(:title) || 'Rails入門' %></title>

演習

先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
rel="noopener"を追加するだけ

リスト 10.5のパーシャルを使って、new.html.erbビュー(リスト 10.6)とedit.html.erbビュー(リスト 10.7)をリファクタリングしてみましょう(コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます
→コピペするだけでOK

10.1.2 編集の失敗

createアクションと同じような構造で、updateアクションを作成する
editビューのフォームから送信されたparamsハッシュを受け取り、ユーザーを更新する。
無効な情報の場合は編集ページを描写する

app/controllers/users_controller.rb
class UsersController < ApplicationController


  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def update  #createと構造同じ
    @user = User.find(params[:id])
    if @user.update(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end
end

updateへの呼び出しでuser_paramsを使っている
以前利用したStrong Parametersを使ってマスアサインメントの脆弱性を防止している(user_paramsはprivete以降に記述あり)

エラーに関しても、Userモデルのバリデーションとエラーメッセージのパーシャルが既にあるため、自動でエラーメッセージを表示してくれる

演習

編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
→確認

10.1.3 編集失敗時のテスト

テストのガイドラインに従って、エラーを検知するための統合テストを書く

まずは統合テストを生成

$ rails generate integration_test users_edit

最初は編集失敗時の簡単なテストを追加していく

  1. まず編集ページにアクセスし、editビューが描画されるかどうかをチェック
  2. 無効な情報を送信してみて、editビューが再描画されるかどうかをチェック

ここで、PATCHリクエストを送るためにpatchメソッドを使っていることに注意

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)  #@userの編集ページを取得
    assert_template 'users/edit'  #editページが表示されているか
    patch user_path(@user), params: { user: { name:  "",  #おかしいparamsをpatchへ送信
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'  #editビューが描写されているか
  end
end

これで、テストを実行すると green

演習

リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。

 test "unsuccessful edit" do
    get edit_user_path(@user)  #@userの編集ページを取得
    assert_template 'users/edit'  #editページが表示されているか
    patch user_path(@user), params: { user: { name:  "",  #おかしいparamsをpatchへ送信
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'  #editビューが描写されているか
    assert_select "div.alert","The form contains 4 errors."
  end

10.1.4 TDDで編集を成功させる

ユーザーの編集フォームが動作するようにする。
プロフィール画像の編集は、Gravatarで画像のアップロードも既に動作するようになっている

快適にテストをするためには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利
そのテストを「受け入れテスト(Acceptance Tests)」として呼ばれていて、ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決めるテストとして知られている
同じ手法を使って(テスト駆動開発)ユーザーの編集機能を実装していく

テストの内容は上のテストを参考に書いていく

  1. ユーザー情報を更新する正しい振る舞いをテストで定義(今回は有効な情報を送信するように修正)。
  2. flashメッセージが空でないかどうかと、プロフィールページにリダイレクトされるかどうかをチェック
  3. データベース内のユーザー情報が正しく変更されたかどうかも検証

以下のテストコードになる

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

test "successful edit" do
    get edit_user_path(@user) 
    assert_template 'users/edit'
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,#有効な情報
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?  #flashが空でないか
    assert_redirected_to @user #userビューページへ飛んでいるか
    @user.reload  #データテーブルを再度読み込む
    assert_equal name,  @user.name  #設定したnameとDBのnameが一致しているか
    assert_equal email, @user.email
end

テストがパスするために、updateアクションも変更

app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
end

1)テストコードのパスワードとパスワード確認が空であることに注目。
ユーザー名やメールアドレスを編集するときに毎回パスワードを入力するのは不便なので、(パスワードを変更する必要が無いときは)パスワードを入力せずに更新できると便利になる
2)また、@user.reloadを使って、データベースから最新のユーザー情報を読み込み直して、正しく更新されたかどうかを確認している点にも注目。(受け入れテストでは先にテストを書くので、効果的なユーザー体験について考えるようになる)

このテストはまだ red のまま。
なぜならパスワードの長さに対するバリデーションがあるので、パスワードやパスワード確認の欄を空にしているため引っかかってしまうから。
テストがパスするためにパスワードのバリデーションに対して、空だったときの例外処理を加える必要がある
allow_nil: trueオプションを使ってvalidatesに追加する
パスワードが空のままでも更新できるようになる

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  .
  .
  .
end

パスワードが空のままでも更新できるようになってしまうと、「新規ユーザー登録時に空のパスワードが有効になってしまうのか」と心配になるが、
has_secure_passwordでは(追加したバリデーションとは別に)オブジェクト生成時に存在性を検証するようになっているため、空のパスワード(nil)が新規ユーザー登録時に有効になることはない。
問題であった空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグもこれで解決する

テストはパスするはず

演習

実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
もしGravatarと紐付いていない適当なメールアドレス(foobar@example.comなど)に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。
→OK Gravatarのデフォルトの画像が表示される

10.2 認可

ウェブアプリケーションでは、認証(authentication)はサイトのユーザーを識別することであり、認可(authorization)はそのユーザーが実行可能な操作を管理すること

今時点、どのユーザーでもあらゆるアクションにアクセスできるため、誰でも(ログインしていないユーザーでも)ユーザー情報を編集できてしまう。
ここでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御していく
(こういったセキュリティ上の制御機構をセキュリティモデルと呼ぶ)

10.2.1 ユーザーにログインを要求する

ログインしてないユーザがURLを直接触って編集できないようにするには、Usersコントローラの中でbeforeフィルターを使う。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?#logged_inがfalseなら(logged_inはsessions_helperで定義ずみ)
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるため、ここでは:onlyオプション(ハッシュ)を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかける

これで直接URLを触ってeditやupdateはできなくなったが、テストが古いのでテストは失敗する
原因は、ログインしていないユーザーのままのテストだから

log_in_asヘルパーを使ってテストでもログインする。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    log_in_as(@user) #@userとしてログイン
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user) #@userとしてログイン
    get edit_user_path(@user)
    .
    .
    .
  end
end

これでテストスイートがパスする
しかし、beforeフィルターの実装はまだ終わってない

なぜならbeforeフィルターをコメントアウトしてもテストが通ってしまうから

beforeフィルターは基本的にアクションごとに適用していくので、
Usersコントローラのテストもアクションごとに書いていく。

具体的には

①正しい種類のHTTPリクエストを使う
②editアクションとupdateアクションをそれぞれ実行させてみる
③flashにメッセージが代入されるかどうか検証
④ログイン画面にリダイレクトされたかどうか

HTTPリクエストは、 editにはget、updateにはpatchを書く

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

コメントアウトしていた箇所を元に戻すと、テストがパスするはず

演習

デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。
→エラーになる

10.2.2 正しいユーザーを要求する

次はユーザーが自分の情報だけを編集できるようにする。

まずはユーザーの情報が互いに編集できないことを確認するために、サンプルユーザーをもう一人追加

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

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

log_in_asメソッドを使って、editアクションとupdateアクションをテストする。
このとき、既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクトしている

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer) 
  end
  .
  .
  .
 test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user) #違うユーザーとしてログイン
    get edit_user_path(@user)#@user_pathを取得
    assert flash.empty?#flashが空か確認
    assert_redirected_to root_url#Homeビューへ帰るか確認
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)#違うユーザーでログイン
    patch user_path(@user), params: { user: { name: @user.name, #@userのparamsをcreateへ
                                              email: @user.email } }
    assert flash.empty?#フラッシュが空か
    assert_redirected_to root_url
  end
end

別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする
beforeフィルターのcorrect_userで@user変数を定義しているため、editとupdateの各アクションから、@userへの代入文を削除している。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

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

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user == current_user#後にリファクタリング
    end
end

今度はテストスイートがgreen になる

最後に、一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装。
correct_userというbefore filterの中で使えるようにしたいので、Sessionsヘルパーの中にcorrect_userメソッドを追加します。このメソッドを使うと今までの

unless @user == current_user
といった部分が、次のようになる
unless current_user?(@user)

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーがカレントユーザーであればtrueを返す
  def current_user?(user)
    user && user == current_user
  end

リファクタリングしたので、users_controller.rbを書き換える

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

演習

何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
→知らん人が勝手に他のユーザーを変えたら困るから
上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
→edit DB関係なくテストできる

10.2.3 フレンドリーフォワーディング

例えばユーザーの編集ページに行こうとして、ログインせよと弾かれた場合、ログイン後はその編集ページに飛ばしてあげるのが親切
そういう機能を追加していく
フレンドリーフォワーディングのテストは非常にシンプルで、ログインした後に編集ページへアクセスする、という順序を逆にしてあげるだけ

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

 test "successful edit with friendly forwarding" do
    get edit_user_path(@user)#@userの編集ページを取得
    log_in_as(@user)#ログインしてなかったからできないので@userでログイン
    assert_redirected_to edit_user_url(@user)#editページへリダイレクトしたか
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,#編集
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?#flashが空でないか
    assert_redirected_to @user#ユーザーのshowページへリダイレクトしたか
    @user.reload#userのDBを再取得
    assert_equal name,  @user.name#編集した名前とDBの名前が一致しているか
    assert_equal email, @user.email
  end
end

フレンドリーフォワーディングを実装し、ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる。
この動作をstore_locationとredirect_back_orの2つのメソッドを使って実現していく

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
# 記憶したURL(もしくはデフォルト値)にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
    #リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合はデフォルトのURLにリダイレクト
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
    #リクエストが送られたURLをsession変数の:forwarding_urlキーに格納
    #ただし、GETリクエストが送られたときだけ
  end
end

store_locationメソッドでは、 リクエストが送られたURLをsession変数の:forwarding_urlキーに格納。ただし、GETリクエストが送られたときだけ。
これによって、例えばログインしていないユーザーがフォームを使って送信した場合、転送先のURLを保存させないようにできる
滅多に起きないが起こり得る話で、例えばユーザがセッション用のcookieを手動で削除してフォームから送信するケースなどで、POSTや PATCH、DELETEリクエストを期待しているURLに対して、(リダイレクトを通して)GETリクエストが送られてしまい、場合によってはエラーが発生する。このため、if request.get?という条件文を使ってこのケースに対応

定義したstore_locationメソッドをbeforeフィルター(logged_in_user)をに加える

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

end

フォワーディング自体を実装するには、redirect_back_orメソッドを使い、
リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトさせる
デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクト

redirect_back_orメソッドでは、次のようにor演算子||を使う

session[:forwarding_url] || default

このコードは、値がnilでなければsession[:forwarding_url]を評価し、そうでなければデフォルトのURLを使う
またsession.delete(:forwarding_url)という行を通して転送用のURLを削除している点にも注意。
削除しないと次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまう

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

これでテストもパスし、認可も終わり

演習

フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。

users_edit_test.rb
test "successful edit with friendly forwarding" do
    get edit_user_path(@user)                                                   # @userのユーザー編集ページを取得
    assert_equal session[:forwarding_url], edit_user_url(@user)                 # 渡されたURLに転送されているか確認
    log_in_as(@user)                                                            # @userでログイン
    assert_nil session[:forwarding_url]                                         # forwarding_urlの値がnilならtrue(deleteが効いてる)
    name  = "Foo Bar"                                                           # フォーム欄に値を入力する
    email = "foo@bar.com"                                                       
    patch user_path(@user), params: { user: { name: name,                       # 引数としてわざと失敗する値を持ったuserIDをpatchリクエストで送信(更新)する
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?                                                     # エラー文が空じゃなければtrue
    assert_redirected_to @user                                                  # michaelのユーザーidページへ移動できたらtrue
    @user.reload
    assert_equal name,  @user.name                                              # DB内の名前と@userの名前が一致していていたらtrue
    assert_equal email, @user.email                                             # DB内のEmailと@userの名前が一致
  end

7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。
→bootstrapがうまく動かなかったので割愛

10.3 すべてのユーザーを表示する

ユーザーの一覧ページ(indexページ)を作る
ページネーションも学ぶ

10.3.1 ユーザーの一覧ページ

まずはセキュリティモデルから考える
ユーザーのshowページは、サイトを訪れたすべてのユーザーから見えるようにしておくが、
ユーザーのindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限していく

indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテストを書いていく(テスト駆動開発)

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end

  test "should get new" do
    get signup_path
    assert_response :success
  end

  test "should redirect index when not logged in" do
    get users_path  #indexページへのパスを取得
    assert_redirected_to login_url#ログインページまで戻るか
  end
  .
  .
  .
end

次に、beforeフィルターのlogged_in_userにindexアクションを追加して、このアクションを保護

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]#indexを追加
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装
User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入させる(データの読み込みの問題はあるが後で直す)

app/controllers/users_controller.rb

  def index
    @users = User.all#全てのuserを取得
  end

eachメソッドを使って,ユーザーごとにliタグで囲むビューを作成する
それぞれの行をリストタグulで囲いながら、各ユーザーのGravatarと名前を表示

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>
app/helpers/users_helper.rb
module UsersHelper

  # 渡されたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    size         = options[:size]
    gravatar_id  = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

CSSも修正

app/assets/stylesheets/custom.scss
.
.
.
/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

ヘッダーにユーザー一覧表示用のリンクを追加(users_path)

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", users_path %></li>#追加
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", edit_user_path(current_user) %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

これでテストgreenに

演習

レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

site_layout_test.rb
test "layout links login" do
    log_in_as(@user)
    get root_path
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", signup_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
  end

10.3.2 サンプルのユーザー

Rubyを使ってユーザーを一気に増やす

まず、GemfileにFaker gemを追加
これは、実際にいそうなユーザー名を作成するgem
faker gemは開発環境以外では普通使わないが、今回は例外的に本番環境でも適用させる予定

source 'https://rubygems.org'

gem 'rails',                   '6.0.3'
gem 'bcrypt',                  '3.1.13'
gem 'faker',                   '2.1.2'
gem 'will_paginate',           '3.1.8'
gem 'bootstrap-will_paginate', '1.0.0'
$ bundle install

サンプルユーザーを生成するRubyスクリプト(Railsタスクとも呼ぶ)を追加

db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

# 追加のユーザーをまとめて生成する
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

これでExample Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成できる

データベースをリセットして、Railsタスクを実行(db:seed)

$ rails db:migrate:reset
$ rails db:seed

システムによっては数分かかる
(rails serverは止めたほうがいい)

終わるとユーザーが100人になってる

演習

試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。
→確認

10.3.3 ページネーション

今100このアカウントが1つのページに表示されてしまっているので、ページネーション(pagination)を実装して1つのページに30人だけユーザーを表示する

Railsには豊富なページネーションメソッドがあるが、最もシンプルかつ堅牢なwill_paginateメソッドを使っていく
まずはGemfileにwill_paginate gembootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する

source 'https://rubygems.org'

gem 'rails',                   '6.0.3'
gem 'bcrypt',                  '3.1.13'
gem 'faker',                   '2.1.2'
gem 'will_paginate',           '3.1.8'
gem 'bootstrap-will_paginate', '1.0.0'
.
$ bundle install

実行したら、新しいgemが正しく読み込まれるように、Webサーバーを再起動

indexビューにページネーションのコードを追加する
またindexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もある

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

will_paginateメソッドは,usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成してくれる。ただし、このままでは動かず、
will_paginateではpaginateメソッドを使った結果が必要のため

必要なpagineteメソッドの動きはこんな感じ

$ rails console
>> User.paginate(page: 1)
  User Load (1.5ms)  SELECT "users".* FROM "users" LIMIT 11 OFFSET 0
   (1.7ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
>> User.paginate(page: 1).length
  User Load (3.0ms)  SELECT "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 30],
  ["OFFSET", 0]]
=> 30
```

paginateでは、キーが:pageで値がページ番号のハッシュを引数に取る
User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ(デフォルトでは30)を取り出す
つまり1ページ目は1から30のユーザー、2ページ目は31から60のユーザーといった具合にデータが取り出される。
pageがnilの場合、 paginateは単に最初のページを返す

indexアクション内のallをpaginateメソッドに置き換えると使えるようになる


```app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  def index
    @users = User.paginate(page: params[:page])
  end
 ```

環境によってはRailsを再起動する必要があるが、これでページネーションが動くようになった

### 演習
Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。
先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。
→確認ずみ

```
>> user = User.all
>> page = User.paginate(page: 1)
>> user.class
=> User::ActiveRecord_Relation
>> page.class
=> User::ActiveRecord_Relation
```

### 10.3.4 ユーザー一覧のテスト
ページネーションに対する簡単なテストも書いておく

1 ログイン
2indexページにアクセス
3最初のページにユーザーがいることを確認
4ページネーションのリンクがあることを確認
といった順でテスト
3,4のステップでは、テスト用のデータベースに31人以上のユーザーがいる必要がある

そのためfixtureファイルに30人のユーザーを追加する(rubyもサポートされている)

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

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>
```

indexページ用の統合テストを生成

```
$ rails generate integration_test users_index
```

テストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認

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

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end
```

テストはパスし、ページネーションのテストは完了

### 演習
試しにリスト 10.45にあるページネーションのリンク(will_paginateの部分)を2つともコメントアウトしてみて、リスト 10.48のテストが red に変わるかどうか確かめてみましょう。
→確認
先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
→` assert_select   'div.pagination', count: 2`でpaginationクラスのdivが2つ存在することを確認する

### 10.3.5 パーシャルのリファクタリング

リファクタリングしていく
まずは、呼び出し側の`render user`をセット

```app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>
```

renderをパーシャル(ファイル名の文字列)に対してではなく、Userクラスのuser変数に対して実行している
この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する

```app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>
```

さらに改良
renderを@users変数に対して直接実行する

```app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">#@userにし、eachを削除した
  <%= render @users %>
</ul>

<%= will_paginate %>
```

Railsは@users をUserオブジェクトのリストであると推測し、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力するようになる

### 演習
リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が red に変わることを確認してみましょう。
→確認

## 10.4 ユーザーを削除する
ユーザーの一覧ページはOK
残るはdestroyを実装
ユーザーを削除するためのリンクを追加しdestroyアクションも実装
その前に、削除を実行できる権限を持つ管理(admin)ユーザーのクラスを作成する
承認(authorization)においては、このような特権のセットをroleと呼ぶ


### 10.4.1 管理ユーザー
論理値をとるadmin属性をUserモデルに追加し、管理ユーザーを識別する
Userモデルに追加することで、自動的にadmin?メソッド(論理値を返す)も使えるようになる

```
$ rails generate migration add_admin_to_users admin:boolean
```

default: falseをマイグレーションファイルへ渡す
渡さなくてもnilになり、問題ないが、明示的にfalseにしておくことでrailsと開発者にわかりやすくなる

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

```
$ rails db:migrate
```

Railsコンソールで動作を確認

```
$ rails console --sandbox
>> user = User.first
>> user.admin? #adminメソッドも使えるように
=> false
>> user.toggle!(:admin)#toggle!メソッドはfalseからtrueに反転させる
=> true
>> user.admin?
=> true
```

最初のユーザーだけをデフォルトで管理者にする

```db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)#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
```

データベースをリセット

```
$ rails db:migrate:reset
$ rails db:seed
```

Strong Parametersの復習
最初のユーザーに初期化ハッシュにadmin: trueを設定することでユーザーを管理者にしている

ここでは、荒れ狂うWeb世界にオブジェクトを晒すことの危険性を改めて強調している。

もし、任意のWebリクエストの初期化ハッシュをオブジェクトに渡せるとなると、攻撃者は次のようなPATCHリクエストを送信してくるかもしれない。

patch /users/17?admin=1
このリクエストは、17番目のユーザーを管理者に変えてしまう。
ユーザーのこの行為は少なくとも重大なセキュリティ違反となる可能性があるし、それだけでは済まされない。

このような危険があるからこそ、編集してもよい安全な属性だけを更新することが重要になる。

これを、Strong Parametersを使って対策

次のように、paramsハッシュに対してrequireとpermitを呼び出す。

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

上のコードでは、許可された属性リストにadminが含まれていないことに注目。

これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できる。
この問題は重大であるため、編集可能になってはならない属性に対するテストを作成してみる。

### 演習
Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します

```test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)  #違うユーザーてログイン
    assert_not @other_user.admin?  #違うユーザーにadmin属性がないことを期待
    patch user_path(@other_user), params: {
                                    user: { password:              "password",
                                            password_confirmation: "password",
                                            admin: true } }
    assert_not @other_user.reload.admin?
  end
```

### 10.4.2 destroyアクション
destroyアクションへのリンクを追加
ユーザーindexページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限していく
これによって、現在のユーザーが管理者のときに限り [delete] リンクが表示される

```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>
```

`method: :delete`に注意
また、各リンクをif文で囲い、管理者にだけ削除リンクが表示されるようにしている

ブラウザはネイティブではDELETEリクエストを送信できないので、RailsではJavaScriptを使って偽造
つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になる

削除リンクが動作するためには、destroyアクションを追加する
destroyアクションは
1該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除
2ユーザーindexに移動

```app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]#loginしないとできないように
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end
```

まだ終わっておらず、このままではコマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができてしまう
なのでdestroyアクションへのアクセスに制限をかける

beforeフィルターを使う

```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?#adminがfalseならrootに返す
    end
end
```

### 演習
管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?

### 10.4.3 ユーザー削除のテスト

ユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にしてテストを書いていく

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

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>
```

Usersコントローラをテストするために、アクション単位でアクセス制御をテストする
ログアウトのテストと同様に、削除をテストするために、DELETEリクエストを発行してdestroyアクションを直接動作させる
このとき2つのケースをチェック
1つは、ログインしていないユーザーであれば、ログイン画面にリダイレクトされること
1つは、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされること

```test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  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 #User数が変わらないことを確認
      delete user_path(@user)    #ログインしてない状態で@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 #User数が変わらないことを確認
      delete user_path(@user)  #adminがない状態で削除
    end
    assert_redirected_to root_url
  end
end
```

`assert_no_differenceメソッド`でユーザー数が変化しないことを確認

管理者ではないユーザーの振る舞いについて検証するが、管理者ユーザーの振る舞いと一緒に確認できるといい。
そこで、管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、今回のテストを追加していくことにする。

これにより、後ほど追加する管理者の振る舞いについても簡単にテストが書けそう。

今回のテストで唯一の手の込んだ箇所は、管理者が削除リンクをクリックしたときに、
ユーザーが削除されたことを確認する部分。

```
assert_difference 'User.count', -1 do
  delete user_path(@other_user)
end
```

ユーザーが削除されたことを確認
DELETEリクエストを適切なURLに向けて発行し、User.countを使ってユーザー数が 1減ったかどうかを確認

まとめるとこうなる


```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)   #adminがある状態でログイン
    get users_path       #indexを取得
    assert_template 'users/index'  #indexページが表示されるか
    assert_select 'div.pagination'  #pagenationがあるか
    first_page_of_users = User.paginate(page: 1)#1ページ目のユーザを代入
    first_page_of_users.each do |user|  #それぞれにリンクなどがあるか確認
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin  #admin以外のuserにdeleteがあるか確認
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do  #admin以外のユーザを削除したらUserの数が1減るか
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin) #non_adminとしてログイン
    get users_path  #indexページ取得
    assert_select 'a', text: 'delete', count: 0  #deleteが1つもないか確認
  end
end
```

各ユーザーの削除リンクをテストするときに、ユーザーが管理者であればスキップしている点にも注目

これで、削除に関するコードに対して、よくテストできている状態になった

### 演習
試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が red に変わることを確認してみましょう。
→確認

## 10.5 最後に

次の章に進む前に、すべての変更をmasterブランチにマージ

```
$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push
アプリケーションを本番展開したり、サンプルデータを本番データとして作成することもできます(本番データベースをリセットするにはpg:resetタスクを使います)。

$ rails test
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
```

※注意 ローカル環境でやっていると
git push後のtestで
paninateメソッドが定義されてないとかいうエラーが発生しました

ググっても
https://qiita.com/LotK/items/f49a1df5c9d9a510baa2
こういったエラーではなく、おかしかったのですが、
しばらく放置したら直りました。良くわかりません。
今までテストがパスしてたのに、いきなりエラーになった場合はしばらく放置するのも手かと

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?