紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【8章】Ruby on Railsチュートリアル演習まとめ&解答例【8.1 セッション】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 8章 8.1 セッションの演習まとめ&解答例です。

個人の解答例なので、誤りがあればご指摘ください。

8.1.1 Sessionsコントローラ

本章での学び

Sessionsコントローラの作成

$ rails generate controller Sessions newのコマンドで、sessionsコントローラ、sessionsコントローラのテスト、newアクションなどを自動生成できる。

yokoyan:~/workspace/sample_app (basic-login) $ rails generate controller Sessions new
Running via Spring preloader in process 5829
Expected string default value for '--jbuilder'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/sessions_controller.rb
       route  get 'sessions/new'
      invoke  erb
      create    app/views/sessions
      create    app/views/sessions/new.html.erb
      invoke  test_unit
      create    test/controllers/sessions_controller_test.rb
      invoke  helper
      create    app/helpers/sessions_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/sessions.coffee
      invoke    scss
      create      app/assets/stylesheets/sessions.scss

名前付きルーティングの設定

/config/routes.rbにて、ログインに関するルーティングの設定を行う。 RESRfulなルーティングをフルセットで付与する場合は、resources :usersのように、resourcesを付与するが、今回はフルセットは不要。 必要なHTTPリクエストのタイプに対するURLと、それに対応するコントローラのアクションを定義する。

  get     '/login',   to: 'sessions#new'
  post    '/login',   to: 'sessions#create'
  delete  '/logout',  to: 'sessions#destroy'

また、rails generateで自動生成される以下のルーティングも削除する。

  get 'sessions/new'
  get 'users/new'

テストの修正

自動生成されたテストケースは以下の通り。

  test "should get new" do
    get sessions_new_url
    assert_response :success
  end

getメソッド部分を、新しく追加した名前付きルーティングlogin_pathをテストに修正する。

  test "should get new" do
    get login_path
    assert_response :success
  end

現在設定しているルーティングの確認

rails routesで、現在設定しているルーティングの確認が行える。 Prefix部分が、login_pathなど名前付きルーティングの名称となる。

yokoyan:~/workspace/sample_app (basic-login) $ rails routes
   Prefix Verb   URI Pattern               Controller#Action
     root GET    /                         static_pages#home
     help GET    /help(.:format)           static_pages#help
    about GET    /about(.:format)          static_pages#about
  contact GET    /contact(.:format)        static_pages#contact
   signup GET    /signup(.:format)         users#new
          POST   /signup(.:format)         users#create
    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create
   logout DELETE /logout(.:format)         sessions#destroy
    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

以下の部分が、routes.rbresources :usersを付与した場合に自動で設定されるフルセットのルーティング設定。

    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

演習1

GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。

    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create

GET login_pathの場合、ログインページへ遷移する際に使用する。 sign upボタンクリック時に、URL:/loginに対してGETすると、sessionsコントローラのnewアクションが動く。 GET時に付与したパラメータがある場合は、URLに表示される。

POST login_pathは、ログイン情報を送信する際に使用する。 フォームタグで入力したログイン情報を、URL:/loginに対してPOSTすると、sessionsコントローラのcreateアクションが動く。 POSTした情報はURLには表示されない。

演習2

ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? ヒント: パイプやgrepの使い方が分からない場合は Learn Enough Command Line to Be Dangerousの Section on Grep (英語) を参考にしてみてください。

rails routes | grep キーワードで表示できる。

Usersリソースに関するルーティングのみを表示。 rails console上では、users#部分が赤字で強調されて表示される。

yokoyan:~/workspace/sample_app (basic-login) $ 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

Sessionsリソースに関するルーティングのみを表示。 現時点では3つのリソースがある。

yokoyan:~/workspace/sample_app (basic-login) $ rails routes | grep sessions#
    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create
   logout DELETE /logout(.:format)         sessions#destroy

8.1.2 ログインフォーム

本章での学び

ビューの作成

ログインフォームの見た目は、ユーザ登録フォームとほぼ同じになる。

ユーザ登録フォームとログインフォームの違い

ユーザ登録フォームの場合

ユーザ登録フォームでは、form_forヘルパーに、Userモデルのインスタンス変数である@userを引数に渡して、<%= form_for(@user) do |f| %>で実現していた。

また、form_for(@user)と記載することで、Railsが自動的に「フォームのアクションは、/usersというURLへのPOSTである」と判断していた。

ログインフォームの場合

セッションにはSessionモデルがないため、@sessionのようなインスタンス変数に相当するものがない。そのため、リソースの名前とそれに対応するURLを以下のように具体的に指定する必要がある。

form_for(:session, url: login_path)

「フォームのアクションは、login_path(/login)というURLへのPOSTである。POSTするリソースは、:sessionである」

生成されたHTML

ユーザ登録フォーム

フォームでsubmitされるハッシュ情報は、params[:user]となる。

    <form class="new_user" id="new_user" action="/signup" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" /><input type="hidden" name="authenticity_token" value="s4yYXtGa8OgmyvCWrN2Dwg97t0NI1WqO0K/V+D1yZP80QtDRfjbalkoxq+adYNgAxYWTOgVLRuCU+y23CQsCwA==" />
   
      <label for="user_name">Name</label>
      <input class="form-control" type="text" name="user[name]" id="user_name" />
      
      <label for="user_email">Email</label>
      <input class="form-control" type="email" name="user[email]" id="user_email" />
      
      <label for="user_password">Password</label>
      <input class="form-control" type="password" name="user[password]" id="user_password" />
      
      <label for="user_password_confirmation">Confirmation</label>
      <input class="form-control" type="password" name="user[password_confirmation]" id="user_password_confirmation" />
      
      <input type="submit" name="commit" value="Create my account" class="btn btn-primary" data-disable-with="Create my account" />
</form>
ログインフォーム

フォームでsubmitされるハッシュ情報は、params[:session]となる。

    <form action="/login" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" /><input type="hidden" name="authenticity_token" value="jwuSENMsYEUhX0ugB6hnKTNGJenkfcLV0a8FKWKogQp4v9+FFhbVjQVmNOQ0xf9pSE/OfsFH0TJFHTZDumFpSA==" />
      <label for="session_email">Email</label>
      <input class="form-control" type="email" name="session[email]" id="session_email" />
      
      <label for="session_password">Password</label>
      <input class="form-control" type="password" name="session[password]" id="session_password" />
      
      <input type="submit" name="commit" value="log in" class="btn btn-primary" data-disable-with="log in" />
</form>    

演習1

リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。

開発環境のログインフォームにて、log inボタンをクリックすると、下記エラーが発生する。 Sessionコントローラのcreateアクションに到達している。

image

これは、ログインフォームで

form_for(:session, url: login_path)

と記載することで、URL:login_path(/login)に対して、POSTしているため。 結果、route.rbで定義したとおり、対応するsessionコントローラのcreateアクションに到達している。

yokoyan:~/workspace/sample_app (basic-login) $ rails routes | grep sessions#
    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create
   logout DELETE /logout(.:format)         sessions#destroy

8.1.3 ユーザーの検索と認証

本章での学び

セッション情報の取得

ユーザー情報は、sessionキーの下に、emailとpasswordがある。

  session: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
    email: user@example.com
    password: foobar
  commit: log in
  controller: sessions
  action: create

つまり、paramsハッシュがネストしたハッシュになっている。

params[:session]

paramsハッシュの中に、sessionハッシュがあり、さらにsessionハッシュの中にpasswordとemailが含まれている。

{ session: { password: "foobar", email: "user@example.com" } }

そのため、以下のようにするとemailの値が取得できる。

params[:session][:email]

同様に、パスワードの値を取得できる。

params[:session][:password] `

ユーザの検索と認証

ユーザを検索するためには、User.find_byメソッドを使う。 データベース内に小文字で格納されているため、downcaseメソッドで確実に一致するようにしている。

user = User.find_by(email: params[:session][:email].downcase)

ユーザの認証は、authenticateメソッドを使う。

user && user.authenticate(params[:session][:password])

userとauthenticateの組み合わせは以下となる。

User     パスワード   判定結果   
存在しない 何でも良い false(nil && [オブジェクト])
存在する 一致しない false (true && false)
存在する 一致する  true (true && true)

演習1

Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。ヒント: 必ず論理値オブジェクトとなるように、4.2.3で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate(’foobar’))

user = nilの場合。期待値はfalse。

yokoyan:~/workspace/sample_app (basic-login) $ rails console
Running via Spring preloader in process 2949
Loading development environment (Rails 5.0.0.1)
>> user = nil
=> nil
>> !!(user && user.authenticate('foobar'))
=> false
>> 
?> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ...">
>> !!(user && user.authenticate('barfoo'))
=> false
>> 
?> !!(user && user.authenticate('foobar'))
=> true
>> 

user = User.firstで取得し、パスワード誤りの場合。 期待値はfalse。

?> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ...">
>> !!(user && user.authenticate('barfoo'))
=> false

user = User.firstで取得し、パスワード一致の場合。 期待値はtrue。

?> !!(user && user.authenticate('foobar'))
=> true
>> 

8.1.4 フラッシュメッセージを表示する

本章での学び

ログイン失敗時のメッセージ

ユーザ登録ページでは、Userモデル(特定のActive Recordオブジェクト)に関連付けられていたため、Userモデルのエラーメッセージがそのまま使用できた。 ログインページでは、セッションモデルがないため、フラッシュメッセージで代用する。

flash[:danger] = 'Invalid email/password combination'

[:danger]はbootstrapのクラス名。 描画されたHTMLでは、以下のようにメッセージが赤で表示される。

<div class="alert alert-danger">Invalid email/password combination</div>

この方式の問題点

リクエストのフラッシュメッセ―ジが一度表示されると消えずに残ってしまう。 リダイレクトした時と異なり、renderメソッドで強制定期に再レンダリングしてもリクエストとしては見なされないため、 リクエストのメッセージは消えない。

8.1.5 フラッシュのテスト

本章での学び

結合テストコードの生成

rails generate integration_test users_loginを実行する。

yokoyan:~/workspace/sample_app (basic-login) $ rails generate integration_test users_login
Running via Spring preloader in process 2308
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  test_unit
      create    test/integration/users_login_test.rb

結合テストコードの単独実行

rails testの引数にテストファイルを指定すると、そのテストファイルのみ実行できる。

yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb 
Running via Spring preloader in process 2549
Started with run options --seed 15900

 FAIL["test_login_with_invalid_information", UsersLoginTest, 5.106406192004215]
 test_login_with_invalid_information#UsersLoginTest (5.11s)
        Expected false to be truthy.
        test/integration/users_login_test.rb:15:in `block in <class:UsersLoginTest>'

  1/1: [=================================================================================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.11290s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips

flash.now

flash.nowは、レンダリングが終わっているページで特別にフラッシュメッセージを表示する。 flashと異なり、flash.nowのメッセージはその後のリクエストが発生すると消滅する。

flash.nowに書き換えると、テストコードがgreenになることを確認。

yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb 
Running via Spring preloader in process 2598
Started with run options --seed 42458

  1/1: [=================================================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.84829s
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips

演習1

8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。

存在しないEmail、Passwordを入力する。

image

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

image

ユーザ登録画面を表示して、フラッシュメッセージが表示されないことを確認。

image

おわりに

いよいよSessionが出てきました。 ログイン後のユーザ情報が保持できるようになるのは次章なので楽しみです! 記事も4本目になりました。マークダウンで記載するのはとても便利ですね。 Rails以外の記事も投稿していきたいと思います。