第10章Railsチュートリアル演習問題と解答まとめ

Ruby on Railsチュートリアルの演習問題と解答をまとめる。

第10章 ユーザーの更新・表示・削除 - Railsチュートリアル

f:id:yukitoku_sw:20191019165508p:plain

アウトプットすることで、より自分の理解を深めることを目的としています。 自分なりに調べて考えた回答のため、記載内容に誤りがある場合はコメントいただけると幸いです。


演習10.1.1 編集フォーム

問題1

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

解答

[app/views/users/edit.html.erb]

<% provide(:titl, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
(中略)
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">changes</a>
    </div>
  </div>
</div>  


問題2

リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3。(関連するリスト 7.27の演習課題を既に解いている場合、この演習課題をうまく解けない可能性があります。うまく解けない場合は、既存のコードのどこに差異があるのか考えながらこの課題に取り組んでみましょう。例えば筆者であれば、リスト 10.5で用いた変数を渡すテクニックを使って、リスト 10.6やリスト 10.7で必要になるURLをリスト 10.5に渡してみるでしょう。)

解答

引数にシンボル(url:)を入れるのを忘れずに!

[app/views/users/_form.html.erb]

    <%= form_for(@user, url: yield(:url)) 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 yield(:button_text), class: "btn btn-primary" %>
    <% end %>
[app/views/uesrs/new.html.erb]

<% provide(:title, "Sign up") %>
<% provide(:button, "Create my account") %>
<% provide(:url, signup_path) %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
[app/views/users/edit.html.erb]

<% provide(:titl, "Edit user") %>
<% provide(:title, 'Save changes') %>
<% provide(:url, user_path) %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">changes</a>
    </div>
  </div>
</div>

routesより、usersコントローラのupdateアクションを行いたい場合はuser_pathを使えばいいとわかる。

$ rails routes | grep users
   signup GET    /signup(.:format)         users#new
          POST   /signup(.:format)         users#create
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PATCH  /users/:id(.:format)      users#update
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy


演習10.1.2 編集の失敗

問題1

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

解答

models/user.rbに指定した通りにvalidationしてくれます。


演習10.1.3 編集失敗時のテスト

問題1

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

解答

[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: "",
                                  email: "foo@invalid",
                                  password:  "foo",
                                  password_confirmation: "bar" } }
    assert_template 'users/edit'
    assert_select "div.alert", "The form contains 4 errors."
  end
end


演習10.1.4 TDDで編集を成功させる

問題1

実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。

解答

パスワードがからでも更新でき、
更新と同時に自分のページに移動し、フラッシュが表示される


問題2

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

解答

プロフィール画像が切り替わる


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

問題1

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

解答

[app/controllers/users_controller.rb]

class UsersController < ApplicationController
  before_action :logged_in_uesr # , only: [:edit, :update]

 (中略)   
end

only:をコメントアウトすると

$ rails test
(中略)
33 tests, 80 assertions, 4 failures, 0 errors, 0 skips

4つのエラーが発生しました。


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

問題1

何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

解答

updateだけ保護してeditを保護しないのは、何も盗まないなら自由にいつでも家に上がっていいよ。と言っているようなものではないか。 そもそもusers_controllerのアクションは全て保護するべきだと思う。 (セキュリティは可能な限り厳重な方がいい(きっとね)) しかし、newとcreateはその保護すべきユーザーを作るアクションなので保護しようがない。
showに関しては、他のユーザーに公開(見せても良い部分のみ)する機能なので保護する必要がない(保護するなら必要がない) 残るedit, update, destroyは保護すべきではなかろうか!(destroyはまだ実装していないから保護していないだけ(のはずだ))


問題2

上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

解答

editアクションはGETメソッドなのでブラウザから簡単にテストすることができる


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

問題1

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

解答

[test/integration/users_login_test.rb]

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  (中略)
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    assert_nil session[:forwarding_url]
    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
$ rails test
Running via Spring preloader in process 44381
Started with run options --seed 46594

 FAIL["test_successful_edit_with_friendly_forwarding", UsersEditTest, 1.0018939999863505]
 test_successful_edit_with_friendly_forwarding#UsersEditTest (1.00s)
        --- expected
        +++ actual
        @@ -1 +1 @@
        -"http://www.example.com/users/762146111/edit"
        +"/users/762146111/edit"
        test/integration/users_edit_test.rb:23:in `block in <class:UsersEditTest>'

expectedとactualを比較すると”http://www.example.com”が足りないらしい
完全なURLを求めているらしい
"http://www.example.com"など設定した覚えはない、、
とりあえずテストに追加してみる

[test/integration/users_login_test.rb]

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
(中略)
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], "http://www.example.com" + 
    edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    assert_nil session[:forwarding_url]
    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

テスト結果がGREENとなった!

"http://www.example.com"の謎に迫るべく調査をしてみた。

ありとあらゆるsuggestionをgoogle様に投げかけた

すると正しく私が求めていたサイトを表示してくれた<oh my google!!!

Railsでのテストにおいて、リクエストホスト名はデフォルトだと www.example.com - コード日進月歩

答えは全てここにあった!

しかし、凡の人である私には記事内の「どこでやっているのか」を理解できなかった。

サラミ調べる

GitHub内にRails自体のコードとやらを発見!

github.com

この中でしんくう様(神が表示した神のサイトの作者)のサイトに書いてある場所に辿り着く

rails/integration.rb at b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c · rails/rails · GitHub

このコードの69.70行目に

    class Session
      DEFAULT_HOST = "www.example.com"


問題2

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

解答


演習10.3.1 ユーザーの一覧ページ

問題1

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

解答

全てのリンクをテストする( Newsのみ省きました)
ログイン済みユーザーのテストの際にlog_in_asヘルパーを使うのでsetupで@userに代入する

[test/integration/site_layout_test.rb]

require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  
  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    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=?]", login_path
    get contact_path
    assert_select "title", full_title("Contact")
    get signup_path
    assert_select "title", full_title("Sign up")
  end

  def setup
    @user = users(:michael)
  end

  test "layout links when logged in user" 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
end

演習10.3.2 サンプルのユーザー

問題1

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

解答

URLに「localhost:3000/users/3/edit」(他人の編集ページ)と入力しアクセスしようとすると、root_urlに飛ばされる。


演習10.3.3 ページネーション

問題1

Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。

解答

>> User.paginate(page: nil)
  User Load (1.7ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.2ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created
_at: "2019-10-28 11:21:10", updated_at: "2019-10-28 11:21:10", password_digest: "$2a$10$D.3mgSDRKMkrWVSKL2MD
OummYTymNM5XmaKhbbzrj00...", remember_digest: nil>, (省略)>
  


問題2

先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。

解答

paginationをpageに
User.allをallに代入して比較する

>> page = User.paginate(page: nil)
  (省略)
>> page.class
=> User::ActiveRecord_Relation
>> all = User.all
(省略)
>> all.class
=> User::ActiveRecord_Relation

どちらもUser::ActiveRecord_Relationクラスである。


演習10.3.4 ユーザー一覧のテスト

問題1

試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストが redに変わるかどうか確かめてみましょう。

解答

コメントアウトしてテストするとREDとなる

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


問題2

先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

解答

div.paginationにcount: 2を追加する。

[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', count: 2
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end


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

問題1

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

解答
renderの行をコメントアウトするとテストがREDになる

[app/views/users/index.html.erb]

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

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

<%= will_paginate %>


演習10.4.1 管理ユーザー

問題1

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
  
  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end

(中略)
  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 } }
    assert_not @other_user.reload.admin?
  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


演習10.4.2 destroyアクション

問題1

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

解答

destroyが実行されデータベースから情報をdeleteしました

[サーバーログ]

Started DELETE "/users/6" for 127.0.0.1 at 2019-10-28 22:00:16 +0900
Processing by UsersController#destroy as HTML
  Parameters: {"authenticity_token"=>"ubNRZ9iQ0ow2LEKHFABbOUV73rKF5oGC/mRGyrTYZg3Pjm1YqfE2zsHDH8d+KD1HUvyH5m2NX1SKt3NBbGe+dQ==", "id"=>"6"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 6], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.9ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 6]]
   (1.5ms)  commit transaction
Redirected to http://localhost:3000/users
Completed 302 Found in 9ms (ActiveRecord: 2.9ms)


10.4.3 ユーザー削除のテスト

問題1

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

解答

before_action :admin_userをコメントアウトするとテストがREDに変わる

[app/controllers/users_controller.rb]

class UsersController < ApplicationController
  before_action :logged_in_uesr, only: [:index, :edit, :update, :destroy]
  before_action :correct_user, only: [:edit, :update]
  # before_action :admin_user, only: :destroy
(中略)
end



yukitoku-sw.hatenablog.com