LoginSignup
6
1

More than 5 years have passed since last update.

Ruby on Rails Tutorial 8章 メモ

Last updated at Posted at 2018-04-22

基本用語

メモ化

引数に対するメソッドの戻り値を保存しておき、再び同じ引数でメソッドが呼び出された時にその値を再利用することにより、同じ計算を何度もすることを防ぐ最適化手法のひとつ。

文法

ページ内リンクを作る

# 飛びたいタグにid名をつける
<h2 id="example">これは見出しです。</h2>

# URLのかわりにhref=”#ジャンプ先のid名”というように書く
<a href="#example">ここをクリック!</a>

# 違うページの途中に飛ぶためには、"飛びたいページのURL#id名"
<a href="https://saruwakakun.com#example">ここをクリック!</a>

rails routes

現状のルーティングを確認することができる。

rails routes

grep

Usersリソースに関するルーティングだけを表示させる。

rails routes | grep users#

特定のテストファイルだけを実行する

rails test test/integration/users_login_test.rb

cookiesを調べる

以下を参照。
GoogleChromeで特定のサイトのクッキー(Cookie)情報を確認する方法

セッション

HTTPはステートレス(Stateless) なプロトコル。
文字通り「状態(state)」が「ない(less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われる(しかし、だからこそこのプロトコルは非常に頑丈)。
この本質的な特性のため、ブラウザのあるページから別のページに移動したときに、ユーザーのIDを保持しておく手段がHTTPプロトコル内「には」まったくない。
ユーザーログインの必要なWebアプリケーションでは、セッション(Session)と呼ばれる半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)に別途設定する。
セッションはHTTPプロトコルと階層が異なる(上の階層にある)ので、HTTPの特性とは別に (若干影響は受けるものの)接続を確保できる。

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法。
cookiesとは、ユーザーのブラウザに保存される小さなテキストデータ。
cookiesは、あるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できる。
アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができる。

セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利。
ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった具合。
ただしUsersリソースと異なるのは、UsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使う点。
ログインの仕組みの大半は、cookiesを使った認証メカニズムによって構築されている。

Sessionsコントローラ

ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。
ログインのフォームは、newアクションで処理する。
createアクションにPOSTリクエストを送信すると、実際にログインする。
destroyアクションにDELETEリクエストを送信すると、ログアウトする。

Sessionsコントローラを生成する

Sessionsコントローラとnewアクションを生成する。

rails generate controller Sessions new

rails generateでnewアクションを生成すると、それに対応するビューも生成される。
createやdestroyには対応するビューが必要ないので、無駄なビューを作成しないためにここではnewだけを指定している。

Usersリソースのときは専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使う。

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new' # loginフォーム
  post   '/login',   to: 'sessions#create' # login
  delete '/logout',  to: 'sessions#destroy' # loggout
  resources :users
end

ルーティングと同様に、テストを更新し、新しいログイン用の名前付きルートを使うようにする必要がある。

test/controllers/sessions_controller_test.rb
require 'test_helper'

class SessionsControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get login_path # login_pathに変更
    assert_response :success
  end
end
HTTPリクエスト URL 名前付きルート アクション名 用途
GET /login login_path new 新しいセッションのページ (ログイン)
POST /login login_path create 新しいセッションの作成 (ログイン)
DELETE /logout logout_path destroy セッションの削除 (ログアウト)

セッションルールによって提供されるルーティング

ログイン

今回扱うセッションはActive Recordオブジェクトではないので、ユーザー登録フォームのようにActive Recordがよしなにエラーメッセージを表示してくれるということは期待できない。
そこで今回は、フラッシュメッセージでエラーを表示する。

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

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

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

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>
# ユーザー登録フォーム
form_for(@user)

# ログインフォーム
form_for(:session, url: login_path)

ユーザー登録フォームではform_forヘルパーを使い、ユーザーのインスタンス変数@userを引数にとっていたが、セッションにはSessionモデルというものがなく、そのため@userのようなインスタンス変数に相当するものもないため、リソースの名前とそれに対応するURLを具体的に指定する必要がある(ユーザー登録フォームの場合は上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定していた)。

ユーザーの検索

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

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

送信されたメールアドレスを使って、データベースからユーザーを取り出している。

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

&&(論理積(and)) は、取得したユーザーが有効かどうかを決定するために使う。
Rubyではnilとfalse以外のすべてのオブジェクトは、真偽値ではtrueになる。
そのため、入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if文がtrueになる(「ユーザーがデータベースにあり、かつ、認証に成功した場合にのみ」)。

User Password a && b
存在しない 何でもよい (nil && [オブジェクト]) == false
有効なユーザー 誤ったパスワード (true && false) == false
有効なユーザー 正しいパスワード (true && true) == true

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

前述のように、セッションではActive Recordのモデルを使っていないため、ログインに失敗したときには代わりにフラッシュメッセージを表示する。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
      render 'new'
    end
  end

  def destroy
  end
end

実は上のコードのままでは、リクエストのフラッシュメッセージが一度表示されると消えずに残ってしまう。
ユーザー登録フォームでリダイレクトを使ったときとは異なり、表示したテンプレートをrenderメソッドで強制的に再レンダリングしてもリクエストと見なされないため、リクエストのメッセージが消えない。
例えばわざと無効な情報を入力して送信してエラーメッセージを表示してから、Homeページをクリックして移動すると、そこでもフラッシュメッセージが表示されたままになっている。

フラッシュのテスト

フラッシュメッセージが消えない問題は、テストのガイドラインに従えば、「エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く」に該当する状況。
さっそく、ログインフォームの送信について簡単な統合テストを作成することから始めていく。
この統合テストは、そのままバグのドキュメントにもなり、今後の回帰バグ発生を防止する効能もある。
さらに、今後この統合テストを土台として、より本格的な統合テストを作成するときにも便利。

前述した問題をテストコードで再現する必要がある。
基本的な流れを次に示す。

  1. ログイン用のパスを開く
  2. 新しいセッションのフォームが正しく表示されたことを確認する
  3. わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
  4. 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
  5. 別のページ (Homeページなど) にいったん移動する
  6. 移動先のページでフラッシュメッセージが表示されていないことを確認する
test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path # 1
    assert_template 'sessions/new' # 2
    post login_path, params: { session: { email: "", password: "" } } # 3
    assert_template 'sessions/new' # 4
    assert_not flash.empty? # 4
    get root_path # 5
    assert flash.empty? # 6
  end
end
flash.now

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

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

ログイン

cookiesを使った一時セッションでユーザーをログインできるようにする。
このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。

セッションを実装するには、様々なコントローラやビューでおびただしい数のメソッドを定義する必要がある。
Rubyのモジュール機能を使うと、そうしたメソッドを一箇所にパッケージ化できる。
Sessionsコントローラを生成した時点で既にセッション用ヘルパーモジュールも (密かに) 自動生成されている。
さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれる。
Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになる。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper # Sessionモジュールを読み込ませる
end

log_inメソッド

同じログイン手法を様々な場所で使い回せるようにするために、Sessionsヘルパーにlog_inという名前のメソッドを定義する。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

sessionメソッド

このコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。
sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了する。
sessionメソッドで作成した一時cookiesは自動的に暗号化され、このコードは保護される。
ここで重要なことは、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないこと。
ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しない(cookiesメソッドで作成した「永続的セッション」ではそこまで断言はない。永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまとう)。

session[:user_id] = user.id
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user # ユーザーにログインする
      redirect_to user # redirect_to user_url(user)に変換される
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

current_userメソッド

ユーザーIDを一時セッションの中に安全に置けるようになったので、そのユーザーIDを別のページで取り出す。

def current_user
  User.find_by(id: session[:user_id])
end

find_byメソッド

ユーザーIDが存在しない状態でfindを使うと例外が発生してしまう。
findのこの動作は、プロフィールページでは適切だった(IDが無効の場合は例外を発生してくれなければ困るから)。
しかし、「ユーザーがログインしていない」などの状況が考えられる今回のケースでは、session[:user_id]の値はnilになりえる。
この状態を修正するために、createメソッド内でメールアドレスの検索に使ったのと同じfind_byメソッドを使う。
find_byメソッドは、IDが無効な場合 (=ユーザーが存在しない場合) にもメソッドは例外を発生せず、nilを返す。

User.find_by(id: session[:user_id])

current_userメソッドの改良

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end
# 1
def current_user
  User.find_by(id: session[:user_id])
end

# 2
def current_user
  if @current_user.nil?
    @current_user = User.find_by(id: session[:user_id])
  else
    @current_user
  end
end

# 3
def current_user
  @current_user = @current_user || User.find_by(id: session[:user_id])
end

# 4
def current_user
  @current_user ||= User.find_by(id: session[:user_id])
end
  1. 正常に動作はする。
  2. current_userメソッドが1リクエスト内で何度も呼び出されると、呼び出された回数と同じだけデータベースにも問い合わせされてしまう。そこでRubyの慣習に従って、User.find_byの実行結果をインスタンス変数に保存することにする。こうすることで、データベースの読み出しは最初の一回だけになり、以後の呼び出しではインスタンス変数を返すようになる。地味なようだが、Railsを高速化させるための重要なテクニック。
  3. or演算子「||」を使えれば、2の「メモ化」コードがたった1行で書ける。
  4. 「||=(or equals)記法で短縮化する。
「||=」(or equals)

「||=」(or equals)という代入演算子はRubyで広く使われているイディオム。
Rubyでは、「変数の値がnilなら変数に代入するが、nilでなければ代入しない(変数の値を変えない)」という操作が非常によく使われる。
or equalsという概念は一見不思議にみえるが、他のものになぞらえて考えれば難しくない。

  x    =   x   +   1      ->     x     +=   1
  x    =   x   *   3      ->     x     *=   3
  x    =   x   -   8      ->     x     -=   8
  x    =   x   /   2      ->     x     /=   2
  @foo = @foo || "bar"    ->     @foo ||= "bar"

logged_in?メソッド

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end

ログインの有無でのレイアウトの変更

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %> # ログインしているとき
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", '#' %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %> # ログインしていないとき
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ドロップダウン機能を有効にする

ドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示する。

app/assets/javascripts/application.js
//= require rails-ujs
//= require jquery # jQueryを読み込む
//= require bootstrap # Bootstrapに同梱されているJavaScriptライブラリを読み込む
//= require turbolinks
//= require_tree .

ログインの有無でのレイアウトの変更のテスト

  1. ログイン用のパスを開く
  2. セッション用パスに有効な情報をpostする
  3. ログイン用リンクが表示されなくなったことを確認する
  4. ログアウト用リンクが表示されていることを確認する
  5. プロフィール用リンクが表示されていることを確認する

ログイン成功時のレイアウトの変更を確認するためには、テスト時に登録済みユーザーとしてログインしておく必要がある。
当然ながら、データベースにそのためのユーザーが登録されていなければならない。
Railsでは、このようなテスト用データをfixture (フィクスチャ) で作成できる。
このfixtureを使って、テストに必要なデータをtestデータベースに読み込んでおくことができる。

digestメソッド

テスト中にそのユーザーとして自動ログインするために、そのユーザーの有効なパスワードも用意して、Sessionsコントローラのcreateアクションに送信されたパスワードと比較できるようにする必要がある。
Userモデルを見ると、password_digest属性をユーザーのfixtureに追加すればよいことが分かる。
そのために、digestメソッドを独自に定義することにする。

has_secure_passwordでbcryptパスワードが作成されるので、同じ方法でfixture用のパスワードを作成する。
Railsのsecure_passwordのソースコードを調べてみると、次の部分でパスワードが生成されていることが分かる。

BCrypt::Password.create(string, cost: cost)

上のstringはハッシュ化する文字列、costはコストパラメータと呼ばれる値。
コストパラメータでは、ハッシュを算出するための計算コストを指定する。
コストパラメータの値を高くすれば、ハッシュからオリジナルのパスワードを計算で推測することが困難になりますので、本番環境ではセキュリティ上重要。
しかしテスト中はコストを高くする意味はないので、digestメソッドの計算はなるべく軽くしておきたい。
この点についても、secure_passwordのソースコードには次の行が参考になる。

cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost

少々込み入っているが、コストパラメータをテスト中は最小にし、本番環境ではしっかりと計算する方法がわかれば十分。

このdigestメソッドは、今後様々な場面で活用するので、このdigestメソッドはUserモデル (user.rb) に置いておく。
この計算はユーザーごとに行う必要はないので、fixtureファイルなどでわざわざユーザーオブジェクトにアクセスする必然性はない(つまり、インスタンスメソッドで定義する必要がない)。
そこで、digestメソッドをUserクラス自身に配置して、クラスメソッドにすることにする (クラスメソッドの作り方についてはここで簡単に説明した)。

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end
test/fixtures/users.yml
 michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

上のコードにあるように、fixtureではERbを利用できる。

<%= User.digest('password') %>

上のERbコードでテストユーザー用の有効なパスワードを作成できる。

has_secure_passwordで必要となるpassword_digest属性はこれで準備できたが、ハッシュ化されていない生のパスワードも参照できると便利。
しかし残念なことに、fixtureではこのようなことはできない。
さらに、fixtureにpassword属性を追加すると、そのようなカラムはデータベースに存在しないというエラーが発生する。
実際、データベースにはそんなカラムはない。
この状況を切り抜けるため、テスト用のfixtureでは全員同じパスワード「password」を使うことにする (これはfixtureでよく使われる手法)。

テストの実装

有効なユーザー用のfixtureを作成できたので、テストでは次のようにfixtureのデータを参照できるようになる。

user = users(:michael)

上のusersはfixtureのファイル名users.ymlを表し、:michaelというシンボルはユーザーを参照するためのキーを表す。

test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with valid information" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert_redirected_to @user # リダイレクト先が正しいかどうかをチェック
    follow_redirect! # そのページに実際に移動する
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0 # ログイン用リンクが表示されなくなったことを確認
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
end

ユーザー登録時にログイン

app/controllers/users_controller.rb
 class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user # ユーザー登録中にログインする
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

動作をテストするために、test/integration/users_signup_test.rbの"valid signup information"テストに1行追加して、ユーザーがログイン中かどうかをチェックする。
そのために、logged_in?ヘルパーメソッドとは別に、is_logged_in?ヘルパーメソッドを定義しておくと便利。
このヘルパーメソッドは、テストのセッションにユーザーがあればtrueを返し、それ以外の場合はfalseを返す。
残念ながらヘルパーメソッドはテストから呼び出せないので、current_userを呼び出せない。
sessionメソッドはテストでも利用できるので、これを代わりに使う。
ここでは取り違えを防ぐため、logged_in?の代わりにis_logged_in?を使って、ヘルパーメソッド名がテストヘルパーとSessionヘルパーで同じにならないようにしておく。

test/test_helper.rb
 ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end
end
test/integration/users_signup_test.rb
 require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in? # ユーザー登録後のログインのテスト
  end
end

ログアウト

これまで、SessionsコントローラのアクションはRESTfulルールに従っていた。
newでログインページを表示し、createでログインを完了するといった具合。
セッションを破棄するdestroyアクションも、引き続き同じ要領で作成することにする。
ただし、ログインの場合と異なり、ログアウト処理は1か所で行えるので、destroyアクションに直接ログアウト処理を書くことにする。
この設計 (および若干のリファクタリング) のおかげで認証メカニズムのテストが行い易くなる。

app/helpers/sessions_helper.rb
 module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id) # 現在のユーザーをnilに設定する
    @current_user = nil
  end
end

ここで定義したlog_outメソッドは、Sessionsコントローラのdestroyアクションでも同様に使っていく。

app/controllers/sessions_controller.rb
 class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy # セッションを破棄する (ユーザーのログアウト)
    log_out
    redirect_to root_url
  end
end

ログアウト機能のテスト

ログアウト機能をテストするために、test/integration/users_login_test.rbの"login with valid information"テスト(ユーザーログインテスト)に手順を若干追加する。
ログイン後、deleteメソッドでDELETEリクエストをログアウト用パスに発行し、ユーザーがログアウトしてルートURLにリダイレクトされたことを確認する。
ログイン用リンクが再度表示されること、ログアウト用リンクとプロフィール用リンクが非表示になることも確認する。

test/integration/users_login_test.rb
 require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do # テスト名の変更
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in? # ログインしているか
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path # ログアウトする
    assert_not is_logged_in? # ログインしていないか
    assert_redirected_to root_url # リダイレクト先が正しいかどうかをチェック
    follow_redirect! # root urlにリダイレクト
    assert_select "a[href=?]", login_path # ログインパスがあるか
    assert_select "a[href=?]", logout_path,      count: 0 # ログアウトパスがないか
    assert_select "a[href=?]", user_path(@user), count: 0 # ユーザーページがないか
  end
end

セッションのdestroyアクションの定義とテストも完成したので、ついにサンプルアプリケーションの基本となる「ユーザー登録・ログイン・ログアウト」の機能すべてが完成した。

参考文献

Rubyで任意のメソッドをメモ化する
【HTML】ページ内リンクの作り方:記事の途中に飛ばすには?
GoogleChromeで特定のサイトのクッキー(Cookie)情報を確認する方法

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1