ぞえの技術めも

Ruby on Rails勉強中

【119日目】【1日20分のRailsチュートリアル】【第9章】演習の4.

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

今日は「9.6 演習」の4.から。

9.6 演習

4.リスト9.60のパーシャルを使用して、new.html.erbビューとedit.html.erbビューをリファクタリングし、コードの重複を取り除いてください。
このとき、リスト9.61のようにフォーム変数fを明示的にローカル変数として渡す必要があることに注意してください。
また、provide関数を使うと、パーシャル化したnewフォームやeditフォームの重複をさらに取り除くことも可能です。

まずリスト9.60のパーシャルを作成する。
ファイルを作成して

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

下記のように更新。

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

new.html.erbビューをリスト9.61のようにリファクタリング

<% provide(:button_text, 'Create my account') %>を2行目に追加して、<%= render 'form' %>でパーシャル使う。

app/views/users/new.html.erb

<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
      <%= render 'form' %>
  </div>
</div>

edit.html.erbビューも同じようにリファクタリング

app/views/users/edit.html.erb

<% provide(:title, "Edit user") %>
<% provide(:button_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">change</a>
    </div>
  </div>
</div>

サーバー起動してユーザー登録画面とユーザー情報編集画面表示させてみたけど特に問題なさそう。
リファクタリングできたかな。

$ rails server -b $IP -p $PORT

f:id:kt_zoe:20170308123906p:plain

f:id:kt_zoe:20170308123917p:plain

画面によってボタンの文字列もちゃんと変わってる。

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

次は「第10章 アカウントの有効化とパスワードのリセット」から。

やっと10章。まだまだ先は長いね…。

【118日目】【1日20分のRailsチュートリアル】【第9章】演習の3.

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

今日は「9.6 演習」の3.から。

9.6 演習

3.Web経由でadmin属性を変更できないことを確認してください。
リスト9.59に示したように、PATCHリクエストを updateメソッドに直接発行するテストを作成してください。
テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストはREDになるはずです。

リスト9.59ベースにPATCHリクエストをupdateメソッドに直接発行するテストを追加する。

assert_not @other_user.reload.admin?はこれでいいのか自信ない。

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 :update, id: @other_user, user: { password:              "password",
                                            password_confirmation: "password",
                                            admin: true }
    assert_not @other_user.reload.admin?
  end

ひとまずテストが通ることは確認。

$ bundle exec rake test
42 tests, 168 assertions, 0 failures, 0 errors, 0 skips

テストが正しい振る舞いをしているかどうかを確認するために、adminをuser_paramsメソッド内の許可されたパラメータ一覧に追加してみる。

app/controllers/users_controller.rb

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

これでテスト実行すると失敗するはず。

$ bundle exec rake test
  :
 FAIL["test_should_not_allow_the_admin_attribute_to_be_edited_via_the_web", UsersControllerTest, 2017-02-17 04:27:57 +0000]
 test_should_not_allow_the_admin_attribute_to_be_edited_via_the_web#UsersControllerTest (1487305677.59s)
        Expected true to be nil or false
        test/controllers/users_controller_test.rb:52:in `block in <class:UsersControllerTest>'
  :
42 tests, 168 assertions, 1 failures, 0 errors, 0 skips

今回追加したテストのassert_not @other_user.reload.admin?でエラーになることを確認。@other_userのadmin属性が変更できてしまっている、ってことですね。
テストは上手く動いてるよう。

許可されたパラメータ一覧に追加したadmin属性を削除してテスト終わり。

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

次は「9.6 演習」の4.から。

【117日目】【1日20分のRailsチュートリアル】【第9章】演習の2.

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

今日は「9.6 演習」の2.から。

9.6 演習

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

リスト5.25でテストしているのはHome、Help、About、Contactへのリンクについて。
Log inのリンクとかはログインの有無によってリンクが変わるのでその辺のテストを書けばいいのかな。

test/integration/site_layout_test.rb

class SiteLayoutTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "layout links" do
    :
    assert_select "a[href=?]", login_path
    :
  end

  test "layout links at login" do
    log_in_as(@user)
    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=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
  :

元々あったテストにログインのリンクを確認するコードを追加して、
ログイン時のリンクを確認するテストも追加した。

$ bundle exec rake test
41 tests, 166 assertions, 0 failures, 0 errors, 0 skips

とりあえずテストは通ったし大丈夫かな。。。なにか足りてないかもしれないけど。

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

次は「9.6 演習」の3.から。

【116日目】【1日20分のRailsチュートリアル】【第9章】演習の1.

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

今日は「9.6 演習」から。

9.6 演習

なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。

まず演習用にブランチ分けます。

$ git checkout updating-users
$ git checkout -b updating-users-exercises

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

リスト9.26のテストに4行追加してみた。(# addってつけてるとこ)

test/integration/users_edit_test.rb

  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert session[:forwarding_url].include?(edit_user_path(@user)) # add
    log_in_as(@user)
    assert_redirected_to edit_user_path(@user)
    assert_nil session[:forwarding_url] # add
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), 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
    
    log_in_as(@user) # add
    assert_redirected_to user_path(@user) # add
  end

assert session[:forwarding_url].include?(edit_user_path(@user))は間違ってる気しかしない。。。
assert_equal session[:forwarding_url], edit_user_path(@user)だとセッションに保存してるURLがフルパス、edit_user_path(@user)が相対パス、でテストNGになった。
なので含まれているかどうかをテストするようにしたけど…もっといい書き方あるんじゃないかな。。

最後の2行もこれでいいのかよく分からない。でもテストは通った。

難しいなぁ…。

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

次は「9.6 演習」の2.から。

【115日目】【1日20分のRailsチュートリアル】【第9章】第9章の内容をコミット&本番環境で動かしてみる

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

今日は「9.5 最後に」から。

9.5 最後に

メールアドレスを使ってアカウントを有効化する機能と (すなわち本当に有効なメールアドレスか検証するプロセスと)、ユーザーがパスワードを忘れてしまったときのためのパスワードリセット機能です。

そんな機能まで…!
チュートリアルでしょ、って思ってたけどこれやれば実際のサービスに必要な最低限の機能は実装できるんだね。。

次の章に進む前に、すべての変更をmasterブランチにマージしておきましょう。

変更をコミットしてmasterブランチにマージする。

$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push

コミットできました。

f:id:kt_zoe:20170228124652p:plain

アプリケーションを本番展開したり、サンプルデータを本番データとして作成することもできます (本番データベースをリセットするにはpg:resetタスクを使用します)。

本番環境にもpushしておきましょう。

$ bundle exec rake test
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed
$ heroku restart

$ heroku pg:reset DATABASEで下記のようなワーニングが表示されたので言われた通りにコンソールにinfinite-depths-40805入力した。
データベースリセットしてもいいの?ほんとに?っていう警告っぽい。

 !    WARNING: Destructive Action
 !    This command will affect the app: infinite-depths-40805
 !    To proceed, type "infinite-depths-40805" or re-run this command with --confirm infinite-depths-40805

それは、図9.16が示すように、サンプルユーザーの表示順序が変化してしまい、図9.11にあるようなローカル環境での表示順序と異なってしまうことです。

f:id:kt_zoe:20170228124729p:plain

ほんとだ…1ページ目の一番上にあったはずの管理者権限のユーザーがいない。。。

f:id:kt_zoe:20170228124739p:plain

4ページ目の一番下にいた。

9.5.1 本章のまとめ

・Strong Parametersを使うことで、安全にWeb上から更新させることができる
・beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる
・beforeフィルターを使って、認可 (アクセス制御) を実現した
・認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した

管理者権限も肝な機能だけどこの辺が難しいかなぁ…。1回じゃなかなか身につかない。

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

次は「9.6 演習」から。

【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 ユーザー削除のテスト」から。