Ruby on Rails Tutorialのエッセンスを自分なりに整理8
[Rails] RSpecのリファクタリング
http://qiita.com/kidachi_/items/ec184deb106e4e5c90c3
の続き。
Ruby on Rails Tutorial(chapter6)
http://railstutorial.jp/chapters/modeling-users?version=4.0#top
概要
認証機能をもつユーザ管理用のModelをテスト駆動で実装していく。
RSpecの導入方法はこちら
[Rails] RSpecによるBDD(振舞駆動開発)の基本 [SporkとGuardも]
http://qiita.com/kidachi_/items/cb8910eb74e924456df9
データモデルの定義
column | type |
---|---|
id | integer |
name | string |
string | |
cretaed_at | datetime |
updated_at | datetime |
モデルの生成とマイグレーション実行。
$ rails generate model User name:string email:string
$ bundle exec rake db:migrate
データ構造をモデルに示すgemの導入
annotate(gem)を用いることで、モデルにデータ構造をコメント形式で残すことができる。
group :development do
gem 'annotate', '2.5.0'
end
$ bundle install
$ bundle exec annotate
Annotated (1): User
モデルへデータモデルを表すコメントが追加された。
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
end
※dbスキーマに変更がある度にbundle exec annotateする必要がある。
Rails4.0のモデルについて
3系まではattr_accessibleを定義するのが普通だが、
4系からはstrong_parametersで行うためattr_accessibleは記述しない。
class User < ActiveRecord::Base
# attr_accessible :name, :email # 4系では不要
end
strong_parametersについては以下等を参照
[Rails4.0] フォームの基本とStrongParametersを理解する
http://qiita.com/kidachi_/items/99f2c90788bd931ea3ee
Rails4 の Strong Parameters でリクエストパラメータを検証する
http://www.techscore.com/blog/2013/01/29/rails4-%E3%81%AE-strong-parameters-%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B/
バリデーションの実装
各カラムに対するバリデーションをテスト駆動で実装する。
テストの準備
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
before~とsubject~
@userをitで表す。
※beforeやsubjectの詳細は以下参照
[Rails] RSpecのリファクタリング
http://qiita.com/kidachi_/items/ec184deb106e4e5c90c3#refactor1
respond_to?
シンボルを引数として受け取り、そのメソッド(またはメンバ)を
オブジェクトが保持している場合はtrueを、そうでない場合はfalseを返す。
Rubyで記述した場合
@user.respond_to?(:name)
RSpecで記述した場合
@user.should respond_to(:name)
# or
@user.should be_respond_to(:name)
# or
expect(@user).to respond_to(:name)
真偽値を返すメソッドに関するRubyとRSpecの関係
Rubyで真偽値を返すfoo?というメソッドに応答するのであれば、
RSpecではそれに対応するbe_fooというテストメソッドが存在する。
Red to Green
現時点ではテスト用dbを準備していないのでRed
# テスト用DBの準備
$ rake db:test:prepare
これでGreenになる。
オブジェクトと各メンバの存在検証
オブジェクトが有効かどうか(obj.valid?)を検証する。
テストの準備
describe User do
before { @user = User.new(name: "test", email:"test@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = "" }
it { should_not be_valid }
end
describe "when email is not present" do
before { @user.email = "" }
it { should_not be_valid }
end
end
Red to Green
class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true
end
rails console(sandbox)で動作確認
※sandboxにしておくと、console終了時に変更を全てrollbackしてくれる。
$ rails c --sandbox
> user = User.new(name:"", email:"test@example.com")
=> #<User id: nil, name: "", email: "test@example.com", created_at: nil, updated_at: nil>
> user.save
(0.1ms) SAVEPOINT active_record_1
(0.1ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
# name属性が欠けた状態での保存が出来ない事を確認
# valid?はfalseを返す
> user.valid?
=> false
> user.errors.full_messages
=> ["Name can't be blank"]
メールアドレス検証
メールアドレスについて、より詳細に正当性を検証する。
テストコードの準備
正規表現を用いて、適切なメールアドレスの形式を保っているかチェック。
require 'spec_helper'
describe User do
before { @user = User.new(name: "test", email:"test@example.com") }
subject { @user }
~
describe "when email format is invalid" do
it "should be invalid" do
addresses = %w[user@foo..com user_at_foo,org example.user@foo.
foo@bar_baz.com foo@bar+baz.com]
addresses.each do |invalid_address|
@user.email = invalid_address
should_not be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
addresses.each do |valid_address|
@user.email = valid_address
should be_valid
end
end
end
end
Red to Green
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end
今回用いた正規表現
表現 | 意味 |
---|---|
/ | 正規表現の開始を示す |
\A | 文字列の先頭 |
[\w+-.]+ | 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、 ドット (.) のいずれかを少なくとも1文字以上繰り返す |
@ | アットマーク |
[a-z\d-.]+ | 英小文字、数字、ハイフン、ドットのいずれかを 少なくとも1文字以上繰り返す |
. | ドット |
[a-z]+ | 英小文字を少なくとも1文字以上繰り返す |
\z | 文字列の末尾 |
/ | 正規表現の終わりを示す |
i | 大文字小文字を無視するオプション |
Ruby正規表現チェック&リファレンスは以下
http://www.rubular.com/
一意(ユニーク)性検証
メンバのユニーク性を検証する。
テストコードの準備
describe "when email address is already taken" do
before do
#@userと同一の属性をuser_with_same_emailコピー
user_with_same_email = @user.dup
#メールアドレスでは大文字小文字が区別されないため、upcaseで統一。
user_with_same_email.email = @user.email.upcase
user_with_same_email.save
end
it { should_not be_valid }
end
Red to Green
validates uniqueness: { case_sensitive: false }
で
大文字小文字を識別しない形で重複を排除する。
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
validates: uniquenessの問題
高トラフィックのタイミングでリクエストが短時間に連続送信された場合、
それぞれが検証をパスしてしまうことがある。
対策として、データベースレベルでも重複を禁止する。
DB側での一意(ユニーク)性検証
Unique INDEX の追加
$ rails g migration add_index_to_users_email
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique:true
end
end
rake db:migrate
するとエラーが出た。
$ rake db:migrate
rake aborted!
An error has occurred, this and all later migrations canceled:
SQLite3::BusyException: database is locked: commit transaction
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
rails console --sandbox
のセッションがDBをロックしている様子。
$ ps au |grep rails
68910 0.0 0.0 2512656 4 s007 T 9:58PM 0:02.28 /Users/taniguchidaiki/.rbenv/versions/2.0.0-p195/bin/ruby bin/rails c --sandbox
$ kill -9 68910
プロセスをkillして再度rake db:migrate
で通った。
$ rake db:migrate
== AddIndexToUsersEmail: migrating ===========================================
-- add_index(:users, :email, {:unique=>true})
-> 0.0011s
== AddIndexToUsersEmail: migrated (0.0012s) ==================================
ユニークインデックス追加完了。
小文字変換処理の追加
全てのデータベースが常に大文字小文字を区別するインデックスを
使っているとは限らないため、メールアドレスを保存する前に
すべての文字を小文字に変換する処理を追加する。
テストコードの準備
describe "email address with mixed case" do
let(:mixed_case_email) { "Foo@ExAMPle.CoM"}
it "should be saved as all lower-case" do
@user.email = mixed_case_email
@user.save
expect(@user.reload.email).to eq mixed_case_email.downcase
end
end
Red to Green
コールバックメソッド(※)before_save
を用いる。
※Active Recordオブジェクトが持続している間の任意のタイミングで呼び出される。
class User < ActiveRecord::Base
before_save { self.email = email.downcase } # { email.downcase! }でも可
~
end
これで重複データの保管がリクエストされると、
ActiveRecord::StatementInvalid
例外を投げる。
セキュアなパスワードの実装
ユーザ認証用のパスワードを生成する。
認証の流れは以下。
- パスワードの送信
- 暗号化
- db内の暗号化された値との比較
暗号化を挟むことにより、万が一db内の情報が流出してもパスワードの安全性は保たれる。
password追加(データモデルの変更)
テストの準備
describe User do
~
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should be_valid }
~
end
Red to Green
password_digestの追加(データモデルを変更する)
column | type |
---|---|
id | integer |
name | string |
string | |
password_digest | string |
cretaed_at | datetime |
updated_at | datetime |
マイグレーションファイルの作成
$ rails g migrate add_password_digest_to_users password_digest:string
※rails g migrate add_column名_to_table名 追加カラム名:カラムの型
という
形式にしたがってgenerateすると、自動で適切なchangeメソッドが出力される。
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
マイグレーションの反映(とテストDBの準備)
$ rake db:migrate
$ rake test:prepare # db:migrateの後に必要になることも。
passwordの入力受付と、正当性検証
必要な機能
- password属性とpassword_confirmation属性の追加
- passwordが存在することを要求
- passwordが最低限の文字数を保持していることを要求
- passwordとpassword_confirmationが一致することを要求
- 暗号化されたパスワードとpassword_digestを比較、認証するauthenticateメソッドの実装
テストコードの準備
describe User do
before do
@user = User.new(name: "test", email:"test@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
~
# passwordの長さ検証
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
# authenticateメソッド検証
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
# passwordが正しい場合
describe "with valid password" do
it { should eq found_user.authenticate(@user.password) }
end
# passwordが不正である場合
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
end
eq
オブジェクト同士が同値であるかを調べる。
specify
itと同義であり、itを使用すると英語として不自然な場合にこれで代用する。
「it should not equal wrong user」(itはユーザーなど) とするのは英語として自然だが、
「user: user with invalid password should be false」は不自然。
「specify: user with invalid password should be false」とすれば自然になる。
let
以下を参照
[Rails] RSpecのリファクタリング
http://qiita.com/kidachi_/items/ec184deb106e4e5c90c3#refactor3
パスワードを不可逆に暗号化するための準備
bcrypt(gem)を利用する。
~
gem 'bcrypt-ruby', '~> 3.1.2'
~
$ bundle install
Red to Green
必要な機能
- password属性とpassword_confirmation属性の追加
- passwordが存在することを要求
- passwordが最低限の文字数を保持していることを要求
- passwordとpassword_confirmationが一致することを要求
- 暗号化されたパスワードとpassword_digestを比較、認証するauthenticateメソッドの実装
実は、このうち3以外は全てhas_secure_passwordが自動的に追加してくれる。
has_secure_passwordの追加と、passwordの長さ検証の実装
has_secure_password
validates :password, length: { minimum: 6 }
ユーザが実際に作成できるか検証
$ rails c
# ユーザの作成
> User.create(name:"test", email:"test@example.com", password:"foobar", password_confirmation:"foobar")
(0.1ms) begin transaction
User Exists (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('test@example.com') LIMIT 1
Binary data inserted for `string` type on column `password_digest`
SQL (4.0ms) INSERT INTO "users" ("created_at", "email", "name", "password_digest", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Sun, 24 Nov 2013 09:59:11 UTC +00:00], ["email", "test@example.com"], ["name", "test"], ["password_digest", "$2a$10$XJmRT5H6kIAvYA3PsFy1t.UV0HvRsA0V6ei0QzRz.yy.B6FUF8jm2"], ["updated_at", Sun, 24 Nov 2013 09:59:11 UTC +00:00]]
(2.0ms) commit transaction
=> #<User id: 1, name: "test", email: "test@example.com", created_at: "2013-11-24 09:59:11", updated_at: "2013-11-24 09:59:11", password_digest: "$2a$10$XJmRT5H6kIAvYA3PsFy1t.UV0HvRsA0V6ei0QzRz.yy....">
# 作成したユーザをdbから取得
> user = User.find_by(email:"test@example.com")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'test@example.com' LIMIT 1
=> #<User id: 1, name: "test", email: "test@example.com", created_at: "2013-11-24 09:59:11", updated_at: "2013-11-24 09:59:11", password_digest: "$2a$10$XJmRT5H6kIAvYA3PsFy1t.UV0HvRsA0V6ei0QzRz.yy....">
# 作成したユーザのpassword_digestを確認
> user.password_digest
=> "$2a$10$XJmRT5H6kIAvYA3PsFy1t.UV0HvRsA0V6ei0QzRz.yy.B6FUF8jm2"
# authenticateメソッドの動作確認(不正なパスワードを弾く)
> user.authenticate("invalid")
=> false
# authenticateメソッドの動作確認(正当なパスワードを承認する)
> user.authenticate("foobar")
=> #<User id: 1, name: "test", email: "test@example.com", created_at: "2013-11-24 09:59:11", updated_at: "2013-11-24 09:59:11", password_digest: "$2a$10$XJmRT5H6kIAvYA3PsFy1t.UV0HvRsA0V6ei0QzRz.yy....">
うまくいった。
以下に続く
[Rails4.0] フォームの基本とStrongParametersを理解する
http://qiita.com/kidachi_/items/7c1a9b61dd1853d5a7dc