おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

30歳まで残り2年の僕は人生を変えるためにRailsチュートリアルを始めようと思う(第10章)

こんにちは。opiyoです。

今回は、第10章をやっていきます。

第10章はユーザー登録以外の「表示」「編集」「削除」の方法です。

10.1.1 演習 編集フォーム

10.1.1.1

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

<回答>

# app/views/users/edit.html.erb
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>

10.1.1.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のテクニックをリスト 10.5に適用してみたりするでしょう。)

<回答>

# app/views/users/_form.html.erb
<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

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

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

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

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

  <%= f.submit yield(:btn_text), class: "btn btn-primary" %>
<% end %>
# app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, 'Create my account') %>
<h1><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(:title, 'Edit user') %>
<% provide(:btn_text, 'Save changes') %>
<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">change</a>
    </div>
  </div>
</div>

10.1.2 演習 編集の失敗

10.1.2.1

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

<回答>

The form contains 2 errors.
Email is invalid
Password is too short (minimum is 6 characters)
["Email is invalid", "Password is too short (minimum is 6 characters)"]

エラーメッセージが表示されることを確認。

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

10.1.3.1

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

<回答>

# test/integration/users_edit_test.rb
  test "unsuccessful edit" do
    log_in_as(@user)
    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", "The form contains 4 errors." # この行を追加した!
  end

各パラメータがvalidateに引っかかるので、4 errorsのメッセージが表示されることをチェック!

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

10.1.4.1

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

<回答> - 問題なく更新できる。 - パスワード欄が空白で更新できる。

10.1.4.2

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

<回答>

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

10.2.1.1

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

<回答> 問題なし

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

10.2.2.1

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

<回答> 自身データが修正されてしまうことを保護するため。

10.2.2.2

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

<回答>

editアクション。

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

10.2.3.1

<問題>フレンドリーフォワーディングで、最初に渡されたURLにのみ確実に転送されていることを確認するテストを作成してみましょう。続けて、ログインを行った後、転送先のURLはデフォルト (プロフィール画面) に戻る必要もありますので、これもテストで確認してみてください。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうかを確認するテストを追加してみましょう。

<回答>

# test/integration/users_edit_test.rb
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert session[:forwarding_url] # sessionに値が入っていることをチェック
    assert_equal edit_user_url(@user), session[forwarding_url] # 正しい値かチェックする    
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user) # 今まではログイン後はログインユーザーのプロフィール画面に遷移してたけど、記憶した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

10.2.3.2

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

<回答>

[1, 10] in /Users/taku/rails/railstutorial/railstutorial_10/app/controllers/sessions_controller.rb
    1: class SessionsController < ApplicationController
    2:   def new
    3:     debugger
=>  4:   end
    5:
    6:   def create
    7:     user = User.find_by(email: params[:session][:email].downcase)
    8:     if user && user.authenticate(params[:session][:password])
    9:       # ユーザーログイン後にユーザー情報のページにリダイレクトする
   10:       log_in user
(byebug) session[:forwarding_url]
"http://localhost:3000/users/1/edit"
(byebug) request.get?
true

10.3.1 演習 すべてのユーザーを表示する

10.3.1.1

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

<回答>

# test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end

  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=?]", login_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", "http://news.railstutorial.org/"
    # ログインする
    log_in_as(@user)
    get root_path
    assert_select "a[href=?]", root_path
    assert_select "a[href=?]", help_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 演習 サンプルのユーザー

10.3.2.1

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

<回答>

/users/XXXX/editにアクセスすると、トップページにリダイレクトされる(XXXXはユーザーID)

10.3.3 演習 ページネーション

10.3.3.1

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

<回答>

> User.paginate(page: nil)
  User Load (4.4ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 30], ["OFFSET", 0]]
=> [#<User:0x007fd36897ceb0
  id: 1,
  name: "Example User",
  email: "example@railstutorial.org",
  created_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00,
  updated_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00,
  password_digest: "$2a$10$mjXbcC/DgUxXF.uhaGsZDeueTiLUbdsXsQVso79iYlmi1Nq7hMu0q",
  remember_digest: nil,
  admin: true>,
 #<User:0x007fd36894fcf8

10.3.3.2

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

<回答>

> paginate_user = User.paginate(page: nil)
> paginate_user.class
=> User::ActiveRecord_Relation
> all_user = User.all
> all_user.class
=> User::ActiveRecord_Relation

クラスは一緒だよ!

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

10.3.4.1

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

<回答>

 FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 4.353926331999901]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (4.35s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:14:in `block in <class:UsersIndexTest>'

divタグのpaginationクラスが無いからエラーになる

10.3.4.2

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

<回答>

  • 1つだけコメントアウトした場合、テストが greenのままであることを確認
  • 2つとも存在していることをテストする
# test/integration/users_index_test.rb
    assert_select 'div.pagination', count: 2 # count: 2を追加

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

10.3.5.1

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

<回答>

 FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.8776025120005215]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.88s)
        Expected at least 1 element matching "a[href="/users/14035331"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:17:in `block (2 levels) in <class:UsersIndexTest>'
        test/integration/users_index_test.rb:16:in `block in <class:UsersIndexTest>'

一覧に表示されるはずのユーザーへのリンクパスが見つから無いのでエラーになります。

10.4.1 演習 管理ユーザー

10.4.1.1

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

<回答>

# 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_confirmation,
        admin: true
      }
    }
    assert_not @other_user.reload.admin?
  end

10.4.2 演習 destroyアクション

10.4.2.1

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

<回答>

10.4.3 演習 ユーザー削除のテスト

10.4.3.1

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

<回答>

 FAIL["test_should_redirect_destroy_when_logged_in_as_a_non-admin", UsersControllerTest, 4.160433043001831]
 test_should_redirect_destroy_when_logged_in_as_a_non-admin#UsersControllerTest (4.16s)
        "User.count" didn't change by 0.
        Expected: 34
          Actual: 33
        test/controllers/users_controller_test.rb:56:in `block in <class:UsersControllerTest>'

画面からだとdeleteリンクがでないから、多分これありえないけど攻撃される時はきっとテストみたいな感じで直接やられるのだろう。 それをテストでしっかりブロックできてることを確認できるのは良いね。 で、patchとかdeleteメソッドってどうやってやるんだろうか?

メモ

  • target=“_blank"が使われていますが、これを使うとリンク先を新しいタブ (またはウィンドウ) で開くようになる
<a href="http://gravatar.com/emails" target="_blank">change</a>
  • form_forの引数にインスタンス変数を使うと中身がある場合はそれをRailsが勝手に表示する
  • :allow_nil → trueならば、nilの検証はスキップ。つまり空文字(‘ ’)はキャッチする!
  • beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用される
  • フレンドリーフォワーディング … リダイレクト先は、ユーザーが開こうとしていたページにしてあげること
  • will_paginatebootstrap-will_paginate gemを使えばページネーションが作れる
  • will_paginateメソッドをviewに追加して、paginateメソッドでデータを取ってくるだけですと?
# view
<%= will_paginate %>

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

-統合テスト = integration_test - $ rails generate integration_test users_index - toggleメソッド使うと反転できる?

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

はまった

$ rails test
Running via Spring preloader in process 13479
Started with run options --seed 32097

ERROR["test_index_as_non-admin", UsersIndexTest, 1.2183937259997037]
 test_index_as_non-admin#UsersIndexTest (1.22s)
NoMethodError:         NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0>
            app/controllers/users_controller.rb:7:in `index'
            test/integration/users_index_test.rb:29:in `block in <class:UsersIndexTest>'

ERROR["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.2559454040001583]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.26s)
NoMethodError:         NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0>
            app/controllers/users_controller.rb:7:in `index'
            test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'

  37/37: [==========================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.95285s
37 tests, 93 assertions, 0 failures, 2 errors, 0 skips

rh0257:railstutorial_10 taku$ spring stop # これやったら治った!
Spring stopped.
rh0257:railstutorial_10 taku$ rails test
Running via Spring preloader in process 13604
Started with run options --seed 8584

  37/37: [==========================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.85845s
37 tests, 156 assertions, 0 failures, 0 errors, 0 skips

くそー動かしてみて問題無いのなら、一先ずほっとくのもありだね。 忘れた頃に治ってるかも。