今回達成すること
前回作成した
そして、本チャプターでの変更をHerokuにデプロイし、本番環境でもトークンのユーザーが正しく返されるかを確認します。
テスト環境のCookieの保存先
実装に入る前に、テスト環境ではCookieの保存先が違うためその説明をします。
(ハマった...)
Railsのテスト環境でCookieが保存されるクラスは、Rack::Test::CookieJar(ラック・テスト・クッキージャー)になります。
# テスト環境
> cookies
=> #<Rack::Test::CookieJar:0x0000559ef7bd91d8 
 @cookies=[], 
 @default_host="www.example.com">
コード: Class: Rack::Test::CookieJar — Documentation for brynary ...
対して、開発環境、本番環境ではActionDispatch::Cookies::CookieJar(アクションディスパッチ・クッキーズ・クッキージャー)クラスに保存されます。
# 開発環境、本番環境
> ActionDispatch::Cookies::CookieJar.new([])
=> #<ActionDispatch::Cookies::CookieJar:0x00005640811815b0
 @committed=false,
 @cookies={},
 @delete_cookies={},
 @request=[],
 @set_cookies={}>
コード: Class: ActionDispatch::Cookies::CookieJar - RubyDoc.info
開発環境のCookieに保存されたか?を検証するには
createアクションを介して保存されたクッキーは、後者のActionDispatchクラスに保存されます。
よって、ActionDispatchクラスのCookieに保存されたかを検証する必要があります。
ActionDispatchのCookieを参照するには、@request.cookie_jar[<キー>]メソッドを使用します。
参考: https://www.rubydoc.info/docs/rails/4.1.7/ActionDispatch/Request:cookie_jar
以上を理解した上で、ログインコントローラーをテストしていきましょう。
createアクションのCookieをテストする
まずは、
api/test/controllers/api/v1/user_token_controller_test.rb(以下、省略)
require 'test_helper'
class Api::V1::UserTokenControllerTest < ActionDispatch::IntegrationTest
  # 追加
  def user_token_logged_in(user)
    params = { auth: { email: user.email, password: "password" } }
    post api_url("/user_token"), params: params
    assert_response 200
  end
  def setup
    @user = active_user
    @key = UserAuth.token_access_key
    user_token_logged_in(@user)
  end
  # ここまで
end
Cookieが保存されているかテストする
ActionDispatchクラスにCookieが保存されているかをテストします。
  ...
  def setup
    @user = active_user
    @key = UserAuth.token_access_key
    user_token_logged_in(@user)
  end
  # 追加
  test "create_action" do
    # アクセストークンはCookieに保存されているか
    cookie_token = @request.cookie_jar[@key]
    assert cookie_token.present?
  end
end
保存されたCookieのオプションを取得する
Cookieのオプションは、ActionDispatchのインスタンス変数である@set_cookiesに保存されています。
@set_cookiesの中身
@set_cookies=
{"access_token"=>
  {:value=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJ...",
   :expires=>2020-10-14 17:22:41 +0900,
   :secure=>false,
   :http_only=>true,
   :path=>"/"}}
このインスタンス変数を取得するには、Rubyのinstance_variable_get(インスタンス バリアブル ゲット)メソッドを使用します。
 ...
  test "create_action" do
    # アクセストークンはCookieに保存されているか
    cookie_token = @request.cookie_jar[@key]
    assert cookie_token.present?
    # 追加
    ## Cookieオプションの取得
    cookie_options = @request.cookie_jar.instance_variable_get(:@set_cookies)[@key.to_s]
  end
end
Cookieのオプションをテストする
Cookieのオプションをテストします。
テスト項目は、
expires(エクスパイアーズ)の値は正しいかsecure(セキュア)は開発環境でfalseかhttp_onlyはtrueであるか
の3つです。
  ...
  test "create_action" do
    ...
    ## Cookieオプションの取得
    cookie_options = @request.cookie_jar.instance_variable_get(:@set_cookies)[@key.to_s]
    # 以下、追加
    # expiresは一致しているか
    exp = UserAuth::AuthToken.new(token: cookie_token).payload["exp"]
    assert_equal(Time.at(exp), cookie_options[:expires])
    # secureは開発環境でfalseか
    assert_equal(Rails.env.production?, cookie_options[:secure])
    # http_onlyはtrueか
    assert cookie_options[:http_only]
    # ここまで
  end
end
それではテストを実行してみましょう。
root $ docker-compose run --rm api rails t controllers
Finished in 2.27994s
9 tests, 69 assertions, 0 failures, 0 errors, 0 skips
OK!!続いてレスポンスをテストします。
createアクションのレスポンスをテストする
レスポンス有効期限のテスト
有効期限は、デコードしたpayloadのexpの値とレスポンスのexpが一致していればOKです。
  ...
  test "create_action" do
    ...
    # http_onlyはtrueか
    assert cookie_options[:http_only]
    # 追加
    ## レスポンスのテスト
    # レスポンス有効期限は一致しているか
    assert_equal exp, response_body["exp"]
  end
end
レスポンスユーザーのテスト
ユーザーは、トークンを発行したユーザーとレスポンスユーザーが一致していればOKです。
  ...
  test "create_action" do
    ...
    ## レスポンスのテスト
    # レスポンス有効期限は一致しているか
    assert_equal exp, response_body["exp"]
    # 追加
    # レスポンスユーザーは一致しているか
    assert_equal @user.my_json, response_body["user"]
  end
end
テストを実行してみましょう。
root $ docker-compose run --rm api rails t controllers
Finished in 1.73585s
9 tests, 71 assertions, 0 failures, 0 errors, 0 skips
destroyアクションをテストする
destroyアクションは、クッキーが削除されているかをテストします。
		...
    # レスポンスユーザーは一致しているか
    assert_equal @user.my_json, response_body["user"]
  end
  # 追加
  test "destroy_action" do
    assert @request.cookie_jar[@key].present?
    delete api_url("/user_token")
    assert_response 200
    # Cookieは削除されているか
    assert @request.cookie_jar[@key].nil?
  end
  # ここまで
end
最後のテスト実行です。
root $ docker-compose run --rm api rails t controllers
Finished in 1.99193s
10 tests, 75 assertions, 0 failures, 0 errors, 0 skips
以上で
pushする
サーバーサイドのログイン機能は完成です。
今のブランチをマージしてpushしましょう。
root $ cd api
api $ git commit -am "add_user_token_controller_test.rb"
api $ git checkout master
api $ git merge <ブランチ名>
api $ git push
Herokuにもpushします。
api $ git push heroku
pushが終わったら、
- ユーザー一覧ページ(
/users)が削除されているか - current_userページ(
/users/current_user)がアクセス不能か 
を確認しましょう。
# "ページが見つかりません"になる
api $ heroku open /api/v1/users
# "HTTP ERROR 401"が返ってくる
api & heroku open /api/v1/users/current_user
本番環境の動作確認
curlコマンドを使って本番環境の動作確認を行います。
curlコマンドはHTTPリクエストを行うためのコマンドで、Macに標準装備されています。
curlのインストール確認
インストールを確認するには-vもしくは--versionを実行します。
api $ curl -V
curl 7.64.1 (x86_64-apple-darwin19.0) ...
もし、エラーが出る場合はインストールされていません。
その場合、Homebrew経由でインストールが可能です。
api $ brew install curl
インストールを詳しく => curl入門(mac編) - Qiita
ログインリクエストの動作確認
ログインするには、/user_tokenにPOSTメソッドでアクセスします。
パラメーターとして、DBに保存されているユーザーのメールアドレスとパスワードが必要です。
api $ curl -v -X POST \
https://<Herokuアプリ名>.herokuapp.com/api/v1/user_token \
-H 'Content-Type: application/json' \
-d '{"auth":{"email":"user0@example.com", "password":"password"}}'
-v... HTTPリクエストの詳細を表示する。-X... HTTPメソッドの指定。-H or --header... HTTPヘッダーの追加。-d or --data... リクエストパラメーターの追加。
成功すれば、有効期限とユーザーが返ってきます。
{"exp":1602732306,
"user":{"id":1,
"name":"user0",
"email":"user0@example.com",
"created_at":"2020-08-25T10:39:06.160+09:00"}}
curlのJSONレスポンスを見やすくする
レスポンスのJSONを見やすくするには、curlコマンドの最後にpython -m json.toolをパイプで繋ぎます。
...
-d '{"auth":{"email":"user0@example.com", "password":"password"}}' \
| python -m json.tool
ユーザーアクションの動作確認
ユーザーコントローラーのshowアクションにアクセスし、トークンのユーザーが返ってきているか確認します。
まず、本番環境で使用するトークンを用意する必要があります。
herokuコマンドを使って、本番環境のRailsコンソールに入りましょう。
api $ heroku run rails c
コンソールに入ったら、ユーザーID「1」のユーザーのトークンを発行し、コピーします。
> User.find(1).to_token | pbcopy
=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleH..."
コピーできたら、コンソールからは抜けます。
> exit
--cookieオプションを使って、/users/current_userにアクセスします。
api $ curl -v \
--cookie "access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleH..." \
https://<Herokuアプリ名>.herokuapp.com/api/v1/users/current_user \
| python -m json.tool
- 
--cookie "key=value"... Cookieをセットする。keyには「access_token」、valueにはトークンを指定します。 
ユーザーID「1」のユーザーが返されました。
{
    "created_at": "2020-08-25T10:39:06.160+09:00",
    "email": "user0@example.com",
    "id": 1,
    "name": "user0"
}
全ての確認が取れたら、「root」ディレクトリに戻ります。
api $ cd ..
まとめ
今回は
これでサーバーサイドのログイン認証の実装を終わります。
お疲れさまでした。
このチャプターまとめ
チャプター「サーバーサイドのログイン認証」では、以下のことを行いました。
- JWTとは何か?
 - ログイン認証の流れを解説。JWT初期設定ファイルの作成
 - JWTの発行と検証を行うAuthTokenクラスの作成
 - JWTを検証するAuthenticatorモジュールの作成
 - JWTの発行とデコードをUserクラスに追加
 - AuthTokenクラスろAuthenticatorモジュールをテストする
 - ログインコントローラーの作成
 - ログインコントローラーのテストと本番環境へのデプロイ(今ここ)
 
次回からは?
次回から、Nuxt.jsサイドのログイン認証の実装に入ります。
Nuxt.jsでは、トークンではなく有効期限でログイン状態を判定します。
それではまた次回お会いしましょう。