(色々あって)ある日突然、 一刻も早く Ruby on Rails を使いこなせる状態にならなければいけなくなった ので、先輩エンジニアに泣きついて教えてもらった Ruby on Rails 入門の決定版、Rails チュートリアル を進めています。
Ruby on Rails チュートリアルとは
Ruby で書かれた Web 開発フレームワークである Ruby on Rails を使って、全14章に分けて開発をしていきます。Git, Bitbucket, Heroku などを用い、テスト駆動開発(= TDD)を実践しながら進めていくので非常に実践的なチュートリアルになっていて、ボリュームは満点ですがめちゃくちゃ勉強になります。
わからなかったらProgateとかやって基本知識を身につけてから挑戦しよう
Rails チュートリアルは解説が非常に丁寧とはいっても、初学者にはなかなかハードルが高いのも事実。コマンドラインの基本的ないじり方、プログラミング言語の基本的な文法や仕組みなどの知識に不安がある人は、 Progate などもう少しやさしいサービスで知識をつけてから挑戦するのが良いかと思います。
勉強開始時期と当時のスペック
- 2019-07-14 に開始
- Ruby on Rails って何?という状態
- Python で競技プログラミングを少しやっていたのでコードを読むことに抵抗はそこまでないが、Ruby はほとんど知らない状態
第1章: ゼロからデプロイまで
1章では、文字通りかんたんなアプリをゼロから作ってデプロイ(アプリとして皆が使えるように公開する)まで一気に駆け抜けます。Hello World! みたいなのが表示されるだけだから怖くない。
学べること
- Rails とは何か
- AWS Cloud9 の環境構築
- MVC モデルとは何か
- Git を使ったバージョン管理
- Heroku へのデプロイ
app/ | モデル・ビュー・コントローラ・ヘルパーなどを含む、主要なアプリケーションコード |
app/assets | アプリケーションで使う CSS、JS、画像などのアセット |
bin/ | バイナリ実行可能ファイル |
config/ | アプリケーションの設定(configuration) |
db/ | データベース関連のファイル |
doc/ | マニュアルなど、アプリケーションのドキュメント |
lib/ | ライブラリモジュール |
lib/assets | ライブラリで使う CSS、JS、画像などのアセット |
log/ | アプリケーションのログファイル |
public/ | エラーページなど、一般に直接公開するデータ |
bin/rails | コード生成、コンソール起動、ローカルの Web サーバの立ち上げなどで使う Rails スクリプト |
test/ | アプリケーションのテスト |
tmp/ | 一時ファイル |
vendor/ | サードパーティのプラグイン、gem など |
vendor/assets | サードパーティのプラグインや gem で使う CSS、JS、画像などのアセット |
README.md | アプリケーションの簡単な説明 |
Rakefile | rake コマンドで使えるタスク |
Gemfile | このアプリケーションに必要な gem の定義ファイル |
Gemfile.lock | アプリケーションで使われる gem のバージョンを確認するためのリスト |
config.ru | Rack ミドルウェア用の設定ファイル |
.gitignore | Git に取り込みたくないファイルを指定するためのパターン |
第2章 Toyアプリケーション(所要時間: 2h)
この章では、Toy アプリケーションという簡単なアプリを scaffold
を利用して簡単に作成し、Rails アプリケーションの概要や MVC モデルの挙動をざっくり理解します。
学べること
- かんたんなアプリを
scaffold
を使って作成する - REST アーキテクチャ
- データモデルの作成
-
rails console
の使い方
HTTPの4つの基本操作:
GET | Web上のデータを取得するときに使う。読み取り専用で、サーバー上のデータを変更しない |
POST | Webページ上のフォームに入力した値をブラウザからサーバーに送信するときに使う。新しいリソースの作成やデータの送信に使う |
PATCH | サーバー上のリソースの一部を更新するときに使う |
DELETE | サーバー上のリソースを削除するときに使う |
第3章 ほぼ静的なページの作成(所要時間: 1.5h)
この章では、今後改修を重ねていくアプリである sample_app の基本的な部分を作っていきます。主に静的なページを作成し、自動化テストの雰囲気を掴みます。
学ぶこと:
- RED, GREEN, REFACTOR サイクル
- テスト駆動開発(TDD)
- 埋め込み Ruby(Embedded Ruby)
-
.erb
は埋め込みRubyの拡張子で、次のような構文が使えて便利-
<% ... %>
中に書いたコードを実行する -
<%= ... %>
中に書いたコードを実行し、実行結果を templates の中に挿入する
-
-
第4章 Rails 風味の Ruby(所要時間: 2h)
この章では Ruby の基本的な文法を rails console
を使いながら学んでいきます。
演習4.4.5
example_user.rb
を次のように修正した
class User
attr_accessor :first_name, :last_name, :email
def initialize(attributes = {})
@first_name = attributes[:first_name]
@last_name = attributes[:last_name]
@email = attributes[:email]
end
def full_name
@full_name = "#{@first_name} #{@last_name}"
end
def alphabetical_name
@alphabetical_name = "#{@last_name}, #{@first_name}"
end
def formatted_email
"#{@full_name} <#{@email}>"
end
end
コンソール上で処理を確認してみる
>> require './example_user'
=> true
>> exam = User.new
=> #<User:0x0000000003136598 @first_name=nil, @last_name=nil, @email=nil>
>> exam.first_name = "Michael"
=> "Michael"
>> exam.last_name = "Hartl"
=> "Hartl"
>> exam.email = "[email protected]"
=> "[email protected]"
>> exam.full_name
=> "Michael Hartl"
>> exam.formatted_email
=> "Michael Hartl <[email protected]>"
>> exam.alphabetical_name
=> "Hartl, Michael"
>> exam.full_name.split == exam.alphabetical_name.split(', ').reverse
=> true
第5章 レイアウトを作成する(所要時間: 1.5h)
この章では、アプリケーションに Bootstrap フレームワークを埋め込み、カスタムスタイルを追加していきます。パーシャル、Rails のルーティング、Asset Pipeline、Sass についても勉強していきます。
学ぶこと
- パーシャル機能を使うといい感じに整理できる
- Sass のネスト構造と変数
- Bootstrap フレームワークを使うと、いい感じのデザインをすばやく実装できる
演習5.1.3
layouts/_rails_default.html.erb
として以下のファイルを作成
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application',
'data-turbolinks-track': 'reload' %>
第6章 ユーザーのモデルを作成する(所要時間: 2.5h)
この章からはユーザー登録・ログイン周りを扱っていきます。データベース(DB)の基礎を学んだり、Rubular を使って試しながら正規表現を利用してユーザー認証機能を実装したり。
学ぶこと
- DB Browser for SQLite で DB の構造をみる
- 正規表現
- ユーザーの検証(存在性・長さ・フォーマット・一意性)
-
has_secure_password
メソッドで、モデルに対してセキュアなパスワードを追加する
演習6.2.4
演習6.3.2
>> u = User.new(name: "Satoooh", email: "[email protected]")
=> #<User id: nil, name: "Satoooh", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: nil>
>> u.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]]
=> false
>> u.errors.messages
=> {:password=>["can't be blank"]}
演習6.3.3
>> u = User.new(name: "hogekosan", email: "email.hoge.com", password: "hoge")
=> #<User id: nil, name: "hogekosan", email: "email.hoge.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$dPKFHrcT0Vi3RL2Pb3y93OsbLacqovP2ZL6wMLzcM4K...">
>> u.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "email.hoge.com"], ["LIMIT", 1]]
=> false
>> u.errors.messages
=> {:email=>["is invalid"], :password=>["is too short (minimum is 6 characters)"]}
演習6.3.4
>> user = User.find_by(email: "[email protected]")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]", created_at: "2019-07-16 07:05:28", updated_at: "2019-07-16 07:05:28", password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5L...">
>> user.name = "Satoooh"
=> "Satoooh"
>> user.save
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "[email protected]"], ["id", 1], ["LIMIT", 1]]
(0.1ms) rollback transaction
=> false
save
できなかったのは、パスワードも更新することになるから?
てことは :name
だけ更新する形なら可能なのかな?
>> user.update_attribute(:name, "El Duderino")
(0.5ms) begin transaction
SQL (1.9ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "El Duderino"], ["updated_at", "2019-07-16 07:15:41.704158"], ["id", 1]]
(5.9ms) commit transaction
=> true
>> user.name
=> "El Duderino"
できた
第7章 ユーザー登録(所要時間: 2.5h)
デバッグを導入したり、 Gravatar で画像表示したり、フォームの処理を実装したり、flash メッセージを出したり、プロのデプロイをしたり盛り沢山です。なんか急に難しくなった。
学ぶこと
-
debug
メソッドでデバッグ情報を表示する - よくわからない挙動が Rails アプリケーション内にあったら、
debugger
を差し込んで調べてみよう -
flash
変数の使い方 - セキュアな通信・ハイパフォーマンスのための SSL, Puma の導入
演習7.1.1
/about
にアクセスしたときのコントローラとアクション
- controller: static_pages
- action: about
>> user = User.first
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "El Duderino", email: "[email protected]", created_at: "2019-07-16 07:05:28", updated_at: "2019-07-16 07:15:41", password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5L...">
>> puts user.attributes.to_yaml
---
id: 1
name: El Duderino
email: [email protected]
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2019-07-16 07:05:28.752110000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2019-07-16 07:15:41.704158000 Z
zone: *2
time: *3
password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5LB.8kBAL7XK"
=> nil
>> y user.attributes
---
id: 1
name: El Duderino
email: [email protected]
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2019-07-16 07:05:28.752110000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2019-07-16 07:15:41.704158000 Z
zone: *2
time: *3
password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5LB.8kBAL7XK"
=> nil
演習7.4.2
>> "#{:success}"
=> "success"
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> "#{flash[:success]}"
=> "It worked!"
>> "#{flash[:danger]}"
=> "It failed."
演習7.4.4.4
$ rails t
Running via Spring preloader in process 16365
Started with run options --seed 21445
FAIL["test_invalid_signup_information", UsersSignupTest, 0.3632701909991738]
test_invalid_signup_information#UsersSignupTest (0.36s)
Expected at least 1 element matching "div#error_explanation", found 0..
Expected 0 to be >= 1.
test/integration/users_signup_test.rb:14:in `block in <class:UsersSignupTest>'
FAIL["test_valid_signup_information", UsersSignupTest, 0.3775088170004892]
test_valid_signup_information#UsersSignupTest (0.38s)
"User.count" didn't change by 1.
Expected: 1
Actual: 0
test/integration/users_signup_test.rb:21:in `block in <class:UsersSignupTest>'
21/21: [===============================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.47808s
21 tests, 43 assertions, 2 failures, 0 errors, 0 skips
@user.save
が実行されておらず、 User.count
が増えていないからエラーが起きていると考えられる
第8章 基本的なログイン機構(所要時間: 1.5h)
この章では、ログインの基本的な仕組みを主に実装し、ユーザーがログイン・ログアウトできるようにします。
学ぶこと
- セッションの実装
- ログイン・ログアウト機能の実装
演習8.1.3
>> user = nil
=> nil
>> !!(user && user.authenticate('foobar'))
=> false
>> user = User.first
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">
>> !!(user && user.authenticate('foobar'))
=> true
演習8.2.2
>> User.find_by(id: 100)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
=> nil
>> session = {}
=> {}
>> session[:user_id] = nil
=> nil
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ? [["LIMIT", 1]]
=> nil
>> session[:user_id] = User.first.id
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> 1
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">
>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">
第9章 発展的なログイン機構(所要時間: 2.5h)
この章では永続クッキーを利用して remember me 機能を実装します。
学ぶこと
- remember me 機能(ログイン状態の保持)の実装
- 永続クッキー(permanent cookies)
- 永続セッションは記憶トークンと記憶ダイジェストをユーザーごとに関連付けて実現する
- 三項演算子
Cookieを盗み出す4つの方法と対策
方法 | 対策 |
---|---|
管理の甘いネットワークを通過するネットワークバケットから直接 cookie を取り出す | SSL をサイト全体に適用し、ネットワークデータを暗号化で保護する |
データベースから記憶トークンを取り出す | 記憶トークンのハッシュ値を保存する |
クロスサイトスクリプティング(XSS)を使う | Rails がビューのテンプレートで入力した内容をすべて自動的にエスケープしてくれる |
ユーザーがログインしているデバイスを直接操作してアクセスを奪い取る | 根本的に防衛することは出来ないが、デジタル署名を導入するなどして二次被害を最小限に留めることは可能 |
演習9.1.1.1
>> user = User.first
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: nil, remember_digest: nil>
>> user.remember
(0.1ms) SAVEPOINT active_record_1
SQL (1.6ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2019-07-17 04:57:19.875840"], ["remember_digest", "$2a$10$p8U/ODxRMxsHQL84SCEdr.bgAAeZVUS5fCfYbUzUIJbSMmQwQnq1K"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.remember_token
=> "RPvZ1MInKPX_E_y7xDkMzg"
>> user.remember_digest
=> "$2a$10$p8U/ODxRMxsHQL84SCEdr.bgAAeZVUS5fCfYbUzUIJbSMmQwQnq1K"
三項演算子について
論理値によって分岐する制御がたくさん出てくるが、これらは次のような三項演算子で書き換えることが出来る。
if boolean?
何かをする
else
別のことをする
end
論理値? ? 何かをする : 別のことをする
例
if boolean?
var = foo
else
var = bar
end
var = boolean? ? foo : bar
第10章 ユーザーの更新・表示・削除(所要時間: 3h)
この章では、User リソース用の REST アクションのうち、これまで実装していなかった edit
, update
, index
, destroy
のアクションを実装して REST アクションを完成させます。
このへんまで進んでくると、あっちを修正したらこっちが error になって、こっち修正したらあっちが error になってと大変です。実際の開発もこんな雰囲気なんでしょうね。。。
学ぶこと
- フレンドリーフォワーディング
- ページネーションの実装
- 管理者権限の実装
演習10.1.1.1
gravatar の a タグに rel="noopener"
を追記
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
演習10.1.3
test "unsuccessfil edit"
内の一番下に以下を追記。
assert_select 'div.alert', "The form contains 4 errors."
ところで、リスト10.32 の redirect_back_or
user
って redirect_back_or
@user
じゃないのかな?
第11章 アカウントの有効化(所要時間: 3h)
この章では、アカウントの有効化のためのメール送信に関連する実装を行います。長く続いた登録周りの実装ももうすぐすべて終わります。
学ぶこと
- アカウント有効化のメール送信
- 安全なアカウント有効化のための諸々の実装
- メタプログラミング
- SendGrid を用いたメール送信
演習11.1.2.3
app/models/user.rb
で定義した downcase_email
メソッドを以下のように変更する。
# メールアドレスをすべて小文字にする
def downcase_email
self.email.downcase!
end
演習11.2.1
>> CGI.escape('[email protected]')
=> "foo%40example.com"
>> CGI.escape("Don't panic!")
=> "Don%27t+panic%21"
リスト11.16 develop 環境のメール設定 について
development.rb
をいじるときに自分の開発環境のホスト名を記入する必要がある。普通に AWS Cloud9 を利用している場合は次のように書けばOK。
Rails.application.configure do
.
.
.
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'us-east-2.console.aws.amazon.com'
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
.
.
.
end
演習11.3.3
# アカウントを有効にする
def activate
update_columns(activated: true, activated_at: Time.zone.now)
end
def index
@users = User.where(activated: true).paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless @user.activated?
end
最後にテスト作成。non-activated
なユーザーについてのテストなので、まずは fixture に有効化されてないユーザーを作ってしまう。
non_activated:
name: Non Activated
email: [email protected]
password_digest: <%= User.digest('password') %>
activated: false
activated_at: <%= Time.zone.now %>
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
@non_activated_user = users(:non_activated)
end
.
.
.
test "should not allow the non-activated attribute" do
log_in_as (@non_activated_user)
assert_not @non_activated_user.activated?
get users_path
assert_select "a[href=?]", user_path(@non_activated_user), count: 0
get user_path(@non_activated_user)
assert_redirected_to root_url
end
end
第12章 パスワードの設定(所要時間: 2h)
この章では、パスワードの再設定機能を実装します。11章のメール認証によって、ユーザーのメアドが本人のものであるという確証がもてるようになっているのでがんばります。
学ぶこと
- パスワード再設定のメール送信
演習12.1.3
コンソールを確認すると reset_digest
と reset_sent_at
があることが確認できる。
>> user = User.find_by(email: "[email protected]")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "[email protected]", created_at: "2019-07-18 10:19:59", updated_at: "2019-07-18 19:25:28", password_digest: "$2a$10$sPAoPOxaoWGHOMDbgBa7/..QBg1ykmJWn3MmfoWrZC1...", remember_digest: nil, admin: true, activation_digest: "$2a$10$qACNkbRLJk5agboZegn.PeG2t/UvsStgF08NTrSssAZ...", activated: true, activated_at: "2019-07-18 10:19:58", reset_digest: "$2a$10$FCIa5J6CuCyHodOkojNz7ugU7yMpxku0WlN/.DVdcoJ...", reset_sent_at: "2019-07-18 19:25:28">
>> user.reset_digest
=> "$2a$10$FCIa5J6CuCyHodOkojNz7ugU7yMpxku0WlN/.DVdcoJsDDpWy45Im"
>> user.reset_sent_at
=> Thu, 18 Jul 2019 19:25:28 UTC +00:00
演習12.2.1
Rails サーバーのログに載っている生成されたメールの内容は次のようになっている。
----==_mimepart_5d30caa7feb7_188218e22e434e
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
https://us-east-2.console.aws.amazon.com/password_resets/p1vTQEI12jYizTl92x7ZjQ/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_5d30caa7feb7_188218e22e434e
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://us-east-2.console.aws.amazon.com/password_resets/p1vTQEI12jYizTl92x7ZjQ/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_5d30caa7feb7_188218e22e434e--
>> user.reset_digest
=> "$2a$10$HcqqTj1V7OylgtoKHdhXFeu1c6EUU5ed3dcHgXNNzAgPZ/hVLHa3i"
>> user.reset_sent_at
=> Thu, 18 Jul 2019 19:38:15 UTC +00:00
演習12.3.1
演習12.3.2
password と confirmation の文字列をわざと間違えると次の画面のように表示される。
>> user.password_digest
=> "$2a$10$sPAoPOxaoWGHOMDbgBa7/..QBg1ykmJWn3MmfoWrZC1eEIsbvcMAu"
>> user.reload
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "[email protected]", created_at: "2019-07-18 10:19:59", updated_at: "2019-07-18 20:06:20", password_digest: "$2a$10$4odk9k7j4KV9jMBtCKTgC.C0auEoKY9FvGGUn9y/IEW...", remember_digest: nil, admin: true, activation_digest: "$2a$10$qACNkbRLJk5agboZegn.PeG2t/UvsStgF08NTrSssAZ...", activated: true, activated_at: "2019-07-18 10:19:58", reset_digest: "$2a$10$HcqqTj1V7OylgtoKHdhXFeu1c6EUU5ed3dcHgXNNzAg...", reset_sent_at: "2019-07-18 19:38:15">
>> user.password_digest
=> "$2a$10$4odk9k7j4KV9jMBtCKTgC.C0auEoKY9FvGGUn9y/IEWdmJ7ad5ojS"
演習12.3.3
# パスワード再設定用の属性を設定する
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
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
.
.
.
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", response.body
end
end
4.では3.で実装した「パスワードの再設定に成功したらダイジェストを nil にする」のテストを書けば OK です。
# 有効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert_nil user.reload.reset_digest
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user