noggy’s blog

自分用の備忘録です。。。

7.3 ユーザー登録失敗

フォームを理解するにはユーザー登録の失敗のときが最も参考になる。
ここでは、無効なデータ送信を受け付けるユーザー登録フォームを作成し、ユーザー登録フォームを更新してエラーの一覧を表示。

7.3.1 正しいフォーム

7.1.2で、resources :usersにより、自動的にRailsアプリケーションがRESTful URIに応答するようになった。
特に、/usersへのPOSTリクエストはcreateアクションに送られる。
ここでは、次のように機能を実装
(1)createアクションでフォーム送信を受け取り

(2)User.newで新しいユーザーオブジェクトを作成し

(3)ユーザーを保存(または保存に失敗)し

(4)再度の送信用のユーザー登録ページを表示

ユーザー登録の失敗に対応できるcreateアクション

controllers/users_controller.rb

  def create ←追加
    @user = User.new(params[:user])  #実装が終わってないことに注意!ストロングパラメータを使ってないから。     
    if @user.save                        
      #保存の成功
    else
      render 'new'                       
    end
  end

このコードの動作を理解するもっともよい方法は、実際に無効なユーザー登録データを送信してみること。
実際に送信すると失敗する。

送信失敗の原因(デバッグ情報より)

デバッグ情報のパラメーターハッシュのuserの部分を見てみる。

"user"=>{"name"=>"", 
         "email"=>"", 
         "password"=>"[FILTERED]", 
         "password_confirmation"=>"[FILTERED]"},

これは、"user" => □というハッシュ(userがキー、□が値)で、値□は{"name"=>"", … , "password_confirmation"=>"[FILTERED]"}というハッシュ。ハッシュの入れ子構造になっている。このハッシュはUsersコントローラにparamsとして渡されるので、値□を取り出したいときにはparams[:user]とすればよさそう(ただし、先の送信失敗で分かるように、これだとエラーが出る。解決のためには後述するStrong Parametersを使う)

このハッシュのキーが、inputタグにあったname属性の値になる。例えば次のように、

<input id="user_email" name="user[email]" type="email" />

"user[email]"という値は、userハッシュの:emailキーの値と一致する。

昔のバージョンのRailsでは

@user=User.new(params[:user])

このコードでも動いたが、悪意のあるユーザーによってアプリケーションのDBが書き換えられないように慎重な対策をとる必要がでた。そこでRails4.0以降では、上のコードをエラーとすることでセキュリティーを高め、また、Strong Parametersと呼ばれるテクニックで対策することにした。

7.3.2 Strong Parameters

次のコードでは

@user=User.new(params[:user])

paramsハッシュ全体を初期化しているため、セキュリティー上、極めて危険。
これは、ユーザーが送信したデータをまるごとUser.newに渡していることになる。

例えば、Userモデルに管理者権限のadmin属性があるとし、admin='1'という値をparams[:user]の一部に紛れ込ませて渡せば、この属性をtrueにすることができる。

すなわち、paramsハッシュがまるごとUser.newに渡されてしまうと、どのユーザーでもadmin='1'をWebリクエストに紛れ込ませるだけで、Webサイトの管理者権限を奪うことができるというわけ。うーん、怖い!!

Strong Paramaters

Rails4.0ではコントローラ層で、Strong Parametersというテクニックを使うことが推奨されている。Strong Parametrsを使うことで、必須のパラメータと許可されたパラメータを指定することができる。

今の場合、paramsaハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性を許可し、それ以外を許可しないようにしたい。

class UsersController < ApplicationController
  :
  def create
    @user = User.new(user_params)                                               
    if @user.save
      # 保存の成功
    else
      render 'new'                                                             
    end
  end

  private                                                                       

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

この時点で、送信ボタンを押してもエラーが出ないという意味で、ユーザー登録フォームは動く。
また、
(1)間違った送信しても何もフィードバックは返ってこない。
(2)有効なユーザー情報を送信しても新しいユーザーが作成されない。
以下、(1)、(2)を解決していく。

7.3.3 エラーメッセージ

ユーザー登録に失敗した場合の最後の手順として、エラーメッセージを追加する。

コンソールで確認

Railsでは、エラーメッセージはUserモデルの検証時に自動的に生成してくれる。
例えば、ユーザー情報のメールアドレスが無効で、パスワードが短すぎる状態で保存しようとしてみる。

$ rails c
>> user = User.new(name: "Foo Bar", email: "foo@invalid", 
                                 password: "dude", password_confirmation: "dube")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password confirmation doesn't match Password", "Password is too short (minimum is 6 characters)"]

errors.full_messagesオブジェクトは、エラーメッセージの配列を持っている点に注目。

エラーメッセージの表示

エラーメッセージをブラウザで表示するには、ユーザーのnewページでエラーメッセージのパーシャルを出力する。
このとき、form-controlというCSSクラスも一緒に追加。
すると、Bootstrapがうまく取り扱ってくれる。
views/users/new.html.erb

<% provide(:title, "Sign up") %>
<h1>Sign up</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'%> ←classを追加
      
      <%= f.label :email %>
      <%= f.text_field :email,class: 'form-control' %> ←classを追加
      
      <%= f.label :password %>
      <%= f.password_field :password ,class: 'form-control'%> ←classを追加
      
      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation,class: 'form-control' %> ←classを追加
    
      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

ここで、'shared/error_messages'というパーシャルをrenderしている点に注目。
Rails全般の習慣として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」によく置かれる。

エラーメッセージを表示するパーシャル
<% if @user.errors.any? %>  ←①                                                    
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>←②
    </div>
    <ul>
      <%= @user.errors.full_messages.each do |msg| %>                           
      <li><%= msg %></li>                                                       
      <% end %>
    </ul>
  </div>
<% end %>

①:any?メソッドはオブジェクトが空の場合はtrue、それ以外はfalseを返す。empty?の逆

>> user.errors.empty?
=> false
>> user.errors.any?
=> true

②:countメソッドは、ここでは、エラーの数を返す。

>> user.errors.count
=> 2

pluralizeテキストヘルパーは次のようになる。

>> helper.pluralize(1, "error")                                                                                                                                
=> "1 error"
>> helper.pluralize(5, "error")                                                                                                                                
=> "5 errors"

pluralizeを使うことで、コードは次のようになる。

<%= pluralize(@user.errors.count, "error") %>.

7.3.4 失敗時のテスト

Railsではフォーム用のテストを書くことができ、このようなプロセスを自動化することができる!ラッキー!
ここでは、無効な送信をしたときの正しい振る舞いについてテスト、とくに統合テストを書いていく。
統合テストとは、手続きや関数といった個々の機能を結合させて、うまく連携・動作しているかを確認するテスト(weblioより)

新規ユーザー登録用の統合テスト

新規ユーザー登録用の統合テストを生成する。コントローラーの慣習である「リソース名は複数形」にちなんで、統合テストのファイル名はusers_signup

$ rails g integration_test users_signup
       invoke  test_unit
      create    test/integration/users_signup_test.rb←①

すると、「integration」フォルダの下に「users_sighup_test.rb」というファイルが生成される①。

このテストでは、ユーザー登録ボタンを押したとき、ユーザーが作成されないことを確認する。
test/integration/users_signup_test.rb

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do                                         
    get signup_path ←①                                                          
    assert_no_difference 'User.count' do ←②                                       
      post users_path, params: { user: { name: "", ←③
                                         email: "user@invalid",
                                         password:                       "foo",
                                         password_confirmation: "bar" } }
    end
  assert_template 'users/new'
end

①:getメソッドを使ってユーザー登録ページにアクセス
②:assert_no_difference(expressions, message = nil, &block)
式を評価した結果の数値は、ブロックで渡されたものを呼び出す前と呼び出した後で「違いがない」と主張。
今の場合ですと、assert_no_differenceのブロックを実行する前後で引数の値(User.count)が変わらないことをテスト。

すなわち、ユーザー数を覚えた後にデータを投稿してみて、
ユーザ数が変わっていなければ、ユーザー生成が失敗でtrue、変わっていればfalseというわけ。

③:users_pathに対して、POSTリクエストを送信している。params[:user]というハッシュに、User.newで必要なデータをまとめている。ただし、User.countがかわらなくするため、ユーザーが生成されないデータであることに注意。