1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RSpecによるTDDでRailsAPIを実装してみた。part2 -user認証-

Last updated at Posted at 2020-05-30

初めに

この記事は
RSpecによるTDDでRailsAPIを実装してみた。part1
この記事のpart2です。もしよろしければpart1からご覧ください。
今回の目標はoctokitを使ってUser認証のログイン機能とログアウト機能を扱えるようになるまでです。
この記事は結構長いです。記事だけの断片的なコードだと理解しづらい部分は多いですので、適度に自分のコードを読んで、内容を理解していってください。また、わかりづらい表現等がありましたら、コメントください。
それでは初めて行きます。

GithubAPIとの通信

Githubに登録

まずはGithubのApiを使って通信をするためにgithubでアプリケーション登録をする必要がある。
https://github.com/settings/apps
このページに飛び、New Github Appから登録に行く。

登録事項は以下。

Application name:
-> 一意で自由にアプリケーションの名前をつける

Homepage URL:
-> http://localhost:3000
開発用のurlを登録します。

Application description:
-> 自由にわかりやすいように説明を入れる

Authorization callback URL:
-> http://localhost:3000/oauth/github/callback
リダイレクト用のURLの設定

入力が終わったら、Register Applicationを押します。
するとかのよような表示が返ってくる。

Owned by: @user_name

App ID: xxxxx

Client ID: Iv1.xxxxxxxxxxxxxxxxxxxxx

Client secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

このClientIDとClientSecreteを使ってgitubAPIに接続します。どこかにコピ-しておく。

octokit

次にoctokitというgemを導入していく。

公式
https://github.com/octokit/octokit.rb

octokitを使うことで、より簡単にgithubとの連携をとることができるらしい。(中で何が起きているかはあまり知らない)

そして既に最初にoctokitのgemは追加してあるので、そのまま続けていく。

ターミナルに移る。

$ GITHUB_LOGIN='githubuser_name' GITHUB_PASSWORD='github_password' rails c

まず、二つの値を環境変数に入れておく。これは普段githubにログインする時に使うusernameとpassword。そしてconsoleが開くことを確認する。
一応
ENV['GITHUB_LOGIN']
などを打って中身が入っていることを確認しておく。

$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

そしてoctokitに接続して、user情報がしっかりと取れていることを確認する。

これはただの演習です。今後、この仕組みを使って実装していく。

User.rb生成

では、Userモデルを作っていく。

$ rails g model login name url avatar_url provider

migrationファイルにデータベースレベルの制限をつけていく。

xxxxxxxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :login, null: false
      t.string :name
      t.string :url
      t.string :avatar_url
      t.string :provider

      t.timestamps
    end
  end
end

ファイルが生成されているので、login属性にnull: falseをつけておく。

$ rails db:migrate

バリデーションテスト

次にモデルレベルでの制限をつけていく。
validationをつけていきたいところですが、まずはテストから書いていく。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#validations' do

    it 'should have valid factory' do
      user = build :user
      expect(user).to be_valid
    end

    it 'should validate presence of attributes' do
      user = build :user, login: nil, provider: nil
      expect(user).not_to be_valid
      expect(user.errors.messages[:login]).to include("can't be blank")
      expect(user.errors.messages[:provider]).to include("can't be blank")
    end

    it 'should validate uniqueness of login' do
      user = create :user
      other_user = build :user, login: user.login
      expect(other_user).not_to be_valid
      other_user.login = 'newlogin'
      expect(other_user).to be_valid
    end
  end
end

最初のテストはfactorybotが起動しているかを確認するテスト
二つ目は、loginとproviderが入っているかを確認するテスト
三つ目は、loginがuniqueかどうかを確認するテスト

あと、factorybotが現時点だと、何度createしても同じuserを追加してしまうので、それを修正する。

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    sequence(:login) { |n| "a.levine #{n}" }
    name { "Adam Levine" }
    url { "http://example.com" }
    avatar_url { "http://example.com/avatar" }
    provider { "github" }
  end
end

sequenceを使って解決する。これで毎回作ったuserのloginは一意になる。

これでテストを実行する。

$ rspec spec/models/user_spec.rb

ここで、typoがなく、正常にエラーが出ていることを確認する。最初のfactorybotが正常に動いているかを確認するテストは成功する。

validation実装

これからvalidationを実装していく。

models/user.rb
class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true
end

テストを実行し成功することを確認する。

次に、githubとやりとりをするためのコードを書いていく。

UserAuthenticator.rb作成

app/libディレクトリを作成
その下に、app/lib/user_authenticator.rbを作成する。

app/lib/user_authenticator.rb
class UserAuthenticator
  def initialize
  end
end

本来先にテストコードを書くのがTDDだが、先にclassを定義してしまった方が、正しいエラーが吐き出されるので、ファイル作成とclassの定義は先にしてしまった方が早い。

codeが正しくない場合のテスト

それからテストを書いていく。
libディレクトリと、ファイルを作成する。
spec/lib/user_authenticator_spec.rb

spec/lib/user_authenticator_spec.rb
require 'rails_helper'

describe UserAuthenticator do
  describe '#perform' do
    context 'when code is incorrenct' do
      it 'should raise an error' do
        authenticator = described_class.new('sample_code')
        expect{ authenticator.perform }.to raise_error(
          UserAuthenticator::AuthenticationError
        )
        expect(authenticator.user).to be_nil
      end
    end
  end
end

今回はperformというインスタンスメソッドを使って、サインインやログインを実行していく

まずは、codeが、不適切なものだった時。
(ちなみにcodeは、githubが発行する一度きりのtokenのことで、今回はそのcodeを実際に受け取ることがないので、codeはただの文字列を使い、そのコードに対して、どうgithubが振舞うか、という部分をモックを使うことで、実際に発行されたcodeなしでテストを完結させるようにしている。codeはgithubuser一意のtokenと交換するために使う。)

described_class.newでインスタンスを作成、authenticator.performでメソッドを実行する。
UserAuthenticator::AuthenticationErrorは独自のクラスで定義する。

テストを実行すると、.performがないと言われる。そして、さらに.userが使えないと言われる。

なので、実際に書いていく。

user_authentiator#perform実装

app/lib/user_authenticator.rb
class UserAuthenticator
  class AuthenticationError < StandardError; end

  attr_reader :user

  def initialize(code)

  end

  def perform
    raise AuthenticationError
  end
end

attr_readerdでいつでもuserを読み込めるようにしておく。
そして、performも定義しておく。
StandardErrorを継承したAuthenticationError を定義し、UserAuthenticatorにネストさせておく。
performのなかでraiseさせているのはとりあえずテストを成功させるため。

これで、テストを実行すると成功する。
$ rspec spec/lib/user_authenticator_spec.rb

codeが正しい場合のテスト

そして次は、codeが正しい場合のテストを書く。しかしその前にshould raise an errorで使っている

authenticator = described_class.new('sample_code') authenticator.perform

この二つの部分を

spec/lib/user_authenticator_spec.rb
  describe '#perform' do
    let(:authenticator) { described_class.new('sample_code') }
    subject { authenticator.perform }

このように定義しておいて、これから書くwhen code is correctでも使っていく。

なので今の全体像は以下のようになる。

spec/lib/user_authenticator_spec.rb
  describe '#perform' do
    let(:authenticator) { described_class.new('sample_code') }
    subject { authenticator.perform }
    context 'when code is incorrenct' do
      it 'should raise an error' do
        expect{ subject }.to raise_error(
          UserAuthenticator::AuthenticationError
        )
        expect(authenticator.user).to be_nil
      end
    end
  end

ではcodeがただしい時のテストも書く

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      it 'should save the user when does not exists' do
        expect{ subject }.to change{ User.count }.by(1)
      end
    end

userがあらかじめdatabaseに存在しないuserだった場合は、User.countが1増える。
これはuserの新規登録という事。

これで、テストを実行するがもちろん失敗する。それはperformアクションでは何があってもraise AuthenticationErrorというふうに書いてあるから。
なので、performメソッドを実装していく。

実行部分の記述

app/lib/user_authenticator.rb
  def perform
    client = Octokit::Client.new(
      client_id: ENV['GITHUB_CILENT_ID'],
      client_secret: ENV['GITHUB_CILENT_SECRET'],
    )
    res = client.exchange_code_for_token(code)
    if res.error.present?
      raise AuthenticationError
    else

    end
  end

ここでやっていることは、まず、記事の最初にプロジェクトをgithubに認証させている。
この記事の最初にこのプロジェクトをgithubに登録した時にclient_idとclient_secretを表示されたその二つの値を、この環境変数の中に入れる。しかし今回、実際の値は使わない。とりあえず、いったんそこは後で説明する。

client.exchange_code_for_token(code)
この部分がそのままではあるが、codeをtokenと交換している。
tokenは上記したようにgithubAPIが生成した一時的なものでしかない。

そして、もしも、返ってきたresponseがエラーの場合はres.errorで取り出すことができるので、errorが入っていた場合にのみエラーをraiseする。

これでいったん、テストを実行する。

404 - Error: Not Found

おそらく404が吐き出される。これはGITHUB_CILENT_IDとGITHUB_CILENT_SECRETの中身がからだから。
しかし、これはテストなのでここで本当の値を入れるわけにはいかない。
できるだけ、テストはネットワーク環境などを排除して、テストのみで完結するようにするのが理想とされている。

mock実装

そこでテストがわでモックを使う。モックとはgithubの通信の代わりとなるものをこちら側で作成して、テストで完結させるためのもの。

spec/lib/user_authenticator_spec.rb
    context 'when code is incorrenct' do
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return(error)
      end

そこでこのようにbeforeを使い、allow_any_instance_ofというメソッドを使う。

allow_any_instance_of(インスタンス名).to receive(:メソッド名).and_return(返り値)

このようにして使う。これを使って、指定したインスタンスの指定したメソッドが呼び出されたときの返り値を指定することができる。

Octokit::Clientのインスタンスからexchange_code_for_tokenメソッドを呼び出した時にerrorが返る。

その返り値のerrorを定義する。

spec/lib/user_authenticator_spec.rb
    context 'when code is incorrenct' do
      let(:error) {
        double("Sawyer::Resource", error: "bad_verification_code")
      }

doubleはモックを生成する時のメソッド。
Sawyer::Resourceはクラス名で、そのクラスのメソッドとして、errorを使うことができる。
実際のエラーを忠実に再現することができる。

これでテストを実行すると一つめが成功するが、もう一つは失敗する。
404なので、先ほどと同じ。

二つ目のテストもさっきのモックと同じ要領で定義していく。

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
      end

しかし今度は、errorを出すのではなく、validaccesstokenを返す。実際に何か意味がある文字列ではないが、errorではないという意味でこの値でも、テストとしては十分有効なtokenとして機能する。

テストを実行。

undefined method `error' for "validaccesstoken":String

というメッセージが出る。

これは

app/lib/user_authenticator.rb
    if res.error.present?

この部分のことだが、resにerrorがない時にもerrorを読み込もうとしているのでエラーが出た。
なので、errorがない時はnilを返すように書く。

app/lib/user_authenticator.rb
    if res.try(:error).present?

これでテストを実行する。

expected User.count to have changed by 1, but was changed by 0

まだ保存する操作を書いていないので正常なメッセージだと言える。
なので、データを保存していく処理を書いていく。

#perform 保存処理実装

app/lib/user_authenticator.rb
    client = Octokit::Client.new(
      client_id: ENV['GITHUB_CILENT_ID'],
      client_secret: ENV['GITHUB_CILENT_SECRET'],
    )
    token = client.exchange_code_for_token(code)
    if token.try(:error).present?
      raise AuthenticationError
    else
      user_client = Octokit::Client.new(
        access_token: token
      )
      user_data = user_client.user.to_h
        slice(:login, :avatar_url, :url, :name)
      User.create(user_data.merge(provider: 'github'))
    end

このように書き換える。
codeと交換して返ってきたtokenを使って、githubuserのインスタンスを作る。


user_client = Octokit::Client.new(
        access_token: token
      )

上記のこの部分だが、loginとpasswordを使ってインスタンスを生成するのと同じことをしている。tokenを使ってもloginとpasswordを使ってもどちらも同じ結果が出力される。

// ただのサンプルなので実際に打たなくても良い
$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

この記事の最初の方で、このようなコマンドをコンソールで打ったが、これと全く同じことをしている。実際にclient.userをするとgithubuserのデータを取得できる。しかし、形式が、Sawyer::Resourceというもので、非常に扱いづらい。なので、一度to_hでハッシュに変換してからsliceメソッドで中身を取り出している。そしてそのままcreateメソッドを使ってdatabaseに保存している。providerをmergeしているのは、providerは取り出したデータの中にはないので、自分でつける必要がある。もしつけなかったらvalidationに引っかかる。

ついでにだが、resをtokenに変更しておいた。実際にロジック的にどういう意味を持つかを変数名にする方が好ましいから。

そしてテストを実行する。

401 - Bad credentials

次はこのようなメッセージがかえる。
401はログインなどができなかったりする場合に返ってくるエラーのよう
しかし今回はただのモックで作ったインスタンスなので、実際に認証をするができている必要はない。

app/lib/user_authenticator.rb
      user_data = user_client.user.to_h.
        slice(:login, :avatar_url, :url, :name)

現在このuser_client.userの部分でエラーが起きている。
なので、user_client.userをした時にどう返すかというものをモックで再現する。

spec/lib/user_authenticator_spec.rb
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end

したの:userの方を追加する。そして、変数のuser_dataを追加する。

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      let(:user_data) do
        {
          login: 'a.levine 1',
          url: 'http://example.com',
          avatar_url: 'http://example.com/avatar',
          name: 'Adam Levine'
        }
      end

これで、テストを実行して成功する。

ついでに、保存されている値が正しいかも確認しておく。

spec/lib/user_authenticator_spec.rb
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end
      it 'should save the user when does not exists' do
        expect{ subject }.to change{ User.count }.by(1)
        expect(User.last.name).to eq('Adam Levine')
      end

一番下の行を追加しておく。

これでテストを実行して通ることを確認する。

しかし、毎回新しいuserを生成しているが、一度createしたuserは使いまわしたい。当たり前だが、毎回新規登録をするようなものなので、効率が悪い。
なので使いまわせるようにコードを記述していく。

一度保存したuserを使いまわす

まずはテストから書いていく。

spec/lib/user_authenticator_spec.rb
      it 'should reuse already registerd user' do
        user = create :user, user_data
        expect{ subject }.not_to change{ User.count }
        expect(authenticator.user).to eq(user)
      end

一度userを作って、それと同じuser_dataを使って、authenticator.performを行う。
そして、そのauthenticator.performをして作ったuserとfactorybotで作ったuserが同じものかを確認する。

テストを実行して、失敗することを確認する。今はまだ使い回すのではなく、毎回createをしている。
なので、使いまわせるように記述していく。

app/lib/user_authenticator.rb
-      User.create(user_data.merge(provider: 'github'))
+      @user = if User.exists?(login: user_data[:login])
+        User.find_by(login: user_data[:login])
+      else
+        User.create(user_data.merge(provider: 'github'))
+      end

このように書き換える。もし同じuserが存在している時はfind_byを使うという分岐を作る。

テストを実行すると成功する。

リファクタリング

しかし現時点だと、performメソッドの記述量が多すぎることと、performメソッドの責任が曖昧になっている。performメソッドはいわゆる実行、という意味を持つので、実行するためだけのメソッドであることが好ましい。なので、値を生成したり、整えたりしているロジックを別のメソッドに書き出す。

app/lib/user_authenticator.rb
  def perform
-    client = Octokit::Client.new(
-      client_id: ENV['GITHUB_CILENT_ID'],
-      client_secret: ENV['GITHUB_CILENT_SECRET'],
-    )
-    token = client.exchange_code_for_token(code)
    if token.try(:error).present?
      raise AuthenticationError
    else
-     user_client = Octokit::Client.new(
-        access_token: token
-      )
-      user_data = user_client.user.to_h.
-        slice(:login, :avatar_url, :url, :name)
-      @user = if User.exists?(login: user_data[:login])
-        User.find_by(login: user_data[:login])
-      else
-        User.create(user_data.merge(provider: 'github'))
-      end
+      prepare_user
    end

この部分をざくりと削除して他の場所に移していく。移す場所はprivateメソッドで定義する。理由は別に外部のクラスから呼び出す必要のない値を定義するから。

app/lib/user_authenticator.rb
  private

+  def client
+    @client ||= Octokit::Client.new(
+      client_id: ENV['GITHUB_CILENT_ID'],
+      client_secret: ENV['GITHUB_CILENT_SECRET'],
+    )
+  end
+
+  def token
+    @token ||= client.exchange_code_for_token(code)
+  end
+
+  def user_data
+    @user_data ||= Octokit::Client.new(
+      access_token: token
+    ).user.to_h.slice(:login, :avatar_url, :url, :name)
+  end
+
+  def prepare_user
+    @user = if User.exists?(login: user_data[:login])
+      User.find_by(login: user_data[:login])
+    else
+      User.create(user_data.merge(provider: 'github'))
+    end
+  end

  attr_reader :code
end

こんな感じで書きだす。下のメソッドが上のメソッドを呼び出すという構造になっていて、きれいに責任を分離している。

これで、テストを実行して失敗しないことを確認する。

これでいったんリファクタリングは終わり。

次にいく。

User認証用のtoken生成

次はこの今作っているrailsapi専用のaccess_tokenを作っていく。
exchange_code_for_tokenメソッドを使って手に入るtokenはあくまでもgithubAPIにアクセスしてuser情報を取得するためのtokenなのでそれを僕たちが作っているrailsAPIのリクエストを認証するために使うことはできない。

今からは今作っているrailsAPIのリクエスト認証をするためのtokenを作っていく。このtokenが必要になるのは、createアクションをするときや、deleteアクションをする時に必要になる。逆に、indexアクションやshowアクションをする時はtokenがなくてもリクエストを受け付けるようにする。
しかしそれはそれぞれアプリケーション次第ではある。

token生成のテスト

ではそのtokenを作っていくのだが、まずはテストから書いていく。

spec/lib/user_authenticator_spec.rb
      it "should create and set user's access token" do
        expect{ subject }.to change{ AccessToken.count }.by(1)
        expect(authenticator.access_token).to be_present
      end

末尾のこのテストを追加。

そして、その後に、performメソッドを編集していく。

app/lib/user_authenticator.rb
     else
       prepare_user
+      @access_token = if user.access_token.present?
+                 user.access_token
+               else
+                 user.create_access_token
+               end
     end

このように、tokenをインスタンスのattributeとしておく。

app/lib/user_authenticator.rb
attr_reader :user, :access_token

さらにaccess_tokenを呼び出せるようにしておく。
とりあえず説明はのちに詳しくする。

AccessTokenモデル生成

$ rails g model access_token token user:references

とりあえず、access_tokenモデルを作成していく。
これにより、belongs_to :userを持ったaccess_tokenモデルが作成される。

userモデルの方にもアソシエーションを設定する。

app/models/user.rb
class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true

  has_one :access_token, dependent: :destroy # 追加
end
db/migrate/xxxxxxxxx_create_access_tokne.rb
class CreateAccessTokens < ActiveRecord::Migration[6.0]
  def change
    create_table :access_tokens do |t|
      t.string :token, null: false
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

migrationファイルも確認しておく、tokenにはnill: falseをつけておく。

rails db:migrateを実行。

次にaccesstokenのテストも準備しておく。

spec/models/access_token_spec.rb
require 'rails_helper'

RSpec.describe AccessToken, type: :model do
  describe '#validations' do
    it 'should have valid factory' do

    end

    it 'should validate token' do

    end
  end
end

諸々準備ができたのでテストを実行する。
$ rspec spec/lib/user_authenticator_spec.rb

SQLite3::ConstraintException: NOT NULL constraint failed: access_tokens.token

するとこのようなメッセージが吐かれる。
このエラーは、databaseレベルでのnull: falseをつけているのに、nullだった場合に起こるようだ。

ではnullにならないようにtokenを生成するロジックを書く。その前にテストを書く。

spec/models/access_token_spec.rb
  describe '#new' do
    it 'should have a token present after initialize' do
      expect(AccessToken.new.token).to be_present
    end

    it 'should generate uniq token' do
      user = create :user
      expect{ user.create_access_token }.to change{ AccessToken.count }.by(1)
      expect(user.build_access_token).to be_valid
    end
  end

このコードを末尾に追加する。

一つ目は、AccessTokenをnewした時に、ちゃんと、tokenが入っているかどうか
後で記述するが、newした時に自動的にtokenが入るように後で書く。

二つ目は、AccessTokenのcountが1増えるかどうかと、
validationにひっからないかどうか。
validationにひっからないかどうかだが、いつもならモデルをcreateして、二つ目に一つ目の値を使って、buildをして、validationにちゃんとひっかるかどうかを確認するが、今回は少し特殊でnewした時にtokenが自動生成されるので、そのテストはできない。なぜならAccessToken.new(old_token)のように引数を指定することができないから。AccessToken.newとすれば、tokenは自動ではいる。

token生成ロジック実装

ではtokenを生成するロジックを書いていく。

app/models/access_token.rb
class AccessToken < ApplicationRecord
  belongs_to :user

  after_initialize :generate_token

  private

  def generate_token
    loop do
      break if token.present? && !AccessToken.exists?(token: token)
      self.token = SecureRandom.hex(10)
    end
  end
end

after_inializeで指定したメソッドは、モデルが作成される時に実行される。

loopで回しているのはbreak ifで指定した条件に当てはまらない限り何度でもtokenを作成したいから。
SecureRandomクラスを使ってtokenを生成する。
値はランダムで作成されるので、全く同じ値が生成されないとは限りません。なのでloopさせる。
breakの条件はtokenに値が入っている。かつ、databaseに同じ値が存在していない。
それが当てはまらない限りは何度でもloopする。大抵は一度回ればbreakされる。

テストを実行。
$ rspec spec/models/access_token_spec.rb
$ rspec spec/lib/user_authenticator_spec.rb

このテストが通ることを確認する。

ちなみに、user_authenticator.rbでのuser.create_access_tokenこのメソッドはどこかで定義したわけではなく、railsが自動生成してくれるもの。意味はそのままだが、わかりやすく置き換えると、
AccessToken.create(user_id: user.id)
これと同じ意味になる。

では、token生成のロジックが終わったので、次に行く。

ログイン機能

次はログイン機能の全体像を実装していく。今はtokenを生成する仕組みはできているが、まだそのtokenを利用したログイン機能を実装はできていない。なのでそのあたりを実装していく。

エンドポイントのテスト

しかしまずはテストから書く。今はroutingがまだできていないので、routingのテストから書いていく。
記述するファイルはないので作成する。

spec/routing/access_token_spec.rb
require 'rails_helper'

describe 'access tokens routes' do
  it 'should route to access_tokens create action' do
    expect(post '/login').to route_to('access_tokens#create')
  end
end

記述の説明は割愛。

テストを実行すると、no route match /loginと出るので、routes.rbを編集する。

config/routes.rb
Rails.application.routes.draw do
+  post 'login', to: 'access_tokens#create'
  resources :articles, only: [:index, :show]
end

テスト実行。

A route matches "/login", but references missing controller: AccessTokensController

controllerがないと言われているので、作っていく。

access_tokens_controller 生成

$ rails g controller access_tokens

      create  app/controllers/access_tokens_controller.rb
      invoke  rspec
      create    spec/requests/access_tokens_request_spec.rb

再度テスト実行。テストが通る。これでログインのエンドポイントの設置は終了。

access_tokens_controllerのテスト

ではcontrollerのテストをしていく。次のファイルを作成して記述する。

spec/controllers/access_tokens_controller_spec.rb
require 'rails_helper'

RSpec.describe AccessTokensController, type: :controller do
  describe '#create' do
    context 'when invalid request' do
      it 'should return 401 status code' do
        post :create
        expect(response).to have_http_status(401)
      end
    end

    context 'when success request' do

    end
  end
end

認証をせずに401が返ってくることを期待する。401はunauthorized(不許可)、だが、意味的にはunauthenticated(未認証)であるので、認証がされていない時のレスポンスとして用いることが多い。

今更ではあるが、rails g controllerをするとrequests/access_tokens_request_spec.rbのようなファイルが自動生成されている。これはcontrollerのテストの後継となるものだが、controller_specと書き方が少し変わってくるので、今回はわざわざ自分でファイルを作成して記述していいる。本来はrequest_specで書く方が推奨されている。

テストを実行する。

AbstractController::ActionNotFound: The action 'create' could not be found for AccessTokensController

createアクションが定義されていないので、記述する。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create

  end
end

テストを実行。401を期待しているが204が返って来ている。
204は:no_contentのこと。

なのでとりあえず、テストを通すために、controllerに記述していく。

create実装

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create
    render json: {}, status: 401
  end
end

テストを実行して通ることを確認。

さらにテストを追記していく。

spec/controllers/access_token_controller_spec.rb
    context 'when invalid request' do
+      let(:error) do
+        {
+          "status" => "401",
+          "source" => { "pointer" => "/code" },
+          "title" =>  "Authentication code is invalid",
+          "detail" => "You must privide valid code in order to exchange it for token."
+        }
+      end
      it 'should return 401 status code' do
        post :create
        expect(response).to have_http_status(401)
      end

+      it 'should return proper error body' do
+        post :create
+        expect(json['errors']).to include(error)
+      end
    end

401の場合に正しいerrorのresが返ってくることを期待する。
error文は以下のサイトからコピーして来たものを編集して使っている。
https://jsonapi.org/examples/

そして、テストを実行。

expected: {"detail"=>"You must privide valid code in order to exchange it for token.", "source"=>{"pointer"=>"/code"}, "status"=>"401", "title"=>"Authentication code is invalid"}
got: nil

nilが返って来ているので、controlle側できちんとerrorを返す処理を書く。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create
    error = {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
    render json: { "errors": [ error ] }, status: 401
  end
end

これでテストが通ることを確認する。

現在はcreateアクションが呼び出され時に全てにおいてerrorを出しているが、それを修正する。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform
  end

  private

  def authentication_error
    error = {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
  end

リファクタリングもかねて、コードを編集している。
ここでやっとUserAuthenticator.new(params[:code])を書くことになる。
これまでずっと書いていた、codeとtokenを交換して、userを作成するロジックがUserAuthenticatorには書かれているが、それをここで呼び出す。

そして、performで実行する。

401エラーの本体はメソッドに書き出している。
現時点で想定される返ってくるerrorはUserAuthenticator::AuthenticationErrorなので、rescue_fromによって、rescueする。メソッドに書き出しているので、rescue_fromで呼び出す操作が可能。

後、UserAuthenticator::AuthenticationErrorではcodeがblankの時も同じようにエラーを出したい。
ついでに、リファクタリングが必要なのでしていく。

リファクタリングと修正

app/lib/user_authenticator.rb
  def perform
    raise AuthenticationError if code.blank? || token.try(:error).present?
    prepare_user
    @access_token = if user.access_token.present?
               user.access_token
             else
               user.create_access_token
             end
  end

これでcodeがblankの時はerrorを出すことができる。

もう一度おさらいしておくとcodeはフロントエンドから送られてくるtokenのこと。フロントエンドがgithubからtokenを取得して来てくれてそれをapiに送ってくる。それがcode(github_access_code)。
APIはそのcodeを受け取ってGitHubと通信を行いcodeをtokenと交換してもらう(exchange_code_for_tokenメソッドによって)。そのtokenによって、githubuserの情報をgithubAPIから取得することができる。

それを踏まえた上で、codeは十分にblankである可能性は考えられるので、errorを用意しておく。

テストを実行して通ることを確認。

さらにリファクタリングをする。

app/controlers/access_token_controller.rb
class AccessTokensController < ApplicationController
-  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform
  end

-  private
-
-  def authentication_error
-    error = {
-      "status" => "401",
-      "source" => { "pointer" => "/code" },
-      "title" =>  "Authentication code is invalid",
-      "detail" => "You must privide valid code in order to exchange it for token."
-    }
-    render json: { "errors": [ error ] }, status: 401
-  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

+  private

+  def authentication_error
+    error = {
+      "status" => "401",
+      "source" => { "pointer" => "/code" },
+      "title" =>  "Authentication code is invalid",
+      "detail" => "You must privide valid code in order to exchange it for token."
+    }
+    render json: { "errors": [ error ] }, status: 401
+  end
end

完全にauthentication_errorはapplication_controllerに任せてしまい、全てのコントローラーでこのエラーを拾って来れるようにする。理由は認証エラーというのはどのコントローラーでも起きる可能性があるから。

テストを実行して、挙動が何も変わっていないことを確認する。

そして、この実装はテストでも同じように使いまわせるとなお良い。
説明が長くなってしまうので、コードはひとまず全ての変更を貼り付ける

spec/controllers/access_token_controller_spec.rb
RSpec.describe AccessTokensController, type: :controller do
  describe '#create' do
-    context 'when invalid request' do
+    shared_examples_for "unauthorized_requests" do
      let(:error) do
        {
          "status" => "401",
@ -11,17 +11,34 @@ RSpec.describe AccessTokensController, type: :controller do
          "detail" => "You must privide valid code in order to exchange it for token."
        }
      end

      it 'should return 401 status code' do
-        post :create
+        subject
        expect(response).to have_http_status(401)
      end

      it 'should return proper error body' do
-        post :create
+        subject
        expect(json['errors']).to include(error)
      end
    end

+    context 'when no code privided' do
+      subject { post :create }
+      it_behaves_like "unauthorized_requests"
+    end
+    context 'when invalid code privided' do
+      let(:github_error) {
+        double("Sawyer::Resource", error: "bad_verification_code")
+      }
+      before do
+        allow_any_instance_of(Octokit::Client).to receive(
+          :exchange_code_for_token).and_return(github_error)
+      end
+      subject { post :create, params: { code: 'invalid_code' } }
+      it_behaves_like "unauthorized_requests"
+    end

    context 'when success request' do

    end

何をしているかはコードをじっくりと読んで欲しいのだが、ここでは二つのテストをshared_examples_forによって使いまわしている。
should return 401 status code
should return proper error body

この二つのテストは今後も使い回すことが多い。また、shared_examples_forを呼び出すには、it_behaves_likeを使って呼び出すことができる。
subjectを使って、DRYにすることで、subjectにはcontextごとに自由に値を入れることができる。

spec/controllers/access_token_controller_spec.rb
      let(:github_error) {
        double("Sawyer::Resource", error: "bad_verification_code")
      }
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return(github_error)
      end

また、この部分だが、この記述は以前もテストで使ったもので、githubAPIに直接接続することなく、mockで再現している。これにより、githubに実際に接続しなくともgithubAPIを再現することができる。

次はcodeが正しい時のテストを書いていく。

spec/controllers/access_token_controller_spec.rb
    context 'when success request' do
      let(:user_data) do
        {
          login: 'a.levine 1',
          url: 'http://example.com',
          avatar_url: 'http://example.com/avatar',
          name: 'Adam Levine'
        }
      end

      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end

      subject { post :create, params: { code: 'valid_code' } }
      it 'should return 201 status code' do
        subject
        expect(response).to have_http_status(:created)
      end
    end

これは単純にmockでcodeが正しいか正しくないかを操作している。
単純にcodeが正しい場合は201が返ってくることを期待している。

テストを実行。

expected the response to have status code :created (201) but it was :no_content (204)

このメッセージが表示される
なので、responseで201が返ってくるようにcontrollerを編集する。

app/controlers/access_token_controller.rb
  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform

    render json: {}, status: :created
  end

renderを追加し、createdを返す。

これで再びテストを実行し、通ることを確認。

次にしっかりとresponseを返すように実装したい。なので、テストから書いていく。

spec/controllers/access_token_controller_spec.rb
      it 'should return proper json body' do
        expect{ subject }.to change{ User.count }.by(1)
        user = User.find_by(login: 'a.levine 1')
        expect(json_data['attributes']).to eq(
          { 'token' => user.access_token.token }
        )
      end

このテストを末尾に追加。
テストの内容は、articleの時と同じように、json_data['attributes']で値を受け取り中身が正しいかを確認する。User.find_byで取り出しているuserは前に記述したuser_dataを使ったmockによって記述されているので、その値と、responseとして返ってくる値は同じである、というテスト。

しかしテストを実行しても、json_dataでは取り出せていない、理由はserializerを使っていないから、json.dataが存在しないから。なので、きちんと整った形式でのresponseをするためにserializerを導入していく。

serializer生成

$ rails g serializer access_token

これにより、作られるファイルに記述していく。

app/serializers/access_token_serializer.rb
class AccessTokenSerializer < ActiveModel::Serializer
  attributes :id, :token
end

tokenの記述を足しておく。これにより、responseにtokenを含めることができる。

そしてcontrollerにもrenderで返す値を指定しておく。

access_tokens_controller.rb
-    render json: {}, status: :created
+    render json: authenticator.access_token, status: :created
  end

これにより、からのハッシュではなく、しっかりと整形されたresponseが返せるようになる。

テストを実行。するとメッセージが出る。

       expected: {"token"=>"6c7c4213cb78c782f6f6"}
            got: {"token"=>"2e4c724d374019f3fb26"}

どこかで、tokenが再度作られてしまい、値が切り替わっている。
これはリロードをすると、tokenがその度に作られてしまっているというバグ。

なので、そのバグ修正のためのテストを書いていく。

spec/models/access_token_spec.rb
    it 'should generate token once' do
      user = create :user
      access_token = user.create_access_token
      expect(access_token.token).to eq(access_token.reload.token)
    end

まずは、バグが再現できているかどうかを確認するために、テストを実行する。

expected: "3afe2f824789a229014c" got: "c5e04c73aa7ff89fd0a1"

きちんと再現できているので、メッセージが出た。

では改善していく。まず、バグが起きているgenerate_tokenメソッドを見てみる。

app/models/access_token.rb
def generate_token
  loop do
    break if token.present? && !AccessToken.exists?(token: token)
      self.token = SecureRandom.hex(10)
  end
end 

ここでおかしいところがある、問題はbreakの条件がいけなかった。
break if token.present? && !AccessToken.exists?(token: token)
この条件は、tokenにしっかりと値が入っている。かつそのtokenはデータベースに存在しない。という条件になる。しかしそれだと、少し矛盾したことになってしまう。tokenが存在しているということはデータベースに保存しているということなので、この条件式は満たされることがない。
なので、指定したtoken以外のtokenで同じtokenを持っているものが存在しないという条件にしていく。

app/models/access_token.rb
-      break if token.present? && !AccessToken.exists?(token: token)
+      break if token.present? && !AccessToken.where.not(id: id).exists?(token: token)

このように今指定のtoken以外のtokenという条件を作ることができる。
これで、テストを実行して通ることを確認する。

ログアウト機能

エンドポイントの追加テスト

それでは、ログアウト機能を実装していく。

spec/routeing/access_token_spec.rb
  it 'should route  to acces_tokens destroy action' do
    expect(delete '/logout').to route_to('access_tokens#destroy')
  end

routingのテストを書く。

config/routes.rb
Rails.application.routes.draw do
  post 'login', to: 'access_tokens#create'
  delete 'logout', to: 'access_tokens#destroy'
  resources :articles, only: [:index, :show]
end

logoutの行を追加。

テストが通る。

実装

次にコントローラーのテストを書いていく。

spec/controllers/access_token_controller.rb
@@ -1,9 +1,9 @@
require 'rails_helper'

RSpec.describe AccessTokensController, type: :controller do
- describe '#create' do
+ describe 'POST #create' do
    shared_examples_for "unauthorized_requests" do
-     let(:error) do
+     let(:authentication_error) do
        {
          "status" => "401",
          "source" => { "pointer" => "/code" },
@ -19,7 +19,7 @@ RSpec.describe AccessTokensController, type: :controller do

      it 'should return proper error body' do
        subject
-       expect(json['errors']).to include(error)
+       expect(json['errors']).to include(authentication_error)
      end
    end

@ -74,4 +74,33 @@ RSpec.describe AccessTokensController, type: :controller do
      end
    end
  end

+ describe 'DELETE #destroy' do
+   context 'when invalid request' do
+     let(:authorization_error) do
+       {
+         "status" => "403",
+         "source" => { "pointer" => "/headers/authorization" },
+         "title" =>  "Not authorized",
+         "detail" => "You have no right to access this resource."
+       }
+     end
+
+       subject { delete :destroy }
+
+     it 'should return 403 status code' do
+       subject
+       expect(response).to have_http_status(:forbidden)
+     end
+
+     it 'should return proper error json' do
+       subject
+       expect(json['errors']).to include(authorization_error)
+     end
+   end
+
+   context 'when valid request' do
+
+   end
+ end
end

元々errorとして扱っていた403エラーだが、役割をはっきりさせるために、命名を変更。
そして、destroy専用のテストを丸々書いていく。
内容は読んだまま。

@@の表記は何行の記述かを表しているコード、実際に書く必要はない。

そして、controllerを実装していく。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError
  end

destroyメソッドを定義する。まずはエラーのレスポンスのテストを通すために、AuthorizationErrorをraiseして、application_controllerに実際にそのエラーの実態を定義していく。

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+ class AuthorizationError < StandardError; end
  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error
+ rescue_from AuthorizationError, with: :authorization_error

  private

@ -12,4 +14,14 @@ class ApplicationController < ActionController::API
    }
    render json: { "errors": [ error ] }, status: 401
  end

+ def authorization_error
+   error = {
+     "status" => "403",
+     "source" => { "pointer" => "/headers/authorization" },
+     "title" =>  "Not authorized",
+     "detail" => "You have no right to access this resource."
+   }
+   render json: { "errors": [ error ] }, status: 403
+ end
end

エラーの内容はテストに書いたものと同じ。

これでテストを実行して、通ることを確認する。

しかし少し重複している記述があるのでDRYにしていく。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    shared_examples_for 'forbidden_requests' do
    end

まず、describeの下にshared_examples_forを使って、記述をまとめていく。

shared_examples_forの中に、入れるのは以下の記述。

spec/controllers/access_tokens_controller_spec.rb
    shared_examples_for 'forbidden_requests' do
      let(:authorization_error) do
        {
          "status" => "403",
          "source" => { "pointer" => "/headers/authorization" },
          "title" =>  "Not authorized",
          "detail" => "You have no right to access this resource."
        }
      end

      it 'should return 403 status code' do
        subject
        expect(response).to have_http_status(:forbidden)
      end

      it 'should return proper error json' do
        subject
        expect(json['errors']).to include(authorization_error)
      end
    end

今まで記述していたテストを一つにまとめる。

spec/controllers/access_tokens_controller_spec.rb
    context 'when invalid request' do
      subject { delete :destroy }
      it_behaves_like 'forbidden_requests'
    end

そしてshared_expample_forを呼び出すのはit_behaves_likesという記述なので、これで文字列でさっき指定したforbidden_requestsを呼び出す。

これでさっきと同じ環境を作り出す事ができたので再度実行して、テストが通ることを確認する。

次にこれらのshared_example_forを使いまわせるようにさらに一つのファイルにまとめていく。今のaccess_tokens_controller_spec.rbにはshared_example_forが二つ存在しているのでその二つを同じファイルにまとめていく。

spec/support/shared/json_errors.rbを作成

中にshared_example_forの記述を入れていく。

spec/support/shared/json_errors.rb
require 'rails_helper'

shared_examples_for 'forbidden_requests' do

  let(:authorization_error) do
    {
      "status" => "403",
      "source" => { "pointer" => "/headers/authorization" },
      "title" =>  "Not authorized",
      "detail" => "You have no right to access this resource."
    }
  end

  it 'should return 403 status code' do
    subject
    expect(response).to have_http_status(:forbidden)
  end

  it 'should return proper error json' do
    subject
    expect(json['errors']).to include(authorization_error)
  end
end

shared_examples_for "unauthorized_requests" do
  let(:authentication_error) do
    {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
  end

  it 'should return 401 status code' do
    subject
    expect(response).to have_http_status(401)
  end

  it 'should return proper error body' do
    subject
    expect(json['errors']).to include(authentication_error)
  end
end

そして、切り取り元の記述は全て消しておく。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    subject { delete :destroy }

subject定義のネストを一段上げておく。
そして、テストを二つ追加する。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    subject { delete :destroy }

    context 'when no authorization header provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid authorization header provided' do
      before { request.headers['authorization'] = 'Invalid token' }

      it_behaves_like 'forbidden_requests'
    end

    context 'when valid request' do

    end
  end

このテストは、subjectを書かないのは、既にshared_example_forにsubject書いてあるので、自動でsubject { delete :destroy }が呼び出されるようになっている。
そして、beforeを使えば、requestの中身を編集する事ができる。
今回はtokenをInvalid_tokenを入れておく事で、認証ができていないuserを作り上げる。
もちろん認証エラーが出るので、それを期待するテスト。

これでテストを実行して、成功することを確認する。

spec/controllers/access_tokens_controller_spec.rb
    context 'when valid request' do
      let(:user) { create :user }
      let(:access_token) { user.create_access_token }

      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it 'should return 204 status code' do
        subject
        expect(response).to have_http_status(:no_content)
      end

      it 'should remove the proper access token' do
        expect{ subject }.to change{ AccessToken.count }.by(-1)
      end
    end

次に、when valid requestのテストを書いていく。
正しいリクエストを送るためにはまず、headers['authorization']にトークンを入れて、アクセス権限を渡す必要がある。
Bearerとは無記名認証のことで、今回はこれを使う。

テストではAccessTokenモデルがデータベースから一つ減っていることを期待している。

では正しくテストが失敗することを確認する。
ここで、正しく失敗することを確認するとtypoが見つかる事が多い。

expected the response to have status code :no_content (204) but it was :forbidden (403)

テストを実行するとこのようなメッセージが表示される。

forbiddenが返って来ているのは、destroyアクションで常にエラーを返すように記述をしているから。

なので実際にdestroyアクションを実装していく。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError
  end

まずこのdestroyでしたいことはrequestを送って来たuserのaccess_tokenをdestroyすること。なので以下のように記述する。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

current_userは現在ログインしているuserのことを指す。
current_userをどのようにして、持ってくるかを考える。

current_userはrequestから一気に取得する事ができない。しかし、request.authorizationとすると、さっきテストで送ったBearer xxxxxxxxxxxxxxxxxxxxx

というようなtokenを取得する事ができる。
なのでそのtokenを使って、current_userを取得していく。

app/controllers/access_tokens_controller.rb
  def destroy
    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
    access_token = AccessToken.find_by(token: provided_token)
    current_user = access_token&.user

    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

まず、request.authorizationでtokenを取得し、データベースでそのtokenを検索するために、gsubメソッドを使って、正規表現で切り取りをしている。tokenの数字の部分だけが取り出せたら、それでAccessToken.find_byで検索をかけて、取り出している。
そして、そのaccess_token.userとすれば、requestを送って来たuserを取り出す事ができる。そして、そのtokenをdestroy
すればログアウトが完了する。

&.の記述はボッチ演算子と言って、nilが帰って来て、undifind methodというふうになるかもしれない事があらかじめわかっているメソッドに対してつけておくと、nilの場合にエラーが出ずにそのままnilを返り値として返してくれるので、エラーが出ない。というもの。今回は、requestでInvalid_tokenが混ざっている場合があるので、その場合はnilが返ってしまうので、ボッチ演算子を使わないとエラーが出る。

これでテストを実行して、テストが全て通ることを確認する。

次にこのコードをリファクタリングしていく。

app/controllers/access_tokens_controller.rb
   def destroy
-    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
-    access_token = AccessToken.find_by(token: provided_token)
-    current_user = access_token&.user
-
-    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

まず、このように記述を切り取る。そして、その記述をapplication_controller.rbに移していく。なぜうつすかというと、まさにこのrequestを受けとり、current_userを生成するロジックはどのコントローラーでも使いたい記述だから。

app/controllers/application_controller.rb
  private

  def authorize!
    raise AuthorizationError unless current_user
  end

  def access_token
    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
    @access_token = AccessToken.find_by(token: provided_token)
  end

  def current_user
    @current_user = access_token&.user
  end

そして、privateしたにこのようにメソッドを記述していく。
authorize!メソッドはcurrent_userが入っていない時に401エラーを出す。
access_tokenメソッドで正しいaccess_tokenを取り出し、
current_userメソッドで、そのtokenのuserを取り出している。
ここでaccess_tokenとcurrent_userを分けているのは、それぞれの役割をはっきりとさせ責任の分離を行うため。

そして、最後にその定義したauthorize!メソッドを常に呼び出せるように記述していく。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  before_action :authorize!, only: :destroy

before_actionで常に呼び出している状況にしている。destroyのみの指定にしているのは、createアクションの時に呼び出してしまうと、呼び出し不可能なメソッドになってしまうため。

これらのアプローチは一般的だが、before_actionを書き忘れたり、もしくは記述があまりにも多くなってしまう。なので、skip_before_actionを使い、逆にskipするメソッドを指定しておく。基本的にauthorize!メソッドに限って言えばcreateさえskipしてしまえば良さそう。

app/controllers/application_controller.rb
  before_action :authorize!

  private

privateの上に常に呼び出す記述を追記。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  skip_before_action :authorize!, only: :create

before_actionとメソッドを変更しておく。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  skip_before_action :authorize!, only: [:index, :show]

そして、article_controllerも忘れずにskipさせておく。
indexとshowは認証をしていなくても行いたいから。

これでテストを実行して、リファクタリング前と同じ結果が得られるかを確認する。

$ bundle exec rspec

全てのテストを実行して、全てが緑になることを確認する。

最後に

お疲れ様でした。これで最初に目標としていたuser認証機能を実装する事ができました。これらはdeviseというgemを使えば代用してしまえるかもしれませんが、その仕組みを知っているかどうかでuser認証周りの問題への対応が変わって来ますし、理解度が全く違うと思います。token周りは非常に想像しづらい部分ですし、oauthを使う場合は、やはり、gemで全てが代用されてしまうものもありますので、仕組みがブラックボックス化されてしまいがちです。なので今回はこのようにuser認証を行いました。

続き追加しました

RSpecによるTDDでRailsAPIを実装してみた。part3

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?