Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #10 リメンバーミー機能編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#9 永続セッション, cookie編](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)
次回：[#10.5 RSpecでTutorialのテストを書き直す](https://qiita.com/aokyo17/items/fcd22c7d9428c3ea8a3a)

# 今回の流れ
1. 今回のゴールを把握する
2. リメンバーミーのビューを作る
3. リメンバーミーのコントローラーを作る
4. テストを作る

※ この記事は、ポートフォリオを作る理由を**web系自社開発企業に転職するため**とします。
※ 2020年4月6日、記事を大幅に更新しました。

# 今回のゴールを把握する
ゴールは、自動ログインを選択する、リメンバーミー機能を作ることです。

まずは、リメンバーミーのためのチェックボックスをビューで作ります。
続いて、Sessionsコントローラーを編集します。
最後に、チェックボックスと永続セッションのテストを作ります。
その際、current_userのテストも作ります。

リメンバーミーの完成には、[#9](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)を進める必要があります。
まだの方は、[#9](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)と合わせてご覧ください。

以上です。

# リメンバーミーのビューを作る
リメンバーミーのためのチェックボックスをビューで作ります。
スコープをsession、チェックボックスをremember_meとしています。
よって、params[:session][:remember_me]の形で取得できます。

```erb:app/views/sessions/new.html.erb
<% provide(:title, "ログイン") %>
<div class="container form-container login-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">ログイン</h1>
      <%= form_with(scope: :session, url: login_path, local: true) do |form| %>

        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>

        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>
        
        <div class="form-group form-check">
          <%= form.check_box :remember_me, class: 'form-check-input' %>
          <%= form.label :remember_me, '次から保存（ログイン省略）', class: 'form-check-label' %>
        </div>

        <div class="form-group">
          <%= form.submit "ログイン", class: "btn btn-info btn-lg form-submit" %>
        </div>
      <% end %>
      
      <p class="form-go-to-signup-or-login">新しくはじめる方は<%= link_to "こちら", signup_path %></p>
    </div>
  </div>
</div>
```

```scss:/app/assets/stylesheets/application.scss.erb
// 中略
.form-submit {
  width: 100%;
  // margin-top: 1rem; // 削除
}
```

<img width="414" alt="lantern_lantern_checkbox.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/cc6e23cb-266c-5c1a-2917-affbf32392fa.png">

# リメンバーミーのコントローラーを作る
Sessionsコントローラーを編集します。
params[:session][:remember_me]に保存される値は、オン→'1'、オフ→'0'です。
参考演算子を使って、場合分けします。

```rb:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# 中略
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end
# 中略
```

# テストを作る
リメンバーミーのテストを作ります。
説明の都合上、紹介しなかった[#9](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)のテストも作ります。

ここでの手順は、以下の通りです。

* リメンバーミーと永続セッションのテストを作る
* current_userのテストを作る

## リメンバーミーと永続セッションのテストを作る
チェックボックスと永続セッションのテストを作ります。
テスト前に、spec内でログインできるローカルメソッドを用意しています。
テストの内容は、以下の通りです。

* 二度ログアウトできたとして、それでもエラーは起こらないか（[#9](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)参照）
* チェックボックスがオンの時、トークンが作られているか
* チェックボックスがオフの時、トークンが作られていないか
* チェックボックスがオンの時、ログアウト後のログインでトークンが残っていないか

```rb:spec/requests/users_logins_spec.rb
require 'rails_helper'

RSpec.describe "UsersLogins", type: :request do
  include SessionsHelper
  
  let(:user) { create(:user) }

  # ログインのメソッド
  def post_valid_information(remember_me = 0)
    post login_path, params: {
      session: {
        email: user.email,
        password: user.password,
        remember_me: remember_me
      }
    }
  end

  it "does not log out twice" do
    get login_path
    post_valid_information(0)
    expect(is_logged_in?).to be_truthy
    follow_redirect!
    expect(request.fullpath).to eq '/users/1'
    delete logout_path
    expect(is_logged_in?).to be_falsey
    follow_redirect!
    expect(request.fullpath).to eq '/'
    delete logout_path
    follow_redirect!
    expect(request.fullpath).to eq '/'
  end

  it "succeeds remember_token because of check remember_me" do
    get login_path
    post_valid_information(1)
    expect(is_logged_in?).to be_truthy
    expect(cookies[:remember_token]).not_to be_nil
  end
  
  it "has no remember_token because of check remember_me" do
    get login_path
    post_valid_information(0)
    expect(is_logged_in?).to be_truthy
    expect(cookies[:remember_token]).to be_nil
  end
  
  it "has no remember_token when users logged out and logged in" do
    get login_path
    post_valid_information(1)
    expect(is_logged_in?).to be_truthy
    expect(cookies[:remember_token]).not_to be_empty
    delete logout_path
    expect(is_logged_in?).to be_falsey
    expect(cookies[:remember_token]).to be_empty
  end
  # 中略
end
```

post_valid_informationメソッドは、引数でチェックボックスを指定します。

マッチャであるbe_nilとbe_emptyを使い分ける理由は、以下の通りです。

* be_nilを使う理由 → be_emptyはnilを判定しないから
* be_emptyを使う理由 → forgetメソッドでcookiesをdeleteする際の中身は、空ではあるが、nilではないから

## current_userのテストを作る
current_userのテストを作ります。
テストの内容は、以下の通りです。

* 永続セッションでユーザーを特定できているか
* 不適なトークンをremember_digestに代入した時、current_userはnilか

```rb:spec/helpers/sessions_helper_spec.rb
require 'rails_helper'

RSpec.describe SessionsHelper, type: :helper do
  
  let(:user) { create(:user) }
  
  describe "#current_user" do
    it "returns right user when session is nil" do
      remember(user)
      expect(current_user).to eq user
      expect(is_logged_in?).to be_truthy
    end
    
    it "returns nil when remember digest is wrong" do
      remember(user)
      user.update_attribute(:remember_digest, User.digest(User.new_token))
      expect(current_user).to be_nil
    end
  end
end
```

1つ目のテストの必要性について解説します。
理由は、以下のelsifがテストできていないためです。
わざと例外を発生させるraiseを挿入しても、テストが通ってしまいます。

```rb:app/helpers/sessions_helper.rb
# 中略
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id]) # この部分
      raise # 追加
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
# 中略
```

そのためのテストでした。
今回は以上です。

参考になりました↓
[Rails Tutorial 9.3.1 [Remember me] ボックスをテストする](https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-testing_the_remember_me_box)

---

前回：[#9 永続セッション, cookie編](https://qiita.com/aokyo17/items/c2e4cd1d607746753158)
次回：[#10.5 RSpecでTutorialのテストを書き直す](https://qiita.com/aokyo17/items/fcd22c7d9428c3ea8a3a)