Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #7 ログイン機能の下準備編

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

前回：[#6 ユーザー登録画面, エラー日本語化編](https://qiita.com/aokyo17/items/6ee827c240907514681b)
次回：[#8 ログイン/ログアウト, FactroyBot編](https://qiita.com/aokyo17/items/9d464e5ee8838557d4ad)

# 今回の流れ
1. 今回のゴールを把握する
2. ログイン画面のビューを作る
3. Sessionsコントローラーを作る
4. ログイン画面のテストを作る

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

# 今回のゴールを把握する
ゴールは、ログイン画面の外見だけを作ることです。
実際にログインさせる機能は、[#8](https://qiita.com/aokyo17/items/9d464e5ee8838557d4ad)で行います。

まずは、ログイン画面のビューを作ります。
続いて、ログインを管理するSessionsコントローラーを作ります。
最後に、ログイン画面のテストを作ります。

以上です。


# ログイン画面のビューを作る
ログイン画面のビューを作ります。
突然ですが、ジェネレーターでコントローラーを生成するとビューも生成されます。
後ほどコントローラーが必要なので、ジェネレーターではコントローラーを選択します。

```shell:shell
$ rails g controller Sessions new
```

ビュー作成前に、以下の2つについて解説します。

* form_for(:session, url: login_path)というRails Tutorialの記述について
* form_forからform_withの書き換えについて

## form_for(:session, url: login_path)について
form_for(:session, url: login_path)という、Rails Tutorialの記述について考えます。
まずは、Rails Tutorialによるこちらをご覧ください。
>
```rb:
form_for(@user)
```
Railsでは上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。
>
```rb:
form_for(:session, url: login_path)
```

つまり、以下のような違いが生まれます。

```
form_for(@user) → /usersにparams[:user]として情報を渡す
form_for(:session, url: login_path) → /loginにparams[:session]として情報を渡す
```

リソース名を'session'にすることで、params[:session]にキーと値が保存されます。

参考になりました↓
[rails ログイン機能のform_forの作りについての疑問](https://teratail.com/questions/28922)
[[Rails4.0] フォームの基本とStrongParametersを理解する](https://qiita.com/kidach1/items/7c1a9b61dd1853d5a7dc)

## form_forからform_withの書き換えについて
form_forは非推奨のため、form_withを使います。
その際はリソース名をscopeに指定する必要があります。
よって、以下のような記述になります。

```erb:
<%= form_with(scope: :session, url: login_path, local: true) do |form| %>
```

## 改めてログイン画面のビューを作る
以上2点を参考に、改めてビューを作ります。

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

ヘッダーも修正します。
'Login'ボタンに、リンク先を設定します。

```erb:app/views/layouts/_header.html.erb
<!-- 中略 -->
<div id="menu" class="collapse navbar-collapse">
  <ul class="navbar-nav ml-auto">
    <li class="nav-item">
      <%= link_to "Login", login_path, class: "btn btn-info btn-md" %>
    </li>
  </ul>
</div>
<!-- 中略 -->
```

参考になりました↓
[【Ruby】チュートリアルのform_forをform_withで書き換え (おまけ:capybaraでのテスト)](https://thinline196.hatenablog.com/entry/2018/11/18/222929)

# Sessionsコントローラーを作る
ログインを管理するSessionsコントローラーを作ります。
Sessionsコントローラーは、[Rails Tutorial 8.1 セッション](https://railstutorial.jp/chapters/basic_login?version=5.1#sec-sessions_and_failed_login)と同じです。

```rb: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] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end

  def destroy
    # 次回：ここに、ログアウトさせる記述を加える
  end
end
```

createアクションに関する、以下の2つについて解説します。

* userがインスタンス変数でないことについて
* どのようにif文が成立するかについて

1つ目は、今回createアクションに対するビューを作りません。
フォーム送信後、ログイン画面に戻すか、ユーザーページに遷移します。
よってビューがないため、ビューに引き渡すuser情報が不要です。
したがって、インスタンス変数である必要がありません。

2つ目は、nilとfalse以外のオブジェクトはtrueになる、という性質を利用しています。
userが存在する場合は、nilではないためtrueになります。
authenticateは、パスワードが正しい場合trueになります。
つまりif文は、userが存在しフォームからのパスワードが正しい時、という意味になります。

# ログイン画面のテストを作る
ログイン画面のテストを作ります。
[Tutorial 8.1.5 フラッシュのテスト](https://railstutorial.jp/chapters/basic_login?version=5.1#sec-a_flash_test)を参考にします。

```bash:bash
$ rails g rspec:request users_login
$ touch spec/systems/login_spec.rb
```

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

RSpec.describe "UsersLogins", type: :request do
  
  let(:user) { create(:user) }
  
  describe "GET /login" do
    it "has a danger flash message because of invalid login information" do
      get login_path
      post login_path, params: {
        session: {
          email: "",
          password: ""
        }
      }
      expect(flash[:danger]).to be_truthy
    end
    
    it "has no danger flash message because of valid login information" do
      get login_path
      post login_path, params: {
        session: {
          email: user.email,
          password: user.password
        }
      }
      expect(flash[:danger]).to be_falsey
    end
  end
end
```

```rb:spec/systems/login_spec.rb
require 'rails_helper'

RSpec.describe "Logins", type: :system do
  
  let(:user) { create(:user) }

  context "login with invalid information" do
    it "is invalid because it has no information" do
      visit login_path
      expect(page).to have_selector '.login-container'
      fill_in 'メールアドレス', with: ''
      fill_in 'パスワード', with: ''
      find(".form-submit").click
      expect(current_path).to eq login_path
      expect(page).to have_selector '.login-container'
      expect(page).to have_selector '.alert-danger'
    end
    
    it "deletes flash messages when users input invalid information then other links" do
      visit login_path
      expect(page).to have_selector '.login-container'
      fill_in 'メールアドレス', with: ''
      fill_in 'パスワード', with: ''
      find(".form-submit").click
      expect(current_path).to eq login_path
      expect(page).to have_selector '.login-container'
      expect(page).to have_selector '.alert-danger'
      visit root_path
      expect(page).not_to have_selector '.alert-danger'
    end
  end
    
  context "login with valid information" do
    it "is valid because it has valid information" do
      visit login_path
      fill_in 'メールアドレス', with: user.email
      fill_in 'パスワード', with: 'password'
      find(".form-submit").click
      expect(current_path).to eq user_path(1)
      expect(page).to have_selector '.show-container'
    end
  end
end
```

今回は以上です。

---

**<追記>**
ログイン成功時のフラッシュメッセージの有無の確認を行う場合、事前にユーザがActiveRecord内に存在する必要があります。
そのためにFactoryBotを使いますが、#7ではまだ導入していません。
（[#8](https://qiita.com/aokyo17/items/9d464e5ee8838557d4ad)で導入します。）

よってテストは失敗します。
\#7では以下のブロックをコメントアウトするとテストが通ります。

```rb:
# let(:user) { create(:user) }
# it "has no danger flash message because of valid login information" do
# context "login with valid information" do
```

参考になりました↓
[Ruby on Rails ガイド 5.2 Flash](https://railsguides.jp/action_controller_overview.html#flash)

---
前回：[#6 ユーザー登録画面, エラー日本語化編](https://qiita.com/aokyo17/items/6ee827c240907514681b)
次回：[#8 ログイン/ログアウト, FactroyBot編](https://qiita.com/aokyo17/items/9d464e5ee8838557d4ad)