個人的リマインド用
参考
Ruby on Rails チュートリアル プロダクト開発の0→1を学ぼう
基本的なログイン機構
ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄する認証システムのこと。
また、ログインしている人だけがアクセスできるページや使える機能などがあり、このような制限や制御の仕組みを認可モデルという。
セッション
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。
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
また、それに対応してテストの更新。
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を指定する必用がある。
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フォームは
<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]になることが推測できる。
ユーザーの検索と認証
ログインでセッションを作成する場合に最初に行うのは、入力が無効な場合の処理。
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メソッドを使って、ユーザーのログイン部分を実装したものがこちら
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になる。
フラッシュメッセージを表示する
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.移動先のページでフラッシュメッセージが表示されていないことを確認する
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を使う。これを使えばメッセージはその後のリクエストが発生した時に消滅する。
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コントローラ」に自動生成されたセッション用ヘルパーを読み込ませ、どのコントローラからでもログイン関連のメソッドを呼び出せるようにする。
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ヘルパーにメソッドを定義する。
app/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
セッションはセッションハイジャックに対して脆弱。セッションハイジャックとは、攻撃者があるユーザーのセッションidのコピーを手に入れ、そのユーザーとしてログインする攻撃方法。
またセッション固定という攻撃も危険で、これは攻撃者が既に持っているセッションidをユーザーに使わせるように仕向けることで、攻撃者がユーザーとセッションを共有するというもの。対策としては、ユーザーがログインする直前にセッションを必ず即座にリセットすること。reset_sessionメソッドで対策可能。
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を返す。
def current_user
if session[:user_id]
User.find_by(id: session[:user_id])
end
end
nilを返してほしい理由としては、user_currentメソッドが1リクエスト内の処理で何度も呼び出されると、呼び出された回数と同じだけデータベースへの問い合わせが発生してしまうから。
またRubyの慣習にしたがい、find_byの実行結果をインスタンス変数に保存する工夫もしている。これでデータベースへの問い合わせが最初の一回で済み、以降の呼び出しではインスタンス変数の結果を再利用する。これをメモ化という。
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メソッドを実行し、無駄なデータベースへの読み出しが行われなくなる。
最終結果として、
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ではないということ。
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
この時点でいろいろレイアウトが書き換えられるように
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クラスの振る舞いを定義して、メニューが表示されるようにする
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コードを書く
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に認識させる
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にインポートする
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クラス自身に配置し、クラスメソッドにすることにする。
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というシンボルはユーザーを参照するためのキー。
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も記述。
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メソッドはテストでも使えるので、これを代わりに使う。
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
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メソッドで、すべてのセッション変数を確実にリセットする方がいい。
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
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リクエスト後のリダイレクトが正しく振る舞うようにする必用がある。
ログアウト機能のテストのため若干追加。
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。長いので後々分割予定