何をするのか
Railsチュートリアル本文13.3.3にて、「ユーザー自身のポストを含むマイクロポストのフィード」を実装しました。さらに、14章ここまでで「ユーザーのフォロー」という機能も実装しました。今度は「ユーザー自身のポストと、フォローしているユーザーのポストの両方を含むマイクロポストのフィード」を実装しよう、というわけです。
ステータスフィードの完成形は、Railsチュートリアル本文においては、図 14.21にて示されています。
動機と計画
別記事にて解説しています。
Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード実装の動機と計画(データモデルの解説とテストの実装)
演習 - 動機と計画
1. マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)
を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。
ヒント: 13.1.4で実装した
default_scope
を思い出してください。
そもそもuser.feed.map(&:id)
というのは、「ステータスフィードに含まれる全てのマイクロポストのidが含まれる配列を返す」というメソッドチェーンです。また、Micropostsモデルのdefault_scope
の実装が以下のようになっているため、「ステータスフィードに含まれるマイクロポストは、作成日時の降順に整列された状態である」というのもポイントです。
class Micropost < ApplicationRecord
belongs_to :user
default_scope -> { order(created_at: :desc) }
#...略
end
本演習で出てくる図 14.22のuser.feed
を前提とし、また、「単純にidが大きくなるごとにマイクロポストの作成日時も新しくなっていく」という関係が成り立つとすると、user.feed.map(&:id)
の戻り値は以下のようになると想定されます。
>> user.feed.map(&:id)
=> [10, 9, 7, 5, 4, 2, 1]
フィードを初めて実装する
別記事にて解説しています。
Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - フィードを初めて実装する
演習 - フィードを初めて実装する
別記事にて解説しています。
Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フィードを初めて実装する」
サブセレクト
別記事にて解説しています。
Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - サブセレクト
演習 - サブセレクト
1. Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
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
上記コードの動作におけるポイントは以下です。
- マイクロポストの内容のみを得るには、
micropost.content
とする -
assert_match
メソッドで、期待するマイクロポストの内容がresponse.body
に含まれていることをテストする- 期待するマイクロポストは、「
@user.feed.paginate(page: 1)
に含まれるマイクロポスト」である
- 期待するマイクロポストは、「
# <Micropost id: 300, content: "Accusamus veniam voluptatibus voluptatum sapiente ...", user_id: 6, created_at: "2020-01-29 23:14:31", updated_at: "2020-01-29 23:14:31", picture: nil>
CGI.escapeHTML
というメソッドは、上記Micropostオブジェクトの内容そのものを受け取ることはできません。本当に必要なのはmicropost.content
ということですね。
では、実際に当該テストを実行してみましょう。
# rails test test/models/user_test.rb
Running via Spring preloader in process 560
Started with run options --seed 933
15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.28291s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
無事テストが通りました。
2. リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTML
メソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escape
と同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。
ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。
上記のようにdebugger
を挿入し、response.body
の内容を見てみることとします。
# rails test test/integration/following_test.rb
Running via Spring preloader in process 110
Started with run options --seed 1594
7/2: [========== ] 28% Time: 00:00:04, ETA: 00:00:13
[58, 67] in /var/www/sample_app/test/integration/following_test.rb
58: end
59:
60: test "feed on Home Page" do
61: get root_path
62: @user.feed.paginate(page: 1).each do |micropost|
63: debugger
=> 64: assert_match CGI.escapeHTML(micropost.content), response.body
65: end
66: end
67: end
(byebug) response.body
すると、response.body
には以下のような文字列が含まれているのがわかります。
I'm sorry. Your words made sense, but your sarcastic tone did not.
上記の文字列は、test/fixtures/microposts.yml
の以下の部分に相当します。
tone:
content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
created_at: <%= 10.minutes.ago %>
user: :rusami
以下がポイントです。
-
response.body
の内容はERBのテンプレートに基づいて生成される -
content
中にアポストロフィ('
)が含まれている
ERBのテンプレートに文字列が与えられた場合、HTML的に意味がある文字(<``>``&``"``'
)は、脆弱性につながることを防ぐために自動でエスケープされます。'
もエスケープの対象なので、'
と出力されているのです。
というわけで、response.body
に含まれるのは「HTML的に意味がある文字がエスケープされた文字列」となります。assert_match
による検索文字列もまた、HTML的に意味がある文字をエスケープしなければ、正しい結果を得ることができません。「期待されるHTMLをCGI.escapeHTML
メソッドでエスケープする理由」としては以上です。
CGI.escapeHTML
というのは、「引数として与えた文字列から、HTML的に意味がある文字をエスケープする」というメソッドです。まさに今回のユースケースで必要となる動作ですね。