###有効化のテストとリファクタリング
アカウント有効化の統合テストを追加します
####ユーザー登録のテストにアカウント有効化を追加する
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
# setupメソッドでこれを初期化しておかないと、並行して行われる
# 他のテストでメールが配信されたときにエラーが発生してしまいます
end
test "invalid signup information" do
get signup_path
# signupのページにアクセス
assert_no_difference 'User.count' do
# ユーザー数が変わらないかをテストする
post users_path, params: { user: { name: "",
# ハッシュのハッシュのようになっているように見える
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
# postリクエストを送信 フォーム送信をテストする
# データの投稿した後 assert_no_difference で違いを比べるらしい。
end
assert_template 'users/new'
end
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
#
# 第二引数でデータベースとの差異は1である。
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
assert_equal 1, ActionMailer::Base.deliveries.size
# 配列deliveriesは変数
user = assigns(:user)
# assignsメソッドを使うと対応するアクション内のインスタンス変数に
# アクセスできるようになります
# assigns(:user)と書くとこのインスタンス変数に
# アクセスできるようになる
assert_not user.activated?
# 有効化していない状態でログインしてみる
log_in_as(user)
assert_not is_logged_in?
# 有効化トークンが不正な場合
get edit_account_activation_path("invalid token", email: user.email)
assert_not is_logged_in?
# トークンは正しいがメールアドレスが無効な場合
get edit_account_activation_path(user.activation_token, email: 'wrong')
assert_not is_logged_in?
# 有効化トークンが正しい場合
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect!
# POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッド
assert_template 'users/show'
# showアクションのビューを表示させる。
assert is_logged_in?
# ログイン中であるかを確認
end
end
ubuntu:~/environment/sample_app (account-activation) $ rails t
Running via Spring preloader in process 4782
Started with run options --seed 57756
44/44: [============================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.78627s
44 tests, 183 assertions, 0 failures, 0 errors, 0 skips
####Userモデルにユーザー有効化メソッドを追加する
app/models/user.rb
class User < ApplicationRecord
.
.
.
# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
# 条件に一致するモデルオブジェクトを更新
# バリデーションはスキップ
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
# 有効化メールを送信
end
private
.
.
.
end
####ユーザーモデルオブジェクトからメールを送信する
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
# before_actionメソッドを使って何らかの処理が実行される直前に
# 特定のメソッドを実行する仕組み
# ログインをさせる
# :editと:updateアクションだけ
.
.
.
def create
@user = User.new(user_params)
# 外部メソッドを使う
if @user.save
@user.send_activation_email
# リファクタリングされた
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
# リダイレクト先をプロフィールページからルートURLに変更
# urlを指定して表示する
# 保存成功したらurlを表示する
else
render 'new'
# 保存に成功しなければnewアクションに移動する
# 失敗したらまた戻る
end
end
.
.
.
end
####ユーザーモデルオブジェクト経由でアカウントを有効化する
app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
# paramsハッシュで渡されたメールアドレスに対応するユーザーを認証します
if user && !user.activated? && user.authenticated?(:activation, params[:id])
# 攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、
# 本当のユーザーとしてログインできてしまいます。
user.activate
# リファクタリングされた
# ユーザーを認証するには、ユーザーを認証してから
# activated_atタイムスタンプを更新する必要
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
# トークンが無効になるようなことは実際にはめったにありませんが、
# もしそうなった場合はルートURLにリダイレクトされる仕組み
end
end
end
メソッドを使うことでリファクタリングされるのか。
リファクタリングとは、ソフトウェアの挙動を変えることなく、その内部構造を整理することです
###演習
1.
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
class User < ApplicationRecord
.
.
.
# アカウントを有効にする
def activate
update_columns(activated: true, activated_at: Time.zone.now)
# update_attributeを2回呼び出さずに済む
end
.
.
.
end
現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
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
# before_actionメソッドを使って何らかの処理が実行される直前に
# 特定のメソッドを実行する仕組み
# ログインをさせる
# :editと:updateアクションだけ
def index
@users = User.where(activated: true).paginate(page: params[:page])
# allをpaginateメソッドに置き換えます
# :pageパラメーターにはparams[:page]が使われていますが
# 、これはwill_paginateによって自動的に生成されます
end
def show
@user = User.find(params[:id])
# データベースからユーザー情報を取り出す
redirect_to root_url and return unless @user.activated?
# ホーム画面にいき、何かを返す
end
.
.
.
end
ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。