Ruby on Rails チュートリアル第10章をやってみて

#ユーザーの更新・表示・削除
■第10章
`edit`、`update`、`index`、`destroy`アクションを加え、RESTアクションを完成させる。まず、ユーザーが自分のプロフィールを自分で更新できるようにできる。

##10.1 ユーザーを更新する
ユーザー情報を編集するパターンは、新規ユーザーの作成と似てるらしい。

###10.1.1 編集フォーム
Usersコントローラに`edit`アクションを追加して、それに対応するeditビューを実装する。

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

editビューも作成。

```ruby: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' %>

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

リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまうため、`target="_blank"`で新しいページを開くときはセキュリティ上の問題がある。


###10.1.2 編集の失敗
編集に失敗した場合について扱っていく。以下のコードを追加。

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

###10.1.3 編集失敗時のテスト
統合テスト生成から始める。

```rb
$ rails generate integration_test users_edit
```

中身も追記する。無事テストGREENに。

###10.1.4 TDDで編集を成功させる
今度は編集フォームが動作するようにする。

編集の成功に対するテストを追記。

```ruby:users_edit_test.rb
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?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
end
```

コントローラの`update`アクションを弄って、パスワードが空のままでも更新できるようにする。空だったときの例外処理を加える`allow_nil: true`を追記。

```ruby:user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
```

これでテストがGREEN。

##10.2 認可
ログインすると誰でも他のユーザー情報を編集できる状態なので、他の人のユーザー編集ページにアクセスしようとしたらルートURLにリダイレクトするようにする。

###10.2.1 ユーザーにログインを要求する
ログイン画面に転送するために、beforeフィルターを使う。

beforeフィルターに`logged_in_user`を追加。

```ruby:users_controller.rb
before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
   private

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

このままじゃテストRED。原因は、editアクションやupdateアクションでログインを要求するようになったためなので、ログインしておくようにする。

```ruby:users_edit_test.rb
  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
```

beforeフィルターの実装が終わってないので追加してく。

`edit`と`update`アクションの保護に対するテストする

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

今の状態だとまだログインさえしてしまえば他の人のページを編集できてしまうので、次以降でに進めていく。

###10.2.2 正しいユーザーを要求する
ユーザーが自分の情報だけを編集できるようにする。

ユーザーの情報が互いに編集できないことを確認するために、2人目のユーザーを追加。

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

```

間違ったユーザーが編集しようとしたときのテスト。

```ruby:users_controller_test.rb
def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
```

`current_user?`メソッドも追加。


論理値を返すメソッドにする。

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

beforeフィルターを使って編集/更新ページを保護する。

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

```

テストも無事GREEN。

###10.2.3 フレンドリーフォワーディング
アクセスしたページ→ログイン→アクセスしたページとなるにする。

統合テストの書き換え。

```rb:users_edit_test.rb
test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
```

失敗するテストが書けた。
希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。`store_location`と`redirect_back_or`の2つのメソッドでこの動作を実現させる。

```ruby:sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

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

GETリクエストが送られたときだけ格納するようにしておく。

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

フォワーディング自体を実装するには、`redirect_back_or`メソッドを使う。

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

テストも無事GREEN。

##10.3 すべてのユーザーを表示する
`index`アクションを追加していく。

###10.3.1 ユーザーの一覧ページ
`index`ページを不正なアクセスから守るために、まずは`index`アクションが正しくリダイレクトするか検証するテストを書いていく。

以下を追記。

```ruby:users_controller_test.rb
  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
```

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

```ruby:users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

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

`User.all`を使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数`@users`に代入させる。一気に読み込むと遅いが、それはあとで修正。

```ruby:users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end
```

`index`ビューの作成。;

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

`gravatar_for`ヘルパーにオプション引数を追加。

```ruby:users_helper.rb
module UsersHelper

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

CSSもちょっといじくって、そして、ユーザー一覧ページへのリンクを更新する`users_path`を使う。以下を追加。

```ruby:_header.html.erb
<li><%= link_to "Users", users_path %></li>
```

テストして問題ないことを確認。

###10.3.2 サンプルのユーザー
`Gemfile`にFaker gemを追加。これでダミーのユーザーがたくさん作れるらしい。

100人くらい作るコード。![スクリーンショット 2022-02-18 18.07.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2210462/bcddaab1-40cc-eefa-6280-c3d10fd5a8fc.png)


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

その後データベースをいじったら、無事ダミーユーザーがたくさん出てきました。
![スクリーンショット 2022-02-18 18.07.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2210462/198c354d-7830-91d9-d80c-6b16e19df50b.png)


###10.3.3 ページネーション
1つのページに大量のユーザーが表示されているので、人数制限みたいなのをする。これを解決するのがページネーション。

`Gemfile`に新たなgemを追加。

indexページでpaginationを使う。

```ruby: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アクションでUsersをページネートする。

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

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

fixtureにユーザー追加。

```ruby: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 %>
```

統合テスト作成。

```rb
$ rails generate integration_test users_index
```

最後に`pagination`クラスを持った`div`タグチェックをして、無事テスト完了。

###10.3.5 パーシャルのリファクタリング
ユーザーの`li`を`render`呼び出しに置き換える。

```rb: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 %>
```

各ユーザーを表示するパーシャル。

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

今度はrenderを@users変数に対して直接実行する。

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

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>
```

テストもGREEN。

##10.4 ユーザーを削除する
ユーザーを削除するためのリンクの作成。管理者権限を持つユーザーしか削除できないようにする。

###10.4.1 管理ユーザー
論理値をとる`admin`属性をUserモデルに追加する。型は`boolean`に。

```
$ rails generate migration add_admin_to_users admin:boolean
```
デフォルトでは管理者になれないということを示すために、`default: false`という引数を`add_column`に追加。
boolean型の`admin`属性をUserに追加するマイグレーション

```ruby:[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
```

最初のユーザーだけ管理者にしとく。

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

データベース周りをいじって終わり。

###10.4.2 destroyアクション
管理者のみ削除用のリンクを表示。

```rb:_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>
```

削除リンク動作のために、`destroy`アクションを追加する。

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

サイトを正しく防衛するために、`destroy`アクションにもアクセス制御を行う。

```rb: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人だけ`admin:true`で管理者にする。

ログインしていないユーザーであれば、ログイン画面にリダイレクトされること、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされることの2つをチェックする。

テストの中身を書き換えたら完了。

##感想
やっぱりセキュリティ周りのことはとにかくテストが大事なんだなという印象を受けました。あとテストでエラーが出ても、自力で解決できる力がさらについた気がします。