Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード

# 何をするのか

Railsチュートリアル本文[13.3.3](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-a_proto_feed)にて、「ユーザー自身のポストを含むマイクロポストのフィード」を実装しました。さらに、14章ここまでで「ユーザーのフォロー」という機能も実装しました。今度は「ユーザー自身のポストと、フォローしているユーザーのポストの両方を含むマイクロポストのフィード」を実装しよう、というわけです。

ステータスフィードの完成形は、Railsチュートリアル本文においては、[図 14.21](https://railstutorial.jp/chapters/following_users?version=5.1#fig-home_page_feed_mockup)にて示されています。

# 動機と計画

別記事にて解説しています。

[Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード実装の動機と計画（データモデルの解説とテストの実装）](https://qiita.com/rapidliner00/items/66e3452f1fd3cc7154f3)

# 演習 - 動機と計画

## 1. マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、[図 14.22](https://railstutorial.jp/chapters/following_users?version=5.1#fig-user_feed)のデータセットで`user.feed.map(&:id)`を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。

> ヒント: [13.1.4](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-ordering_and_dependency)で実装した`default_scope`を思い出してください。

そもそも`user.feed.map(&:id)`というのは、「ステータスフィードに含まれる全てのマイクロポストのidが含まれる配列を返す」というメソッドチェーンです。また、Micropostsモデルの`default_scope`の実装が以下のようになっているため、「ステータスフィードに含まれるマイクロポストは、作成日時の降順に整列された状態である」というのもポイントです。

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  #...略
end
```

本演習で出てくる[図 14.22](https://railstutorial.jp/chapters/following_users?version=5.1#fig-user_feed)の`user.feed`を前提とし、また、「単純にidが大きくなるごとにマイクロポストの作成日時も新しくなっていく」という関係が成り立つとすると、`user.feed.map(&:id)`の戻り値は以下のようになると想定されます。

```
>> user.feed.map(&:id)
=> [10, 9, 7, 5, 4, 2, 1]
```

# フィードを初めて実装する

別記事にて解説しています。

[Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - フィードを初めて実装する](https://qiita.com/rapidliner00/items/2483e25efd7fec35748c)

# 演習 - フィードを初めて実装する

別記事にて解説しています。

[Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フィードを初めて実装する」](https://qiita.com/rapidliner00/items/c9c6e8ec139dc9380458)

# サブセレクト

別記事にて解説しています。

[Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - サブセレクト](https://qiita.com/rapidliner00/items/6aef0a85813cd7be273c)

# 演習 - サブセレクト

## 1. Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。[リスト 14.49](https://railstutorial.jp/chapters/following_users?version=5.1#code-home_feed_test)はそのテンプレートです。

```ruby:
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オブジェクトの内容
#<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](https://railstutorial.jp/chapters/following_users?version=5.1#code-home_feed_test)のコードでは、期待されるHTMLを`CGI.escapeHTML`メソッドでエスケープしています (このメソッドは[11.2.3](https://railstutorial.jp/chapters/account_activation?version=5.1#sec-email_tests)で扱った`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&#39;m sorry. Your words made sense, but your sarcastic tone did not.
```

上記の文字列は、`test/fixtures/microposts.yml`の以下の部分に相当します。

```yaml: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的に意味がある文字（`<``>``&``"``'`）は、脆弱性につながることを防ぐために自動でエスケープされます。`'`もエスケープの対象なので、`&#39;`と出力されているのです。

というわけで、`response.body`に含まれるのは「HTML的に意味がある文字がエスケープされた文字列」となります。`assert_match`による検索文字列もまた、HTML的に意味がある文字をエスケープしなければ、正しい結果を得ることができません。「期待されるHTMLを`CGI.escapeHTML`メソッドでエスケープする理由」としては以上です。

`CGI.escapeHTML`というのは、「引数として与えた文字列から、HTML的に意味がある文字をエスケープする」というメソッドです。まさに今回のユースケースで必要となる動作ですね。