AWSにアプリをデプロイする
はじめに
AWSでサーバーを構築してアプリを動かすまでをまとめてみます。
AWSのEC2の作成などはご自身でお願いします。
また、この記事は初心者が備忘録として書いた記事なので参考にする際はお気をつけください。
Ruby : 2.7.3
Rails : 6.1.4
DB : MySQL
WEBサーバー : nginx
Applicationサーバー : puma
EC2インスタンスの作成は省略
この記事ではEC2インスタンス作成後の内容となっています。
EC2はAmazon Linux 2 AMI (HVM) イメージを使用。
EC2インスタンスの作成は他の記事を参照するようお願いします。
サーバーに環境を用意
パッケージのインストール
使用していくパッケージのインストールを行います。
$ sudo yum -y install git gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel libffi-devel libxml2 libxslt libxml2-devel libxslt-devel sqlite-devel libcurl-devel mysql mysql-devel
Nodeのインストール
$ curl -sL https://rpm.nodesource.com/setup_16.x | sudo bash - $ sudo yum -y install nodejs
yarnのインストール
$ curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo $ sudo yum -y install yarn
MySQLのインストール
今回RDSを使わずにMySQLをインストールして使用していきます。
データベースはprivateなネットワークに作成してください。
#mariaDBの削除 $ sudo yum remove mariadb-libs
$ sudo yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
MySQL Serverのインストール
$ sudo yum install mysql-server $ mysqld --version
MySQL Serverの起動
# サーバーの起動 $ sudo systemctl start mysqld.service # サーバーの状態の確認 $ sudo systemctl status mysqld.service
サーバーを起動しようとしたらエラーが出た...以下の記事を参考に解決
Centos7でMysqlの起動にハマった話 | Tips of Rubbish
MySQLにログイン
#rootパスワードを確認 $ sudo cat /var/log/mysqld.log | grep password #mysqlにログイン 確認したパスワードを入力 exitで終了 $ mysql -u root -p #初期設定 $ sudo mysql_secure_installation #新しいパスワードでmysqlにログインできるか確認 $ mysql -u root -p
データベースとユーザーの作成
#ログイン後 #ユーザーの作成 mysql > create user 'ユーザー名(test_user)'@'ホスト名(localhost)' identified by 'パスワード(メモしておく)'; #データベースの作成 mysql > create database データベース名(test_app); #権限の付与 mysql > grant all on *.* to 'ユーザー名(test_user)'@'ホスト名(localhost)'; #権限の変更をデータベースに反映 mysql > flush privileges; mysql > exit;
※Railsアプリのdatabase.ymlのproductionグループのデータベース名とユーザー名を対応しておく
nginxのインストール
ngiinxのインストール
$ sudo amazon-linux-extras install -y nginx1
nginxの起動
$ sudo systemctl start nginx
状態確認
$ systemctl status nginx
nginxの設定ファイル
nginxの設定ファイルは /etc/nginx に置いてある
nginxの設定ファルは以下の順で読み込まれる
/etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
nginxの設定
nginxのインストールが終わったら
nginxとRuby on Railsが連携できるように/etc/nginc/conf.dフォルダ配下に設定ファイルを作成していきます。
今回はrails.confというファイルを作成してその中に設定を記述します。
vimで作業する場合は sudo vim /etc/nginx/conf.d/rails.conf
#リバースプロキシとして扱うための設定
#ここでバックエンドのサーバーを指定する
upstream puma{
#tmp/sockets/puma.sockでpumaのsocketファイルを指定
server unix:///var/www/アプリ名/tmp/sockets/puma.sock;
}
server {
#接続を待ち受けたいポートを指定
listen 80;
listen [::]:80;
server_name puma;
#assetsファイルにアクセスされたときの設定
location ~^ /assets/ {
root /var/www/Test/public;
}
location / {
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_protp;
proxy_set_header X-Forwarded=for $proxy_add_x_forwarded_for;
proxy_pass http://puma;
}
}
ユーザーの作成、アプリを動作させるディレクトリを作成
$ sudo adduser deploy $ sudo mkdir -p /var/www $ sudo chown deploy:deploy /var/www
Railsの環境構築
ここからのデプロイ作業はデプロイユーザーに切り替えて行います。
ルートユーザーからユーザーを切り替えるのは権限の関係で
Rbenvのインストール
# インストール $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv # パスを通す $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile $ source ~/.bash_profile #プラグインのインストール $ mkdir -p "$(rbenv root)"/plugins $ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build #.bash_profileの反映 $ source ~/.bash_profile # セットアップ $ ~/.rbenv/bin/rbenv init $ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash
Rubyのインストール
$ rbenv install 2.7.3 $ rbenv global 2.7.3
Railsのインストール
$ gem install rails -v 6.1.4
アプリのダウンロード
GitHubからクローン
$ cd /var/www $ git clone ご自身のアプリ
※database.ymlのproductionのdatabaseとusernameはMySQLで設定したものを指定
ライブラリを導入
$ cd アプリ名 $ bundle install
MySQLインストール時にエラーが出た
Ruby - bundle install 時に出るエラー Gem::Ext::BuildError: ERROR: Failed to build gem native extension.|teratail
上記記事を参考に解決
シークレットキーの作成
# メモしておく $ rails secret
アプリの設定
.bash_profileに環境変数を設定していきます。
ここではRailsのシークレットキー、MySQLのパスワードなどを適宜設定します。
$ vim ~/.bash_profile export SECRET_KEY_BASE=rails secretで作成したシークレットキー export TEST_DATABASE_PASSWORD=MySQLのユーザー登録で設定したパスワード #.bash_profileの反映 $ source ~/.bash_profile
テーブルの作成
$ rails db:migrate RAILS_ENV=production
MySQLサーバーに接続できないとエラーが出た
以下の記事を参考に解決
Rails アプリ起動時のMysqlエラー を解消 (Mysql2::Error::ConnectionError ・ Can't connect to local MySQL server through socket '/tmp/mysql.sock') - Qiita
アプリの起動
ルートユーザーで作業
nginxの再起動
$ sudo systemctl restart nginx.service
アプリを起動
deployユーザー
$ cd /var/www/アプリ名 $ rails assets:precompile RAILS_ENV=production $ rails server -e production
参考にした書籍
おわりに
RailsとNginxの連携にかなり手こずってしまいました、、
Nginxの設定など詳しく分からないところが多いので今後触る機会に理解していければと思います。
次回はもっとスムーズにアプリをデプロイできるようになりたいです。
Railsのdatabase.ymlについて
はじめに
Railsのデータベース接続の中身をいまいち理解していないまま使っていたので調べてみました。
ActiveRecordでデータベースに接続するにはconfig/database.ymlに設定を定義する必要があります。
そのdatabase.ymlについて深堀りしていきたいと思います。
メモ
デフォルトの設定ファイル
railsアプリを作成したときに用意されたデフォルトのdatabase.ymlです。
この設定ファイルはMySQL仕様になっています。
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
socket: /tmp/mysql.sock
development:
<<: *default
database: MysqlTest_development
test:
<<: *default
database: MysqlTest_test
production:
<<: *default
database: MysqlTest_production
username: MysqlTest
password: <%= ENV['MYSQLTEST_DATABASE_PASSWORD'] %>
yamlについて
そもそもyamlを書いて来なかったのでyamlについて。
アンカー
&を利用することで名前を付け、定義した内容を他の場所で参照できるようにする。
&default
エイリアス
*を利用することでアンカーで定義した内容を参照する。
*default
マージする
<<を利用することでマージをする。
<<: *default
database.ymlのパラメータについて
adapter: 接続するデータベースの種類 encoding: 文字コード pool: コネクションプーリングで使用するコネクションの上限 username: ユーザー名 password: パスワード socket: ソケットファイルのパス
コネクションプールとは??
DBに接続した状態を維持したコネクションをいくつか用意ておき、それを使い回す仕組みのこと
DBとの新規接続を減らすことでパフォーマンスを向上させるらしい、、、
デフォルトでは5つのコネクションを使い回すように設定されているようです。
Railsチュートリアル第14章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
14章
14章ではフォロー機能を実装する。
フォロー機能を実装するには誰が誰をフォローしているのかという情報を保管して置く必要がある。
そこでRelationshipsモデルを生成する。
Relationshipsモデルはfollower_id(誰が)とfollowed_id(誰を)を持つテーブル
誰が誰をフォーローしているかが分かれば誰が誰にフォローされているのかが分かる。といったことを念頭に実装していく。
Relationshipsモデル
フォローしたユーザー、フォローされたユーザーが関係性を持ったデータモデルを作成していく。
ここでは次のようなモデルを作成していく
テーブル名:Relationship
フォローした人 :follower_id
フォローされた人:followed_id
例えば、
user_idが1のユーザーがuser_idが2のユーザーをフォローした場合
follwer_id = 1、followed_id = 2となる
検索の高速化のためのインデックスを追加、フォロー、フォロワーの組み合わせが一意であることを保証するために複合インデックスも追加する。
User/Relationshipの関連付け
今回作成したRelationshipモデルをUserモデルと関連付ける方法が以前と異なる
1. RelationshipモデルをActiveRelationshipモデルとして扱いたい
2.UserモデルからActiveRelationshipモデルのカラムをfollower_idをキーとして検索できるようにしたい
以上の2つが以前のモデルの関連付けのときと違うのかと思う。
1のRelationshipモデルをActiveRelationshipモデルとして扱うには
:class_nameオプションを用いる
実際のモデルを:class_nameオプションに指定することで実現する
2のUserモデルからActiveRelationshipモデルのカラムを検索するには
:foreign_keyオプションを用いる
Userモデルに外部キーを設定していないため:foreign_keyオプションで設定することで実現する
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
.
.
.
end
Userモデルと関連付ける
belongs_toでは1対1のつながりを設定する際にモデル名のシンボルを渡す
以前は以下の書き方でUserモデルとMicropostモデルを関連付けた。
そしてmicropost.user_idで関連付けられた値を取得できた。
class Micropost < ApplicationRecord belongs_to :user . . . end
しかし今回はactive_relationship.follower、active_relationship.followedを使いたい、User_idをfollower_id,follwed_idとして関連付けたいので以下のようなコードになる。
:class_nameオプションで実際のモデル名を指定する。
class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" end
関連付けを行った結果以下のメソッドが利用できるようになる
active_relationship.follower active_relationship.followed user.active_relationships.create(followed_id: other_user.id) user.active_relationships.create!(followed_id: other_user.id) user.active_relationships.build(followed_id: other_user.id)
多対多の関係を持ったモデル
多対多の関係を持たせるには、has_many :through関連付けを行う。
この関連付けをするには2つのモデルの間に第3のモデルが必要になる。
今回UserモデルとUserモデル同士の関連付けを行い、その仲介役としてActiveRelationshipモデルを設ける。
:throughオプションには仲介役のモデルをシンボルで渡す。
今回はactive_relationshipsを渡す。
参考:Active Record の関連付け - Railsガイド
次のコードでは
UserモデルがRelationshipテーブルからfollower_idを検索してフォローしているユーザーを取得できる
has_many :followeds, through: :active_relationships
例えばユーザーID1のユーザーがユーザーID2,3のユーザーをフォローしているして、user.followedでユーザー2,3のオブジェクトを(配列で)取得できる。
英語圏の問題でuser.followedsという名前は不適切なので
user.followingとしてユーザーがフォローしているユーザーを取得できるようにする。上記のコードを変更したコードが下記になる。
has_many :following, through: :active_relationships, source: :followed
:sourceでは関連付けのもとの名前を指定している。
メンバールーティング
resourcesで生成されたRESTfulな7つのルーティングに対して、必要であればルーティングを追加することができる。
resources :users で生成されたRESTfulなルーティングにメンバールーティングを追加。
resources :users do
member do
get :following, :followers
end
end
Ajax
Ajax...AsynchronousJavaScript And XML、非同期処理のこと
チュートリアルではremote: trueとして設定するように書かれていますがそのとおりに実装するとうまくいきませんでした。
Railsガイドを見るとform_withはAjaxをデフォルトで使えることを前提としているようです。
form_withは:localオプションを指定しない場合Ajaxを使うことになります。
なので対処としては、local: falseとするか、local: true自体を削除するかでAjaxを使えるようになります。
local: trueを消して動かしてみたらAjaxが機能しませんでした。Railsのバージョンが関係してくるのでしょうか。。。
Ajaxのテスト
Ajaxでのテストをするときはxhr :trueオプションを使う
assert_difference '@user.following.count', 1 do
post relationships_path, params: { followed_id: @other.id }, xhr: true
endxhr(XmlHttpRequest)オプションをtrueにすることでAjaxでリクエストを発行するようにできる。
mapメソッドの短縮表記
mapメソッド
irb(main):001:0> [1,2,3,4].map{ |i| i.to_s }
=> ["1", "2", "3", "4"]上記を短縮表記で記述
irb(main):002:0> [1,2,3,4].map(&:to_s) => ["1", "2", "3", "4"]
フォローしているユーザーのIDを配列で取り出す
irb(main):003:0> User.first.following.map(&:id) ..... => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
上記のコードは便利なのでActiveRecordeでメソッドが用意されている。
User.first.following_ids ..... => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
このメソッドはhas_many: followingの関連付けをしたときにActiveRecordが自動生成したもの。
取得したIDを文字列として連結
irb(main):005:0> User.first.following_ids.join(', ')
.....
"3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"
whereメソッド
ActiveRecordのwhereメソッドでデータベースから条件に合うデータを取得する。
マイクロソフトからフォローしているユーザーの投稿と自分の投稿を取得
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
SQLのサブセレクト
データベースの問い合わせを減らして高速化を図る
following_idsメソッドを呼ぶとSQLがデータベースに発行される。
よって以下のコードではfollowing_idsでデータベースに問い合わせたあともう一度Micropost.whereで問い合わせており合計2回の問い合わせをしている。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)そこで
following_idsをSQL文に置き換え、これをサブセレクトとして使う。
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"そしてこのfollowing_idsを利用してデータベースに問い合わせる
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
Micropost.where("user_id IN (#{following_ids})
OR user_id = :user_id", user_id: id)このように書くことでデータベースへの問い合わせが1回になり高速化、効率化につながる。
演習
14.1.1
演習1
図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
id=1のユーザーはidが2、7、8、10のユーザーをフォローしている。
よって、user.following.map(&:id)を実行するとid = 2, 7, 8, 10 の配列を返す。
演習2
図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
どのユーザーがid=2のユーザーに対してuser.followingを実行したのかが書かれていないのでid=1のユーザーがid=2のユーザーをフォローしたとする。
id=1のユーザーはid=2のユーザーをすでにフォローしているのでフォローできない。
user.following.map(&:id)を実行するとid=1がフォローしているユーザーid = 2,7,8,10 の配列を取得する。
14.1.2
演習1
コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください
rb(main):001:0> user1 = User.first
(0.8ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.or...
irb(main):002:0> user2 = User.second
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Robin Treutel", email: "example-1@railstutorial...
irb(main):003:0> active_relationship = user1.active_relationships.create!(fo
llowed_id: user2.id)
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Relationship Create (1.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2021-07-20 16:32:13.643703"], ["updated_at", "2021-07-20 16:32:13.643703"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2...
irb(main):004:0> Relationship.first
Relationship Load (0.7ms) SELECT "relationships".* FROM "relationships" ORDER BY "relationships"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2021-07-20 16:32:13.643703000 +0000", updated_at: "2021-07-20 16:32:13.643703000 +0000">
演習2
先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
irb(main):005:0> active_relationship.followed => #<User id: 2, name: "Robin Treutel", email: "example-1@railstutorial.org", created_at: "2021-07-16 10:22:37.201055000 +0000", updated_at: "2021-07-16 10:22:37.201055000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$nlGp6dNoTuBZTTqfYFVKb.VG/meRRlIX8VfYVg2P7FR...", activated: true, activated_at: "2021-07-16 10:22:36.947268000 +0000", reset_digest: nil, reset_sent_at: nil> irb(main):006:0> active_relationship.follower => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-16 10:22:36.621010000 +0000", updated_at: "2021-07-16 10:22:36.621010000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$3UocKEklYfAfEG9ow377meLe8YO9E7pdrWIuscmrghY...", activated: true, activated_at: "2021-07-16 10:22:36.362398000 +0000", reset_digest: nil, reset_sent_at: nil>
14.1.5
演習1
irb(main):001:0> user = User.first
(0.8ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.or...
irb(main):002:1* (2..5).each do |n|
irb(main):003:1* User.find(n).follow(user)
irb(main):004:0> end
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Relationship Create (0.5ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 2], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.930956"], ["updated_at", "2021-07-21 15:02:26.930956"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
TRANSACTION (0.0ms) SAVEPOINT active_record_1
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
Relationship Create (0.8ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 3], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.935993"], ["updated_at", "2021-07-21 15:02:26.935993"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]]
TRANSACTION (0.0ms) SAVEPOINT active_record_1
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]]
Relationship Create (0.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 4], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.939977"], ["updated_at", "2021-07-21 15:02:26.939977"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]MIT ? [["id", 5], ["LIMIT", 1]]
Relationship Create (0.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 5], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.944312"], ["updated_at", "2021-07-21 15:02:26.944312"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
=> 2..5
irb(main):005:0> user.followers.map(&:id)
User Load (0.4ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> [2, 3, 4, 5]
演習2
上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
irb(main):006:0> user.followers.count (0.7ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 4
演習3
user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
結果は演習2と同じ4となっている。
user.followers.to_a.countとするとuserをフォローしているユーザーの情報を一度配列にしてメモリ上に保存する。なのでフォローワーがたくさんいると配列を生成する無駄な処理が増え、また無駄なメモリ資源も出てしまう。
irb(main):007:0> user.followers.to_a.count => 4
14.2.2
演習3
Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
リンクがあるかテスト
test "layout links when logged in user" do
log_in_as(@user)
get root_path
assert_template "static_pages/home"
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
assert_select "a[href=?]", users_path
assert_select "a[href=?]", user_path(@user)
assert_select "a[href=?]", edit_user_path(@user)
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", login_path, count: 0
#演習
assert_select "a[href=?]", following_user_path(@user)
assert_select "a[href=?]", followers_user_path(@user)
end統計情報が表示されているかテスト
test "profile display" do
get user_path(@user)
assert_template 'users/show'
assert_select 'title', full_title(@user.name)
assert_select 'h1', text: @user.name
assert_select 'h1>img.gravatar'
assert_select 'div.stats'
assert_match @user.microposts.count.to_s, response.body
assert_select 'div.pagination', count: 1
@user.microposts.paginate(page: 1).each do |micropost|
assert_match micropost.content, response.body
end
end
end
14.2.6
演習1
リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
format.htmlの行をコメントアウトして行ったテストでエラーが出た。
エラーになったテストケース
test_should_follow_a_user_the_standard_way
test_should_unfollow_a_user_the_standard_way
format.jsの行をコメントアウトして行ったテストではエラーが出なかった。
演習2
リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。
※演習文が理解しづらかったので、自分なりの解釈での解答を記述します。
test_should_follow_a_user_the_standard_wayのxhr: trueがある行を削除してテストを行った。結果はRED(failures:1)だった。
問題について
・演習1でformat.htmlをコメントアウトしてエラーが出た原因
ActionController::UnknownFormatエラーが出ていた
つまり原因としてはアクションに対応するviewファイルがないとのこと
・xhr: trueのある行をを削除したらfailsが出た原因
ブロック内の処理が行われていないから
この2つの問題から起因することを考えてみましたが思いつきませんでした。
14.3.1
演習1
マイクロポストのidが正しく並んでいると仮定して(すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。
user.feedで取り出した投稿のIDを格納した配列が表示される。
またdefault_scopeで投稿日時を降順に取得するようにしているので結果は次のようになる。
[ 10, 9, 7, 5, 4, 2, 1 ]
14.3.2
演習1
リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
フォローしているユーザーの投稿を取得
Micropost.where("user_id IN (?)", following_ids )結果がREDになったテスト
FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010e2c5970 @name="UserTest">, 0.465544999926351]
test_feed_should_have_the_right_posts#UserTest (0.47s)
Expected false to be truthy.
test/models/user_test.rb:99:in `block (2 levels) in <class:UserTest>'
test/models/user_test.rb:98:in `block in <class:UserTest>'
FAIL["test_micropost_interface", #<Minitest::Reporters::Suite:0x000000010bd23398 @name="MicropostsInterfaceTest">, 1.2411800000118092]
test_micropost_interface#MicropostsInterfaceTest (1.24s)
Expected exactly 1 element matching "div.pagination", found 0..
Expected: 1
Actual: 0
test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'
演習2
リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
フォローしているユーザー以外の投稿を取得
Micropost.where("user_id IN (?)", id)結果がREDになったテスト
FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010d851ca0 @name="UserTest">, 0.41781200002878904]
test_feed_should_have_the_right_posts#UserTest (0.42s)
Expected false to be truthy.
test/models/user_test.rb:99:in `block (2 levels) in <class:UserTest>'
test/models/user_test.rb:98:in `block in <class:UserTest>'
FAIL["test_micropost_interface", #<Minitest::Reporters::Suite:0x000000010d017f08 @name="MicropostsInterfaceTest">, 1.1911460000555962]
test_micropost_interface#MicropostsInterfaceTest (1.19s)
Expected exactly 1 element matching "div.pagination", found 0..
Expected: 1
Actual: 0
test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'
演習3
リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
すべてのユーザーの投稿を取得する。
Micropost.all
結果がREDになったテスト
FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010805ca40 @name="UserTest">, 1.4157729999860749]
test_feed_should_have_the_right_posts#UserTest (1.42s)
Expected true to be nil or false
test/models/user_test.rb:103:in `block (2 levels) in <class:UserTest>'
test/models/user_test.rb:102:in `block in <class:UserTest>'
14.3.3
演習1
Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
micropost.contentをエスケープして、マイクロソフトが表示されることを確認
test "feed on Home page" do
get root_path
@user.feed.paginate(page: 1).each do |micropost|
assert_match CGI.escapeHTML(micropost.content), response.body
end
end
演習2
リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています(このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能(Cmd-FもしくはCtrl-F)を使って「sorry」を探すと原因の究明に役立つはずです。
エスケープしないと改行文字や記号が特殊文字で出力されるため。
I'm sorry. Your words made sense, but your sarcastic tone did not.\n
Railsチュートリアル第13章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
マイクロソフトテーブルのインデックス
マイクロポストテーブルのuser_idとcreated_atにインデックスが追加されている
class CreateMicroposts < ActiveRecord::Migration[6.0]
def change
create_table :microposts do |t|
t.text :content
t.references :user, foreign_key: true
t.timestamps
end
add_index :microposts, [:user_id, :created_at]
end
enduser_idに関連づけられたマイクロポストを作成時刻の逆順で取り出しやすくするためにインデックスを追加。
また、user_idとcreated_atをひとつの配列に含めているのは、両方のキーを同時に扱う複合キーインデックスを作成するため。
作られたマイクロポストモデル
外部キーとしてuserのidを指定したため
生成されたマイクロソフトモデルにはbelongs_to :userでユーザーモデルに関連づけられている
class Micropost < ApplicationRecord belongs_to :user end
モデルを関連づける
belongs_to:1対1の関係性を持たせる
has_many :1対多の関係性を持たせる
例
1対1:ひとつの投稿は1人のユーザーによって投稿される
1対多:一人のユーザーは複数の投稿を行うことができる
モデルを関連づけることで使えるメソッド
micropost.user user.microposts user.microposts.create user.microposts.create! user.microposts.build #newするのと同様 user.microposts.find_by()
紐付いているユーザーを通してマイクロポストを作成
こうして作られたマイクロポストは外部キーであるuser_idは自動的に生成元のユーザーのIDに設定される
デフォルトスコープ
データベースからデータを取り出す際に絞り込みや順序を指定することができる
これはデフォルトでのSQLを変更するため、使用する際には注意が必要とのこと
参考:Railsのdefault_scopeは悪ではない。 - Qiita
Dependent: destroy
1対多の関係の1側の要素が削除された時に同時に関連づけられた多の要素も削除される
ユーザーを削除した際に関連づけられている全ての投稿も削除される
class User < ApplicationRecord has_many :microposts, dependent: :destroy . . . end
SQLインジェクション
SQL文の組み立てに問題があり脆弱性がある場合、この問題を利用して攻撃を行うこと。
参考:安全なウェブサイトの作り方 - 1.1 SQLインジェクション:IPA 独立行政法人 情報処理推進機構
対策
直接SQL文を書かないようにすることでSQLインジェクションの対策をする
Micropost.where("user_id = ?", id)
画像のアップロード
ActiveStorageを用いて画像のアップロードを行う
ファイルをクラウドストレージへのアップロードや、Active Recordオブジェクトに結びつける機能を提供する。
ActiveStorageは画像ファイル、平文、PDFなどを取り扱える
参考:Active Storage の概要 - Railsガイド
準備
1. ActiveStorage用マイグレーションファイルを生成
$ rails active_storage:install
2. マイグレーションファイルを実行
$ rails db:migrate
3. ファイルをモデルに結びつける
has_one_attachedメソッドでアップロードされたファイルとモデルを結びつける。
imageを1対1でモデルと結びつける
class モデル < ApplicationRecord ・・ has_one_attached :image ・・ end
1対多で結びつけたい場合has_many_attachedメソッドを利用
4. ファイルを受けつける
<%= f.file_field :image %>
5. ファイルを受け取る
・ストロングパラメータで:imageを許可する
・ActiveStorageのattachメソッドを利用してファイルを受け取る
micropost.image.attach(params[:micropost][:image])
6. ファイルを表示する
<%= image_tag micropost.image if micropost.image.attached? %>
micropost.image.attached?ではマイクロポストがimageを持っているかを調べる。
画像のバリデーション
Active Storageにはバリデーション機能がネイティブでサポートされていない。
そこで'active_storage_validations'gemを追加して設定を行う。
画像フォーマットのバリデーション
content_type: { in: %w[image/jpeg image/gif image/png],
message: "must be a valid image format" }画像サイズのバリデーション
size: { less_than: 5.megabytes,
message: "should be less than 5MB" }
画像のリサイズ
表示する画像のサイズを設定する。
1. ImageMagickを開発環境にインストール
brew install imagemagick
2. gemを追加
gem 'image_processing', '1.9.3' gem 'mini_magick', '4.9.5'
3. インストール
$ bundle install
4. 変換済み画像を作成
保存されている画像を加工
image.variant(resize_to_limit: [500, 500])
これをimage_tagで表示
テストでfixutureで定義されたファイルを使う
/test/fixturesにテストで利用したい画像を追加する
fixuture_file_uploadメソッドを利用してfixtureで定義されたファイルをアップロードしてこれをテストで利用する
演習
13.1.4
演習1
Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。
作成日時を降順に設定したため
最後の投稿より最初の投稿の方が新しい
irb(main):001:0> Micropost.first.created_at (0.7ms) SELECT sqlite_version(*) Micropost Load (1.0ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ? [["LIMIT", 1]] => Fri, 16 Jul 2021 00:10:48.658871000 UTC +00:00 irb(main):002:0> Micropost.last.created_at Micropost Load (0.5ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ? [["LIMIT", 1]] => Thu, 15 Jul 2021 12:28:01.015111000 UTC +00:00
演習2
Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか? ヒント: それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。
デフォルトスコープで設定したorder byが追加されている
ORDER BY "microposts"."created_at" DESC
irb(main):003:0> Micropost.first Micropost Load (0.8ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ? [["LIMIT", 1]] => #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2021-07-16 00:10:48.658871000 +0000", updated_at: "2021-07-16 00:10:48.658871000 +0000">
演習3
データベース上の最初のユーザーを変数userに代入してください。そのuserオブジェクトが最初に投稿したマイクロポストのidはいくつでしょうか? 次に、destroyメソッドを使ってそのuserオブジェクトを削除してみてください。削除すると、そのuserに紐付いていたマイクロポストも削除されていることをMicropost.findで確認してみましょう。
ユーザーが削除されたら削除されたユーザーに関連付けられていた投稿も削除されていることを確認
irb(main):001:0> user = User.first
(0.6ms) SELECT sqlite_version(*)
User Load (0.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.6778...
irb(main):002:0> user.microposts.first.id
Micropost Load (0.8ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> 2
irb(main):003:0> user.destroy
TRANSACTION (0.2ms) SAVEPOINT active_record_1
Micropost Load (0.3ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC [["user_id", 1]]
Micropost Destroy (0.7ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 2]]
User Destroy (1.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 1]]
TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.677832000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$zTggchTpdRrUiMmX/Af1DuLf2Zm8ASlZ079q5wV7WuR...", activated: true, activated_at: "2021-07-14 08:39:30.948680000 +0000", reset_digest: "$2a$12$VdwivaGX9p8cfpggYPhIE.DO4hXpXpnGwRhRM41M0lI...", reset_sent_at: "2021-07-15 05:42:22.677743000 +0000">
irb(main):004:0> Micropost.find(2)
Micropost Load (0.3ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["id", 2], ["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):4
ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=2)
13.2.1
演習1
7.3.3で軽く説明したように、今回ヘルパーメソッドとして使ったtime_ago_in_wordsメソッドは、Railsコンソールのhelperオブジェクトから呼び出すことができます。このhelperオブジェクトのtime_ago_in_wordsメソッドを使って、3.weeks.agoや6.months.agoを実行してみましょう。
irb(main):002:0> helper.time_ago_in_words(3.weeks.ago) => "21 days" irb(main):003:0> helper.time_ago_in_words(6.month.ago) => "6 months"
演習2
helper.time_ago_in_words(1.year.ago)と実行すると、どういった結果が返ってくるでしょうか?
irb(main):006:0> helper.time_ago_in_words(1.years.ago) => "about 1 year"
演習3
micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードにあるように、まずはpaginateメソッド(引数はpage: nil)でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。
b(main):009:0> microposts = Micropost.paginate(page: nil) (3.9ms) SELECT sqlite_version(*) Micropost Load (0.4ms) SELECT "microposts".* FROM "microposts" /* loading for inspect */ ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ? [["LIMIT", 11], ["OFFSET", 0]] => #<ActiveRecord::Relation []> irb(main):010:0> microposts.class => Micropost::ActiveRecord_Relation
13.2.2
演習1
(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。
irb(main):001:0> (1..10).to_a.take(6) => [1, 2, 3, 4, 5, 6]
演習2
先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。
irb(main):002:0> (1..10).take(6) => [1, 2, 3, 4, 5, 6]
演習3
Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント(英語)を眺めながら画面に出力する方法を学び、実際に架空の大学名やHipster IpsumやChuck Norris facts(参考: チャック・ノリスの真実)を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもあります。ぜひ遊んでみてください。)
ドラゴンボールのFakerがあったので試してみました笑
content = Faker::JapaneseMedia::DragonBall.character
13.2.3
演習2
リスト 13.28にあるテストを変更して、will_paginateが1度のみ表示されていることをテストしてみましょう。ヒント: 表 5.2を参考にしてください。
assert_select 'div.pagination', count: 1
13.3.1
演習1
なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。
DRY原則に反するため。
13.3.2
演習1
Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。
views/static_pages/home.html.erb
<% if logged_in? %> <%= render 'static_pages/logged_in_user' %> <% else %> <%= render 'static_pages/not_logged_in_user' %> <% end %>
views/static_pages/_logged_in_user.html.erb
<div class="row">
<aside class="user_info">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
</div>views/static_pages/_not_logged_in_user.html.erb
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>
<%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"),
"https://rubyonrails.org/" %>
演習2
マイクロポストを投稿した直後に、ブラウザの更新ボタンを押すとエラーが表示されます。なぜエラーが表示されるのでしょうか?その原因を考えてみましょう。
マイクロポストを投稿するとPOSTリクエストが/micropostsに送信される。
micropostsコントローラーでは投稿に失敗した時renderメソッドを用いてhomeページが表示される。しかしrenderでは'static_pages/home'を表示しただけでURLを更新していない。つまり投稿に失敗した場合URLは/micropostsの状態になる。
なので/micropostsの状態でページの更新をすると/micropostsに対してGETリクエストが送信され、現段階ではリクエストに対するルートが指定されていないためルーティングエラーになる。
13.4.1
演習2
リスト 13.64に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください(リスト 13.63)。リスト 13.64で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18 。ヒント: image属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。
test "micropost interface" do
log_in_as(@user)
get root_path
assert_select "div.pagination", count: 1
assert_select "input[type=file]"
#無効な送信
assert_no_difference "Micropost.count" do
post microposts_path, params: { micropost: { content: ""} }
end
assert_template 'static_pages/home'
assert_select 'div#error_explanation'
assert_select 'a[href=?]', '/?page=2' #正しいページネーションリンク
#有効な送信
content = "This micropost really ties the room toghether"
image = fixture_file_upload('test/fixtures/kitten.jpg', 'image/jpeg')
assert_difference "Micropost.count", 1 do
post microposts_path, params: { micropost: { content: content, image: image } }
end
assert assigns(:micropost).image.attached?
assert_redirected_to root_url
follow_redirect!
assert_match content, response.body
#投稿を削除する
assert_select 'a', text: 'delete'
first_micropost = @user.microposts.paginate(page: 1).first
assert_difference "Micropost.count", -1 do
delete micropost_path(first_micropost)
end
#違うユーザーのプロフィールにアクセス
get user_path(users(:archer))
assert_select 'a', text: 'delete', count: 0
end
Railsチュートリアル第12章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
演習
12.1.1
演習2
表 12.1の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: アカウント有効化で行った演習(11.1.1.1)と同じ理由です。
メール本文の絶対リンクからリクエストを送るため
12.1.2
12.3.3
演習1
リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.20に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう(これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 green になることも確認してください。ちなみにリスト 12.20にあるコードには、前章の演習(リスト 11.39)の解答も含まれています。
2回のデータベースへの問い合わせをupdate_columnsを使って1回に
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
演習2
リスト 12.21のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐(リスト 12.16)を統合テストで網羅してみましょう(12.21 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです)。期限切れをテストする方法はいくつかありますが、リスト 12.21でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます(なお、大文字と小文字は区別されません)。
test "expred 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
演習3
2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。しかし、もっと良くする方法はまだあります。例えば、公共の(または共有された)コンピューターでパスワード再設定が行われた場合を考えてみてください。仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまいます(しかもそのままログイン機構まで突破されてしまいます!)。この問題を解決するために、リスト 12.22のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう5 。
def update
if params[:user][:password].empty?
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update(user_params)
log_in @user
@user.update_attribute(:reset_digest, nil)
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
おわりに
メールの生成送信は前章と似た内容でした
今回はパスワード再設定のテストを分岐通りにテストしました。
分岐が多いとテストを書くの大変なのがわかりました。
残すはマイクロポストの投稿機能とフォロー機能となりました。
引き続き頑張ります!
Railsチュートリアル第11章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
メールアドレスを利用したアカウントの有効化
ユーザーの新規登録の際にメールアドレスが登録したユーザーのものなのかを確認できるようにする
before_createコールバック
オブジェクトが作成される前に処理を実行する
before_create :create_activation_digest
上記のコードはメソッド参照と呼ばれるものでオブジェクトを生成する前にcreate_acrivation_digestを実行する
before_createのタイミング
チュートリアル内で
”before_createコールバックの方はユーザーが作成される前に呼び出される” "User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになります"
と書かれており、User.newでオブジェクトを生成した時点でbefore_createコールバックが実行されると思ったが違ったようなので記述しておきます。
createメソッドとは
モデルオブジェクトを生成して保存するという一連の処理のこと
ユーザーを作成する = ユーザーオブジェクトを生成して保存する
とのことだったようです。
before_saveとbefore_create どっちが先に呼び出される??
before_saveの方が先に呼び出される
オブジェクトの作成の際に呼び出される順序は以下のようになります
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
参考:Active Record コールバック - Railsガイド
”保存”を起点にコールバックが処理される?
参考:ActiveRecordのコールバックの順序・コールバック内のロールバック処理について - Hack Your Design!
上記の記事を参考にコールバックを可視化してみました。
コールバックを可視化するためのコード
class User < ActiveRecord::Base
before_validation -> { puts "before_validation is called" }
after_validation -> { puts "after_validation is called" }
before_save -> { puts "before_save is called" }
before_update -> { puts "before_update is called" }
before_create -> { puts "before_create is called" }
after_create -> { puts "after_create is called" }
after_update -> { puts "after_update is called" }
after_save -> { puts "after_save is called" }
after_commit -> { puts "after_commit is called" }
end新規レコード作成時
Userモデルをnewしてsave
Userモデルをcreate する
irb(main):003:0>user = User.new(name:"yuy", email: "yuy@yuy.yuy", password: "foobar", pa
ssword_confirmation: "foobar" )
=> #<User id: nil, name: "yuy", email: "yuy@yuy.yuy", created_at: nil, updated_at: n...
irb(main):004:0> user.save
before_validation is called
TRANSACTION (0.2ms) SAVEPOINT active_record_1
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuy@yuy.yuy"], ["LIMIT", 1]]
after_validation is called
before_save is called
before_create is called
User Create (1.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "yuy"], ["email", "yuy@yuy.yuy"], ["
after_create is called
after_save is called
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
after_commit is called
=> true
irb(main):005:0> User.create(name:"yuyy", email: "yuyy@yuy.yuy", password: "foobar", pass
word_confirmation: "foobar" )
before_validation is called
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuyy@yuy.yuy"], ["LIMIT", 1]]
after_validation is called
before_save is called
before_create is called
User Create (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "yuyy"], ["email", "yuyy@yuy.yuy"], ["created_at", "2021-07-14 01:09:11.939377"], ["updated_at", "2021-07-14 01:09:11.939377"], ["password_digest", "$2a$12$UVy2HGBnjaIMadalnA.OmuMzdCYLQGJTnkgaFwWZB4b7mOk6uEFoi"]]
after_create is called
after_save is called
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
after_commit is called
=> #<User id: 105, name: "yuyy", email: "yuyy@yuy.yuy", created_at: "2021-07-14 01:09:11.939377000 +0000", updated_at: "2021-07-14 01:09:11.939377000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil>モデルをnewした時点ではコールバックが呼び出されていません。
save、createするとコールバックが呼び出されるようです。
before_save、before_createどちらも呼び出されてますね。
また、before_updateとafter_update呼び出されていません。
レコード更新時
ユーザーモデルをupdateする
irb(main):006:0> user.update(name: "yuuuuy") before_validation is called TRANSACTION (0.1ms) SAVEPOINT active_record_1 User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "yuy@yuy.yuy"], ["id", 104], ["LIMIT", 1]] after_validation is called before_save is called before_update is called User Update (0.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "yuuuuy"], ["updated_at", "2021-07-14 01:26:17.018216"], ["id", 104]] after_update is called after_save is called TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 after_commit is called => true
こちらは更新なのでbefore_createとafter_createは呼び出されていません。
Action Mailer
アプリケーションでメールの送受信を行えるようにするライブラリ
メイラーの作成
メイラーを生成する
account_activationメイラーとpassword_resetメイラーを生成
$ rails generate mailer UserMailer account_activation password_reset
メイラーごとにテキスト用のメールテンプレートとHTMLメール用のテンプレートが生成される
Running via Spring preloader in process 84968
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
create app/views/user_mailer/account_activation.text.erb
create app/views/user_mailer/account_activation.html.erb
create app/views/user_mailer/password_reset.text.erb
create app/views/user_mailer/password_reset.html.erb
invoke test_unit
create test/mailers/user_mailer_test.rb
create test/mailers/previews/user_mailer_preview.rb
メールに記述するURLの生成
メールを利用したアカウントの有効化では
メールアドレスと有効化トークンのダイジェストを利用してユーザーが本人であるかデータベースから参照して有効化を行う
そこで送るメールに記載するURLにメールアドレスと有効化トークンのダイジェストを含ませておく
URLの生成
edit_account_activation_url(@user.activation_token, email: @user.email)
上記の名前付きルートから生成されるURLの例
email部分はRailsが自動的にエスケープした文字列を生成
account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
送信メールのプレビュー
メールのメッセージのプレビューをその場で見ることができる
config/environments/development.rbを変更する
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = false
host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
# クラウドIDEの場合は以下をお使いください
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
# localhostで開発している場合は以下をお使いください
# config.action_mailer.default_url_options = { host: host, protocol: 'http' }
.
.
.
end
受け取ったパラメータに応じて呼び出すメソッドを切り替える
# トークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if self.remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end
remember部分を変数として扱いたい
self.FOOBAR_digest
変数として扱うことで引数に応じてメソッドを切り替えることができる
メタプログラミング
プログラムでプログラムを作成すること
sendメソッドで実現する
>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
演習
11.1
演習2
表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
ページ外部のメールからリクエストを送りたいから絶対リンクを使う
11.1.2
演習2
コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
yuy@yu sample_app % rails c --sandbox
Running via Spring preloader in process 1705
Loading development environment in sandbox (Rails 6.1.4)
Any modifications you make will be rolled back on exit
irb(main):001:0> User.create(name: "User",email: "user@railstutorial.org",
password: "foobar", password_confirmation: "foobar", admin: false, activated
: false, activated_at: Time.zone.now)
(0.5ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "user@railstutorial.org"], ["LIMIT", 1]]
User Create (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest", "activated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["name", "User"], ["email", "user@railstutorial.org"], ["created_at", "2021-07-13 23:49:56.915739"], ["updated_at", "2021-07-13 23:49:56.915739"], ["password_digest", "$2a$12$u9f0aKuqXqZFCQXrWyMTa.U6nXimKEX8JA9HnkIbS4b4GXbVSfaJe"], ["activation_digest", "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"], ["activated_at", "2021-07-13 23:49:56
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 103, name: "User", email: "user@railstutorial.org", created_at: "2021-07-13 23:49:56.915739000 +0000", updated_at: "2021-07-13 23:49:56.915739000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJm...", activated: false, activated_at: "2021-07-13 23:49:56.643115000 +0000">
irb(main):002:0> User.last.create_activation_digestC LIMIT ? [["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x0000000143b4a688>)
Did you mean? restore_activation_digest!
irb(main):003:0> User.last.activation_digest
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=> "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"
演習3
リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
#メールアドレスを全て小文字にする
def downcase_email
self.email.downcase!
end
11.3.1
演習1
コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
irb(main):001:0> user = User.create(name:"yuy", email: "yuy@yuy.yuy", passwo
rd: "foobar", password_confirmation: "foobar" )
(0.7ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
TRANSACTION (0.0ms) SAVEPOINT active_record_1
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuy@yuy.yuy"], ["LIMIT", 1]]
User Create (1.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "yuy"], ["email", "yuy@yuy.yuy"], ["created_at", "2021-07-14 02:20:18.142063"], ["updated_at", "2021-07-14 02:20:18.142063"], ["password_digest", "$2a$12$2VL3d7UILjexu2t7rmJXiuiyE0jxKbcxr65XsBcW1qIL2/NwqmyPi"], ["activation_digest", "$2a$12$BpedetJAqun5id9xd68UKeT8pC/IYscPU2D5Wk3w7STe8pDSkOsFS"]]
TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 104, name: "yuy", email: "yuy@yuy.yuy", created_at: "2021...記憶トークンと記憶ダイジェストを更新
irb(main):002:0> user.remember TRANSACTION (0.2ms) SAVEPOINT active_record_1 User Update (0.1ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2021-07-14 02:23:13.938806"], ["remember_digest", "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"], ["id", 104]] TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 => true irb(main):003:0> user.remember_token => "6s19ZgORpmwTfsAq1uOU4g" irb(main):004:0> user.remember_digest => "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"
演習2
リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
irb(main):007:0> user.authenticated?(:remember, user.remember_token) => true
11.3.3
演習1
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
update_columnsの使い方には注意
def activate
update_columns(activated: true, activated_at: Time.zone.now)
end
演習2
現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
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
演習3
ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
有効でないユーザが表示されないか、表示できないかのテスト
test "should not show non activated user" do
log_in_as(@admin)
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
おわりに
アカウントの有効化を実装しました。
普段サービスを利用する際にユーザー登録をすると届くメールはこんなふうに作られているんだ、と楽しみながら実装ができました。
今回コールバックでは、はてなが浮かんで納得しきれない部分があったのでコールバックの内容が多めです笑
Railsチュートリアル第10章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
パスワードが空のままでもユーザー情報が編集できるようにする
パスワードのvalidatesにallow_nil: trueを追加する。
ユーザーの新規作成時にはhas_secure_passwordがpasswordとpassword_confirmationの存在性を検証するようになっているため、ユーザーの新規作成時に空のパスワードが有効になることはない。
認可と認証
認証(authentication):サイトのユーザーを識別すること
認可(authorization) :ユーザーが実行可能な操作を管理すること
セキュリティモデル
ログインしたユーザーのみがユーザの情報を変更できるようにする、ログインしたユーザーのみが見ることができるページを設定するような仕組み
Railsではbeforeフィルターを用いて実現する。
フレンドリーフォワーディング
ログイン成功時に元々行きたかったページに転送させる機能
ログインをせずに保護されたページを開いた場合ログイン画面に移動させられ、
移動させられたログイン画面でユーザーがログインすると保護された開きたかったページに転送する仕組み
実装の流れ
ログインしていないユーザーが保護されたページにGETリクエストを送った場合
session変数を利用して開きたかったページの情報を保存しておく
session[:forwarding_url] = request.original_url if request.get?
実際にログインした後にリダイレクトさせるには
redirect_to session[:forwarding_url]
リダイレクトした後に保存しておいた情報を削除することを忘れずに
削除しないと次回ログインした時に保存されたページに転送されてしまう。
session.delete(:forwarding_url)
ページネーション
ひとつのページに決められたユーザー数だけを表示するような仕組み
実装の流れ
gemファイルをインストール
will_paginate gem
bootstrap-will_paginate gem
表示させたいオブジェトをページネーションが理解できるオブジェクトに置き換える
@users = User.paginate(page: params[:page])
ビューにwill_paginateメソッドを追加する
サンプルユーザーの生成
データベースにサンプルデータを生成する
db/seeds.rbファイルに記述。
# メインのサンプルユーザーを1人作成する
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)
endcreate!はcreateアクションと同じ動きをするがcreate!では無効なデータを生成する場合例外を発生させる。
サンプルデータを生成
$ rails db:seed
role
あるユーザーだけにある処理を実行できる権限を与えること
DELETEリクエストの偽造
ブラウザはネイティブでDELETEリクエストを送信できない
RailsではJavaScriptを使ってDELETEリクエストを偽造する
よって、JavaScriptがオフになっているとDELETEのリンクが無効になる
このような場合フォームとPOSTリクエストを使ってDELETEリクエストを偽造することができる
演習
10.1.1
演習1
先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
<div class="gravatar_edit">
<%= gravatar_for(@user) %>
<a href="https://gravatar.com/emails" target="_blank", rel="noopener">change</a>
</div>
10.1.3
演習1
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'
assert_select "div.alert-danger", "The form contains 4 errors"
end
10.2.2
演習1
何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
自分以外のユーザーの情報を悪意のあるユーザーに見られる、あるいは書き換えられる危険性があるから。
演習2
上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
editアクション
URLのID部の値を書き換えるだけで他のユーザーの編集画面を開くことができるから。
10.2.3
演習1
test "successful edit with friendry forwarding" do
get edit_user_path(@user)
assert_equal session[:forwarding_url], edit_user_url(@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_url @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
10.3.1
演習1
レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。
test "layout links when logged in user" do
log_in_as(@user)
get root_path
assert_template "static_pages/home"
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
assert_select "a[href=?]", users_path
assert_select "a[href=?]", user_path(@user)
assert_select "a[href=?]", edit_user_path(@user)
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", login_path, count: 0
end
10.3.4
演習2
先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select "div.pagination", count: 2
User.paginate(page: 1).each do |user|
assert_select "a[href=?]", user_path(user), text: user.name
end
end
10.4.1.2
演習1
Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します( 6.1.5)。
test "should not allow the admin attribute to be edited via the web" do
log_in_as(@other_user)
assert_not @other_user.admin?
patch user_path(@user), params: { user: { password: "password",
password_digest: "password", admin: true } }
assert_not @other_user.reload.admin?
end
おわりに
この章は前章に比べて理解しやすかったです。
9章が鬼門なのかな、、