rails-tutorial第8章

#ログイン機能を作ろう！！

モデルを使わないSessionリソースを扱う。

###Sessionsコントローラを作ろう

$ rails generate controller Sessions new

###ルーティング設定

```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
```

別にresourcesを使ってないからといってrestfulじゃないわけではない。本質は同じurlにhttpリクエストメソッドで指定してアクションを分けるということ。

また、これにより、login_pathなどの名前付きルートが設定される。


###ログインフォーム作る。

```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の引数はsignupの時は@userを渡していたが、:sessionを渡すことにより、
params = {session: {email: ~~~, password: ~~~}}という形で情報を送ることができる。

で、urlオプションに名前付きルートを渡せば完成。 

###次にSessionsコントローラのcreateアクションを実装していこう。

```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
```

find_byに注目。
find()だと、idでしか探すことができず、()の中に数字しか入れられない。

IDがわかっている場合は、findメソッド
IDが不明で、別の条件でレコード検索をしたい場合は、find_byメソッド
このように覚えておこう。

注意点！！

```
user = User.find_by(email: params[:session][:email].downcase)
if user.authenticate(params[:session][:password])
.
.
.
```
上記のように書いてしまうと、致命的な欠陥がある。

find_byはオブジェクトが見つからない時にnilを返すので、もし存在していないメールアドレスを渡してしまうと、、

nil.authenticateとなり、Nomethoderrorが起こってしまう。

なので、

```
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
```

ユーザーが存在するかつ、アドレスの認証が通れば、という条件をif文に当てている。

ちなみに　userが存在しないとわかった時点で、右側の条件式は評価されないという特徴がある。
これにより、nil.authenticateは実行されなくなる。


###ログイン失敗時のメッセージを表示する。

ActiveRecordを継承しているモデルと、バリデーションを設定すれば@user.errors.full_messagesに実際のエラーメッセージが表示されたが、

Sessionsの場合、モデルでもなければバリデーションもないので、

flashを使ってメッセージを表示する必要がある。

```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
```

上記の場合、ちょっとしたバグがある。
それは、flashメッセージがhome画面やhelp画面に移動しても表示されてしまうことだ。

なぜこれが起きるのか？

redirect_toの場合は、それでリクエストが一回。
別のページに飛ぼうとすると２回目なので飛ぶ寸前でflashメッセージは消える。

しかし、今回は、
render 'new'となっている。
renderはリクエストには入らないため、
例えば、home画面に飛ぶので一回目、
再度リロードすると、２回目なので消える、という流れになってしまう。


###回帰バグを防ぐためにテストを書こう。

今回もブラウザを行ったり来たりしているテストなので、インテグレーションテストで行う。

$ rails generate integration_test users_login


```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_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end
```
この状態だとテストは落ちる。

じゃあ、どうやってflashメッセージを１回だけ表示するの？

flashはメソッドなので、flash.now[:danger] = 'Invalid email/password combination'
というようにできる。
.nowは1度目のリクエストが来たらflashメッセージを消す。
これはflashメソッドがrailsで元から設定されたメソッドだからこういうことができる。

これで失敗時の実装は完了


###成功した時の処理を実装しよう

ユーザーが存在し、アドレスの認証が通れば、ログインしている状態を作り出さないといけない。

sessionという特殊な変数を使ってそれを実現する。

session[:user_id] = user.id
ここに何か値が入れば、ログイン中とし、nilになればログアウトとする。

それを実現するためにhelperにloginメソッドを定義しよう。

```app/helpers/sessions_helper.rb
module SessionsHelper

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

このhelperメソッドを使ってログイン機能を実装

```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
  end
end
```

最後に、loginしているかどうかは、様々なコントローラで使うので、loginメソッドを書いたhelperをどのコントローラでも使えるようにしておこう。

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end
```

そのためには、全コントローラの親クラスであるapplication コントローラに



###createアクションを実装したら

その次は、session[:user_id]に値が入っていれば、〜〜〜〜、入っていなければ、〜〜〜〜というようにしていく。

また、今どのユーザーがログインしているかがわからないと、showアクションでどのユーザーページを表示するべきかなどの問題が発生してしまうので、なんとかして、sessionの情報から現在ログインしているユーザーを参照する必要がある。

では、現在ログインしているユーザーを返すメソッドをhelperに定義しよう。

```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
```
@current_userとなっているのは、インスタンス変数をview側で使いたいから。
また、@current_userを使いたいたびにfind_byをするのはパフォーマンス上あまり良くない。
なので、　||=　を使って、存在すれば@current_userを返す。　なかったら、find_byを使うというようにしている。

これで、1リクエストで最大で1問い合わせにできる。


view側でログインユーザーと非ログインユーザーを分けるには

```
<% if logged_in? %>
  # ログインユーザー用のリンク
<% else %>
  # ログインしていないユーザー用のリンク
<% end %>
```
というようにrubyのコードを使ってあげれば良い。

そのために、上記のlogged_in?メソッドを定義しよう。

```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
```

logged_in?メソッドはcurrent_userメソッドを呼び出して、インスタンス変数があれば、trueを返し、なければfalseを返すようにする。

このままだと、nilの時にtrueを返してしまう。

!は否定演算子である。true falseが逆になる。

これで、nilの時はfalseをnilじゃない時はtrueを返すようにする。


###ログインしているかでviewを分ける

```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>
```

ここで注意しなければいけないのは、

```
<li><%= link_to "Profile", current_user %></li>
```

これは本来

```
<li><%= link_to "Profile", user_path(@current_user) %></li>
```
となっている。

ただ、これは　@current_userに省略することができたよね。

だから、current_userメソッドを書くだけで良い。

もう一つ注意する点が、

```
<%= link_to "Log out", logout_path, method: :delete %>
```

link_toメソッドはデフォルトではgetリクエストを送るのでmethod: :deleteを書かないと、

/logout に　getリクエストを送ってしまう。

なので、link_toでgetリクエスト以外を指定するときは、
method: :deleteのようにオプションを追加しなければいけない。

注意点としては、method:　と　:deleteどちらも:が必要だということ。


###bootstrapのドロップダウンを使えるようにするために

```app/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .
```
2,3行目を書き加えるとドロップダウンが使えるようになる。



###ユーザーログインのテスト

テスト時に登録済みユーザーとしてログインしておく必要があります。当然ながら、データベースにそのためのユーザーが登録されていなければなりません。Railsでは、このようなテスト用データをfixture (フィクスチャ) で作成できます。

```test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
```

でもこれじゃあdigestメソッドがないから、ハッシュ化できない。

digestメソッドはUserに関連するときしか使わないので、userモデルに定義する。


```app/models/user.rb
  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
```

###三項演算子について

```
condition ? expr1 : expr2
```

condition
trueかfalseかを評価する式です。
expr1, expr2
各々の値の場合に実行する式です。 conditionがtrueの場合、演算子はexpr1の値を返します。そうでない場合はexpr2の値を返します。


###ユーザーログインテストコード

```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
```

@user = users(:michael)はtest/fixtures/users.ymlで作った:michaelを@userに代入している。

また、

```
assert_redirected_to @user
follow_redirect!
```
１行目は、 @userにリダイレクトされますよね？（行き先は〜〜駅ですよね）
2行目は、　assert_redirected_to @userが通った上で@userにリダイレクトされる。

ちなみにこの状態でテストは通る。


###signup後はそのままログイン済みにしよう。

```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
```
log_in @userでセッション変数に@user.idを保存する。
ちなみに、log_in @userが使えるのはsessions_helperがapplication controllerにincludeされてるから。


###ログインのテスト

テストにもテストのhelperが存在する。
そこに、ログインしているかどうかを判断するメソッドを定義しておこう。

```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
```

先ほどymlにログインしたユーザー、test_helperにis_logged_in?メソッドを定義したので、テストは通る。

###ログアウト機能を実装

ログアウトメソッドもログインメソッドと同じようにsessions_helperに定義しておく。

```app/helpers/sessions_helper.rb
module SessionsHelper

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

ここで気をつけるのが、

sessionというのはハッシュな訳で、session[:user_id] = user.id

session = {user_id: user.id}という形になっている。

で、ハッシュの値を消すときは、

hash.delete(key)

というdeleteメソッドにキーを引数に渡すと実現できる。

で、今回sessionでも同じことが起きている。

session.delete(:user_id)

これにより、sessionというハッシュに格納されていた{user_id: user.id}が消える。

じゃあ、なんで@current_user = nilまでする必要があるの？

これは、なるべくサーバーの問い合わせ？的なものを無くして負荷を少なくするためらしい。


###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
```

ここで注目したいのが、destroyアクションはviewがないので、最後にどのviewに飛ぶかを指定してあげる必要があるということ。これはupdateアクションとかにも言えそうだよね。


###ログアウトのテスト

統合テストのストーリーを拡張しよう。
具体的には、ログインテストの中で、ログアウトのテストのアサーションも書いちゃおうというもの。

```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!
    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
```

assert_not is_logged_in?
is_logged_in?はtest_helperに定義したから使える。






###なぜsession変数はなくならない？なくなる？

調べろ。ブラウザとrails　sに保存されるから？