この章でやること ユーザーの更新・表示・削除
- Usersリソースのうち未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる
- まず、ユーザーが自分のプロフィールを自分で更新できるようにする
- 次に、すべてのユーザーを一覧できるようにする。サンプルデータとページネーション(pagination)も学ぶ
- 最後に、ユーザーを削除し、データベースから完全に消去する機能を追加。ユーザーの削除はどのユーザーにも許可できるものではないので、管理ユーザーという特権クラスを作成し、このユーザーにのみ削除を許可するようにする
10.1 ユーザーを更新する
導入
ユーザー情報を編集するパターンは、新規ユーザーの作成と似ている
新規ユーザー用のビューを出力するnewアクションと同じようにして、ユーザーを編集するためのeditアクションを作成すればよい。
同様に、POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成する
最大の違いは、ユーザー登録は誰でも実行できるが、ユーザー情報を更新できるのはそのユーザー自身に限られる
beforeフィルターを使ってこのアクセス制御を実現していく
updating-usersトピックブランチを作成
$ git checkout -b updating-users
10.1.1 編集フォーム
Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装
editアクションはデータベースからユーザーデータを読み込む
ユーザー編集ページの正しいURLが/users/1/edit。
ユーザーのidはparams[:id]変数で取り出すことができるため@user変数を定義
class UsersController < ApplicationController
def edit
@user = User.find(params[:id])
end
end
対応するユーザーのeditビューを作成
$ touch app/views/users/edit.html.erb
```app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
@user変数を使ったため、自動で既に編集フォームに名前やメールが入った状態になる
検証ツールでHTMLをみると
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" />
.
.
.
</form>
次の入力フィールドに隠し属性がある
<input name="_method" type="hidden" value="patch" />
WebブラウザはネイティブではPATCHリクエストを送信できない。
RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを偽造している。(hiddenを使っている)
railsのform_with(@user)のコードは新規作成newとeditで文章が変わらないが、Railsはどうやって新規ユーザー用のPOSTリクエストとユーザー編集用のPATCHリクエストを区別するのか?
その答えは、Railsは新規ユーザーか、既存のDBにいるユーザーか、
Active Recordのnew_record?論理値メソッドを使って区別できるから。
Rails Consoleで確認してみる。
$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
※もう少し調べてみた
調査
new_record?理論値メソッド
new_record?はDBにレコードが登録されて「いなければ」true 登録されていればfalse
→レコードが登録されていなければPOSTで送り、レコードが登録されていればPATCHを使う
Railsは、form_with(@user)を使ってフォームを構成すると、@user.new_record?がtrueの時にはPOSTリクエスト、falseのときにはPATCHリクエストを使う。
仕上げに、ナビゲーションバーにあるユーザー設定へのリンクを更新する。
Usersリソースの名前付きルートである、edit_user_pathと、current_userというヘルパーメソッドを使うと、実装が簡単。
<%= link_to "Settings", edit_user_path(current_user) %>
<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", edit_user_path(current_user) %></li> #passを定義
<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>
調査 target_blankの調査 なぜ脆弱性があるのか
https://webegins.com/target-blank/
復習 provide メソッド
viewでprovideヘルパーを利用することで、ここのテンプレートからレイアウト側にタイトルを引き渡すことができる。
view/home.html.erb
<% provide :title, 'タイトルの例' %>
レイアウト側からは、yield メソッドを利用することで呼び出せる。
layouts/application.html.erb
<title><%= yield(:title) || 'Rails入門' %></title>
演習
先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
rel="noopener"
を追加するだけ
リスト 10.5のパーシャルを使って、new.html.erbビュー(リスト 10.6)とedit.html.erbビュー(リスト 10.7)をリファクタリングしてみましょう(コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます
→コピペするだけでOK
10.1.2 編集の失敗
createアクションと同じような構造で、updateアクションを作成する
editビューのフォームから送信されたparamsハッシュを受け取り、ユーザーを更新する。
無効な情報の場合は編集ページを描写する
class UsersController < ApplicationController
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
def update #createと構造同じ
@user = User.find(params[:id])
if @user.update(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
end
updateへの呼び出しでuser_params
を使っている
以前利用したStrong Parametersを使ってマスアサインメントの脆弱性を防止している(user_paramsはprivete以降に記述あり)
エラーに関しても、Userモデルのバリデーションとエラーメッセージのパーシャルが既にあるため、自動でエラーメッセージを表示してくれる
演習
編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
→確認
10.1.3 編集失敗時のテスト
テストのガイドラインに従って、エラーを検知するための統合テストを書く
まずは統合テストを生成
$ rails generate integration_test users_edit
最初は編集失敗時の簡単なテストを追加していく
- まず編集ページにアクセスし、editビューが描画されるかどうかをチェック
- 無効な情報を送信してみて、editビューが再描画されるかどうかをチェック
ここで、PATCHリクエストを送るためにpatchメソッドを使っていることに注意
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user) #@userの編集ページを取得
assert_template 'users/edit' #editページが表示されているか
patch user_path(@user), params: { user: { name: "", #おかしいparamsをpatchへ送信
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit' #editビューが描写されているか
end
end
これで、テストを実行すると green
演習
リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。
test "unsuccessful edit" do
get edit_user_path(@user) #@userの編集ページを取得
assert_template 'users/edit' #editページが表示されているか
patch user_path(@user), params: { user: { name: "", #おかしいparamsをpatchへ送信
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit' #editビューが描写されているか
assert_select "div.alert","The form contains 4 errors."
end
10.1.4 TDDで編集を成功させる
ユーザーの編集フォームが動作するようにする。
プロフィール画像の編集は、Gravatarで画像のアップロードも既に動作するようになっている
快適にテストをするためには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利
そのテストを「受け入れテスト(Acceptance Tests)」として呼ばれていて、ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決めるテストとして知られている
同じ手法を使って(テスト駆動開発)ユーザーの編集機能を実装していく
テストの内容は上のテストを参考に書いていく
- ユーザー情報を更新する正しい振る舞いをテストで定義(今回は有効な情報を送信するように修正)。
- flashメッセージが空でないかどうかと、プロフィールページにリダイレクトされるかどうかをチェック
- データベース内のユーザー情報が正しく変更されたかどうかも検証
以下のテストコードになる
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,#有効な情報
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty? #flashが空でないか
assert_redirected_to @user #userビューページへ飛んでいるか
@user.reload #データテーブルを再度読み込む
assert_equal name, @user.name #設定したnameとDBのnameが一致しているか
assert_equal email, @user.email
end
テストがパスするために、updateアクションも変更
class UsersController < ApplicationController
.
def update
@user = User.find(params[:id])
if @user.update(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
end
1)テストコードのパスワードとパスワード確認が空であることに注目。
ユーザー名やメールアドレスを編集するときに毎回パスワードを入力するのは不便なので、(パスワードを変更する必要が無いときは)パスワードを入力せずに更新できると便利になる
2)また、@user.reloadを使って、データベースから最新のユーザー情報を読み込み直して、正しく更新されたかどうかを確認している点にも注目。(受け入れテストでは先にテストを書くので、効果的なユーザー体験について考えるようになる)
このテストはまだ red のまま。
なぜならパスワードの長さに対するバリデーションがあるので、パスワードやパスワード確認の欄を空にしているため引っかかってしまうから。
テストがパスするためにパスワードのバリデーションに対して、空だったときの例外処理を加える必要がある
allow_nil: trueオプションを使ってvalidatesに追加する
→パスワードが空のままでも更新できるようになる
class User < ApplicationRecord
attr_accessor :remember_token
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 }, allow_nil: true
.
.
.
end
パスワードが空のままでも更新できるようになってしまうと、「新規ユーザー登録時に空のパスワードが有効になってしまうのか」と心配になるが、
has_secure_passwordでは(追加したバリデーションとは別に)オブジェクト生成時に存在性を検証するようになっているため、空のパスワード(nil)が新規ユーザー登録時に有効になることはない。
問題であった空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグもこれで解決する
テストはパスするはず
演習
実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
もしGravatarと紐付いていない適当なメールアドレス(foobar@example.comなど)に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。
→OK Gravatarのデフォルトの画像が表示される
10.2 認可
ウェブアプリケーションでは、認証(authentication)はサイトのユーザーを識別することであり、認可(authorization)はそのユーザーが実行可能な操作を管理すること
今時点、どのユーザーでもあらゆるアクションにアクセスできるため、誰でも(ログインしていないユーザーでも)ユーザー情報を編集できてしまう。
ここでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御していく
(こういったセキュリティ上の制御機構をセキュリティモデルと呼ぶ)
10.2.1 ユーザーにログインを要求する
ログインしてないユーザがURLを直接触って編集できないようにするには、Usersコントローラの中でbeforeフィルターを使う。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?#logged_inがfalseなら(logged_inはsessions_helperで定義ずみ)
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるため、ここでは:onlyオプション(ハッシュ)を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかける
これで直接URLを触ってeditやupdateはできなくなったが、テストが古いのでテストは失敗する
原因は、ログインしていないユーザーのままのテストだから
log_in_asヘルパーを使ってテストでもログインする。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user) #@userとしてログイン
get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user) #@userとしてログイン
get edit_user_path(@user)
.
.
.
end
end
これでテストスイートがパスする
しかし、beforeフィルターの実装はまだ終わってない
なぜならbeforeフィルターをコメントアウトしてもテストが通ってしまうから
beforeフィルターは基本的にアクションごとに適用していくので、
Usersコントローラのテストもアクションごとに書いていく。
具体的には
①正しい種類のHTTPリクエストを使う
②editアクションとupdateアクションをそれぞれ実行させてみる
③flashにメッセージが代入されるかどうか検証
④ログイン画面にリダイレクトされたかどうか
HTTPリクエストは、 editにはget、updateにはpatchを書く
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "should redirect edit when not logged in" do
get edit_user_path(@user)
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
assert_not flash.empty?
assert_redirected_to login_url
end
end
コメントアウトしていた箇所を元に戻すと、テストがパスするはず
演習
デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。
→エラーになる
10.2.2 正しいユーザーを要求する
次はユーザーが自分の情報だけを編集できるようにする。
まずはユーザーの情報が互いに編集できないことを確認するために、サンプルユーザーをもう一人追加
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
log_in_asメソッドを使って、editアクションとupdateアクションをテストする。
このとき、既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクトしている
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user) #違うユーザーとしてログイン
get edit_user_path(@user)#@user_pathを取得
assert flash.empty?#flashが空か確認
assert_redirected_to root_url#Homeビューへ帰るか確認
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)#違うユーザーでログイン
patch user_path(@user), params: { user: { name: @user.name, #@userのparamsをcreateへ
email: @user.email } }
assert flash.empty?#フラッシュが空か
assert_redirected_to root_url
end
end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする
beforeフィルターのcorrect_userで@user変数を定義しているため、editとupdateの各アクションから、@userへの代入文を削除している。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
def edit
end
def update
if @user.update(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless @user == current_user#後にリファクタリング
end
end
今度はテストスイートがgreen になる
最後に、一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装。
correct_userというbefore filterの中で使えるようにしたいので、Sessionsヘルパーの中にcorrect_userメソッドを追加します。このメソッドを使うと今までの
unless @user == current_user
といった部分が、次のようになる
unless current_user?(@user)
module SessionsHelper
# 渡されたユーザーがカレントユーザーであればtrueを返す
def current_user?(user)
user && user == current_user
end
リファクタリングしたので、users_controller.rbを書き換える
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
演習
何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
→知らん人が勝手に他のユーザーを変えたら困るから
上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
→edit DB関係なくテストできる
10.2.3 フレンドリーフォワーディング
例えばユーザーの編集ページに行こうとして、ログインせよと弾かれた場合、ログイン後はその編集ページに飛ばしてあげるのが親切
そういう機能を追加していく
フレンドリーフォワーディングのテストは非常にシンプルで、ログインした後に編集ページへアクセスする、という順序を逆にしてあげるだけ
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
test "successful edit with friendly forwarding" do
get edit_user_path(@user)#@userの編集ページを取得
log_in_as(@user)#ログインしてなかったからできないので@userでログイン
assert_redirected_to edit_user_url(@user)#editページへリダイレクトしたか
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,#編集
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?#flashが空でないか
assert_redirected_to @user#ユーザーのshowページへリダイレクトしたか
@user.reload#userのDBを再取得
assert_equal name, @user.name#編集した名前とDBの名前が一致しているか
assert_equal email, @user.email
end
end
フレンドリーフォワーディングを実装し、ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる。
この動作をstore_locationとredirect_back_orの2つのメソッドを使って実現していく
module SessionsHelper
.
.
.
# 記憶したURL(もしくはデフォルト値)にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
#リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合はデフォルトのURLにリダイレクト
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
#リクエストが送られたURLをsession変数の:forwarding_urlキーに格納
#ただし、GETリクエストが送られたときだけ
end
end
store_locationメソッドでは、 リクエストが送られたURLをsession変数の:forwarding_urlキーに格納。ただし、GETリクエストが送られたときだけ。
これによって、例えばログインしていないユーザーがフォームを使って送信した場合、転送先のURLを保存させないようにできる
滅多に起きないが起こり得る話で、例えばユーザがセッション用のcookieを手動で削除してフォームから送信するケースなどで、POSTや PATCH、DELETEリクエストを期待しているURLに対して、(リダイレクトを通して)GETリクエストが送られてしまい、場合によってはエラーが発生する。このため、if request.get?という条件文を使ってこのケースに対応
定義したstore_locationメソッドをbeforeフィルター(logged_in_user)をに加える
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
フォワーディング自体を実装するには、redirect_back_orメソッドを使い、
リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトさせる
デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクト
redirect_back_orメソッドでは、次のようにor演算子||を使う
session[:forwarding_url] || default
このコードは、値がnilでなければsession[:forwarding_url]を評価し、そうでなければデフォルトのURLを使う
またsession.delete(:forwarding_url)という行を通して転送用のURLを削除している点にも注意。
削除しないと次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまう
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_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
これでテストもパスし、認可も終わり
演習
フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。
test "successful edit with friendly forwarding" do
get edit_user_path(@user) # @userのユーザー編集ページを取得
assert_equal session[:forwarding_url], edit_user_url(@user) # 渡されたURLに転送されているか確認
log_in_as(@user) # @userでログイン
assert_nil session[:forwarding_url] # forwarding_urlの値がnilならtrue(deleteが効いてる)
name = "Foo Bar" # フォーム欄に値を入力する
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name, # 引数としてわざと失敗する値を持ったuserIDをpatchリクエストで送信(更新)する
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty? # エラー文が空じゃなければtrue
assert_redirected_to @user # michaelのユーザーidページへ移動できたらtrue
@user.reload
assert_equal name, @user.name # DB内の名前と@userの名前が一致していていたらtrue
assert_equal email, @user.email # DB内のEmailと@userの名前が一致
end
7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。
→bootstrapがうまく動かなかったので割愛
10.3 すべてのユーザーを表示する
ユーザーの一覧ページ(indexページ)を作る
ページネーションも学ぶ
10.3.1 ユーザーの一覧ページ
まずはセキュリティモデルから考える
ユーザーのshowページは、サイトを訪れたすべてのユーザーから見えるようにしておくが、
ユーザーのindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限していく
indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテストを書いていく(テスト駆動開発)
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
test "should get new" do
get signup_path
assert_response :success
end
test "should redirect index when not logged in" do
get users_path #indexページへのパスを取得
assert_redirected_to login_url#ログインページまで戻るか
end
.
.
.
end
次に、beforeフィルターのlogged_in_userにindexアクションを追加して、このアクションを保護
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]#indexを追加
before_action :correct_user, only: [:edit, :update]
def index
end
def show
@user = User.find(params[:id])
end
.
.
.
end
今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装
User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入させる(データの読み込みの問題はあるが後で直す)
def index
@users = User.all#全てのuserを取得
end
eachメソッドを使って,ユーザーごとにliタグで囲むビューを作成する
それぞれの行をリストタグulで囲いながら、各ユーザーのGravatarと名前を表示
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
module UsersHelper
# 渡されたユーザーのGravatar画像を返す
def gravatar_for(user, options = { size: 80 })
size = options[:size]
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
CSSも修正
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
ヘッダーにユーザー一覧表示用のリンクを追加(users_path)
<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", users_path %></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", edit_user_path(current_user) %></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>
これでテストgreenに
演習
レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。
test "layout links login" do
log_in_as(@user)
get root_path
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
assert_select "a[href=?]", signup_path
assert_select "a[href=?]", users_path
assert_select "a[href=?]", user_path(@user)
assert_select "a[href=?]", edit_user_path(@user)
assert_select "a[href=?]", logout_path
end
10.3.2 サンプルのユーザー
Rubyを使ってユーザーを一気に増やす
まず、GemfileにFaker gemを追加
これは、実際にいそうなユーザー名を作成するgem
faker gemは開発環境以外では普通使わないが、今回は例外的に本番環境でも適用させる予定
source 'https://rubygems.org'
gem 'rails', '6.0.3'
gem 'bcrypt', '3.1.13'
gem 'faker', '2.1.2'
gem 'will_paginate', '3.1.8'
gem 'bootstrap-will_paginate', '1.0.0'
$ bundle install
サンプルユーザーを生成するRubyスクリプト(Railsタスクとも呼ぶ)を追加
# メインのサンプルユーザーを1人作成する
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
# 追加のユーザーをまとめて生成する
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
これでExample Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成できる
データベースをリセットして、Railsタスクを実行(db:seed)
$ rails db:migrate:reset
$ rails db:seed
システムによっては数分かかる
(rails serverは止めたほうがいい)
終わるとユーザーが100人になってる
演習
試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。
→確認
10.3.3 ページネーション
今100このアカウントが1つのページに表示されてしまっているので、ページネーション(pagination)を実装して1つのページに30人だけユーザーを表示する
Railsには豊富なページネーションメソッドがあるが、最もシンプルかつ堅牢なwill_paginateメソッドを使っていく
まずはGemfileにwill_paginate gem
とbootstrap-will_paginate gem
を両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する
source 'https://rubygems.org'
gem 'rails', '6.0.3'
gem 'bcrypt', '3.1.13'
gem 'faker', '2.1.2'
gem 'will_paginate', '3.1.8'
gem 'bootstrap-will_paginate', '1.0.0'
.
$ bundle install
実行したら、新しいgemが正しく読み込まれるように、Webサーバーを再起動
indexビューにページネーションのコードを追加する
またindexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もある
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
will_paginateメソッドは,usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成してくれる。ただし、このままでは動かず、
will_paginateではpaginateメソッドを使った結果が必要のため
必要なpagineteメソッドの動きはこんな感じ
$ rails console
>> User.paginate(page: 1)
User Load (1.5ms) SELECT "users".* FROM "users" LIMIT 11 OFFSET 0
(1.7ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
>> User.paginate(page: 1).length
User Load (3.0ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 30],
["OFFSET", 0]]
=> 30
```
paginateでは、キーが:pageで値がページ番号のハッシュを引数に取る
User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ(デフォルトでは30)を取り出す
つまり1ページ目は1から30のユーザー、2ページ目は31から60のユーザーといった具合にデータが取り出される。
pageがnilの場合、 paginateは単に最初のページを返す
indexアクション内のallをpaginateメソッドに置き換えると使えるようになる
```app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
def index
@users = User.paginate(page: params[:page])
end
```
環境によってはRailsを再起動する必要があるが、これでページネーションが動くようになった
### 演習
Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。
先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。
→確認ずみ
```
>> user = User.all
>> page = User.paginate(page: 1)
>> user.class
=> User::ActiveRecord_Relation
>> page.class
=> User::ActiveRecord_Relation
```
### 10.3.4 ユーザー一覧のテスト
ページネーションに対する簡単なテストも書いておく
1 ログイン
2indexページにアクセス
3最初のページにユーザーがいることを確認
4ページネーションのリンクがあることを確認
といった順でテスト
3,4のステップでは、テスト用のデータベースに31人以上のユーザーがいる必要がある
そのためfixtureファイルに30人のユーザーを追加する(rubyもサポートされている)
```test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
```
indexページ用の統合テストを生成
```
$ rails generate integration_test users_index
```
テストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認
```test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
```
テストはパスし、ページネーションのテストは完了
### 演習
試しにリスト 10.45にあるページネーションのリンク(will_paginateの部分)を2つともコメントアウトしてみて、リスト 10.48のテストが red に変わるかどうか確かめてみましょう。
→確認
先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
→` assert_select 'div.pagination', count: 2`でpaginationクラスのdivが2つ存在することを確認する
### 10.3.5 パーシャルのリファクタリング
リファクタリングしていく
まずは、呼び出し側の`render user`をセット
```app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
```
renderをパーシャル(ファイル名の文字列)に対してではなく、Userクラスのuser変数に対して実行している
この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する
```app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
```
さらに改良
renderを@users変数に対して直接実行する
```app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">#@userにし、eachを削除した
<%= render @users %>
</ul>
<%= will_paginate %>
```
Railsは@users をUserオブジェクトのリストであると推測し、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力するようになる
### 演習
リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が red に変わることを確認してみましょう。
→確認
## 10.4 ユーザーを削除する
ユーザーの一覧ページはOK
残るはdestroyを実装
ユーザーを削除するためのリンクを追加しdestroyアクションも実装
その前に、削除を実行できる権限を持つ管理(admin)ユーザーのクラスを作成する
承認(authorization)においては、このような特権のセットをroleと呼ぶ
### 10.4.1 管理ユーザー
論理値をとるadmin属性をUserモデルに追加し、管理ユーザーを識別する
Userモデルに追加することで、自動的にadmin?メソッド(論理値を返す)も使えるようになる
```
$ rails generate migration add_admin_to_users admin:boolean
```
default: falseをマイグレーションファイルへ渡す
渡さなくてもnilになり、問題ないが、明示的にfalseにしておくことでrailsと開発者にわかりやすくなる
```db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :admin, :boolean, default: false
end
end
```
```
$ rails db:migrate
```
Railsコンソールで動作を確認
```
$ rails console --sandbox
>> user = User.first
>> user.admin? #adminメソッドも使えるように
=> false
>> user.toggle!(:admin)#toggle!メソッドはfalseからtrueに反転させる
=> true
>> user.admin?
=> true
```
最初のユーザーだけをデフォルトで管理者にする
```db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)#true
# 追加のユーザーをまとめて生成する
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
```
データベースをリセット
```
$ rails db:migrate:reset
$ rails db:seed
```
Strong Parametersの復習
最初のユーザーに初期化ハッシュにadmin: trueを設定することでユーザーを管理者にしている
ここでは、荒れ狂うWeb世界にオブジェクトを晒すことの危険性を改めて強調している。
もし、任意のWebリクエストの初期化ハッシュをオブジェクトに渡せるとなると、攻撃者は次のようなPATCHリクエストを送信してくるかもしれない。
patch /users/17?admin=1
このリクエストは、17番目のユーザーを管理者に変えてしまう。
ユーザーのこの行為は少なくとも重大なセキュリティ違反となる可能性があるし、それだけでは済まされない。
このような危険があるからこそ、編集してもよい安全な属性だけを更新することが重要になる。
これを、Strong Parametersを使って対策
次のように、paramsハッシュに対してrequireとpermitを呼び出す。
```
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
```
上のコードでは、許可された属性リストにadminが含まれていないことに注目。
これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できる。
この問題は重大であるため、編集可能になってはならない属性に対するテストを作成してみる。
### 演習
Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します
```test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
test "should not allow the admin attribute to be edited via the web" do
log_in_as(@other_user) #違うユーザーてログイン
assert_not @other_user.admin? #違うユーザーにadmin属性がないことを期待
patch user_path(@other_user), params: {
user: { password: "password",
password_confirmation: "password",
admin: true } }
assert_not @other_user.reload.admin?
end
```
### 10.4.2 destroyアクション
destroyアクションへのリンクを追加
ユーザーindexページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限していく
これによって、現在のユーザーが管理者のときに限り [delete] リンクが表示される
```app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</li>
```
`method: :delete`に注意
また、各リンクをif文で囲い、管理者にだけ削除リンクが表示されるようにしている
ブラウザはネイティブではDELETEリクエストを送信できないので、RailsではJavaScriptを使って偽造
つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になる
削除リンクが動作するためには、destroyアクションを追加する
destroyアクションは
1該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除
2ユーザーindexに移動
```app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]#loginしないとできないように
before_action :correct_user, only: [:edit, :update]
.
.
.
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
```
まだ終わっておらず、このままではコマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができてしまう
なのでdestroyアクションへのアクセスに制限をかける
beforeフィルターを使う
```app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
.
.
.
private
.
.
.
# 管理者かどうか確認
def admin_user
redirect_to(root_url) unless current_user.admin?#adminがfalseならrootに返す
end
end
```
### 演習
管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?
### 10.4.3 ユーザー削除のテスト
ユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にしてテストを書いていく
```test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
```
Usersコントローラをテストするために、アクション単位でアクセス制御をテストする
ログアウトのテストと同様に、削除をテストするために、DELETEリクエストを発行してdestroyアクションを直接動作させる
このとき2つのケースをチェック
1つは、ログインしていないユーザーであれば、ログイン画面にリダイレクトされること
1つは、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされること
```test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do #User数が変わらないことを確認
delete user_path(@user) #ログインしてない状態で@userを削除
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as(@other_user)
assert_no_difference 'User.count' do #User数が変わらないことを確認
delete user_path(@user) #adminがない状態で削除
end
assert_redirected_to root_url
end
end
```
`assert_no_differenceメソッド`でユーザー数が変化しないことを確認
管理者ではないユーザーの振る舞いについて検証するが、管理者ユーザーの振る舞いと一緒に確認できるといい。
そこで、管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、今回のテストを追加していくことにする。
これにより、後ほど追加する管理者の振る舞いについても簡単にテストが書けそう。
今回のテストで唯一の手の込んだ箇所は、管理者が削除リンクをクリックしたときに、
ユーザーが削除されたことを確認する部分。
```
assert_difference 'User.count', -1 do
delete user_path(@other_user)
end
```
ユーザーが削除されたことを確認
DELETEリクエストを適切なURLに向けて発行し、User.countを使ってユーザー数が 1減ったかどうかを確認
まとめるとこうなる
```test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@admin = users(:michael)
@non_admin = users(:archer)
end
test "index as admin including pagination and delete links" do
log_in_as(@admin) #adminがある状態でログイン
get users_path #indexを取得
assert_template 'users/index' #indexページが表示されるか
assert_select 'div.pagination' #pagenationがあるか
first_page_of_users = User.paginate(page: 1)#1ページ目のユーザを代入
first_page_of_users.each do |user| #それぞれにリンクなどがあるか確認
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin #admin以外のuserにdeleteがあるか確認
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
end
assert_difference 'User.count', -1 do #admin以外のユーザを削除したらUserの数が1減るか
delete user_path(@non_admin)
end
end
test "index as non-admin" do
log_in_as(@non_admin) #non_adminとしてログイン
get users_path #indexページ取得
assert_select 'a', text: 'delete', count: 0 #deleteが1つもないか確認
end
end
```
各ユーザーの削除リンクをテストするときに、ユーザーが管理者であればスキップしている点にも注目
これで、削除に関するコードに対して、よくテストできている状態になった
### 演習
試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が red に変わることを確認してみましょう。
→確認
## 10.5 最後に
次の章に進む前に、すべての変更をmasterブランチにマージ
```
$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push
アプリケーションを本番展開したり、サンプルデータを本番データとして作成することもできます(本番データベースをリセットするにはpg:resetタスクを使います)。
$ rails test
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
```
※注意 ローカル環境でやっていると
git push後のtestで
paninateメソッドが定義されてないとかいうエラーが発生しました
ググっても
https://qiita.com/LotK/items/f49a1df5c9d9a510baa2
こういったエラーではなく、おかしかったのですが、
しばらく放置したら直りました。良くわかりません。
今までテストがパスしてたのに、いきなりエラーになった場合はしばらく放置するのも手かと