静的型付け言語原理主義者によるRailsチュートリアル(第10章)
第10章 ユーザーの更新・表示・削除
- User リソース用の REST アクションのうち、未実装だった
edit
update
index
destroy
アクションを加え、REST を完成させる
10.1 ユーザーを更新する
- ユーザー情報を編集するには、新規ユーザー用のビューを出力する
new
アクションと同様に、ユーザーを編集するためのアクションを作成する - 同様に、POST リクエストに応答する
create
の代わりに、PATCH リクエストに応答するupdate
アクションを作成する
10.1.1 編集フォーム
- Users コントローラに
edit
アクションを追加し、それに対応する edit ビューを実装する - ユーザーの id は
params[:id]
変数で取り出すことが可能
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
def edit
@user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
- ユーザーの edit ビューを作成
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) 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="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
- Rails によって
@user
変数の属性情報が引き出され、名前やメールアドレスのフィールドに値が自動入力される - Web ブラウザではネイティブで PATCH リクエストを送信できないので、Rails では POST リクエストと隠し
input
フィールドを利用して PATCH リクエストを偽造している- めちゃ微妙。。。
- Rails は
form_for(@user)
を使用してフォームを構成すると、@user.new_record?
がtrue
のときには POST を、false
のときには PATCH を使う- これまた微妙。。。
10.1.2 編集の失敗
- ユーザー情報の編集に失敗した場合
- ユーザーの
update
アクションの初期実装
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
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
update_attributes
への呼び出しでuser_params
を使っている- Strong Parameters を使ってマスアサインメントの脆弱性を防止している
10.1.3 編集失敗時のテスト
- いつものように統合テストを書いていく
- まず編集ページにアクセスし、edit ビューが描画されるかをチェック
- その後無効な情報を送信し、edit ビューが再描画されるかをチェック
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit'
end
end
10.1.4 TDD で編集を成功させる
- 編集フォームが動作するようにするために統合テストも TDD で実装していく
- ユーザー情報を更新する正しい振る舞い
- flash メッセージが空でないか
- プロフィールページにリダイレクトされるか
- DB 内のユーザー情報が正しく変更されたか
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
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?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
- 上記テストをパスするためにユーザーの
update
アクションを追加する
class UsersController < ApplicationController
.
.
.
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
end
- この段階ではまだテストは通らない
- パスワードのバリデーションに対して、空だった場合の例外処理を加える必要がある
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: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
.
.
.
end
- 上記修正により新規ユーザー登録時にからのパスワードが有効になってしまうのではという危惧
has_secure_password
でオブジェクト生成時に存在性を検証するようになっているため、空のパスワードが新規ユーザー登録時に有効になることはない
10.2 認可
- 認証(authentication)
- サイトのユーザーを識別すること
- 認可(ahtuorization)
- そのユーザーが実行可能な操作を管理すること
- 本章の edit アクションと update アクションにおけるセキュリティホール
- どのユーザーでもあらゆるアクションにアクセスできるため、ログインしていないユーザーでもユーザー情報を編集できてしまう
- ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する
- どのユーザーでもあらゆるアクションにアクセスできるため、ログインしていないユーザーでもユーザー情報を編集できてしまう
- ログインしていないユーザーが保護されたページにアクセスしようとしたとき
- ログインページに転送する
- よくあるやつ
- ログインページに転送する
10.2.1 ユーザーにログインを要求する
- Users コントローラの中で before フィルターを使用する
before_action
メソッドで何らかの処理が実行される直前に特定のメソッドを実行する- before フィルターに
logged_in_user
を追加する
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
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
end
- edit アクションや update アクションでログインを要求するようになっため、テストが失敗する
- 上記アクションをテストする前にログインしておく必要がある
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
end
- テストは通るようになったが、セキュリティモデルに関する実装を覗いてもテストが通ってしまう
- before フィルターをコメントアウトしてセキュリティホールが作られたときにはテストで検出できるようになっているべき
- before フィルターは基本的にアクションごとに適用していくので、Users コントローラのテストもアクションごとに書いていく
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
10.2.2 正しいユーザーを要求する
- ログインを要求するだけでは不十分で、ユーザーが自分の情報だけを編集できるようにする必要がある
- 例によって TDD で
- まずはユーザーの情報が互いに編集できないことを確認するために、サンプルユーザーをもう一人追加する
- 次に
log_in_as
メソッドを使って、edit
アクションとupdate
アクションをテストする
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)
assert flash.empty?
assert_redirected_to root_url
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,
email: @user.email } }
assert flash.empty?
assert_redirected_to root_url
end
end
- 別のユーザーのプロフィールを編集しようとした場合はリダイレクトさせたいので、
current_user
というメソッドを作成し、 before フィルターからこのメソッドを呼び出せるようにする
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_attributes(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
- 一般的な慣習に倣って
current_user?
という論理値を返すメソッドを Session ヘルパーに追加し、しれを呼ぶようにリファクタリングを行う- だから慣習って何よ。。。
10.2.3 フレンドリーフォワーディング
- 保護されたページにアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまう
- リダイレクト先はユーザーが開こうとしていたページにしてあげるのが親切
- フレンドリーフォワーディングのテストはシンプルに書くことができる
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_url(@user)
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
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)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
store_location
メソッドで before フィルター(logged_in_user
)を修正してみる
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
def edit
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
- フォワーディング自体を実装するには、
redirect_back_or
メソッドを使用する- リクエストされた URL が存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトの URL にリダイレクトする
- デフォルトの URL は、Session コントローラの
create
アクションに追加し、サインイン成功後にリダイレクトする
session.delete(:forwarding_url)
という行を通して転送用の URL を削除している- 次回ログイン時に保護されたページに転送されるのを防ぐため
- 転送用の URL を削除する動作は
redirect
文の後に置かれていても実行される- 明示的に
return
文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しない- 思わぬバグを踏みそうな仕様だ。。。
- 明示的に
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
- 統合テストがパスし、基本ユーザー認証機能とページ保護機能の実装が完了
- 対して実装していないがこの機能が実現できる凄さよ
10.3 すべてのユーザーを表示する
- すべてのユーザーを一覧表示する
index
アクションを追加する
10.3.1 ユーザーの一覧ページ
- ユーザーの
show
ページについてはすべてのユーザーから見えるようにしておくが、ユーザーのindex
ページはログインしたユーザーにしか見せないようにし、未登録ユーザーがデフォルトで表示できるページを制限する - まずは
index
アクションのリダイレクトをテストする- 例によって TDD
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
test "should redirect index when not logged in" do
get users_path
assert_redirected_to login_url
end
.
.
.
end
- before フィルターの
logged_in_user
にindex
アクションを追加し、このアクションを保護する
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
before_action :correct_user, only: [:edit, :update]
def index
end
def show
@user = User.find(params[:id])
end
.
.
.
end
- すべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示する index ビューを実装する
User.all
を使って DB 上の全ユーザーを取得し、ビューで使えるインスタンス変数@user
に代入する
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all
end
.
.
.
end
- ユーザーのindexビューを作成する
<% 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>
- SCSS 側も修正しておく
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
- 最後にサイト内移動用のヘッダーにユーザー一覧表示用のリンクを追加する
<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>
10.3.2 サンプルのユーザー
- Rubyを使ってユーザーを一気に作成してみる
Gemfile
に Faker gem を追加してbundle install
- 中二心をくすぐる名前
- 実際にいそうなユーザー名を作成する gem
- サンプルユーザーを追加する Ruby スクリプトを追加する
- Rails タスクとも言う
db/seeds.rb
というファイルが標準
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
- DB をリセットし、上記 Rails タスクを実行する
- 100人分なので少し時間がかかる
10.3.3 ページネーション
- 大量のユーザー表示に対応するためにページネーションを実装する
Gemfile
に will_paginate gem と bootstrap-will_paginate gem を追加する- gem で対応できるのが Rails っぽい
- ページネーションが動作するには、ユーザーのページネーションを行うように Rails に指示するコードを index ビューに追加する必要がある
- まずはビューに
will_paginate
メソッドを追加する
- まずはビューに
<% 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
オブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成している- どうやってるの。。。
@users
変数にはUser.all
の結果が含まれているが、will_paginate
ではpaginate
メソッドを使った結果が必要なため上記コードはまだ動かない
paginate
を使うことで、ユーザーのページネーションを行えるようになるindex
アクション内のall
をpaginate
メソッドに置き換える
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.paginate(page: params[:page])
end
.
.
.
end
10.3.4 ユーザー一覧のテスト
- ページネーションに対する簡単なテストを書く
- ログイン
- index ページにアクセス
- 最初のページにユーザーがいることを確認
- ページネーションのリンクがあることを確認
- fixture に大量のユーザーを追加する必要があるが、手動は面倒
- 埋め込み Ruby を利用してさらに30人のユーザーを追加する
- index ページに対するテストを書く
- いつものように TDD で
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.3.5 パーシャルのリファクタリング
- index ビューのリファクタリングとして、ユーザーの
li
をrender
呼び出しに置き換える
<% 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
という名前のパーシャルを探しに行くので、このパーシャルを作成する必要がある
- この場合、Rails は自動的に
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
- さらに
render
を@users
に対して直接実行する
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate %>
- Rails は
@users
をUser
オブジェクトのリストであると推測する - ユーザーのコレクションを与えて呼び出すと、Rails は自動的にユーザーのコレクションを列挙し、それぞれのユーザーを
_user.html.erb
パーシャルで出力する- いつもの黒魔術
10.4 ユーザーを削除する
- ユーザー一覧ページは完成したので、残るは
destroy
だけ - まずは削除を実行できる admin ユーザ−のクラスを作成する
10.4.1 管理ユーザー
- 特権を持つ管理ユーザーを識別するために、論理値をとる
admin
属性を User モデルに追加する- 自動的に
admin?
メソッドも使えるようになる- なぜ?
- 自動的に
- いつものようにマイグレーションを実行して
admin
属性を追加する
$ rails generate migration add_admin_to_users admin:booleanadmin:boolean
- マイグレーションを実行すると、
admin
カラムがusers
テーブルに追加されるdefault: false
という引数をadd_column
に追加することにより、デフォルトでは管理者になれないということを示す
class AddAdminToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :admin, :boolean, default: false
end
end
- いつものようにマイグレーションを実行する
admin?
メソッドも利用できるようになる- うーん黒魔術
- 最初のユーザーだけをデフォルトで管理者にするようにサンプルデータを更新する
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: 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
- DB をリセットし、サンプルデータを再度生成する
- 上記コードでは初期化ハッシュに
admin: true
を設定することでユーザーを管理者にしている- 攻撃者が PATCH リクエストにより任意のユーザーの権限を書き換える可能性がある
- Stroing Parameters で対策
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
- 許可された属性リストに
admin
が含まれていないため、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できる - 演習問題で少しハマる
- 失敗すべきテストが通ってしまう
- has_secure_password によって守られているため、テストでは
@other_user
経由で passowrd と password_confirmation を呼び出せないのが原因(ググると同じ問題にハマっている人がいた) - password と password_confirmation をベタ書きにすることで対応できた
- しかし微妙になっとくできないのでもう少し調べてみよう。。。
10.4.2 destroy アクション
destroy
アクションへのリンクを追加する- ユーザー index ページの各ユーザーに削除用のリンクを追加し、管理ユーザーへのアクセスを制限する
- 現在のユーザーが管理者のときに限り
destroy
リンクが表示されるようになる
- 現在のユーザーが管理者のときに限り
- ユーザー index ページの各ユーザーに削除用のリンクを追加し、管理ユーザーへのアクセスを制限する
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
- やはり erb 内に登場する if 文が気になる。。。
- ブラウザではネイティブで DLETE リクエストを送信できないため、Railsでは JavaScript を使って偽造する
- 削除リンクを動作させるために
destroy
アクションを追加する- 該当するユーザーを見つけて Acticve Record の
destroy
メソッドを使って削除し、最後にユーザー index に移動する
- 該当するユーザーを見つけて Acticve Record の
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
.
.
.
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
private
.
.
.
end
- コマンドラインで DELETE リクエストを直接発行するという方法でサイトの全ユーザーを削除するというセキュリティホールがまだある
destroy
アクションにもアクセス制限を行うことにより、管理者だけがユーザーを削除できるようになる
- before フィルターで
destroy
アクションへのアクセスを制御する
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?
end
end
10.4.3 ユーザー削除のテスト
- ユーザー削除のテストを書く
- fixture ファイルを修正してサンプルユーザーの一人を管理者にする
- Users コントローラをテストするために、アクション単位でアクセス制御をテストする
- 削除をテストするために DELETE リクエストを発行して
destroy
アクションを直接動作させる- ログインしていないユーザーであればログイン画面へリダイレクト
- ログイン済みユーザーであっても管理者でなければ、ホーム画面へリダイレクト
- 削除をテストするために DELETE リクエストを発行して
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
delete user_path(@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
delete user_path(@user)
end
assert_redirected_to root_url
end
end
- 管理者ユーザーの振る舞いも一緒に確認できると良い
- 管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用してテストを追加する
- DELETE リクエストを適切な URL に向けて発行し、
User.count
でユーザー数が1減ったかどうかを確認する - 管理者や一般ユーザーのテスト、ページネーションや削除リンクのテストをすべてまとめる
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)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
end
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
end
test "index as non-admin" do
log_in_as(@non_admin)
get users_path
assert_select 'a', text: 'delete', count: 0
end
end
10.5 最後に
- Web サイトとしての十分な基盤(ユーザーの認証認可)を整えたサンプルアプリケーションが出来上がった
所感
認可の仕組みやページネーションを実装し、よりWebアプリケーションらしさが増してきた一方、本来このチュートリアルから得たかったことからは徐々に離れつつある気もしてきている。Rails の特定な便利機能の動きを学べるのはプラスにはなるのだが、正直言ってあまり興味は持てない。とはいえ残り4章なので、最後まで頑張って走りきってみる。