ぞえの技術めも

Ruby on Rails勉強中

【114日目】【1日20分のRailsチュートリアル】【第9章】ユーザー削除のテストを追加する

Ruby on Railsチュートリアル(第3版)

今日は「9.4.3 ユーザー削除のテスト」から。

9.4.3 ユーザー削除のテスト

ユーザー削除と同じくらい重要なことは、その振る舞いが期待されたかどうかを確かめる良いテストを書くことです。
そこで、まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にしてみます。

サンプルユーザーの1人目に管理者権限を追加。

test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

9.2.1で経験してきたように、Usersコントローラをテストするために、アクション単位でアクセス制御をテストします。
(中略)
1つは、ログインしていないユーザーであれば、ログイン画面にリダイレクトされることです。もう1つは、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされることです。

DELETEリクエストを発行してdestroyアクションを動作させたときの挙動をテストする。

test/controllers/users_controller_test.rb

  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete :destroy, id: @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 :destroy, id: @user
    end
    assert_redirected_to root_url
  end

このとき、リスト9.56ではassert_no_differenceメソッド (リスト7.21) を使って、ユーザー数が変化しないことを確認している点に注目してください。

ふーむ。

管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、リスト9.44のテストに今回のテストを追加していくことにします。

元々あったページネーションを確認するテストに「削除」リンクをクリックしたときのテストも追加。
あと管理者権限をもたないユーザーがログインした場合は「削除」リンクが表示されていないテストも追加。

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

1個目のテストではページネーションの表示と1ページ目に表示されているユーザー(管理者ユーザー以外)に「削除」リンクが表示されていることを確認。
プラス、@non_adminのユーザーを削除してユーザー数が1減ることも確認。

2個目のテストでは管理者権限をもたないユーザーがログインした場合は「削除」リンクが表示されていないことを確認。

うーん、説明読みながらなら解読はできるかな。。。

最後にテストを実行してテストが通ることを確認。

$ bundle exec rake test
40 tests, 152 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【30分】

次は「9.5 最後に」から。

9章の演習が迫ってきた…!

【113日目】【1日20分のRailsチュートリアル】【第9章】destroyアクションを実装する

Ruby on Railsチュートリアル(第3版)

今日は「9.4.2 destroyアクション」から。

9.4.2 destroyアクション

Usersリソースの最後の仕上げとして、destroyアクションへのリンクを追加しましょう。

まず各ユーザーにユーザー削除用のリンクを追加。

app/views/users/_user.html.erb

  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>

ログイン中のユーザーが管理者権限を持っていて、かつログイン中とは違うユーザーだったらリンクを表示する。
自分自身を削除できたら色々問題になるからかな。

追加できたら見た目見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

<ローカルアドレス>/usersにアクセス。

f:id:kt_zoe:20170222123625p:plain

deleteリンクが表示されました。

この削除リンクが動作するためには、destroyアクション (表7.1) を追加する必要があります。

destroyアクションを追加。

app/controllers/users_controller.rb

  :
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  :
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

これでユーザー削除処理が実装できました。

と思いきや

ある程度の腕前を持つ攻撃者なら、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができるでしょう。

それは危ない。防衛しましょう。

9.2.1と9.2.2と同じように、今回はbeforeフィルターを使ってdestroyアクションへのアクセスを制御します。実装するadmin_userフィルターをリスト9.54に示します。

管理者権限をもつユーザーのみがdestroyアクションにアクセスできるようにコード追加。

app/controllers/users_controller.rb

    :
  before_action :admin_user,     only: :destroy
    :
  private
    :
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

これでユーザー削除処理の実装完了。
実際にユーザー削除してみたいけど追加し直すのもめんどくさいので動作確認はテストを書いて確認しよう。

今日の学習時間は【26分】

次は「9.4.3 ユーザー削除のテスト」から。

【112日目】【1日20分のRailsチュートリアル】【第9章】Strong Parametersでadmin属性の変更をガードする

Ruby on Railsチュートリアル(第3版)

今日は「9.4.1 管理ユーザー」の「Strong Parameters、再び」から。

9.4.1 管理ユーザー

Strong Parameters、再び

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

リクエスト送られて改ざんできるのはやばいね。。。ガードしておかないと。

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

app/controllers/users_controller.rb

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

上記コードは7.3.2で追加済み。

上のコードでは、許可された属性リストにadminが含まれていないことに注目してください。これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できます。

許可された属性リストにadmin入れてたら変更できちゃうのか。
アプリケーションに登録した(悪意のある)任意のユーザーが自分自身に管理者権限を与えて好き放題する、ということを防げるんだね。

前の内容も読み返してたら時間かかった。

今日の学習時間は【18分】

次は「9.4.2 destroyアクション」から。

【111日目】【1日20分のRailsチュートリアル】【第9章】管理ユーザーを識別するadmin属性を追加する

Ruby on Railsチュートリアル(第3版)

今日は「9.4 ユーザーを削除する」から。

9.4 ユーザーを削除する

削除を行うのに必要なdestroyアクションも実装します。しかしその前に、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成しましょう。

indexアクションの追加が終わったので次はdestroyアクション。

の前にユーザー削除を実行できる権限を持った管理ユーザーを実装する。

9.4.1 管理ユーザー

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。

マイグレーションを実行してUserモデルにadmin属性を追加。

$ rails generate migration add_admin_to_users admin:boolean
      invoke  active_record
      create    db/migrate/20170215024617_add_admin_to_users.rb

リスト9.50では、default: falseという引数をadd_columnに追加しています。これは、デフォルトでは管理者になれないということを示すためです。

default: falseは追加しなくてもデフォルトで管理者になれないらしいけど、明記しておくことでRailsと開発者にコードの意図を示せるらしい。

というわけでadd_cloumにdefault: falseを追加。

db/migrate/20170215024617_add_admin_to_users.rb

class AddAdminToUsers < ActiveRecord::Migration
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

マイグレーションを更新できたら実行する。

$ bundle exec rake db:migrate
  :
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0051s
  :

admin属性追加できました。

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

Rails consoleでadmin?メソッドが使えることを確認。

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

仕上げに、最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新しましょう。

最初のテストユーザーだけadmin: trueを追加。

db/seeds.rb

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

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

$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

サンプルデータを再生成しました。
これで最初のユーザーだけ管理権限持ってるはず。

アプリケーション設計時にデータベース設計も済ませて実装する、ウォーターフォール型の開発が多かったので こうやって後からデータベースにカラム追加とかなんか新鮮。

でもこれできないと後から機能追加とかしにくいよね。上手くできてますなー

今日の学習時間は【20分】

次は「9.4.1 管理ユーザー」の「Strong Parameters、再び」から。

【110日目】【1日20分のRailsチュートリアル】【第9章】パーシャルのリファクタリングを行う

Ruby on Railsチュートリアル(第3版)

今日は「9.3.5 パーシャルのリファクタリング」から。

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

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

へー。ツール使ってシンプルに書けるならそれに越したことはないね。

リファクタリングの第一歩は、リスト9.41のユーザーのliをrender呼び出しに置き換えることです (リスト9.46)。

<li>タグ使ってた箇所を<%= render user %>に変更。

app/views/users/index.html.erb

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

この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探します。このパーシャルを作成する必要があります (リスト9.47)。

touchコマンドでファイルを作成して、

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

下記内容に更新。
さっき置き換えた内容をパーシャルに持って来る感じ。

app/views/users/_user.html.erb

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

これは間違いなく大きな進歩です。しかしここで終わらせず、さらに改良してみましょう。今度はrenderを@users変数に対して直接実行します (リスト9.48)。

更にすっきりした。eachメソッドすらなくなった。

app/views/users/index.html.erb

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

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

へー。追加したパーシャルの方にeachメソッド移すわけじゃないんだ。
そんなツールがあるんだなぁ…。

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

リファクタリングできたのでテスト実行。

$ bundle exec rake test
37 tests, 117 assertions, 0 failures, 0 errors, 0 skips

問題ないですね。

今日の学習時間は【15分】

次は「9.4 ユーザーを削除する」から。

【109日目】【1日20分のRailsチュートリアル】【第9章】ユーザーインデックスのテストを追加する

Ruby on Railsチュートリアル(第3版)

今日は「9.3.4 ユーザーインデックスのテスト」から。

9.3.4 ユーザーインデックスのテスト

今回のテストでは、ログイン、indexページにアクセス、最初のページにユーザーがいることを確認、ページネーションのリンクがあることを確認、といった順でテストしていきます。

ページネーションに関するテストを追加する。

リスト9.20で2人目のユーザーをfixtureに追加しましたが、今回はもっと多くのユーザーを作成する必要があります。

fixtureでも埋め込みRubyとやらが使えるらしいのでコード書いてテストユーザーを追加する。
コピペでできなくはないにしろ、もし31人以上のテストユーザーを手作業で追加しろとか言われたら拷問だよね……。(単純作業きらい)

test/fixtures/users.yml

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

mallory:
  name: Mallory 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 %>

今後必要になるらしいので名前付きユーザー(“lana"と"mallory”)も追加。

リスト9.43のfixtureファイルができたので、indexページに対するテストを書いてみます。まずは、いつものように統合テストを生成します。

統合テストを生成。

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

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

1ページ目にユーザーがいる(リンクとはユーザー名が表示されている)ことをテストする。

うーん、何のテストかは何となく分かるけど一からは書けない。。。

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

テストが通ることを確認。

$ bundle exec rake test
37 tests, 117 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【19分】

次は「9.3.5 パーシャルのリファクタリング」から。

【108日目】【1日20分のRailsチュートリアル】【第9章】indexページでページネーションを動作させる

Ruby on Railsチュートリアル(第3版)

今日は「9.3.3 ページネーション」から。

9.3.3 ページネーション

これで、最初のユーザーにも仲間ができました。しかし今度は逆に、1つのページに大量のユーザーが表示されてしまっています。
(中略)
これを解決するのがページネーション (pagination) というもので、この場合は、たとえば1つのページに一度に30人だけユーザーを表示するというものです。

数千になってしまうとユーザー情報をデータベースから取得するのに時間かかるしもページを表示するのにも時間かかるし良くないね。

Railsには豊富なページネーションメソッドがあります。今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使用してみましょう。

へー、いくつもメソッドあるんだ。

will_paginateメソッドを使うためにwill_paginate gem とbootstrap-will_paginate gemをGemfileにインクルードする。

Gemfile

gem 'will_paginate',           '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'

bundle installを実行。

$ bundle install
Installing will_paginate 3.0.7
  :
Installing bootstrap-will_paginate 0.0.10
  :

will_paginateメソッドを使う準備ができました。

ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに指示するコードをindexビューに追加する必要があります。
また、indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もあります。

まずindexページの上下でwill_paginateメソッドを呼ぶ。

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オブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成しています。

へー、メソッドに@usersオブジェクトを指定しなくていいのか。

というのも、現在の@users変数にはUser.allの結果が含まれていますが (リスト9.33)、will_paginateではpaginateメソッドを使った結果が必要だからです。

allではなくpaginateメソッドを使うらしい。

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

デフォルトでは30個分のUserを取得する。標準?でこんなメソッドがあるのかー

paginateを使用することで、サンプルアプリケーションのユーザーのページネーションを行えるようになります。具体的には、indexアクション内のallをpaginateメソッドに置き換えます 。

indexアクションにてUser.paginateメソッドを使うように修正。

app/controllers/users_controller.rb

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

ページネーションの動作見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

<ローカルアドレス>/usersにアクセス。

f:id:kt_zoe:20170208123526p:plain

ページの上下にページネーションが表示されてます。

f:id:kt_zoe:20170208123537p:plain

3ページ目も表示できるよ。実装簡単だな~

今日の学習時間は【22分】

次は「9.3.4 ユーザーインデックスのテスト」から。