10章 目標

・これまで未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させます
・まず、ユーザーが自分のプロフィールを自分で更新できるようにします
・次に、すべてのユーザーを一覧できるようにします
・最後に、ユーザーを削除し、データベースから完全に消去する機能を追加します

10.1 ユーザーを更新する

先にユーザーを編集するためのeditアクションを作成。newアクション同様に、POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成します
最大の違いは、ユーザー登録は誰でも実行できますが、ユーザー情報を更新できるのはそのユーザー自身に限られるということです。第8章で実装した認証機構を使えば、beforeフィルター (before filter) を使ってこのアクセス制御を実現できます。

10・1・1 編集フォーム

Usersコントローラにeditアクション追加→対応するeditビューの作成
ここで注意して頂きたいのは、ユーザー編集ページの正しいURLが/users/1/editとなっていることです (ユーザーのidが1の場合)。
ユーザーのidはparams[:id]変数で取り出すことができる

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

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_for(@user) do |f| %>
      <%= render 'shared/error_messages' %> <--注目1

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

      <%= 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="http://gravatar.com/emails" target="_blank">change</a> <--注目3
    </div>
  </div>
</div>

注目1・・・エラーメッセージパーシャルを挿入している

注目2・・・ビューのフィールドにはRailsが自動で@userから引き出したname、emailがそれぞれ値としてわたされている

注目3・・・target="_blank" でリンクを開く際新しいタブで開かれる
image.png

仕上げに、ナビゲーションバーにあるユーザー設定へのリンクを更新します。7章で示したedit_user_pathという名前付きルートと、 9章で定義したcurrent_userというヘルパーメソッドを使うと、実装が簡単です。

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

Active Recordのnew_record?論理値メソッド

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_for(@user) do |f| %>
・
・
・

このform_for...の部分はNewビューのコードと全く同じとなっていて、Railsはどうやって新規ユーザー用のPOSTリクエストとユーザー編集用のPATCHリクエストを区別するのでしょうか

その答えは、Railsは、ユーザーが新規なのか、それともデータベースに存在する既存のユーザーであるかを、Active Recordのnew_record?論理値メソッドを使って区別できるからです。

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

Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使います。

10.1.2 編集の失敗

ユーザー登録に失敗したときと同様編集失敗も扱う

まずはupdateアクションの作成から進めますがupdate_attributes を使って送信されたparamsハッシュに基いてユーザーを更新します。
無効な情報が送信された場合、更新の結果としてfalseが返され、elseに分岐して編集ページをレンダリングします。

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

update_attributesへの呼び出しでuser_paramsを使っていることに注目してください。7章でも説明したように、ここではStrong Parametersを使ってマスアサインメントの脆弱性を防止しています。

Userモデルのバリデーションとエラーメッセージのパーシャルが既にあるので無効な情報を送信すると役立つエラーメッセージが表示されるようになっています

10.1.3 編集失敗テスト

統合テスト作成
$ rails generate integration_test users_edit
invoke test_unit
create test/integration/users_edit_test.rb

かんたんに

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)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "", <--注目1
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  assert_select 'div.alert' , "The form contains 4 errors"<--注目2
  end
end

注目1・・・PATCHリクエストを送るためにpatchメソッドを使っていることに注目してください。これはgetやpost、deleteメソッドと同じように、HTTPリクエストを送信するためのメソッドです。

注目2・・・演習問題から。

10.1.4 TDDで編集を成功させる : 「受け入れテスト (Acceptance Tests)」

アプリケーション用のコードを実装する前にテストをあらかじめかいた方が便利な時がある。
そう言ったテストを「受け入れテスト (Acceptance Tests)」と呼ぶ。
ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決めるテストとして知られています。

今回は有効な情報を更新する正しいふるまいのテストを書いていく

test/integration/users_edit_test.rb
 require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  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:              "", <--注目1
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload <--注目2
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

注目1・・・空になっているのは更新の度パスワードを入力は面倒のため。後ほどひっかかるバリデーションをエスケープするように書き直す

注目2・・・@user.reloadを使って、データベースから最新のユーザー情報を読み込み直して、正しく更新されたかどうかを確認している

更新成功のためのupdateアクション

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

上記のとおりバリデーションを再編集:オプションallow_nill

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: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  .
  .
  .
end

ここで懸念されるのは新規ユーザー登録の際にも空パスが通ってしまうのかというところだがそんなことはなく、has_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。

これで更新成功テストもgreen

10.2 認可

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

現時点での大きなセキュリティーホールとして、だれでもどのページにアクセス可能であると言う事
でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうのです。この節では、ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御してみます

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

1、ログインしていないユーザーが保護されたページにアクセスしようとした際のケースについて対処していきます。こういったケースはアプリケーションを使っていると普通に起こることなので、ログインページに転送して、そのときに分かりやすいメッセージも表示するようにしましょう

2、一方で、許可されていないページに対してアクセスするログイン済みのユーザーがいたら (例えば他人のユーザー編集ページにアクセスしようとしたら)、ルートURLにリダイレクトさせるようにします

上を実現させるために使うのはbefore_actionメソッド。
これをUsersコントローラに使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みです

app/controllers/users_controller.rb
 class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  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
end

注目・・・privateの中で定義している

before_action:logged_in_userといった形式でメソッドを引っ張ってきて、オプションでonlyを持たせ、どのアクションで行うのかを指定している。

テストはred
原因は、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったためです。

なのでテストが行われる前にログインさせるコードをかく

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)
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end
end

このじてんでテストはgreenになるがbeforeフィルターに対するテストがないのでもし攻撃者がフィルターをコメントアウトしてしまうと大きなセキュリティーホールになりかねない!
ので、基本的アクション毎にフィルターを適応させているのでテストもアクション毎に書かなければいけない

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

注目・・・HTTPリクエストはそれぞれ。patchメソッドを使ってuser_path(@user)にPATCHリクエストを送信している点に注目してください7章にあるように、このリクエストはUsersコントローラのupdateアクションへと適切に繋いでくれます

これで完了

10.2.2 正しいユーザーを要求

当然のことですが、ログインを要求するだけでは十分ではありません。ユーザーが自分の情報だけを編集できるようにする必要があります。そこで本項では、セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。(Usersコントローラテスト)

別のユーザーをfixtureに作成

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') %>

これを利用してユーザー2がユーザー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 edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url <--注目
  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,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end
end

注目・・・ログイン済みユーザーなのでログインページではなくルートURL

ここでもbeforeフィルター
別のユーザーのプロフィールを編集しようとしたらルートへリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにします
その際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_attributes(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

少しRuby調にリファクタリングすると

app/helpers/sessions_helper.rb
 module SessionsHelper




  # 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end



app/controllers/users_controller.rb
  # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

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

簡潔にいうとログインしていないユーザーが編集ページにアクセスしようとしていたなら、ログインを要求され、ユーザーがログインした後にはその直前までアクセスしようとしていた編集ページにリダイレクトされるようにする だ

実際のコードは少し複雑ですが、フレンドリーフォワーディングのテストは非常にシンプルに書くことができます。ログインした後に編集ページへアクセスする、という順序を逆にしてあげるだけです

test/integration/users_edit_test.rb
 require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

ユーザーを希望のページに転送するには、
1、リクエスト時点のページをどこかに保存しておき(store_location)
2、その場所にリダイレクトさせる必要があります(redirect_back_or)

として、sessionヘルパーへ

app/helpers/sessions_helper.rb
 module SessionsHelper
  .
  .
  .


  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?<--注目1
  end
end

  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)<--注目2
    session.delete(:forwarding_url)<--注目3
  end

注目1・・・request.original_urlでリクエスト先が取得できます。さらにsessionメソッドにforwarding_urlキーを与えて値として渡している。ただし、GETリクエストが送られたときだけ格納するようにしておきます

注目2・・・このコードは、値がnilでなければsession[:forwarding_url]を評価し、そうでなければデフォルトのURLを使っています

注目3・・・転送用のURLを削除している点にも注意してください。これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます

beforeフィルターに組み込みたい(log_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]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

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

    # beforeアクション

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

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

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

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

レンドリーフォワーディング用統合テストはパスするはずです。成功すれば、基本ユーザー認証機能とページ保護機能の実装は完了

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

indexアクションを整備していきます

image.png

10.3.1 ユーザーの一覧ページ

セキュリティモデルとしてshowビューは全てユーザーが閲覧可能であるのに対し、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 redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

次にbeforeフィルターにindexアクションを追加
indexアクションにはインスタンス変数を用意して全てのユーザーを格納
これが完了した時点でgreenだがindexの整備を完了させてしまう

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

  def index
   @users=User.all
  end

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

実際のindexページを作成するには、ユーザーを列挙してユーザーごとにliタグで囲むビューを作成する必要があります。ここではeachメソッドを使って作成します。それぞれの行をリストタグ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>

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;
  }
}

そしてヘッダーパーシャル
<li><%= link_to "Users", users_path %></li>

これでgreen

10.3.2 サンプルユーザー

この項ではユーザーをある程度大きな数生成する方法を学ぶ
全部手作業でやるわけにはいかないしね

source 'https://rubygems.org'
・
・
gem 'faker',          '1.7.3'

これは、実際にいそうなユーザー名を作成するgemです。ちなみにfaker gemは開発環境以外では普通使いませんが、今回は例外的に本番環境でも適用させる予定なので、次のようにすべての環境で使えるようにしています
bundle installして

では、サンプルユーザーを生成するRubyスクリプト (Railsタスクとも呼びます) を追加してみましょう。Railsではdb/seeds.rbというファイルを標準として使います

db/seeds.rb
 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人のユーザと、それらしい名前とメールアドレス(ユーザー数+1で被る事が無い)を持つ99人のユーザーを作成します

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

image.png

10.3.3 ページネーション

100人のユーザーが作成できたのでここでページネーション (pagination) というもので見易くしていく
この場合は、例えば1つのページに一度に30人だけユーザーを表示するというものです。

Railsには豊富なページネーションメソッドがあります。
が、今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使ってみましょう。これを使うためには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する必要があります

gem 'will_paginate', '3.1.5'
gem 'bootstrap-will_paginate', '1.0.0'

bundle install

サーバーを再起動

ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに指示するコードをindexビューに追加する必要があります。

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

また、indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もあります。
現在の@users変数にはUser.allが含まれていますが、will_paginateではpaginateメソッドを使った結果が必要だからです。必要となるデータの例は次のとおり

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

paginateでは、キーが:pageで値がページ番号のハッシュを引数に取ります。User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ (デフォルトでは30) を取り出します(nilの場合は最初のページ)
ということで

def index
@users = User.paginate(page: params[:page])
end

image.png

ビューで
<%= will_paginate %>
を上下2カ所配置した為ページネーションのリンクも上下に。

10.3.4 ページネーションのテスト

ここのテストは
ログイン、
indexページにアクセス、
最初のページにユーザーがいることを確認、
ページネーションのリンクがあることを確認、

最後の2つのステップでは、テスト用のデータベースに31人以上のユーザーがいる必要があります。

そこでfixtureに。
ここではERBを利用してユーザーを30
と他にこの後必要になる為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') %>

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

$ rails generate integration_test users_index
invoke test_unit
create test/integration/users_index_test.rb

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

green

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

実はRailsにはコンパクトなビューを作成するための素晴らしいツールがいくつもあります。この節ではそれらのツールを使って一覧ページのリファクタリング (動作を変えずにコードを整理すること) を行うことにします

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

注目・・・パーシャル (ファイル名の文字列) に対してではなく、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">
  <%= render @users %>
</ul>

<%= will_paginate %>

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

これに限らず、リファクタリングを行う場合には、アプリケーションのコードを変更する前と後で必ずテストを実行し、いずれも green になることを確認するようにしてください。

大分コンパクトになりました。

10.4 ユーザーを削除する

残ったdestroyを整備していきます。

1、ユーザーを削除するためのリンクを追加、
2、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成、
3、削除を行うのに必要なdestroyアクションも実装

10.4.1 管理ユーザー

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになります
属性の型はbooleanです

$ rails generate migration add_admin_to_users admin:boolean

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

default: falseという引数はデフォルトでは管理者になれないということを示すためです

$ rails db:migrate

Railsコンソールで動作を確認すると、期待どおりadmin属性が追加されて論理値をとり、さらに疑問符の付いたadmin?メソッドも利用できるようになっています。

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

(ここではtoggle!メソッドを使って admin属性の状態をfalseからtrueに反転しています。)

db/seeds.rb
 User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true #最初のユーザーだけをデフォルトで管理者に

次に、データベースをリセットして、サンプルデータを再度生成します。

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

Strong Parameters、再び

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

patch /users/17?admin=1

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

この様な危険があるからこそadmin属性をstrong parameterの許可する属性には置いていけないのです

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

下は演習問題のnon管理ユーザーがpatchでadmin:trueを送ってもfalseのままか?
のテスト

rb: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:               @other_user.password,
                                           password_confirmation:  @other_user.password,
                                           admin: true}}
    @other_user.reload.admin?  
  end

10.4.2 destroyアクション

まずdestroyアクションまでのリンクを作成しますが管理ユーザーにのみビューに表示されるようなコード

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>

注目・・・各リンクをif文で囲い、管理者にだけ削除リンクが表示されるようにしています

アクションをつくって、もちろんログイン済みでないとなので

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]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy<--注目
    flash[:success] = "User deleted"
    redirect_to users_url
  end

  private
  .
  .
  .
end

注目・・・チェーンメソッドで1行に

結果として、管理者だけがユーザーを削除できるようになります (より具体的には、削除リンクが見えているユーザーのみ削除できる)
しかし、直接コマンドラインからdeleteリクエストを送られる可能性がありますのでdestroyアクションにもアクセス制御を行う必要があります

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

10.4.3 削除のテスト

テストで使う1人を管理ユーザーに

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') %>



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
      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
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 #管理者であればdeleteリンクが
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do #deleteリクエスト後ユーザー数が-1
      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 #non管理者のindexにdeleteリンクは0
  end
end

greenです

10.5 最後に

5.4でUsersコントローラをご紹介して以来、長い道のりをたどってきました。あの頃はユーザー登録すらありませんでしたが、今は登録もログインもログアウトもできます。プロフィールの表示も、設定の編集も、すべてのユーザーの一覧画面もあります。さらに、一部のユーザーは他のユーザーを削除することすらできるようになりました。

この時点で、サンプルアプリケーションはWebサイトとしての十分な基盤 (ユーザーを認証したり認可したり) が整ったといえるでしょう。続く第11章と第12章では、さらに2つの改善を加えます。1つはメールアドレスを使ってアカウントを有効化する機能 (すなわち本当に有効なメールアドレスか検証する機能) で、もう1つはユーザーがパスワードを忘れてしまったときのためのパスワードを再設定する機能です。

10.5.1 本章のまとめ

・ユーザーは、編集フォームからPATCHリクエストをupdateアクションに対して送信し、情報を更新する
・Strong Parametersを使うことで、安全にWeb上から更新させることができる
・beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる
・beforeフィルターを使って、認可 (アクセス制御) を実現した
・認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した
・フレンドリーフォワーディングとは、ログイン成功時に元々行きたかったページに転送させる機能である
・ユーザー一覧ページでは、すべてのユーザーをページ毎に分割して表示する
・rails db:seedコマンドは、db/seeds.rbにあるサンプルデータをデータベースに流し込む
・render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する
・boolean型のadmin属性をUserモデルに追加すると、admin?という論理オブジェクトを返すメソッドが自動的に追加される
・管理者が削除リンクをクリックすると、DELETEリクエストがdestroyアクションに向けて送信され、該当するユーザーが削除される
・fixtureファイル内で埋め込みRubyを使うと、多量のテストユーザーを作成することができる

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.