railsチュートリアルまとめ8 基本的なログイン機構

# 個人的リマインド用
参考
[Ruby on Rails チュートリアル プロダクト開発の０→１を学ぼう](https://railstutorial.jp/)
## 基本的なログイン機構
ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄する**認証システム**のこと。
また、ログインしている人だけがアクセスできるページや使える機能などがあり、このような制限や制御の仕組みを**認可モデルという**。
### セッション
HTTPは**ステートレスなプロトコル**。文字通り状態を持たない。この本質的な特性のため、ブラウザのあるページから別のページに移動した時に、ユーザーのIDを保持しておく手段がHTTPプロトコルの中には全くない。そこで、ユーザーログインの必要なWebアプリケーションでは、**セッション(session)と**呼ばれる半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)に別途設定する。

Railsでセッションを実装する方法として最も一般的なのは**cookies**を使う方法。cookiesとは、**ユーザーのブラウザに保持される小さなテキストデータ**。cookiesは別のページに移動しても消えないので、ユーザーIDなどの情報を保存できる。
### Sessionsコントローラ
まず、ログインとログアウトの要素を、Sessionsコントローラの特定のREStアクションにそれぞれ対応づける。
ログインフォームはnewアクションで処理し、createアクションにPOSTリクエストを送信すると、ログインする。また、destroyアクションにDELETEリクエストを送信すると、ログアウトする。
```
rails g controller Sessions new
```
createやdestroyには対応するビューがないので、newだけを指定して作成する。

Usersリソースではresourcesメソッドを使って、RESTfulなルーティングを自動的にフルセットで利用できるようにしたが、今回はフルセットはいらないので「名前付きルーティング」だけを行う。使うのはログイン時のGETとPOST、ログアウトでDELETE。
```ruby
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" # ←ここと
  post   "/login",   to: "sessions#create" # ←ここと
  delete "/logout",  to: "sessions#destroy" # ←ここ!
  resources :users
end
```
また、それに対応してテストの更新。
```ruby
test/controllers/sessions_controller_test.rb

require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get login_path # ←ここ
    assert_response :success
  end
end
```
|HTTPリクエストメソッド|URL|名前付きルーティング|アクション名|用途|
|---|---|---|---|---|
|GET|/login|login_path|new|新しいセッションのページ(ログイン)|
|POST|/login|login_path|create|新しいセッションの作成(ログイン)|
|DELETE|/logout|logout_path|destroy|セッションの削除(ログアウト)|
### ログインフォーム
フォームは[Email]と[Password]だけ。前回はエラーメッセージの表示に専用のパーシャルを使ったが、そのパーシャルではActive Recordによって自動生成されるメッセージを使っていた。しかし、今回扱うセッションはActive Recordオブジェクトではないので、以前の方法は期待できない。そこでフラッシュ、メッセージでエラーを表示する。

**formへの渡し方**
前回はform_with(model: @user)と書くだけで、「フォームのactionは/usersというURLのPOSTである」と自動的に判定していたが、セッションにはセッションモデルがないので、別の方法で行う必要がある。具体的には、リソースの**スコープ**(ここではセッション)とそれに対応するURLを指定する必用がある。
```erb
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_with(url: login_path, scope: :session) 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>
```
生成後のHTMLフォームは
```html
<form action="/login" accept-charset="UTF-8" method="post">
    <input type="hidden" name="authenticity_token"
           value="4d0...FFw" autocomplete="off"/>
    <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>
```
paramsハッシュに入る値が、それぞれparams[:session][:email]とparams[:session][:password]になることが推測できる。
### ユーザーの検索と認証
ログインでセッションを作成する場合に最初に行うのは、入力が**無効な**場合の処理。
```ruby
app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new', status: :unprocessable_entity
  end

  def destroy
  end
end
```
この状態で/sessions/newフォームから送信した際のデバッグ情報は
```
#<ActionController::Parameters
{"authenticity_token"=>"…",
  "session" =>#<ActionController::Parameters
              {"email"=>"user@example.com",
               "password"=>"foobar"} permitted: false>,
               "commit"=>"Log in",
               "controller"=>"sessions",
               "action"=>"create"} permitted: false>
```
ネストしたハッシュになっているので、emailにアクセスしたい時は
```
params[:session][:email]
```
passwordにアクセスしたい時は
```
params[:session][:password]
```
要するにcreateアクションの中では、ユーザーの認証に必用なあらゆる情報をparamsハッシュから簡単に取り出せる。今まで学んだ、find.byとhas_secure_password、authenticateメソッドを使って、ユーザーのログイン部分を実装したものがこちら
```ruby
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', status: :unprocessable_entity
    end
  end

  def destroy
  end
end
```
最初の行は、送信されたメールアドレスを使って、データベースからユーザーを取り出している。downcaseを使っていることに注意。
次の文について、
```
user && user.authenticate(params[:session][:password])
```
論理積である&&は、取得したユーザーが有効かどうかを決定するためにつかう。Rubyではnilとfalse以外がすべてtrueになるので、組み合わせ表は以下の通りになる。
|User|Password|a && b|
|---|---|---|
|存在しない|なんでも良い|(nil && [オブジェクト]) == false|
|有効なユーザー|誤ったパスワード|(true && false) == false|
|有効なユーザー|正しいパスワード|(true && true) == true|

入力されたメールアドレスを持つユーザーがデータベースに存在するかつ、入力されたパスワードがそのユーザーのパスワードである時のみ、if文がfalseになる。
### フラッシュメッセージを表示する
```ruby
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', status: :unprocessable_entity
    end
  end

  def destroy
  end
end
```
どこが間違っているかというと、このコードのままでは**リクエスト**のフラッシュメッセージが一度表示されると消えずに残ってしまう。renderで再レンダリングしてもリクエストとみなされないので消えない。ここは今後修正する。
### フラッシュのテスト
```
rails g integration_test users_login
```
テストの基本的な流れ
1.ログイン用のパスを開く
2.新しいセッションのフォームが正しく表示されたことを確認する
3.わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
4.新しいセッションのフォームが正しいステータスを返し、再度表示されることを確認する
5.フラッシュメッセージが表示されることを確認する
6.別のページ（Homeページなど） にいったん移動する
7.移動先のページでフラッシュメッセージが表示されていないことを確認する
```ruby
test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_response :unprocessable_entity
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end
```
この時点でテストはred。
解決策としてはflash.nowを使う。これを使えばメッセージはその後のリクエストが発生した時に消滅する。
```ruby
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', status: :unprocessable_entity
    end
  end

  def destroy
  end
end
```
テストはgreenになる。
### ログイン
次は有効なフォームの扱い。今回はcookiesを使うが、ブラウザを閉じると自動的に有効期限が切れるものを使う。

ログイン機構を実装するために、これから複数のコントローラからログイン関連のメソッドを呼び出せるようにするが、こんな時に便利なのがヘルパーの仕組み。各ヘルパーはコントローラを生成したときに自動的に用意されるので、あとは「どこから呼び出せるようにしたいか」を決めるだけ。

これから実装するログイン機構はいろんなとこで使うので、全コントローラの親クラスである、「applicationコントローラ」に自動生成されたセッション用ヘルパーを読み込ませ、どのコントローラからでもログイン関連のメソッドを呼び出せるようにする。
```ruby
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include SessionsHelper
end
```
### log_inメソッド
```
session[:user_id] = user.id
```
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに**暗号化済み(盗み出されてもログインされない)のユーザーID**が自動で作成される。この後のページでsession[:user_id]を使ってユーザーIDを一時的に取り出せる。一方でcokkiesメソッド(今後出てくる)とは対照的に、sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了する。

いろんなとこで使いまわせるように、Sessionsヘルパーにメソッドを定義する。
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end
```
セッションは**セッションハイジャック**に対して脆弱。セッションハイジャックとは、攻撃者があるユーザーのセッションidのコピーを手に入れ、そのユーザーとしてログインする攻撃方法。

また**セッション固定**という攻撃も危険で、これは攻撃者が既に持っているセッションidをユーザーに使わせるように仕向けることで、攻撃者がユーザーとセッションを共有するというもの。対策としては、ユーザーがログインする**直前**にセッションを必ず即座にリセットすること。reset_sessionメソッドで対策可能。
```ruby
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])
      reset_session      # ログインの直前に必ずこれを書くこと
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
  end
end
```
### 現在のユーザー
今度は保存したユーザーIDを別のページで取り出す。そのためにはcurrent_userメソッドを定義し、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。current_userメソッドの目的は、次のようなコードが書けること
```
<%= current_user.name %>
```
また、次のようなコードで簡単にユーザープロフにリダイレクトできる
```
redirect_to current_user
```

どう取り出すかで、真っ先に思いつくのがfindメソッドを使うこと
```
User.find(session[:user_id])
```
しかしユーザーIDが存在しない状態でfindを使うと例外が発生する。「ユーザーがログインしていない」などの状況が考えられる今回のケースでは、session[:user_id]の値がnilになる可能性もある。なので今回はfind_byを使う。
```
User.find_by(id: session[:user_id])
```
これで、IDが無効な場合（=ユーザーが存在しない場合）にもメソッドは例外を発生せず、nilを返す。
```ruby
def current_user
  if session[:user_id]
    User.find_by(id: session[:user_id])
  end
end
```
nilを返してほしい理由としては、user_currentメソッドが1リクエスト内の処理で何度も呼び出されると、呼び出された回数と同じだけデータベースへの問い合わせが発生してしまうから。

またRubyの慣習にしたがい、find_byの実行結果をインスタンス変数に保存する工夫もしている。これでデータベースへの問い合わせが最初の一回で済み、以降の呼び出しではインスタンス変数の結果を再利用する。これを**メモ化**という。
```ruby
if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
end
```
これを1行にすると
```
@current_user = @current_user || User.find_by(id: session[:user_id])
```
もっと簡略化
```
@current_user ||= User.find_by(id: session[:user_id])
```
ここで重要なのが、Userオブジェクトそのものの論理値は常にtrueになること。そのおかげで@current_userの何も代入されていない時だけfind_byメソッドを実行し、無駄なデータベースへの読み出しが行われなくなる。
最終結果として、
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

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

  # 現在ログイン中のユーザーを返す（いる場合）
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end
```
### レイアウトリンクを変更する
ログインしてる時には、ユーザー一覧やらユーザー設定やらログアウトやらをnavに追加する。
まずは統合テストを書く。
```
<% if logged_in? %>
  # ログインユーザー用のリンク
<% else %>
  # ログインしていないユーザー用のリンク
<% end %>
```
上記のコードを書くために、まずはlogged_in?メソッドの定義から。
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないということ。
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

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

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

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in? # ←ここ！
    !current_user.nil?
  end
end
```
この時点でいろいろレイアウトが書き換えられるように
```erb
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="#" id="account" class="dropdown-toggle">
              Account <b class="caret"></b>
            </a>
            <ul id="dropdown-menu" 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,
                                       data: { "turbo-method": :delete } %>
              </li>
            </ul>
          </li>
        <% else %> ←ここ！
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>
```
注目点
ログアウトではHTTPのDELETEを使う
Profileではリンクにcurrent_userメソッドを使う(user_path(current_user)が省略されてこの書き方に)
### メニューのtoggle機能
navのaccountが表示されたりされなかったりするやつ。これはJSで行うが、まずRaulsプロジェクトにJSを導入するという課題がある。現在ではImportmapと呼ばれる手法によって標準化された。ImportmapとTurboとStimulusをインストールする。後者2つはHotwireフレームワークの一部。
```
$ rails importmap:install turbo:install stimulus:install
```
通常はrails newをした時点で入っているが、今回は--skip-bundleをしたので手動で追加。上記のコマンドを実行するとJSの**マニフェストファイル**である、manifest.jsや、アプリケーションのレイアウトファイルapplication.html.erbなど、様々なファイルが自動的に更新される。

Accountのドロップダウンメニュー戦略
1.CSSのactiveクラスの振る舞いを定義して、メニューが表示されるようにする
2.JavaScriptを置くためのcustomディレクトリとmenu.jsファイルを新たに作成する
3.menu.jsファイルにtoggleコードを書く
4.Importmapを使って、customディレクトリが存在することをRailsに認識させる
5.menu.jsをapplication.jsにインポートする

1 CSSのactiveクラスの振る舞いを定義して、メニューが表示されるようにする
```scss
app/assets/stylesheets/custom.scss
.
.
.
/* Dropdown menu */

.dropdown-menu.active {
  display: block;
}
```
2 JavaScriptを置くためのcustomディレクトリとmenu.jsファイルを新たに作成する
```
$ mkdir app/javascript/custom
$ touch app/javascript/custom/menu.js
```
3 menu.jsファイルにtoggleコードを書く
```js
app/javascript/custom/menu.js

// メニュー操作

// トグルリスナーを追加してクリックをリッスンする
document.addEventListener("turbo:load", function() {
  let account = document.querySelector("#account");
  account.addEventListener("click", function(event) {
    event.preventDefault();
    let menu = document.querySelector("#dropdown-menu");
    menu.classList.toggle("active");
  });
});
```
4 Importmapを使って、customディレクトリが存在することをRailsに認識させる
```ruby
config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/custom",      under: "custom" # ←ここ
```
5 menu.jsをapplication.jsにインポートする
```js
app/javascript/application.js

// Configure your import map in config/importmap.rb.
// Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import "custom/menu" //←ここ
```
### レスポンシブデザイン
別途勉強するべし

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

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

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

has_secure_passwordでbcryptパスワードが作成されるので、同じ方法でfixture用のパスワードを作成する。Railsのsecure passwordのソースコードを調べると、次の部分でパスワードが生成されることがわかる。
```
BCrypt::Password.create(string, cost: cost)
```
stringはハッシュ化する文字列、costは**コストパラメータ**と呼ばれる値。ハッシュを算出するための計算コストを指定する。テスト中は軽くていいので
```
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost
```
上記の意味は、テスト中はコストパラメータを最小にして、本番環境では通常の高いコストで計算する。

今後digestメソッドは再利用するので、Userモデルのuser.rbに置いておく。この計算はユーザーごとに行う必要はないので、fixtureファイルなどでわざわざユーザーオブジェクトにアクセスする必要はない。つまりインスタンスメソッドで定義する必要はないので、digestメソッドをUserクラス自身に配置し、**クラスメソッド**にすることにする。
```ruby
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: true
  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
```
結果としてfixtureは
```
test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
```
ハッシュ化されていない生のパスワードもあると便利。しかしそれを参照する機能はfixtureにはないので、テスト用のfixtureでは**全員同じパスワード「password」を**使うことにする。
有効なユーザーのfixtureを作成できたので、次のように参照できるようになる。
```
user = users(:michael)
```
usersはfixtureのファイル名users.ymlを表し、:michaelというシンボルはユーザーを参照するためのキー。
```ruby
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
    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
```
テストはgreen
### ユーザー登録時にログインする
Usersコントローラのcreateアクションにlog_inを追加するだけ。また、セッション固定攻撃から保護するためのreset_sessionも記述。
```ruby
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
      reset_session # ←ここと
      log_in @user # ←ここ！
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new', status: :unprocessable_entity
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end
```
動作をテストするために、ユーザーがログイン中かどうかをチェック。is_logged_in?ヘルパーメソッドを定義していると便利。このヘルパーメソッドは、テストのセッションにユーザーがあればtrueを返し、それ以外の場合はfalseを返す。
**ヘルパーメソッドはテストから呼び出せない**ので、current_userを呼び出せない。sessionメソッドはテストでも使えるので、これを代わりに使う。
```ruby
test/test_helper.rb

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  # 指定のワーカー数でテストを並列実行する
  parallelize(workers: :number_of_processors)
  # test/fixtures/*.ymlのfixtureをすべてセットアップする
  fixtures :all

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

require "test_helper"

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    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
```
### ログアウト
ログアウトの処理はdestroyアクション。以下のようにセッションのdeleteメソッドでuser idを消すだけ。
```
session.delete(:user_id)
```
しかし、これよりもreset_sessionメソッドで、**すべて**のセッション変数を確実にリセットする方がいい。
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out # ←ここ
    reset_session
    @current_user = nil   # 安全のため
  end
end
```
```ruby
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])
      reset_session
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
    log_out # ←ここと
    redirect_to root_url, status: :see_other # ←ここ
  end
end
```
status: :see_otherに注目。RailsでTurboを使う時は、このように303 SeeOtherステータスを指定することで、DELETEリクエスト後のリダイレクトが正しく振る舞うようにする必用がある。

ログアウト機能のテストのため若干追加。
```ruby
test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "login with valid email/invalid password" do # ←ここと
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email:    @user.email,
                                          password: "invalid" } } # ←ここと
    assert_not is_logged_in? # ←ここと
    assert_response :unprocessable_entity
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end

  test "login with valid information followed by logout" do # ←ここと
    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_response :see_other # ←ここと
    assert_redirected_to root_url # ←ここと
    follow_redirect! # ←ここと
    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
```
テストはgreen。長いので後々分割予定