概要
はじめに
- 自己紹介:Rails歴~半年、入社歴半年の技術勉強中な新入社員です。
- 社内新人研修の一環として Railsチュートリアル ver5.1 に取り組ませてもらっており、各章ごとに振り返り会 & Advancedなるその章をより深く理解できるような課題を出してもらっています。
- 今回の取り組みは、以下の第12章Advanced課題(社内独自)を取り組んだ内容になっています。
- 「PasswordResetをModelとして実装したバージョンを作成せよ。Model名はPasswordResetではないより適切な名前にしても構わない」
- バージョンは以下のとおりとなっています。
Rails:5.1.6
、Ruby:2.6.3
何をやっている?
Ruby on Rails チュートリアル 12章終了時点からの、User
モデルのソースリファクタリングを行っています。
第12章では既にUser
モデルが少し複雑になってきているため、12章追加機能である「パスワード再設定」機能に関わるUser
モデル内の記述部分を切り離してPasswordReset
モデルを新たに生成しました。またそれに伴う影響範囲の修正なども行っています
※リファクタリングとは
ソフトウェアの外部的振る舞いを保ちつつ、理解や修正が簡単になるように、内部構造を改善すること)参考: - リファクタリングとは何か?)
どんなレベルの人が対象?
Railsチュートリアルを進めている方(特に12章まで進んでいる方) やRails初心者の方を対象としています。
作業方針について
Railsチュートリアルを進めている方はご存じかと思いますが、RailsチュートリアルではRails動作環境の構築だったり、ユーザーモデル作成、ログイン機構の実装から、Twitterのようなユーザーごとにつぶやきを投稿、閲覧できるWebアプリケーションを作成します。
第12章では「パスワードの再設定」機能を実装していますがざっくり以下のようなことを行っています。
PasswordReset
コントローラーの作成PasswordReset
用のビューの作成Userモデル
にPasswordReset
用のカラム・処理の追加 etc
基本的には第9章「発展的なログイン機構」や第11章「アカウント有効化」などもUserに関わる機能はUser
モデル内に追加で処理を記述していくスタイルとなっているようです。
そこで今回の取り組みでは、一度User
モデルに追加された「パスワードの再設定」機能をPasswordReset
モデル(新しく生成)へ移行し、影響のあるファイルの対応を行っています。具体的には以下の作業を実施しました。
PasswordReset
コントローラー、ビューの削除User
モデル内のPasswordReset
用で記述したところを削除User
モデル、PasswordReset
モデルの定義、生成PasswordReset
コントローラー、ビューの作成PasswordReset
モデルへの処理追加 etc
具体的な作業内容は、後述の「作業log」で記載しています。備忘録的にまとめているものなので
簡略化して書いている部分が多々あります。
詳細な修正箇所については作成したPRから確認いただければと思います。よろしくお願いいたします!
作成したPR
作業log
準備
- パスワード再設定機能に関わる記述を保存
※Railsチュートリアル第12章が終わった状態のソースです。Rails tutorialソースサンプルはこちら
model
# app/models/user.rb
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
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
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
end
# パスワード再設定のメールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
view
<!-- app/views/password_resets/edit.html.erb -->
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= 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 "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<!-- app/views/password_resets/new.html.erb -->
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
controller
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if params[:user][:password].empty? # (3) への対応
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update_attributes(user_params) # (4) への対応
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit' # (2) への対応
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
def get_user
@user = User.find_by(email: params[:email])
end
# 正しいユーザーかどうか確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
# トークンが期限切れかどうか確認する
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
- 12章完了時のブランチから、トピックブランチをきる
$ git checkout -b password-reset-new
- パスワード再設定用のコントローラを削除する
$ rails destroy controller PasswordResets new edit --no-test-framework
User
モデルのパスワード再設定用に追加した記述を削除する
model生成
テーブル:users
カラム名 | 型 |
---|---|
id | integer |
string | |
create_at | datetime |
update_at | datetime |
password_digest | string |
remember_digest | string |
admin | boolean |
activation_digest | string |
activated_at | datetime |
テーブル:passwordResets
カラム名 | 型 |
---|---|
id | integer |
reset_digest | string |
reset_sent_at | datetime |
user_id | integer |
create_at | datetime |
update_at | datetime |
users
テーブルのカラムを安全に削除する
$ rails generate migration RemovePasswordResetFromUsers reset_digest:string reset_sent_at:datetime
PasswordReset
モデルを生成する
$ rails generate model PasswordReset reset_digest:string reset_sent_at:datetime user:references
- インデックスが付与されたpasswordResetのマイグレーションを記述する
# db/migrate/[timestamp]_create_password_resets.rb
class CreatePasswordResets < ActiveRecord::Migration[5.1]
def change
create_table :password_resets do |t|
t.string :reset_digest
t.datetime :reset_sent_at
t.references :user, foreign_key: true
t.timestamps
end
add_index :password_resets, [:user_id, :created_at]
end
end
- データベースを更新する
# エラー回避のため①(ActiveRecord::NoEnvironmentInSchemaError)
# https://blog.freedom-man.com/no-environment-in-schema-error
$ rails db:environment:set
# エラー回避のため②(StandardError: An error has occurred, this and all later migrations canceled:))
# https://qiita.com/azusanakano/items/a2847e4e582b9a627e3a
$ rails db:migrate:set
$ rails db:migrate
とおった 🎉
- Model同士の関連付けを宣言する
# app/model/user.rb
class User < ApplicationRecord
has_many :password_resets
.
.
.
end
# app/model/password_reset.rb
class PasswordReset < ApplicationRecord
belongs_to :user, foreign_key: 'user_id'
validates :user_id, presence: true
end
controller、view生成
- パスワード再設定用のコントローラ、ビューを作成する
$ rails generate controller PasswordResets new edit --no-test-framework
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
def new
end
def edit
end
end
<!-- app/views/password_resets/new.html.erb -->
<h1>PasswordResets#new</h1>
<p>Find me in app/views/password_resets/new.html.erb</p>
<!-- app/views/password_resets/edit.html.erb -->
<h1>PasswordResets#edit</h1>
<p>Find me in app/views/password_resets/edit.html.erb</p>
これ以降は、Railsチュートリアル第12章の内容に沿って修正。
新しいパスワード再設定画面ビュー
- 新しいパスワード再設定画面ビュー
<!-- app/views/password_resets/new.html.erb -->
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
表示OK
create
アクションでパスワード再設定
- 12章記載のコードのままだとメールが送れないため、書き換える
12章記載のコード(☆の部分の対応が必要)。
# app/controllers/pasword_resets_controller.rb
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest <- ☆1
@user.send_password_reset_email <- ☆2
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
☆1の対応
# app/models/password_reset.rb
class PasswordReset < ApplicationRecord
attr_accessor :reset_token
.
.
.
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
end
end
# app/controllers/pasword_resets_controller.rb
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@password_reset = @user.password_resets.create
@password_reset.create_reset_digest
@user.send_password_reset_email <- ☆2
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
☆2の対応
# app/models/password_reset.rb
class PasswordReset < ApplicationRecord
.
.
.
# パスワード再設定のメールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
end
# app/controllers/pasword_resets_controller.rb
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@password_reset = @user.password_resets.create
@password_reset.create_reset_digest
@password_reset.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
.
.
.
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.password_reset.subject
#
def password_reset(password_reset)
@password_reset = password_reset
mail to: password_reset.user.email, subject: "Password reset"
end
end
<!-- app/views/user_mailer/password_reset.html.erb -->
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= link_to "Reset password", edit_password_reset_url(@password_reset.reset_token,
email: @password_reset.user.email) %>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
<!-- app/views/user_mailer/password_reset.text.erb -->
To reset your password click the link below:
<%= edit_password_reset_url(@password_reset.reset_token, email: @password_reset.user.email) %>
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
- 確認する
# railsサーバーのログ
Sent mail to example@railstutorial.org (24.4ms)
Date: Wed, 12 Aug 2020 07:19:23 +0000
From: noreply@example.com
To: example@railstutorial.org
Message-ID: <5f3397fbe6734_fc615e15e06949f@ip-172-31-31-137.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5f3397fbe3eb7_fc615e15e0693ca";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_5f3397fbe3eb7_fc615e15e0693ca
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
https://8632ff13d35b4c219446f66f948001f5.vfs.cloud9.us-east-2.amazonaws.com/password_resets/L0-z9YbV3cjzlnfOnjT5ag/edit?email=example%40railstutorial.org
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
----==_mimepart_5f3397fbe3eb7_fc615e15e0693ca
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<a href="https://8632ff13d35b4c219446f66f948001f5.vfs.cloud9.us-east-2.amazonaws.com/password_resets/L0-z9YbV3cjzlnfOnjT5ag/edit?email=example%40railstutorial.org">Reset password</a>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
</body>
</html>
----==_mimepart_5f3397fbe3eb7_fc615e15e0693ca--
Redirected to https://8632ff13d35b4c219446f66f948001f5.vfs.cloud9.us-east-2.amazonaws.com/
Completed 302 Found in 343ms (ActiveRecord: 18.2ms)
OK
パスワード再設定のプレビューメソッド(テスト用)
- 12章記載のコードのままだとプレビューが確認できないため、書き換える
12章記載のコード
# test/mailer/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
.
.
.
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
user = User.first
user.reset_token = User.new_token <--
UserMailer.password_reset(user) <--
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
TOBE
# test/mailer/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
.
.
.
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
user = User.first
reset_token = User.new_token <--
password_reset = user.password_resets.build(reset_token: reset_token, reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now) <--
UserMailer.password_reset(password_reset) <--
end
end
OK
送信メールのテスト
- 12章記載のコードのままだとテストが通らないため、書き換える
12章記載のコード
# test/mailers/user_mailer_test.rb
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
.
.
.
test "password_reset" do
user = users(:michael) <--
user.reset_token = User.new_token <--
mail = UserMailer.password_reset(user) <--
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to <--
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token, mail.body.encoded <--
assert_match CGI.escape(user.email), mail.body.encoded <--
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
TOBE
# test/mailers/user_mailer_test.rb
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
.
.
.
test "password_reset" do
reset_token = User.new_token <--
password_reset = users(:michael).password_resets.build(reset_token: reset_token) <--
mail = UserMailer.password_reset(password_reset) <--
assert_equal "Password reset", mail.subject
assert_equal [password_reset.user.email], mail.to <--
assert_equal ["noreply@example.com"], mail.from
assert_match password_reset.reset_token, mail.body.encoded <--
assert_match CGI.escape(password_reset.user.email), mail.body.encoded <--
end
end
rails test:mailers
を実行し、GREENを確認する
OK
edit
アクションで再設定
- パスワード再設定のフォームを記述する
<!-- app/views/password_resets/edit.html.erb -->
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= 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 "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
- パスワード再設定の
edit
アクションはそのままでは編集画面を開けないため、書き換える
12章記載のコード
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
.
.
.
def edit
end
private
def get_user
@user = User.find_by(email: params[:email])
end
# 正しいユーザーかどうか確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id])) <-- (reset_digest がUserモデルでなくなったため落ちる)
redirect_to root_url
end
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
TOBE
# app/models/password_reset.rb
class PasswordReset < ApplicationRecord
.
.
.
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(token)
# reset_digestはUserに紐づくレコードのうち作成日が最新のもののデータを取得
digest = self.user.password_resets.order(created_at: :desc).limit(1)[0].reset_digest
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
end
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
.
.
.
# 正しいユーザーかどうか確認する
def valid_user
@password_reset = @user.password_resets.build if @user <--
unless (@user && @user.activated? &&
@password_reset.authenticated?(params[:id])) <--
redirect_to root_url
end
end
end
- 確認する
OK
パスワードを更新する
- パスワード再設定の
update
アクションについてそのままだと更新できないため、書き換える
12章記載のコード
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
.
.
def update
if params[:user][:password].empty?
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
private
.
.
# トークンが期限切れかどうか確認する
def check_expiration
if @user.password_reset_expired? <--
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
TOBE
# app/models/password_reset.rb
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
# latest_sent_atはUserに紐づくレコードのうち作成日が最新のもののデータを取得
latest_sent_at = self.user.password_resets.order(created_at: :desc).limit(1)[0].reset_sent_at
latest_sent_at < 2.hours.ago
end
end
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
.
.
.
# トークンが期限切れかどうか確認する
def check_expiration
@password_reset = @user.password_resets.build if @user <--
if @password_reset.password_reset_expired? <--
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
# updateアクションでuserモデルを更新する際に必要なメソッドを定義
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
- 確認する
OK
- パスワード再設定の統合テストを書き換える
12章記載のコード
# test/integration/password_resets_test.rb
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# メールアドレスが無効
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
# メールアドレスが有効
post password_resets_path,
params: { password_reset: { email: @user.email } }
assert_not_equal @user.reset_digest, @user.reload.reset_digest
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
# パスワード再設定フォームのテスト
user = assigns(:user)
# メールアドレスが無効
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# 無効なユーザー
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# メールアドレスが有効で、トークンが無効
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# メールアドレスもトークンも有効
get edit_password_reset_path(user.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
# 無効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" } }
assert_select 'div#error_explanation'
# パスワードが空
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "",
password_confirmation: "" } }
assert_select 'div#error_explanation'
# 有効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
test "expired token" do
get new_password_reset_path
post password_resets_path,
params: { password_reset: { email: @user.email } }
@user = assigns(:user)
@user.update_attribute(:reset_sent_at, 3.hours.ago)
patch password_reset_path(@user.reset_token),
params: { email: @user.email,
user: { password: "foobar",
password_confirmation: "foobar" } }
assert_response :redirect
follow_redirect!
assert_match /expired/i, response.body
end
end
※Rails tutorialソースサンプルの対応箇所はこちら
TOBE
# test/integration/password_resets_test.rb
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# メールアドレスが無効
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
# メールアドレスが有効
assert_difference 'PasswordReset.count', 1 do
post password_resets_path,
params: { password_reset: { email: @user.email } }
end
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
# パスワード再設定フォームのテスト
user = assigns(:user)
password_reset = assigns(:password_reset)
# メールアドレスが無効
get edit_password_reset_path(password_reset.reset_token, email: "")
assert_redirected_to root_url
# 無効なユーザー
user.toggle!(:activated)
get edit_password_reset_path(password_reset.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# メールアドレスが有効で、トークンが無効
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# メールアドレスもトークンも有効
get edit_password_reset_path(password_reset.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
# 無効なパスワードとパスワード確認
patch password_reset_path(password_reset.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" } }
assert_select 'div#error_explanation'
# パスワードが空
patch password_reset_path(password_reset.reset_token),
params: { email: user.email,
user: { password: "",
password_confirmation: "" } }
assert_select 'div#error_explanation'
# 有効なパスワードとパスワード確認
patch password_reset_path(password_reset.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
test "expired token" do
get new_password_reset_path
post password_resets_path,
params: { password_reset: { email: @user.email } }
@user = assigns(:user)
@password_reset = assigns(:password_reset)
@password_reset.update_attribute(:reset_sent_at, 3.hours.ago)
patch password_reset_path(@password_reset.reset_token),
params: { email: @user.email,
user: { password: "foobar",
password_confirmation: "foobar" } }
assert_response :redirect
follow_redirect!
assert_match /expired/i, response.body
end
end
rails test:integration
を実行し、GREENを確認する
OK
rails t
を実行し、GREENを確認する
仕上げ
- git pushし、PRを作成する
$ rails t
$ git add -A
$ git commit -m "Add password reset new"
$ git push
振り返り
- 今後やった方がよさそうなこと
PasswordReset
のModelのテスト記述UserMailer
もPasswordResetMailer
を作って処理を分ける
- 検討?
User
モデル→PasswordReset
モデルの関連付けについてhas_one
の方がよい?- 現在は
has_many
でパスワード再設定メールを送るイベントの度にレコード生成され、削除はしていないので全てのレコードが蓄積される(ソースがシンプルなので今の作りにした)
- 感想
- Railsチュートリアル第12章のドキュメントがあったのでなんとか進められた
- 疲れたけどちゃんと動いているのでよかった