[RUBY] Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2 -Benutzerauthentifizierung-

zunaechst

Dieser Beitrag Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil1 Teil 2 dieses Artikels. Bitte sehen Sie aus Teil1, wenn Sie möchten. Dieses Mal ist es das Ziel, die Anmeldefunktion und die Abmeldefunktion der Benutzerauthentifizierung mit Octokit ausführen zu können. Dieser Artikel ist ziemlich lang. Es gibt viele Teile, die schwer zu verstehen sind, wenn es sich nur um einen fragmentarischen Code für Artikel handelt. Lesen Sie daher Ihren eigenen Code entsprechend und verstehen Sie den Inhalt. Wenn Sie schwer verständliche Ausdrücke haben, kommentieren Sie diese bitte. Dann werde ich zum ersten Mal gehen.

Kommunikation mit der Github-API

Registrieren Sie sich bei Github

Zunächst müssen Sie die Anwendung auf Github registrieren, um mit Api auf Github kommunizieren zu können. https://github.com/settings/apps Gehen Sie zu dieser Seite und registrieren Sie sich über die New Github App.

Die Registrierungselemente lauten wie folgt.

Application name: -> Einzigartig und frei, um Ihre Anwendung zu benennen

Homepage URL: -> http://localhost:3000 Registrieren Sie die URL für die Entwicklung.

Application description: -> Geben Sie eine Erklärung ein, damit diese leicht verständlich ist

Authorization callback URL: -> http://localhost:3000/oauth/github/callback Festlegen der URL für die Umleitung

Klicken Sie auf Anwendung registrieren, wenn Sie fertig sind. Dann wird eine solche Anzeige zurückgegeben.

Owned by: @user_name

App ID: xxxxx

Client ID: Iv1.xxxxxxxxxxxxxxxxxxxxx

Client secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Verwenden Sie diese ClientID und ClientSecrete, um eine Verbindung zur gitub-API herzustellen. Kopiere es irgendwo hin.

octokit

Als nächstes werden wir einen Edelstein namens Octokit vorstellen.

offiziell https://github.com/octokit/octokit.rb

Durch die Verwendung von Octokit scheint es einfacher zu sein, sich mit Github zu verbinden. (Ich weiß nicht wirklich, was drinnen los ist)

Und da ich das Octokit-Juwel bereits zu Beginn hinzugefügt habe, werde ich so weitermachen, wie es ist.

Gehe zum Terminal.

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

Fügen Sie zunächst die beiden Werte in die Umgebungsvariablen ein. Dies ist der Benutzername und das Passwort, mit denen Sie sich normalerweise bei github anmelden. Stellen Sie dann sicher, dass die Konsole geöffnet wird. Vorerst ENV['GITHUB_LOGIN'] Stellen Sie sicher, dass der Inhalt enthalten ist, indem Sie auf klicken.

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

Stellen Sie dann eine Verbindung zu Octokit her und stellen Sie sicher, dass die Benutzerinformationen ordnungsgemäß erfasst wurden.

Dies ist nur eine Übung. In Zukunft werden wir es mit diesem Mechanismus implementieren.

User.rb Generation

Jetzt erstellen wir ein Benutzermodell.

$ rails g model login name url avatar_url provider

Fügen Sie Migrationsdateien Einschränkungen auf Datenbankebene hinzu.

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

Fügen Sie dem Anmeldeattribut null: false hinzu, da die Datei generiert wurde.

$ rails db:migrate

Validierungstest

Als nächstes werden wir Einschränkungen auf Modellebene hinzufügen. Ich möchte eine Validierung hinzufügen, aber zuerst werde ich aus dem Test schreiben.

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

Der erste Test ist ein Test, um zu überprüfen, ob der Factorybot ausgeführt wird Der zweite ist ein Test, um zu überprüfen, ob Login und Anbieter enthalten sind. Der dritte ist ein Test, um zu überprüfen, ob die Anmeldung eindeutig ist.

Wenn sich der Factorybot gerade befindet, wird derselbe Benutzer hinzugefügt, unabhängig davon, wie oft er erstellt wurde. Beheben Sie dies.

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

Löse mit Sequenz. Dadurch wird die Benutzeranmeldung jedes Mal eindeutig erstellt.

Führen Sie nun den Test aus.

$ rspec spec/models/user_spec.rb

Stellen Sie hier sicher, dass kein Tippfehler vorliegt und der Fehler normalerweise ausgegeben wird. Der Test, um festzustellen, ob der erste Factorybot ordnungsgemäß funktioniert, ist erfolgreich.

Validierungsimplementierung

Wir werden von nun an die Validierung implementieren.

models/user.rb


class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true
end

Führen Sie den Test aus, um sicherzustellen, dass er erfolgreich ist.

Schreiben Sie als Nächstes den Code für die Interaktion mit Github.

Erstellen Sie UserAuthenticator.rb

Erstellen Sie ein app / lib-Verzeichnis Erstellen Sie darunter "app / lib / user_authenticator.rb".

app/lib/user_authenticator.rb


class UserAuthenticator
  def initialize
  end
end

Ursprünglich wurde der Testcode zuerst in TDD geschrieben. Wenn Sie jedoch zuerst die Klasse definieren, wird der richtige Fehler ausgegeben, sodass die Datei schneller erstellt und die Klasse zuerst definiert werden kann.

Testen Sie, ob der Code falsch ist

Dann schreibe den Test. Erstellen Sie ein lib-Verzeichnis und Dateien. 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

Dieses Mal verwenden wir eine Instanzmethode namens perform, um sich anzumelden und anzumelden.

Erstens, wenn der Code unangemessen ist. (Code ist übrigens ein einmaliges Token, das von github ausgegeben wird. Da wir diesen Code diesmal nicht erhalten, verwendet der Code nur eine Zeichenfolge und das Verhalten von github für diesen Code. Mit einem Mock wird der Test ohne den tatsächlich ausgegebenen Code abgeschlossen. Der Code wird verwendet, um ein für den Github-Benutzer eindeutiges Token auszutauschen.)

Erstellen Sie eine Instanz mit description_class.new und führen Sie die Methode mit authentulator.perform aus. UserAuthenticator :: AuthenticationError ist in einer eigenen Klasse definiert.

Wenn ich den Test durchführe, heißt es, dass es keine ".perform" gibt. Und es wird gesagt, dass ".user" nicht verwendet werden kann.

Also werde ich es tatsächlich schreiben.

user_authentiator # Implementierung durchführen

app/lib/user_authenticator.rb


class UserAuthenticator
  class AuthenticationError < StandardError; end

  attr_reader :user

  def initialize(code)

  end

  def perform
    raise AuthenticationError
  end
end

Mit attr_readerd können Benutzer jederzeit gelesen werden. Und perform ist auch definiert. Definieren Sie einen "AuthenticationError", der den "StandardError" erbt, und verschachteln Sie ihn im "UserAuthenticator". Der Grund, warum ich es in der Leistung erhöhe, ist, den Test vorerst erfolgreich zu machen.

Wenn ich jetzt den Test durchführe, ist er erfolgreich. $ rspec spec/lib/user_authenticator_spec.rb

Testen Sie, ob der Code korrekt ist

Und als nächstes schreiben Sie einen Test, wenn der Code korrekt ist. Aber vorher habe ich es verwendet, um einen Fehler auszulösen

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

Diese beiden Teile

spec/lib/user_authenticator_spec.rb


  describe '#perform' do
    let(:authenticator) { described_class.new('sample_code') }
    subject { authenticator.perform }

Definieren Sie es so und verwenden Sie es im richtigen Code, den ich schreiben werde.

Das ganze Bild sieht also wie folgt aus.

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

Schreiben Sie dann einen Test, wenn der Code korrekt ist

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

Wenn user ein Benutzer ist, der im Voraus nicht in der Datenbank vorhanden ist, wird User.count um 1 erhöht. Dies ist eine neue Registrierung des Benutzers.

Jetzt führe ich den Test aus, aber natürlich schlägt er fehl. Dies liegt daran, dass in der Aktion "Ausführen" Authentifizierungsfehler ausgelöst wird, egal was passiert. Also werden wir die perform-Methode implementieren.

Beschreibung des Ausführungsteils

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

Was wir hier tun, ist, dass Github das Projekt am Anfang des Artikels zertifiziert. Fügen Sie die beiden Werte, die client_id und client_secret angezeigt haben, als Sie dieses Projekt auf github registriert haben, am Anfang dieses Artikels in diese Umgebungsvariable ein. Diesmal wird der tatsächliche Wert jedoch nicht verwendet. Vorerst werde ich es später erklären.

client.exchange_code_for_token(code) Dieser Teil bleibt wie er ist, aber der Code wird gegen Token ausgetauscht. Das Token wird nur vorübergehend von der Github-API wie oben beschrieben generiert.

Wenn die zurückgegebene Antwort ein Fehler ist, kann sie mit res.error abgerufen werden, sodass der Fehler nur ausgelöst wird, wenn ein Fehler enthalten ist.

Führen Sie nun den Test einmal aus.

404 - Error: Not Found

Wahrscheinlich ist 404 ausgespuckt. Dies liegt daran, dass der Inhalt von GITHUB_CILENT_ID und GITHUB_CILENT_SECRET leer ist. Da dies jedoch ein Test ist, können wir hier nicht den wahren Wert eingeben. Im Idealfall sollte der Test nur durch den Test abgeschlossen werden, wodurch die Netzwerkumgebung so weit wie möglich entfällt.

Scheinimplementierung

Also benutze ich einen Mock zum Testen. Ein Mock dient dazu, auf dieser Seite eine Alternative zur Github-Kommunikation zu erstellen und diese in einem Test abzuschließen.

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

Daher wird before wie folgt verwendet und eine Methode namens allow_any_instance_of wird verwendet.

allow_any_instance_of(Instanzname).to receive(:Methodenname).and_return(Rückgabewert)

Verwenden Sie es so. Mit dieser Option können Sie den Rückgabewert angeben, wenn die angegebene Methode der angegebenen Instanz aufgerufen wird.

Beim Aufrufen der Methode exchange_code_for_token von einer Instanz von Octokit :: Client wird ein Fehler zurückgegeben.

Definieren Sie den Fehler des Rückgabewerts.

spec/lib/user_authenticator_spec.rb


    context 'when code is incorrenct' do
      let(:error) {
        double("Sawyer::Resource", error: "bad_verification_code")
      }

double ist die Methode zum Erstellen eines Mocks. Sawyer :: Resource ist ein Klassenname, und Fehler können als Methode dieser Klasse verwendet werden. Der tatsächliche Fehler kann originalgetreu wiedergegeben werden.

Wenn ich jetzt den Test ausführe, ist der erste erfolgreich, der andere schlägt jedoch fehl. Es ist 404, also ist es das gleiche wie zuvor.

Der zweite Test wird auf die gleiche Weise wie der vorherige Test definiert.

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

Dieses Mal wird jedoch kein Fehler ausgegeben, sondern ein gültiges Zugriffstoken zurückgegeben. Es ist keine wirklich aussagekräftige Zeichenfolge, aber in dem Sinne, dass es kein Fehler ist, funktioniert dieser Wert immer noch als gutes Token zum Testen.

Führen Sie den Test aus.

undefined method `error' for "validaccesstoken":String

Nachricht erscheint.

das ist

app/lib/user_authenticator.rb


    if res.error.present?

In Bezug auf diesen Teil habe ich einen Fehler erhalten, weil ich versucht habe, den Fehler zu lesen, auch wenn in res kein Fehler aufgetreten ist. Wenn also kein Fehler vorliegt, schreiben Sie, um nil zurückzugeben.

app/lib/user_authenticator.rb


    if res.try(:error).present?

Führen Sie nun den Test aus.

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

Es kann gesagt werden, dass es sich um eine normale Nachricht handelt, da der zu speichernde Vorgang noch nicht geschrieben wurde. Also werde ich den Prozess schreiben, um die Daten zu speichern.

#perform Save Processing Implementierung

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

Schreiben Sie so um. Erstellen Sie eine Instanz des Github-Benutzers mit dem im Austausch für Code zurückgegebenen Token.


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

Dieser Teil des oben Gesagten funktioniert genauso wie das Erstellen einer Instanz mit Login und Passwort. Das gleiche Ergebnis wird ausgegeben, unabhängig davon, ob ein Token verwendet wird oder ob Login und Passwort verwendet werden.

//Es ist nur ein Beispiel, damit Sie es nicht wirklich treffen müssen
$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

Zu Beginn dieses Artikels habe ich einen solchen Befehl in die Konsole eingegeben, und er macht genau das Gleiche. Sie können die Daten des Github-Benutzers abrufen, indem Sie tatsächlich client.user ausführen. Das Format ist jedoch Sawyer :: Resource, was sehr schwierig zu handhaben ist. Sobald der Inhalt mit to_h in Hash konvertiert wurde, wird er mit der Slice-Methode entfernt. Und es wird in der Datenbank gespeichert, während es die Methode create verwendet. Der Anbieter wird zusammengeführt, da der Anbieter nicht in den abgerufenen Daten enthalten ist. Sie müssen ihn daher selbst hinzufügen. Wenn Sie es nicht anhängen, bleiben Sie bei der Validierung hängen.

Übrigens habe ich res in token geändert. Es ist vorzuziehen, den Variablennamen als das zu verwenden, was er tatsächlich in Bezug auf die Logik bedeutet.

Führen Sie dann den Test aus.

401 - Bad credentials

Als nächstes ändert sich eine solche Nachricht. 401 scheint ein Fehler zu sein, der zurückgegeben wird, wenn Sie sich nicht anmelden können usw. Diesmal handelt es sich jedoch nur um eine verspottete Instanz, sodass Sie sich nicht authentifizieren müssen.

app/lib/user_authenticator.rb


      user_data = user_client.user.to_h.
        slice(:login, :avatar_url, :url, :name)

Derzeit ist ein Fehler in diesem Teil user_client.user aufgetreten. Die Rückkehr nach user_client.user wird also mit einem Mock reproduziert.

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

Ich habe hinzugefügt: Benutzer. Fügen Sie dann die Variable user_data hinzu.

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

Jetzt läuft der Test und ist erfolgreich.

Stellen Sie außerdem sicher, dass die gespeicherten Werte korrekt sind.

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

Fügen Sie die unterste Zeile hinzu.

Führen Sie nun den Test aus und stellen Sie sicher, dass er erfolgreich ist.

Obwohl jedes Mal ein neuer Benutzer erstellt wird, möchte ich den einmal erstellten Benutzer wiederverwenden. Offensichtlich ist es so, als würde man jedes Mal eine neue Registrierung vornehmen, also ist es ineffizient. Also werde ich den Code schreiben, damit er verwendet werden kann.

Verwenden Sie den Benutzer nach dem Speichern erneut

Zunächst werde ich aus dem Test schreiben.

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

Erstellen Sie einen Benutzer einmal und verwenden Sie dieselben Benutzerdaten, um authentulator.perform auszuführen. Überprüfen Sie dann, ob der von authentulator.perform erstellte Benutzer und der von factorybot erstellte Benutzer identisch sind.

Führen Sie den Test aus, um sicherzustellen, dass er fehlschlägt. Im Moment verwende ich es noch nicht wieder, sondern erstelle es jedes Mal. Also werde ich es beschreiben, damit es verwendet werden kann.

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

Schreiben Sie so um. Wenn derselbe Benutzer vorhanden ist, erstellen Sie einen Zweig, der find_by verwendet.

Das Ausführen des Tests ist erfolgreich.

Refactoring

Zu diesem Zeitpunkt ist der Umfang der Beschreibung der Perform-Methode jedoch zu groß, und die Verantwortung der Perform-Methode ist nicht eindeutig. Da die perform-Methode die Bedeutung der sogenannten Ausführung hat, ist es vorzuziehen, dass es sich nur um eine Methode zur Ausführung handelt. Schreiben Sie also die Logik, die den Wert generiert, und ordnen Sie ihn einer anderen Methode zu.

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

Löschen Sie diesen Teil grob und verschieben Sie ihn an einen anderen Ort. Der zu verschiebende Ort wird durch die private Methode definiert. Der Grund ist, dass es einen Wert definiert, der nicht von einer externen Klasse aufgerufen werden muss.

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

Schreiben Sie es so. Die Struktur ist so, dass die untere Methode die obere Methode aufruft und die Verantwortlichkeiten sauber getrennt sind.

Führen Sie nun den Test aus, um sicherzustellen, dass er nicht fehlschlägt.

Dies ist das Ende des Refactorings.

Nächster.

Generieren Sie ein Token für die Benutzerauthentifizierung

Als nächstes werde ich ein access_token für Railsapi erstellen, das ich jetzt mache. Das mit der Methode exchange_code_for_token erhaltene Token ist nur ein Token für den Zugriff auf die Github-API und das Abrufen von Benutzerinformationen. Daher kann es nicht zur Authentifizierung der von uns gestellten Rails-API-Anforderung verwendet werden.

Von nun an werde ich ein Token für die Anforderungsauthentifizierung der Rails-API erstellen, die ich jetzt erstelle. Dieses Token wird benötigt, wenn eine Erstellungs- oder Löschaktion ausgeführt wird. Im Gegenteil, wenn Sie eine Indexaktion oder eine Showaktion ausführen, akzeptieren Sie die Anforderung, auch wenn kein Token vorhanden ist. Aber es kommt auf die Anwendung an.

Token-Generierungstest

Dann werde ich das Token machen, aber zuerst werde ich es aus dem Test schreiben.

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

Dieser Test wurde am Ende hinzugefügt.

Bearbeiten Sie anschließend die Methode 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

Auf diese Weise wird Token als Attribut der Instanz festgelegt.

app/lib/user_authenticator.rb


attr_reader :user, :access_token

Ermöglichen Sie außerdem den Aufruf von access_token. Die Erklärung wird vorerst später ausführlich erläutert.

AccessToken-Modellgenerierung

$ rails g model access_token token user:references

Erstellen Sie vorerst ein access_token-Modell. Dadurch wird ein access_token-Modell mit includes_to: user erstellt.

Legen Sie die Zuordnung auch für das Benutzermodell fest.

app/models/user.rb


class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true

  has_one :access_token, dependent: :destroy #hinzufügen
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

Überprüfen Sie auch die Migrationsdatei und fügen Sie nill: false zum Token hinzu.

Führen Sie Rails db: migrate aus.

Bereiten Sie sich als Nächstes auf den Zugriffstoken-Test vor.

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

Nachdem alles fertig ist, führen Sie den Test aus. $ rspec spec/lib/user_authenticator_spec.rb

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

Dann wird eine solche Nachricht ausgespuckt. Dieser Fehler scheint aufzutreten, wenn Sie auf Datenbankebene null: false haben, aber null.

Schreiben Sie dann die Logik, um das Token so zu generieren, dass es nicht null ist. Schreiben Sie vorher einen Test.

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

Fügen Sie diesen Code am Ende hinzu.

Die erste ist, ob das Token ordnungsgemäß enthalten ist, wenn das AccessToken erneuert wird. Ich werde es später schreiben, aber ich werde es später schreiben, damit das Token automatisch eingegeben wird, wenn es neu ist.

Die zweite ist, ob sich die AccessToken-Anzahl um 1 erhöht. Ist es nicht in der Validierung gefangen? Unabhängig davon, ob die Validierung nicht getroffen wird oder nicht, erstelle ich normalerweise ein Modell, verwende den ersten Wert im zweiten, erstelle und überprüfe, ob die Validierung ordnungsgemäß getroffen wird, diesmal jedoch ein wenig Da das Token automatisch generiert wird, wenn Sie ein spezielles neues erstellen, können Sie es nicht testen. Weil Sie kein Argument wie AccessToken.new (old_token) angeben können. Mit AccessToken.new erfolgt das Token automatisch.

Implementierung der Token-Generierungslogik

Schreiben wir nun die Logik zum Generieren des Tokens.

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

Die von after_inialize angegebene Methode wird beim Erstellen des Modells ausgeführt.

Ich drehe es in einer Schleife, weil ich ein Token so oft erstellen möchte, wie ich möchte, es sei denn, die durch break angegebenen Bedingungen sind erfüllt. Generieren Sie ein Token mit der SecureRandom-Klasse. Die Werte werden zufällig erstellt, daher kann nicht garantiert werden, dass genau dieselben Werte generiert werden. Also lasst uns eine Schleife machen. Die Unterbrechungsbedingung hat einen Wert in Token. Und der gleiche Wert existiert nicht in der Datenbank. Schleife so oft du willst, es sei denn, das ist der Fall. Normalerweise bricht es, sobald es sich dreht.

Führen Sie den Test aus. $ rspec spec/models/access_token_spec.rb $ rspec spec/lib/user_authenticator_spec.rb

Stellen Sie sicher, dass dieser Test erfolgreich ist.

Übrigens ist "user.create_access_token" in user_authenticator.rb nicht irgendwo definiert, sondern wird automatisch von Rails generiert. Die Bedeutung bleibt gleich, aber wenn Sie sie auf leicht verständliche Weise ersetzen, AccessToken.create(user_id: user.id) Es hat die gleiche Bedeutung wie diese.

Nachdem die Token-Generierungslogik beendet ist, fahren wir fort.

Anmeldefunktion

Als nächstes implementieren wir das Gesamtbild der Anmeldefunktion. Derzeit wurde ein Mechanismus zum Generieren eines Tokens eingerichtet, eine Anmeldefunktion, die dieses Token verwendet, wurde jedoch noch nicht implementiert. Also werde ich diesen Bereich implementieren.

Endpunkttest

Aber schreibe zuerst aus dem Test. Ich habe das Routing noch nicht durchgeführt, daher beginne ich mit dem Routing-Test. Es gibt keine zu beschreibende Datei. Erstellen Sie sie daher.

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

Die Erläuterung der Beschreibung entfällt.

Wenn ich den Test ausführe, heißt es, dass keine Routenübereinstimmung / Anmeldung erfolgt. Bearbeiten Sie daher route.rb.

config/routes.rb


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

Testlauf.

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

Es wird gesagt, dass es keinen Controller gibt, also werde ich einen machen.

access_tokens_controller-Generierung

$ rails g controller access_tokens

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

Führen Sie den Test erneut aus. Der Test besteht. Damit ist die Installation des Anmeldeendpunkts abgeschlossen.

Testen von access_tokens_controller

Testen wir nun den Controller. Erstellen und beschreiben Sie die folgende Datei.

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

Ich erwarte, dass 401 ohne Authentifizierung zurückgegeben wird. 401 ist nicht autorisiert, aber semantisch nicht authentifiziert. Daher wird es häufig als Antwort verwendet, wenn es nicht authentifiziert ist.

Obwohl es noch neu ist, werden bei Verwendung des Rails g-Controllers automatisch Dateien wie request / access_tokens_request_spec.rb generiert. Dies ist der Nachfolger des Controller-Tests, aber die Art und Weise, wie er geschrieben wird, unterscheidet sich geringfügig von controller_spec. Deshalb habe ich die Datei dieses Mal absichtlich selbst erstellt und beschrieben. Ursprünglich wird empfohlen, in request_spec zu schreiben.

Führen Sie den Test aus.

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

Da die Aktion zum Erstellen nicht definiert ist, schreiben Sie sie.

app/controllers/access_tokens_controller.rb


class AccessTokensController < ApplicationController
  def create

  end
end

Führen Sie den Test aus. Ich erwarte 401, aber 204 ist zurück. 204 bedeutet: no_content.

Daher werde ich es vorerst in den Controller schreiben, um den Test zu bestehen.

Implementierung erstellen

app/controllers/access_tokens_controller.rb


class AccessTokensController < ApplicationController
  def create
    render json: {}, status: 401
  end
end

Führen Sie den Test aus, um sicherzustellen, dass er erfolgreich ist.

Ich werde weitere Tests hinzufügen.

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

Erwarten Sie, dass im Fall von 401 ein korrekter Fehler res zurückgegeben wird. Die Fehleranweisung wird bearbeitet und verwendet, indem sie von der folgenden Site kopiert wird. https://jsonapi.org/examples/

Führen Sie dann den Test aus.

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

Da nil zurückgegeben wird, schreiben Sie einen Prozess, der einen Fehler auf der Steuerungsseite korrekt zurückgibt.

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

Stellen Sie sicher, dass dies den Test besteht.

Derzeit wird beim Aufrufen der Aktion "Erstellen" in allen Fällen ein Fehler ausgegeben, der jedoch behoben wird.

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

Ich bearbeite auch den Code für das Refactoring. Zuletzt werden wir "UserAuthenticator.new (params [: code])" schreiben. Die Logik zum Erstellen eines Benutzers durch Austauschen von Code und Token, die ich ständig geschrieben habe, ist in UserAuthenticator geschrieben, aber ich nenne sie hier.

Führen Sie es dann mit perform aus.

Der Hauptteil des 401-Fehlers wird in die Methode geschrieben. Der an dieser Stelle zurückgegebene Fehler lautet "UserAuthenticator :: AuthenticationError", daher wird recovery_from zur Rettung verwendet. Da es in die Methode geschrieben ist, kann es mit recovery_from aufgerufen werden.

Später in UserAuthenticator :: AuthenticationError möchte ich denselben Fehler ausgeben, auch wenn der Code leer ist. Übrigens muss ich umgestalten.

Refactoring und Fixing

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

Jetzt können Sie eine Fehlermeldung erhalten, wenn der Code leer ist.

Zusammenfassend ist Code das vom Frontend gesendete Token. Das Frontend erhält den Token von Github und sendet ihn an API. Das ist Code (github_access_code). Die API empfängt den Code und kommuniziert mit GitHub, um den Code gegen Token auszutauschen (mithilfe der Methode exchange_code_for_token). Mit diesem Token können Github-Benutzerinformationen von der Github-API abgerufen werden.

Auf dieser Grundlage ist es möglich, dass der Code ausreichend leer ist. Bereiten Sie daher einen Fehler vor.

Führen Sie den Test aus, um sicherzustellen, dass er erfolgreich ist.

Weiterer Refactor.

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

Überlassen Sie den Authentifizierungsfehler vollständig dem Anwendungscontroller, damit alle Controller diesen Fehler erkennen können. Der Grund dafür ist, dass auf jedem Controller Authentifizierungsfehler auftreten können.

Führen Sie den Test aus, um sicherzustellen, dass sich nichts geändert hat.

Und es ist noch besser, diese Implementierung auch in Tests zu verwenden. Der Code fügt vorerst alle Änderungen ein, da die Beschreibung lang sein wird

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

Ich möchte, dass Sie den Code sorgfältig lesen, um zu sehen, was Sie tun, aber hier verwenden wir zwei Tests mit shared_examples_for. should return 401 status code should return proper error body

Diese beiden Tests werden in Zukunft häufig wiederverwendet. Sie können shared_examples_ auch aufrufen, um it_behaves_like zu verwenden. Wenn Sie den Betreff verwenden und auf TROCKEN einstellen, können Sie für jeden Betreff einen Wert frei eingeben.

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

In Bezug auf diesen Teil wurde diese Beschreibung bereits im vorherigen Test verwendet und wird mit mock reproduziert, ohne dass eine direkte Verbindung zur Github-API hergestellt wird. Auf diese Weise können Sie die Github-API reproduzieren, ohne tatsächlich eine Verbindung zu Github herzustellen.

Als nächstes werde ich einen Test schreiben, wenn der Code korrekt ist.

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

Dies ist einfach eine Scheinmanipulation, ob der Code korrekt oder falsch ist. Ich erwarte einfach, dass 201 zurückgegeben wird, wenn der Code korrekt ist.

Führen Sie den Test aus.

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

Diese Meldung wird angezeigt Bearbeiten Sie den Controller so, dass 201 als Antwort zurückgegeben wird.

app/controlers/access_token_controller.rb


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

    render json: {}, status: :created
  end

Render hinzufügen und erstellt zurückgeben.

Führen Sie nun den Test erneut aus und stellen Sie sicher, dass er erfolgreich ist.

Als nächstes möchte ich es so implementieren, dass es eine Antwort fest zurückgibt. Also werde ich aus dem Test schreiben.

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

Fügen Sie diesen Test zum Ende hinzu. Erhalten Sie für den Inhalt des Tests wie im Fall des Artikels den Wert mit json_data ['Attribute'] und prüfen Sie, ob der Inhalt korrekt ist. Da der von User.find_by abgerufene Benutzer durch Mock unter Verwendung der zuvor beschriebenen user_data beschrieben wird, ist der Test, dass der Wert und der als Antwort zurückgegebene Wert identisch sind.

Selbst wenn ich den Test ausführe, kann ich ihn nicht mit json_data abrufen, da ich keinen Serializer verwende und json.data nicht vorhanden ist. Wir werden also den Serializer einführen, um eine Antwort in einem ordentlichen Format zu erstellen.

Serializer-Generierung

$ rails g serializer access_token

Dies wird in der erstellten Datei beschrieben.

app/serializers/access_token_serializer.rb


class AccessTokenSerializer < ActiveModel::Serializer
  attributes :id, :token
end

Fügen Sie die Beschreibung des Tokens hinzu. Dadurch kann die Antwort ein Token enthalten.

Geben Sie außerdem den Wert an, der vom Rendern im Controller zurückgegeben werden soll.

access_tokens_controller.rb


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

Auf diese Weise können Sie eine wohlgeformte Antwort anstelle eines Hashs von zurückgeben.

Führen Sie den Test aus. Dann erscheint eine Meldung.

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

Irgendwo wurde das Token neu erstellt und der Wert wurde umgeschaltet. Dies ist ein Fehler, bei dem Token bei jedem Neuladen erstellt werden.

Also werde ich einen Test schreiben, um den Fehler zu beheben.

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

Führen Sie zunächst einen Test durch, um festzustellen, ob der Fehler reproduziert werden kann.

expected: "3afe2f824789a229014c" got: "c5e04c73aa7ff89fd0a1"

Ich konnte es richtig reproduzieren und bekam eine Nachricht.

Lass uns verbessern. Schauen wir uns zunächst die fehlerhafte Methode generate_token an.

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 

Hier stimmt etwas nicht, das Problem war, dass der Pausenzustand nicht gut war. break if token.present? && !AccessToken.exists?(token: token) Diese Bedingung hat einen festen Wert im Token. Und das Token ist nicht in der Datenbank vorhanden. Es wird eine Bedingung. Das wäre aber ein Widerspruch. Das Vorhandensein des Tokens bedeutet, dass es in der Datenbank gespeichert ist, sodass dieser bedingte Ausdruck nicht erfüllt werden kann. Daher ist es eine Bedingung, dass es kein anderes Token als das angegebene Token gibt, das dasselbe Token hat.

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)

Auf diese Weise ist es möglich, eine Bedingung zu erstellen, die als ein anderes Token als das aktuell angegebene Token bezeichnet wird. Führen Sie nun den Test aus und stellen Sie sicher, dass er erfolgreich ist.

Abmeldefunktion

Zusätzlicher Endpunkttest

Lassen Sie uns nun die Abmeldefunktion implementieren.

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

Schreiben Sie einen Routing-Test.

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

Abmeldezeile hinzugefügt.

Der Test besteht.

Implementierung

Als nächstes werde ich einen Test für den Controller schreiben.

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

Ursprünglich als Fehler 403 behandelt, aber zur Verdeutlichung der Rolle umbenannt. Dann werde ich einen ganzen Test schreiben, der der Zerstörung gewidmet ist. Lesen Sie den Inhalt weiter.

Die Notation von @@ ist ein Code, der angibt, wie viele Zeilen beschrieben werden, und es ist nicht erforderlich, ihn tatsächlich zu schreiben.

Implementieren Sie dann die Steuerung.

app/controllers/access_tokens_controller.rb


  def destroy
    raise AuthorizationError
  end

Definieren Sie die Zerstörungsmethode. Um den Fehlerantworttest zu bestehen, müssen Sie zunächst den AuthorizationError auslösen und den tatsächlichen Fehlerstatus in application_controller definieren.

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

Der Inhalt des Fehlers entspricht dem, was im Test geschrieben wurde.

Führen Sie nun den Test aus und stellen Sie sicher, dass er erfolgreich ist.

Da es jedoch eine leicht duplizierte Beschreibung gibt, werde ich es trocken machen.

spec/controllers/access_tokens_controller_spec.rb


  describe 'DELETE #destroy' do
    shared_examples_for 'forbidden_requests' do
    end

Verwenden Sie zunächst shared_examples_for unter description, um die Beschreibung zusammenzufassen.

Die folgende Beschreibung ist in shared_examples_for enthalten.

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

Kombinieren Sie die Tests, die Sie bisher geschrieben haben, zu einem.

spec/controllers/access_tokens_controller_spec.rb


    context 'when invalid request' do
      subject { delete :destroy }
      it_behaves_like 'forbidden_requests'
    end

Und da es die Beschreibung ist, die it_behaves_likes shared_expample_for aufruft, ruft es verbotene_Anfragen auf, die zuvor in der Zeichenfolge angegeben wurden.

Nachdem wir dieselbe Umgebung wie zuvor erstellt haben, führen Sie sie erneut aus und stellen Sie sicher, dass der Test erfolgreich ist.

Als nächstes werden wir diese shared_example_for in einer Datei kombinieren, damit sie verwendet werden können. In der aktuellen access_tokens_controller_spec.rb befinden sich zwei shared_example_for. Fügen Sie sie also in derselben Datei zusammen.

Erstellen Sie spec / support / shared / json_errors.rb

Fügen Sie die Beschreibung von shared_example_for ein.

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

Dann wird die gesamte Beschreibung der Schneidquelle gelöscht.

spec/controllers/access_tokens_controller_spec.rb


  describe 'DELETE #destroy' do
    subject { delete :destroy }

Erhöhen Sie die Verschachtelung der Subjektdefinition um einen Schritt. Fügen Sie dann zwei Tests hinzu.

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

In diesem Test wird der Betreff nicht geschrieben, da der Betreff bereits in shared_example_for geschrieben ist, sodass "subject {delete: destroy}" automatisch aufgerufen wird. Und wenn Sie zuvor verwenden, können Sie den Inhalt der Anfrage bearbeiten. Dieses Mal erstellen wir durch Einfügen von Invalid_token in ein Token einen Benutzer, der nicht authentifiziert wurde. Natürlich tritt ein Authentifizierungsfehler auf, also ein Test, der ihn erwartet.

Führen Sie nun den Test aus, um sicherzustellen, dass er erfolgreich ist.

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

Schreiben Sie als Nächstes einen Test für eine gültige Anforderung. Um die richtige Anfrage zu senden, müssen Sie zuerst das Token in "headers ['authorisation']" setzen und die Berechtigungen übergeben. Träger ist Trägerauthentifizierung, und dieses Mal werden wir sie verwenden.

Beim Testen wird erwartet, dass das AccessToken-Modell aus der Datenbank um eins reduziert wird.

Stellen Sie nun sicher, dass der Test korrekt fehlschlägt. Hier wird häufig ein Tippfehler gefunden, wenn Sie bestätigen, dass er korrekt fehlschlägt.

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



 Wenn ich den Test durchführe, erhalte ich folgende Meldung:

 Verboten wird zurückgegeben, da es so beschrieben ist, dass die Zerstörungsaktion immer einen Fehler zurückgibt.

 Also werden wir die Zerstörungsaktion tatsächlich implementieren.


#### **`app/controllers/access_tokens_controller.rb`**
```rb

  def destroy
    raise AuthorizationError
  end

Zunächst möchten Sie mit dieser Zerstörung das access_token des Benutzers zerstören, der die Anforderung gesendet hat. Schreiben Sie also wie folgt.

app/controllers/access_tokens_controller.rb


  def destroy
    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

current_user bezieht sich auf den Benutzer, der gerade angemeldet ist. Überlegen Sie, wie Sie current_user bringen können.

current_user kann nicht sofort auf Anfrage abgerufen werden. Wenn Sie jedoch request.authorization verwenden, den Träger, den Sie zuvor im Test gesendet haben

Sie können einen solchen Token erhalten. Verwenden Sie dieses Token, um den aktuellen Benutzer zu erhalten.

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

Um das Token mit request.authorization abzurufen und das Token in der Datenbank zu durchsuchen, wird es zunächst mit der Methode gsub mit einem regulären Ausdruck geschnitten. Wenn Sie nur den Nummernteil des Tokens abrufen können, suchen Sie mit AccessToken.find_by und rufen Sie ihn ab. Wenn Sie diesen access_token.user verwenden, können Sie den Benutzer abrufen, der die Anforderung gesendet hat. Und zerstöre dieses Zeichen Dann ist die Abmeldung abgeschlossen.

Die Beschreibung von & . wird als Boccia-Operator bezeichnet. Wenn Sie sie zu einer Methode hinzufügen, von der Sie im Voraus wissen, dass nil zurückkommt und wie eine undifind-Methode wird, tritt im Fall von nil ein Fehler auf. Wird nicht angezeigt und nil wird als Rückgabewert zurückgegeben, sodass kein Fehler auftritt. etwas wie. Dieses Mal kann Invalid_token in der Anforderung gemischt werden. In diesem Fall wird nil zurückgegeben, sodass ein Fehler auftritt, sofern nicht der Bocchi-Operator verwendet wird.

Führen Sie nun den Test aus und stellen Sie sicher, dass alle Tests bestanden wurden.

Als nächstes werden wir diesen Code umgestalten.

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

Schneiden Sie zuerst die Beschreibung wie folgt aus. Verschieben Sie dann die Beschreibung in application_controller.rb. Der Grund dafür ist, dass die Logik, die diese Anforderung empfängt und den current_user generiert, eine Beschreibung ist, die jeder Controller verwenden möchte.

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

Dann schreiben Sie die Methode wie folgt privat. Die Methode authorize! Gibt einen 401-Fehler aus, wenn current_user nicht enthalten ist. Verwenden Sie die Methode access_token, um das richtige access_token abzurufen Die current_user-Methode ruft den Benutzer für dieses Token ab. Der Grund, warum access_token und current_user hier getrennt sind, besteht darin, ihre Rollen und Verantwortlichkeiten zu klären.

Und schließlich schreiben Sie, damit die definierte authorize! -Methode immer aufgerufen werden kann.

app/controllers/access_tokens_controller.rb


class AccessTokensController < ApplicationController
  before_action :authorize!, only: :destroy

Die Situation wird immer von before_action aufgerufen. Der Grund, warum nur destroy angegeben wird, ist, dass es sich um eine Methode handelt, die nicht aufgerufen werden kann, wenn sie während der Erstellungsaktion aufgerufen wird.

Diese Ansätze sind üblich, aber Sie vergessen, before_action zu schreiben oder zu viel zu schreiben. Verwenden Sie also skip_before_action und geben Sie die Methode an, die im Gegenteil übersprungen werden soll. Grundsätzlich scheint es in Bezug auf die Methode authorize! Gut zu sein, sogar create zu überspringen.

app/controllers/application_controller.rb


  before_action :authorize!

  private

Es wurde eine Beschreibung hinzugefügt, die immer über privat aufgerufen wird.

app/controllers/access_tokens_controller.rb


class AccessTokensController < ApplicationController
  skip_before_action :authorize!, only: :create

Ändern Sie before_action und method.

app/controllers/articles_controller.rb


class ArticlesController < ApplicationController
  skip_before_action :authorize!, only: [:index, :show]

Und vergessen Sie nicht, article_controller zu überspringen. Ich möchte ohne Authentifizierung indizieren und anzeigen.

Führen Sie nun den Test aus, um festzustellen, ob Sie dieselben Ergebnisse wie vor dem Refactoring erhalten.

$ bundle exec rspec

Führen Sie alle Tests durch und stellen Sie sicher, dass alle grün sind.

Schließlich

Danke für deine harte Arbeit. Damit konnten wir die Benutzerauthentifizierungsfunktion implementieren, die unser ursprüngliches Ziel war. Diese können durch die Verwendung eines Edelsteins namens "devise" ersetzt werden. Abhängig davon, ob Sie den Mechanismus kennen oder nicht, ändert sich die Reaktion auf Probleme bei der Benutzerauthentifizierung, und ich denke, dass der Grad des Verständnisses völlig unterschiedlich ist. .. Der Bereich um den Token ist sehr schwer vorstellbar, und wenn Sie oauth verwenden, gibt es immer noch einige Edelsteine, die alles ersetzen, so dass der Mechanismus in der Regel schwarz-boxed ist. Diesmal habe ich also die Benutzerauthentifizierung wie diese verwendet.

Fortsetzung hinzugefügt

Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil3

Recommended Posts

Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2 -Benutzerauthentifizierung-
Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil3-Aktionsimplementierung mit Authentifizierung
Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil1-Aktionsimplementierung ohne Authentifizierung-
[Schienen] Test mit RSpec
Ich habe mit Spring Framework eine API-Domain erstellt. Teil 1
Erstellen Sie mit Rails 6 # 18 eine Bulletin-Board-API mit Zertifizierung und Autorisierung. ・ Implementierung des Endbenutzer-Controllers
Erstellen Sie eine Bulletin Board-API mit Zertifizierung und Autorisierung mit Rails 6 # 3 RSpec. FactoryBot wird eingeführt und ein Post-Modell erstellt
Erstellen Sie eine Bulletin-Board-API mit Autorisierung in Rails 6 # 12 Assoziation von Benutzer und Beitrag
[Rails] Ich habe die Validierungsfehlermeldung mit asynchroner Kommunikation implementiert!
[Ruby on Rails] Implementieren Sie die Anmeldefunktion von add_token_to_users mit API
Wovon ich süchtig war, als ich die Google-Authentifizierung mit Rails implementierte
Implementierte Authentifizierungsfunktion mit Spring Security ②
Implementierte Authentifizierungsfunktion mit Spring Security ③
REST-API-Test mit REST Assured Part 2
[Schienen] Machen wir einen Unit-Test mit Rspec!
# 16 Richtlinieneinstellung zum Erstellen einer Bulletin Board-API mit Zertifizierungsberechtigung in Rails 6
# 8 Seed-Implementierung zum Erstellen einer Bulletin Board-API mit Zertifizierungsautorisierung in Rails 6
Erstellen Sie eine Bulletin-Board-API mit Zertifizierung und Autorisierung mit Rails 6 # 1 Environment Construction
Erstellen Sie eine Bulletin Board-API mit Zertifizierungsberechtigung in Rails 6 # 13 Grant-Authentifizierungsheader
Einführung des Serializers Nr. 9 zum Erstellen einer Bulletin-Board-API mit Zertifizierung und Autorisierung in Rails 6