おっさん初学者のRailsチュートリアル学習記録(9章)

プログラミング年少組のど素人が、アウトプット用に素人目線の解説を書いていきます。
自分が自分に教えていることを想像しながら進めていきます。
下記と同じような境遇の方であれば参考になるかも

- まっさらなプログラミング初学者目線
- オール独学
- 勉強は得意じゃない
- 社会人のため、1日に勉強できる量は限られる。

電子書籍のwebテキスト(第６版)を購入して使用
Progateの「Web開発パス」完走済
ドットインストールのプレミアム会員卒業
Railsチュートリアル解説動画を見ながら学習中
#9章 発展的なログイン機構#
8章ではブラウザ閉じたらログアウトしちゃってましたが、９章ではブラウザ閉じてもしばらくログイン状態が続いているように設定していきます。
「ログイン状態を保持する」的なチェックボックスを作ったりもします。
難易度は高めですが大体のWebサイトで見られる機能ですので頑張って理解できるようにしましょう

まあ、実際9章飛ばしても10章以降を進めて行くことは可能みたいですし、どうしても理解できなければ先に10章〜14章やってから戻ってくるパターンもありみたいです。
とにかく難しい章であることは間違いありません。。。
<font color="red">いや、ダメです。ここ理解してないと11章で必ずつまずきます。多少時間かかっても完走しましょう</font>

また、9章はこれまでで一番解説動画の恩恵を感じる章です。
初学者ペーペーはテキスト読むだけでは本当にちんぷんかんぷんです。

**まずは全体像の把握から**
とにかく9章は正直これまでとは難易度がまるで違います。
なので、コードを書く前にまずは全体像を把握する必要があります。

セッションの期限切れまでの時間はサービスによって異なります
期限短めですぐにログアウトしちゃうもの（安全性重視）
機密性の高いもの。銀行決済など。
期限長めで毎回ログインする必要がないもの（利便性重視）
個人情報を取り扱うサービス。SNSなど。

**セッションを延ばすことで引き起こされるセキュリティ問題とその対策**
問題１：ネットワークに流れるパケット情報を解析されてしまう
対策：SSLを使って暗号化して対策（httpをhttpsにする）
問題２：クラッキングしてトークンを盗まれてしまう
対策：暗号化して対策
問題３：XSS脆弱性を突いて任意のアクションを実行する
対策：・・・Railsが対策してくれる
問題４：物理的に端末の盗難にあう
対策：ログアウト機能でトークンを無効化する
よく分からない用語が多発してますが、いろいろ問題はあるけどもそれぞれに対策は考えています、ということでしょう

**永続的セッションで新たに準備するもの**
セッションの期限を延ばすにはデータベースとブラウザ間でやり取りする仕組みを作ります
そのために準備するものが**「トークン」**です。ログイン時にユーザーのブラウザのcookieにパスワードみたいな感じで保存します（**ユーザーがログインで使うものとは別！**）

データベースの中にはトークンをハッシュ化したものを**「remember_digest」**に保存します。セッションするごとにトークンと「remember_digest」に保存された情報とを比較して、同じなら「ユーザーとしてログイン許可する！」となるわけです。
ですので、ユーザーモデルには後ほど「remember_digest」属性を作成します。

**単発ログインと永続的なセッションを比較すると・・・**
これまで学んだ仕組み（ログイン）
ユーザーが記憶していたパスワードをハッシュ化させ、データベースの情報（保存されているのはハッシュ化している）と比較
比較するデータはデータベースの「password_digest」に入ってる
整合性が取れたらユーザーに対してログインを許可する

今回学習する仕組み（永続的なセッション）
ブラウザ（cookie）に記憶した2つの情報（「トークン」「暗号化させたユーザーID」）と、データベースの情報を比較
暗号化させたユーザーIDを復号化して元の形に戻したら、データベースからそのIDを検索
（比較するトークンデータはデータベースの「remember_digest」にハッシュ化されて入っている）
検索したIDの「remember_digest」の値とcookieに記憶していた「トークン」の値をハッシュ化したものを比較
整合性が取れたらブラウザに対してログイン状態を許可する

この、cookieに保存されているデータとデータベースの内容を比較するためには「authenticateメソッド」を使います。

**全体の流れ完結版**
以上のことを踏まえて流れを整理すると
ログイン時にブラウザのcookieに、トークンを記憶させる際にはトークン情報かを分かるようにするため、トークンだけでなく**「暗号化させたユーザーID」**も一緒に記憶させます
（セキュリティ強化のために記憶させるIDは暗号化するのです）
データベースにはトークンをハッシュ化させたものを記憶させます
データベースはauthenticateメソッドを使ってログイン中のユーザーかどうかを「ハッシュ化したトークン」と「user_id」を使ってチェックします
整合性が取れたら本人と認証し「ログイン可！」となります

細かいところまで突っ込むと、アプリの中で暗号化されたIDをユーザーのブラウザのcookieの中に入れておいて、そいつをアプリを通して復号化する。復号化したら実際のIDが返ってきます。
返ってきた実際のIDをfind_byでデータベースの中から見つけてきて、トークンをbcryptでハッシュ化の処理をした後にauthenticateを使って認証を行う流れは8章と同じですね
同じことを何度も繰り返し言っていますが、繰り返すことでより頭に入る。ということにしておきます

**ハッシュ化と暗号化の違い**
ハッシュ化
👉元の状態に戻せない状態にする完全一方通行。
「password_digest」にはハッシュ化したものを入れる
暗号化
👉復号化すれば元の状態に戻せる状態にする。テキストでは「署名付きID」とかって書かれてますけど、これはいわゆる「暗号化したID」です。

**8章のセッションとの違い**
8章のセッション
ブラウザを閉じたら消える一時的なログイン
「ブラウザ（cookies）とサーバー」で照合を行う
9章のremember me
永続的にログイン状態を保持できる
「ブラウザ（cookies）とデータベース」で照合を行う
ユーザーIDを暗号化したもの + トークンをcookiesに保存 + ハッシュ化したトークンをデータベースの「remember_digest」に保存
#9.1 Remember me 機能#
この節ではさっそくログイン状態を保持する機能「remember me」を実装していきますので、とりあえずいつものようにトピックブランチを

```
$ git checkout -b advanced-login
```
##9.1.1 記憶トークンと暗号化##
ここからテキストの内容に戻ります
全体像の把握で説明したものと重複しまくりますが、それはそれとして見ていきましょう

セッションの永続化（ログイン状態の保持）の設定方法とそのリスクとリスク対策について説明していきます。

まずは設定方法からです
ここで**「トークン」**という聞き慣れない用語が頻発しています。

**トークンとは**
パスワードと同じようなもので本人かどうかを認証する時に使います
パスワードはユーザーが作成・管理する情報であるのに対して、トークンはコンピュータが作成・管理する情報です

セッションの永続化には記憶トークンなるものを使うそうですが、それを使うにあたってはセキュリティ上いろいろ問題があるみたいです。
ごちゃごちゃ書いてますけど、データが盗まれちゃうリスクがあるみたいです。
リスクと対策については前置きの説明に入れてますよね

セキュリティ上の問題を考慮しながら以下の手順で永続的セッションを作成していきます

1. 記憶トークンにはランダムな文字列生成して用いる
2. ブラウザのcookiesにトークンを保存する時は、有効期限を設定する。（機密性の高いものほど短めに！）
3. トークンはハッシュ値に変換してからデータベースに保存する（またdigestメソッドみたいな訳わからんのが出てくるんかいな！）
4. ブラウザのcookiesに保存するユーザーIDは暗号化しておく（パッと見でバレないように・・・）
5. 永続ユーザーIDを含むcookiesを受け取ったら、暗号化されているのを復号化します。復号化で見える化したら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確かめる（authenticateメソッドの登場や！）

それでは最初に、必要となるremember_digest属性をデータベースのUserモデルに追加します
値にはハッシュ化されたトークンの情報が入ります。
属性の追加はやったことはあるやつです

```
$ rails generate migration add_remember_digest_to_users remember_digest:string
```
ファイル名はなんでもいい訳ではなくて、「to_users」みたいにすることによって、これがUsersテーブルであることをRailsに指示しています！！

```
$ rails generate migration add_カラム名_to_テーブル名 カラム名:データ型
```
マイグレーションファイルを開いてみると・・・

```_add_remember_digest_to_users.rb
  def change
    add_column :users, :remember_digest, :string
  end
```
なんて書いてあるので、そう言うことなんでしょう。
モデルをいじったら忘れてはならないのが・・・

```
$ rails db:migrate
```
これでデータベースの中に「remember_digest」カラムができました

**ブラウザのcookiesに保存するトークンってどうやって作るの？**
先ほど「記憶トークンにはランダムな文字列生成して用いる」としましたが、問題はどうやってランダムな文字列を作るかです。。。
ここで**urlsafe_base64メソッド**を使います。
このメソッドは、`A–Z、a–z、0–9、"-"、"_"のいずれかの文字（64種類）からなる長さ22のランダムな文字列を返します`
64種類の22文字のランダムな文字列ならまずカブることはなさそうですね。。。
今回これを使うには8章で見たように、クラスメソッドにしてから使います。テキストでは何気なくそうしてるので。。。
SecureRandomクラスにあるみたいなので、**「SecureRandom.urlsafe_base64」**としましょう

ユーザーを記憶するには、記憶トークンを作成して、そのトークンをダイジェストに変換したもの（ハッシュ化したもの）をデータベースに保存します
ランダムに作った22文字もの文字列をさらにハッシュ化して・・・やっとこさこれが「remember_digest」カラムに入るわけです。。。

コード上で「SecureRandom.urlsafe_base64メソッド」を使いやすくするために、「new_tokenメソッド」を定義します（微妙に長いですしそっちの方がパッと見で分かりますし）
digestメソッド同様にUserモデルの中に作ります

```models/user.rb
def User.new_token
 SecureRandom.urlsafe_base64
end
```

「remember_digest」に入れる値の話はまだまだ続きます・・・
いきなり**「user.rememberメソッド」**を作成する！とかなんとか言ってます。
このメソッドは記憶トークンとユーザーを関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する機能を搭載します。
よくよく見ると、２つの機能を兼ね備えているということでしょうか。。。

①ユーザーに対して新しいトークンを発行する
②記憶トークンをハッシュ化した記憶ダイジェストを保存する

ってか実際テキストに書いてあるのは「rememberメソッド」なんやけども・・・どして？「user」はいずこへ・・・

そしてランダムな文字列の置き場として、4章で少し出てきた「attr_accessor(アトリビュートアクセサ)」も出てきます。。。
完全に忘れてたけど・・・「データベースではないけど保存できるやつ的なやつを作る」的なやつだったよな確か。。
「has_secure_password」を思い出しましょう。
あの時は、「password」や「password_confirmation」を保存する仮想の場所を「has_secure_password」が勝手に作ってくれてました。
「has_secure_password」は"2つのペアの仮想的な属性（passwordとpassword_confirmation）が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される"
とまあこんな感じで説明していましたが、当時はポカーンでしたが今となっては「なるほどね」といった感じでしょうか
今回は同じことを手動でやりましょうという話です。

「attr_accessor(アトリビュートアクセサ)」よくわからんな・・・
ってな時にめちゃめちゃスッキリ落とし込めた説明がしてある記事見つけました
👉[Railsから入った人へ【attr_accessor】って？](https://qiita.com/Hassan/items/0e034a1d42b2335936e6)

まあまあ、「rememberメソッド」の完成形を先に見てみましょう

```models/user.rb
attr_accessor :remenber_token
 :
 :
def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token) )
end
```
まずは**「attr_accessor: remember_token」**とすることで、「remember_token」という仮想の属性を作成しているみたいです。
要はデータベースではないけど保存できる「remember_token」カラム作ってますよ的な。
これで**「remember_token」を変数のように扱うことができ、値を取得したり（getter）設定したり（setter）できるようになります**
イメージ的には
変数に値を代入するのがsetter
user = 'Daisuke'
その変数を呼び出す動作をgetter
user
=> 'Daisuke

「setter」してる動きをコンソールで見てみましょう

```
u = User.new # インスタンス変数を作る
u.remember_token
=> nil # 「remember_token」には何も入ってない
u.remember_token=('foobar') # 「remember_token」に書き込む
=> "foobar" # これがいわゆる「setter」的な機能です
u.remember_token = 'foo' # 省略した書き方
=> "foo"
```
この原理で今回は、ランダムな文字列User.new_tokenを受けとってます

```
u.remember_token = User.new_token
```
また、「self.remember_token」で、「self」がついているのは「remember_token」をローカル変数にしないようにするためだそうです。
remember_token　を呼び出すと
=> トークン情報　となるのではなくて
自分の.remember_token を呼び出すと
=> トークン情報　となるようにしたい。

そして最後は「update_attribute」です

```
User.digest(u.remember_token)
=> # User.digestに(u.remember_token)の値が入る
```
見た感じですと
**update_attribute(A,B)で、「AをBの状態に更新する」**と言った感じでしょうか
なぜ「update_attribute」を使っているのかと言うと、**バリデーションをスキップするためです!!**

ランダムな文字列を仮想の「remember_token」に入れて、それを暗号化したものと、「remember_digest」の値をその値に更新する。そこでやっと「remember_digest」に値が入ると言うことでしょうか。長すぎや！
##9.1.2 ログイン状態の保持##
冒頭からいきなり「user.rememberメソッド」が動作するようになり、`ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました`とあります。
整理しますと、cookiesには「暗号化したユーザーID」と「トークン」を入れるという話でした。この「トークン」の準備が整ったと言いたいみたいです。

まずは「トークン」をcookiesに入れていきましょう
cookiesに入れるには文字通りの**cookiesメソッド**を使います。特殊な感じで使うメソッドで
<font color="red">**cookies[:入れる場所] = { 値, 期限 }**</font>
てな感じで使います。
今回だと

```
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }
```
「入れる場所」が「:remember_token」。
「値」が「remember_token」。
「期限」が「20年」てとこでしょうか。
この**「期限」が「20年」**はかなり使われるパターンみたいで**「parmanent」**という専用メソッドが作られていますのでそっち使うとこんな形になります。

```
cookies.permanent[:remember_token] = remember_token
```
これで一応20年とは書いてないけど20年になってます。

さてお次は「暗号化したユーザーID」の保存です
「ユーザーID」をcookiesに保存するには、sessionメソッドで使ったのと同じパターン（リスト 8.14）を使います

```
cookies[:user_id] = user.id
```
`cookies[:入れるもの] = { 値, 期限 }`がデフォルトでしたが、まだ「期限」が入っていません。

期限を加える前に、「暗号化したユーザーID」にするため、これに**signedメソッド**付けて暗号化します

```
cookies.signed[:user_id] = user.id
```
**「signed」**を付けるだけで暗号化できるんですね〜

これにお待ちかねの期限を加えるため、さっそく「permanent」メソッドを使います
signedとpermanentをメソッドチェーンで繋いで使います

```
cookies.permanent.signed[:user_id] = user.id
```
cookiesを設定すると、以後のページのビューでこのようにcookiesからユーザーを取り出せるようになります
以上でめでたくcookiesに「暗号化したユーザーID」と「トークン」を入れることができました

あとは、cookiesから「暗号化したユーザーID」と「トークン」を取り出して、データベース上の「ユーザーID」や「ハッシュ化したトークン」とを比較し、認証が通れば晴れて「ログイン状態！」となるわけです・・・まだまだ先は長い・・・

さてさてやっていきますか
まずは、cookiesから「暗号化したユーザーID」をほじくり出します

```
User.find_by(id: cookies.signed[:user_id])
```
「signed」でユーザーIDは暗号化されてcookiesに保存されていますが、データベース上のユーザーIDは暗号化されていません。ですので、cookiesに保存されているユーザーIDをまずは**復号化**して分かるようにする必要がありそうです。。。
が、cookies.signed[:user_id]では自動的にユーザーIDのcookiesの暗号が解除され、元に戻りますのでめちゃめちゃ簡単です！

IDが分かってデータベース上のどのユーザーか特定できたら、今度は「remember_token」に保存されたトークンをハッシュ化して、「remember_digest」の情報と一致するかを確認します。
ハッシュ化と言えば・・・**「bcrypt」**です！
bcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認します
bcryptはパスワードの時にこんな風に使ってました

```
BCrypt::Password.new(password_digest) == unencrypted_password
```
・・・そーだったっけ・・・それを今回トークンではこう使います。

```
BCrypt::Password.new(remember_digest) == remember_token
```

これって
「bcryptでハッシュ化したもの == ハッシュ化してないもの」
あ、ちなみに**「==」って代入じゃなくて「比較」です**

これを別の形で表すとこうなるみたいです

```
BCrypt::Password.new(remember_digest).is_password?(remember_token)
```
「is_password?」とか急に出てきてしかも何の説明もありません。
「BCrypt::Password.new」に「remember_digest」を入れてる※ハッシュ化した値
「is_password?」に「remember_token」を入れてる※そのままの値
そしてこの2つを比較している・・・ということでしょうか・・・
ハッシュ化してるものとしてないものを比較していて大丈夫かな？となりそうですが、それを大丈夫にするのが「bcrypt」ってやつです！

これを元に目的であったユーザーのブラウザに記憶した記憶トークンとデータベースに記憶した記憶ダイジェストが一致するかを確認する**authenticated?メソッド**を作ります（前回の「authenticate」メソッドは「has_secure_password」が勝手にやってくれてましたが、今回は手動で作ります）

```models/user.rb
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
 BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```
比較ですね。比較してOKなら「true」。ダメなら「false」を返すようにしています。

ログインしたユーザーを記憶する処理の準備が整いました
ので、sessionコントローラーでログインした時の動作を修正していきましょう

```sessions_controller.rb
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user # これを追加
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
```
本題はここの部分ですよね

```
   log_in(user) => session[:user_id] # 引数で渡されたユーザーをログイン状態にする！
   remember(user) => 引数で渡されたユーザーのセッションを永続的にする！
```
ふ〜んって感じですけど、まずremember(user)って何だっけとなります。
「models/user.rb」で定義した「remember」は「永続セッションのためにユーザーをデータベースに記憶する！」とあります
データベースに記憶するだけで、ユーザーのセッションが永続的になるのでしょうか。それはちょっと微妙な感じです。cookiesに永続的な期間が指定されたデータが登録されて初めてセッションが永続的になるはずです。

```models/user.rb
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
```
改て見るとこれのどこに永続的なものを示すものがあるのでしょうか。全く見当たりません。
どうやらこの「remember」とは話が違うみたいです。
テキストを読み進めていくと、なんとまあ「sessionsヘルパー」にremember(user)メソッドを定義しているじゃありませんか

```helper/sessions_helper.rb
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
```
最初からいきなり「user.remember」とあります。
予想するにこれが、さっきから見てた「remember」にあたるのではないかと思っています
この「remember(user)」メソッドの流れとしては
「user.remember」
👉永続セッションのためにユーザーをデータベースに記憶する
「cookies.permanent.signed[:user_id] = user.id」
👉ユーザーIDを暗号化して20年スパンでcookiesに保存する
「cookies.permanent[:remember_token] = user.remember_token」
👉ユーザートークンを20年スパンでcookiesに保存する

これが全部そろってこそ**引数で渡されたユーザーのセッションを永続的にする！**ということですよね
ですので、先ほどから問題にしてた2つの「remember」については後ほど明らかにしていきます

お次はこちらを見ていきます（めちゃめちゃ複雑ですこれ・・・）

```helper/sessions_helper.rb
  # 現在ログインしているユーザーを返す（いる場合）
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
```
ここではそもそも「current_user」とは何だったっけ？ということからほじくり返していきます
これは8章で定義したやつでした。8章では永続セッションではなく、ブラウザを閉じたらログアウトしてしまう一時セッションしかやってないので、この「current_user」も永続セッションには対応した作りにはなっておりませぬ。

組み立て方としては・・・
永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする形にします
サーバー上にデータがある場合とサーバーになくてcookie上にある場合とで切り分けするんですね
それがこちら

```helpers/sessions_helper.rb
if session[:user_id]
  @current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
  user = User.find_by(id: cookies.signed[:user_id])
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end
```
で、無駄を省くためにこう修正されています

```helpers/session_helper.rb
if (user_id = session[:user_id])
  @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
  user = User.find_by(id: user_id)
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end
```
current_userは、セッションでダメならcookiesから探してくる流れに変わりました
##9.1.3 ユーザーを忘れる##
ログアウトする仕組みとして、データベースの「remember_digest」にある永続トークンを消すための「user.forget」メソッドを定義します。

```models/user.rb
  def forget
    update_attribute(:remember_digest, nil)
  end
```
これを置いたら確かに「remember_digest」に入ってるデータは「nil」に変更されそうです。

さっきからテキスト見てたら「remember」メソッドの時もそうでしたが、「models/user.rb」で定義した「remember」メソッドのことを「user.remember」と言い、今回も「forget」メソッドのことを「user.forget」メソッドと言っている・・・
これは・・・何か裏がありそうだ！
よくよく見ると、別に定義した「authenticated?」メソッドも「sessions_helper.rb」で使われる時は「user.authenticated?」になってるし
と、言うことは今定義した「forget」メソッドも他で使われる時は「user.forget」となっているはず！

テキストを読み進めていくと、cookie上のユーザー情報を消去するための「forget」メソッドを定義しているようでして

```helpers/sessions_helper.rb
  # 永続的セッションを破棄する
  def forget(user)
      user.forget # データベース上の情報を消し
      cookies.delete(:user_id) # cookies上のユーザーIDを消し
      cookies.delete(:remember_token) # cookies上のトークン情報を消し
  end
```
なるほど！データベース上の何かをするメソッドは「user.〜」で使うようになってんのね。今更気づいたのでありました。
要は、「remember(user)」と「user.remember」は別物。「forget(user)」と「user.forget」も別物ということです。そもそも前者は（引数）が「必ず」必要になるのに対して、後者は取ってないので必要ではなさそうです。ましてや定義している場所も違いますしね。。。ただ、全くもってややこしい

そんなこんなでこれでデータベース上にもcookies上にもユーザーのデータがなくなったので晴れて「ログアウト！」ということにできそうです
しれっと「log_out」メソッドの内容も変更しとります

```helpers/sessions_helper.rb
  # 現在のユーザーをログアウトする
  def log_out
      forget(current_user) # ここの部分ね
      session.delete(:user_id)
      @current_user = nil
  end
```
##9.1.4 2つの目立たないバグ##
同じアカウントを使って複数のページでログインを行っていた場合に起こるバグについて2つ紹介されています
ま、ありえないこと、ではないですよね
①手順
・同じブラウザ（chromeとchromeなど）で2つのタブでサイトを開いておく
・その中の1つだけログアウトする
・もう一方でもログアウトする
👉エラー発生！
原因
1回目のログアウトでcurrent_userがnilになった後、2回目のログアウトでforget(current_user)を実行しようとするとforget(nil)となり失敗する
対策
ユーザーがログイン中の場合にのみログアウトさせるようにする

この流れをテストで見ていくと・・・

```users_login_test.rb
     :
     :
 # ログアウトから
    delete logout_path # 1つ目のログアウト
    assert_not is_logged_in?
    assert_redirected_to root_url
    # 2番目のウィンドウでログアウトをクリックするユーザーをシュミレートする
    delete logout_path # 2つ目のログアウト
    follow_redirect!
```
これが失敗するという話なので修正する
ログアウトの修正なので「destroy」アクションを見ていきます

```sessions_controller.rb
def destroy
 log_out
 redirect_to root_url
end
```
この「log_out」を「logged_in?（ユーザーがログインしていればtrue、その他ならfalseを返す）」がtrueの場合にのみ発動するように変えます

```sessions_controller.rb
def destroy
 log_out if logged_in?
 redirect_to root_url
end
```
これだけでOKです！

②手順
・違う種類のブラウザ（chromeとsafariなど）で複数ページを開いておく
・chromeでログアウトする
・safariの方はログアウトせずブラウザを閉じて終了させる
・safariで再度ページを開く
👉エラー発生！
原因
chromeのログアウトでデータベース上の「remember_digest」とchromeのcookie上の情報は消えているが、safariではログアウトしていないためcookiesには情報が残った状態。また、一度ブラウザを閉じているためセッションも消えている。
この時のsafariの状態をまとめると
remember_digest：なし
session[:user_id]：なし
cookies.signed[:user_id]：あり
これでは「current_user」は存在することになってしまい、以下の処理が走ってしまう・・・

```
BCrypt::Password.new(remember_digest).is_password?(remember_token)
```

対策
「remember_digest」が存在しないときはfalseを返す処理をauthenticated?に追加する

まずはテストを追加

```users_login_test.rb
  test "authenticated? should return false for a user with nil digest" do
   assert_not @user.authenticated?(' ')
  end
```
「remember_digest」がなかったらそもそも処理すんなよ。ということにauthenticated?メソッドを修正

```models/user.rb
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
```
これで解決「rails test」で確認してみましょう
めでたしめでたし
#9.2 [Remember me]チェックボックス#
ログイン状態を保持する機能を搭載しましたので、あとは使うかどうかをユーザーに選択してもらう仕組みを作っていきましょう
場所はログイン画面です。
よく「ログイン状態を保持する [チェックボックス]」みたいなものを見かけますよね。

まずは見た目から作っていきます
「パスワード確認」の下に配置し、内容も他とほぼ同じですがチェックボックスがつきます。
細かいところはbootstrapにやってもらいましょう
内容はテキストをコピペしときます
・・・見た目だけなので、あっという間にそれっぽいものが完成します

問題はここからで、チェックボックスに実際の機能を持たせていきます
チェックボックにチェックを入れた時
👉ログイン状態を保持するように設定する
チェックボックスにチェックを入れなかった時
👉サイトを閉じるとログアウトするように設定する

ログインフォームからsessionコントローラーに送られてくる値はこれで受け取ります

```
params[:session][:remember_me]
```
これは、チェック入れた時は「1」。入れなかった時は「0」が入ります。
要は
「1」の時は「remember(user)」メソッド発動！
「0」の時は「forget(user)」メソッド発動！
といった流れにすればよさそうです

```
if params[:session][:remember_me] == '1'
   remember(user)
else
   forget(user)
end
```
値が「1」ならremember(user)へ
そうでないならforget(user)へ
これを入れれば晴れて完成！・・・とはさせてくれませんでした
1行で表現する方法があるそうです

```
params[:session][:remember_me] == '1'? remember(user) : forget(user)
```
これで同じ意味を表現しています
値が「1」ならremember(user)へ
そうでないならforget(user)へ
さっそくcreateアクションに組み込みましょう

```controller/sessions_controller.rb
def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in(user) # 引数で渡されたユーザーをログインさせる
      params[:session][:remember_me] == '1'? remember(user) : forget(user)
      redirect_to user
```
先ほどまではremember(user)としか書かれてなかったので、強制的にログイン保持になってました。
##コラム 9.2##
先ほどサラッと流したあの形

```
params[:session][:remember_me] == '1'? remember(user) : forget(user)
```
について簡単に説明があってます
この型には名前がついていて「三項演算子」と呼ばれるそうです
よく見かけるこの形

```
if boolean? # trueなら
 何かをする
 else # falseなら
 別のことをする
end
```
👉 **論理値? ? 何かをする : 別のことをする**で表現できます

```
  if boolean?
    var = foo
  else
    var = bar
  end
```
で書いたものであれば

```
var = boolean? ?foo : bar
```
と書き換えちゃってもOKというわけです。
trueなら「foo」を実行する。
falseなら「bar」を実行する。

```
params[:session][:remember_me] == '1'? remember(user) : forget(user)
```
このように書かれていたら、「remember_me」が「１(true)」なら「remember(user)」を実行する。
「remember_me」が「１ではない(false)」なら「forget(user)」を実行する

まあ、それだけの話です・・・どこかで遭遇して「ナンジャコリャ！！」とならないように覚えておきましょう
ifもなくなってますし、知ってないと間違いなく理解不能です
#9.3 [Remember me] のテスト#
機能を追加したのでもちろんテストがついてきます・・・
しかもどうやら新しいテクニックが〜なんて書いてあります。とりあえず見てきますか
##9.3.1 [Remember me] ボックスをテストする##
今回作るテストではユーザーをログインさせまずが、「users_login_test.rb」でやったみたいに、
「post login_path, params: { session:〜」
の部分をテスト内で書かずにヘルパーメソッドにして見た目をスッキリさせます。

テストで使うヘルパーメソッドなので以前test配下のルートディレクトリに作った「test_helper.rb」に書いていきます

```test_helper.rb
  # テストユーザーとしてログインする
  def log_in_as(user)
    session[:user_id] = user.id
  end
```
まずは「コントローラーテスト」で使う便利メソッド「log_in_as(user)」の定義からです
「コントローラー」にあるメソッドは使えるので、書いてあるとおり「session」メソッドが使えます

次は統合テスト内のヘルパーの実装です。
ActiveSupport::TestCaseクラスの中にActionDispatch::IntegrationTestクラスを入れて、そのクラスの中で統合テスト用のヘルパーを定義しているみたいです

```
>> ActionDispatch::IntegrationTest.superclass
=> ActiveSupport::TestCase
```
こうして見ると、ActionDispatch::IntegrationTestクラスの親玉がActiveSupport::TestCaseということが分かります。

```test_helper.rb
class ActiveSupport::TestCase
   :
   :
 class ActionDispatch::IntegrationTest # クラスの中にクラス

  # テストユーザーとしてログインする
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
 end
```
統合テストではsessionを直接取り扱うことができないので、代わりにSessionsリソースに対してpostを送信することで代用します・・・というのは、どういうことでしょうか・・・
今回やりたいことは、userをログイン状態にすることです
統合テストではsessionに書き込むことができないので、ログイン時にsessionに入力するデータをpostリクエストで送って、ログイン状態に持っていきます

メソッド名が前置きで書いた「log_in_as」と同じになっているのは「あえて」そうしています。
テスト中にログイン済みの状態に持っていくテストしたい場合はとりあえず「log_in_as」としちゃえ〜とできるからです。。

それではこの「log_in_as(user)」を使って「remember_me」の動作を見るテストを書いていきますと・・・

```integration/users_login_test.rb
  test "login with remembering" do
    # remember_meを「1」にしてログイン
    log_in_as(@user,remember_me: '1')
    # cookiesに記憶トークン入ってますよね
    assert_not_empty cookies[:remember_token]
  end
  
  test "login without remembering" do
    # remember_meを「1」にしてログインすることで、cookieに一度記憶トークンを保存
    log_in_as(@user, remember_me: '1')
    # ログアウトしてcookiesの記憶トークン消す
    delete logout_path
    # remember_meが「0」でログイン
    log_in_as(@user, remember_me: '0')
    # remember_meが「0」なので、cookiesには記憶トークン入ってないですよね
    assert_empty cookies[:remember_token]
  end
end
```
そのままですね。記憶トークンを保存するかしないかを選べる「remember_me」の機能が見れてます
##9.3.2 [Remember me]をテストする##
ここでは目立たないバグを見つける時のテクニックが紹介されています
それが**raiseメソッド**です
テストしたかどうか曖昧な部分にこのメソッドを差し込んで強制的にエラーを発生させます
今回は「current_user」にこのメソッドを差し込んでいます
ちなみに「current_user」に関するテストはこれまでやっていませんでした

ではさっそく「current_user」に「raise」メソッドを差し込んでみましょう

```sessions_helper.rb
 def current_user
      if (user_id = session[:user_id])
          @current_user ||= User.find_by(id: user_id)
      elsif (user_id = cookies.signed[:user_id])
          user = User.find_by(id: user_id)
        raise # こんな感じでさりげなくバグを仕込む
       if user && user.authenticated?(cookies[:remember_token])
           log_in user
            @current_user = user
       end
      end
  end
```
この状態で「rails test」を走らせるも成功してしまう。
↓
わざわざバグを発生させるように作り変えたのにテストが成功するのはおかしい
↓
ここの部分はテストしてないな
↓
よし、テストを作ってここの部分でテストが失敗したらOKなはず
・・・これまでやってきたテストが全部ここをスルーしていたのか・・・
「current_user」なんていろんなところで登場してきて複雑に入り込んでいたのに・・・

ということなので、エラーが出るようにテストを作成します
sessions_helper.rbに対するテストは作ってないので、ファイルから作成する必要があります

```
$ touch test/helpers/sessions_helper_test.rb
```
こうすればコマンドでファイル作成ができます
「sessionヘルパー」のテストなのでそこで定義したlog_in(user)やremember(user)がテスト内で使えます

テストの手順です
1.テストで使うユーザーをsetupで定義する。fixtureに入れてるやつを使う。
2.渡されたユーザーをremember(user)メソッドで記憶する
3.current_userが、渡されたユーザーと同じであることを確認します

```test/session_helper_test.rb
require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

    def setup
        @user = users(:michael)
        remember(@user)
    end
    
    test "current_user returns right user when session is nil" do
        # @userとcurrent_userが同じか確認
        assert_equal @user, current_user
        # current_userがnilでないか確認
        assert is_logged_in?
    end
    
    test "current_user returns nil when remember digest is wrong" do
        # @userさんのデータベースのremember_digestを新しいトークン情報に更新
        @user.update_attribute(:remember_digest, User.digest(User.new_token))
        assert_nil current_user
    end
```
「assert_equal」の使い方
**assert_equal 期待する値,実際の値**

これでわざわざ仕込んだバグに反応してくれるようになります
「rails test」すればfaildとなります
これを確認したら「sessions_helper.rb」の「raise」は消しときましょう
#9.4 最後に#
いつもの流れをやってこの章を終わりにしましょう

```
$ rails test
$ git add -A
$ git commit -m "Implement advanced login"
$ git checkout master
$ git merge advanced-login
$ rails test
$ git push
```
今回新しく紹介されていることがあります

```
$ heroku maintenance:on
```
規模のでかいサイトでしか関係ないことですが、herokuでマイグレーションを実行するまでの間？にアクセスができない状況になるので、メンテナンスモードをONにする必要があるみたいです

```
$ git push heroku
$ heroku run rails db:migrate
```
herokuへのpushが終わったらメンテナンスモードを元に戻しときましょう

```
$ heroku maintenance:off
```
#9章の要約をようやく作り終えての感想#
まさに初学者殺しの章でした。
テキストだけ追って進めても今何をやっているのかすぐに迷子になっていたと思います。まさに動画を購入していて良かったと感じました。始めに全体像の説明があったので、迷子にならずに進めることができました。