[RUBY] J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2 -authentification de l'utilisateur-

en premier

Cet article J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part1 Partie 2 de cet article. S'il vous plaît voir de part1 si vous le souhaitez. L'objectif cette fois-ci est de pouvoir gérer la fonction de connexion et la fonction de déconnexion de l'authentification de l'utilisateur à l'aide d'octokit. Cet article est assez long. Il y a de nombreuses parties difficiles à comprendre s'il s'agit d'un code fragmentaire uniquement pour les articles, veuillez donc lire votre propre code de manière appropriée et comprendre le contenu. De plus, si vous avez des expressions difficiles à comprendre, veuillez commenter. Ensuite, j'irai pour la première fois.

Communication avec l'API Github

Inscrivez-vous sur Github

Tout d'abord, vous devez enregistrer l'application sur github afin de communiquer en utilisant Api sur Github. https://github.com/settings/apps Accédez à cette page et inscrivez-vous à partir de la nouvelle application Github.

Les éléments d'enregistrement sont les suivants.

Application name: -> Unique et gratuit pour nommer votre application

Homepage URL: -> http://localhost:3000 Enregistrez l'URL pour le développement.

Application description: -> Entrez une explication pour qu'elle soit facile à comprendre

Authorization callback URL: -> http://localhost:3000/oauth/github/callback Définition de l'URL pour la redirection

Appuyez sur Enregistrer une application lorsque vous avez terminé. Ensuite, un affichage comme celui-ci est renvoyé.

Owned by: @user_name

App ID: xxxxx

Client ID: Iv1.xxxxxxxxxxxxxxxxxxxxx

Client secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Utilisez ce ClientID et ClientSecrete pour vous connecter à l'API gitub. Copiez-le quelque part.

octokit

Ensuite, nous allons introduire une gemme appelée octokit.

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

En utilisant octokit, il semble qu'il soit plus facile de se lier avec github. (Je ne sais pas vraiment ce qui se passe à l'intérieur)

Et comme j'ai déjà ajouté la gemme octokit au début, je continuerai telle quelle.

Déplacez-vous vers le terminal.

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

Tout d'abord, mettez les deux valeurs dans les variables d'environnement. Il s'agit du nom d'utilisateur et du mot de passe que vous utilisez normalement pour vous connecter à github. Assurez-vous ensuite que la console s'ouvre. Pour le moment ENV['GITHUB_LOGIN'] Assurez-vous que le contenu est inclus en appuyant sur.

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

Connectez-vous ensuite à octokit et assurez-vous que les informations utilisateur sont correctement collectées.

Ceci est juste un exercice. À l'avenir, nous le mettrons en œuvre en utilisant ce mécanisme.

Génération User.rb

Maintenant, créons un modèle utilisateur.

$ rails g model login name url avatar_url provider

Ajoutez des restrictions au niveau de la base de données aux fichiers de 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

Puisque le fichier a été généré, ajoutez null: false à l'attribut de connexion.

$ rails db:migrate

Test de validation

Ensuite, nous ajouterons des restrictions au niveau du modèle. Je voudrais ajouter une validation, mais j'écrirai d'abord à partir du test.

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

Le premier test est un test pour vérifier si le factorybot fonctionne Le second est un test pour vérifier si la connexion et le fournisseur sont inclus. Le troisième est un test pour vérifier si la connexion est unique.

De plus, si le factorybot est en ce moment, le même utilisateur sera ajouté quel que soit le nombre de fois qu'il est créé, donc corrigez cela.

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

Résolvez en utilisant la séquence. Cela rend la connexion utilisateur créée à chaque fois unique.

Maintenant, lancez le test.

$ rspec spec/models/user_spec.rb

Ici, confirmez qu'il n'y a pas de faute de frappe et que l'erreur est normalement émise. Le test pour voir si le premier factorybot fonctionne correctement est réussi.

mise en œuvre de la validation

Nous mettrons en œuvre la validation à partir de maintenant.

models/user.rb


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

Exécutez le test pour vous assurer qu'il réussit.

Ensuite, écrivez le code pour interagir avec github.

Créer UserAuthenticator.rb

Répertoire ʻCreate app / lib En dessous, créez ʻapp / lib / user_authenticator.rb.

app/lib/user_authenticator.rb


class UserAuthenticator
  def initialize
  end
end

À l'origine, le code de test est d'abord écrit en TDD, mais si vous définissez d'abord la classe, l'erreur correcte sera rejetée, il est donc plus rapide de créer le fichier et de définir la classe en premier.

Tester si le code est incorrect

Ensuite, écrivez le test. Créez un répertoire et des fichiers 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

Cette fois, nous utiliserons une méthode d'instance appelée perform pour nous connecter et nous connecter.

Premièrement, lorsque le code est inapproprié. (À propos, le code est un jeton unique émis par github, et cette fois il ne reçoit pas réellement ce code, donc le code utilise juste une chaîne de caractères et comment github se comporte pour ce code. En utilisant une maquette pour la pièce, j'essaie de terminer le test sans que le code ne soit réellement émis. Le code est utilisé pour échanger contre un jeton unique à l'utilisateur github.)

Créez une instance avec described_class.new et exécutez la méthode avec authentication.perform. ʻUserAuthenticator :: AuthenticationError` est défini dans sa propre classe.

Quand j'exécute le test, il est dit qu'il n'y a pas de ".perform". Et il est dit que «.user» ne peut pas être utilisé.

Je vais donc l'écrire.

user_authentiator # exécuter l'implémentation

app/lib/user_authenticator.rb


class UserAuthenticator
  class AuthenticationError < StandardError; end

  attr_reader :user

  def initialize(code)

  end

  def perform
    raise AuthenticationError
  end
end

Rendez possible la lecture de l'utilisateur à tout moment avec attr_readerd. Et la performance est également définie. Définissez ʻAuthenticationError qui hérite de StandardError et imbriquez-la dans ʻUserAuthenticator. La raison pour laquelle je l'élève dans perform est pour que le test soit réussi pour le moment.

Maintenant, lorsque j'exécute le test, il réussit. $ rspec spec/lib/user_authenticator_spec.rb

Tester si le code est correct

Et ensuite, écrivez un test lorsque le code est correct. Mais avant cela, je l'ai utilisé dans devrait soulever une erreur

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

Ces deux parties

spec/lib/user_authenticator_spec.rb


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

Définissez-le comme ceci et utilisez-le dans le quand le code est correct que je vais écrire.

Le tableau d'ensemble est donc maintenant le suivant.

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

Ensuite, écrivez un test lorsque le code est correct

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

Si l'utilisateur est un utilisateur qui n'existe pas dans la base de données à l'avance, User.count est incrémenté de 1. Il s'agit d'un nouvel enregistrement d'utilisateur.

Maintenant, je lance le test mais bien sûr, il échoue. C'est parce que l'action d'exécution dit "Lever une erreur d'authentification" quoi qu'il arrive. Nous allons donc implémenter la méthode perform.

Description de la partie exécution

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

Ce que nous faisons ici, c'est que github certifie le projet au début de l'article. Placez les deux valeurs que client_id et client_secret ont été affichées lorsque vous avez enregistré ce projet sur github au début de cet article dans cette variable d'environnement. Mais cette fois, la valeur réelle n'est pas utilisée. Pour le moment, je vous l'expliquerai plus tard.

client.exchange_code_for_token(code) Cette partie reste telle quelle, mais le code est échangé contre un jeton. Le jeton n'est généré que temporairement par l'API github comme décrit ci-dessus.

Ensuite, si la réponse renvoyée est une erreur, elle peut être récupérée avec res.error, de sorte que l'erreur n'est déclenchée que lorsqu'une erreur est incluse.

Maintenant, exécutez le test une fois.

404 - Error: Not Found

Probablement 404 est craché. En effet, les contenus de GITHUB_CILENT_ID et GITHUB_CILENT_SECRET sont vides. Cependant, comme il s'agit d'un test, nous ne pouvons pas entrer la valeur vraie ici. Dans l'idéal, le test ne devrait être complété que par le test, en éliminant autant que possible l'environnement réseau.

mise en œuvre fictive

J'utilise donc une simulation pour tester. Une simulation consiste à créer une alternative à la communication github de ce côté et à la compléter dans un test.

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

Par conséquent, before est utilisé comme ceci, et une méthode appelée allow_any_instance_of est utilisée.

allow_any_instance_of(Nom de l'instance).to receive(:Nom de la méthode).and_return(Valeur de retour)

Utilisez-le comme ça. Vous pouvez l'utiliser pour spécifier la valeur de retour lorsque la méthode spécifiée de l'instance spécifiée est appelée.

Une erreur est renvoyée lors de l'appel de la méthode exchange_code_for_token à partir d'une instance d'Octokit :: Client.

Définissez l'erreur de la valeur de retour.

spec/lib/user_authenticator_spec.rb


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

double est la méthode pour créer une maquette. Sawyer :: Resource est un nom de classe et l'erreur peut être utilisée comme méthode de cette classe. L'erreur réelle peut être fidèlement reproduite.

Maintenant, lorsque j'exécute le test, le premier réussit, mais l'autre échoue. C'est 404, donc c'est la même chose qu'avant.

Le deuxième test est défini de la même manière que la simulation précédente.

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

Mais cette fois, au lieu d'émettre une erreur, il renvoie un jeton d'accès valide. Ce n'est pas vraiment une chaîne significative, mais même cette valeur dans le sens où ce n'est pas une erreur fonctionne comme un jeton suffisamment valide pour les tests.

Exécutez le test.

undefined method `error' for "validaccesstoken":String

Un message apparaît.

c'est

app/lib/user_authenticator.rb


    if res.error.present?

Concernant cette partie, une erreur s'est produite car j'essayais de lire l'erreur même s'il n'y avait pas d'erreur dans res. Donc, s'il n'y a pas d'erreur, écrivez pour retourner nil.

app/lib/user_authenticator.rb


    if res.try(:error).present?

Maintenant, lancez le test.

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

On peut dire que c'est un message normal car l'opération de sauvegarde n'a pas encore été écrite. Donc, je vais écrire le processus pour enregistrer les données.

#perform save implémentation du traitement

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

Réécrivez comme ça. Créez une instance d'utilisateur github à l'aide du jeton retourné en échange de code.


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

Cette partie de ce qui précède fait la même chose que la création d'une instance à l'aide du login et du mot de passe. Le même résultat est produit indépendamment du fait que le jeton soit utilisé ou que le login et le mot de passe soient utilisés.

//C'est juste un échantillon donc vous n'avez pas à le frapper
$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

Plus tôt dans cet article, j'ai tapé une commande comme celle-ci sur la console, et elle fait exactement la même chose. Vous pouvez obtenir les données de l'utilisateur github en faisant réellement client.user. Cependant, le format est Sawyer :: Resource, qui est très difficile à gérer. Ainsi, une fois converti en hachage avec to_h, le contenu est retiré avec la méthode slice. Et il est enregistré dans la base de données car il utilise la méthode create. Le fournisseur est fusionné car le fournisseur ne figure pas dans les données récupérées, vous devez donc l'ajouter vous-même. Si vous ne le joignez pas, vous serez bloqué dans la validation.

Incidemment, j'ai changé res en token. Il est préférable d'utiliser le nom de la variable comme ce qu'il signifie réellement en termes de logique.

Exécutez ensuite le test.

401 - Bad credentials

Ensuite, un tel message change. 401 semble être une erreur renvoyée lorsque vous ne pouvez pas vous connecter, etc. Cependant, cette fois, il ne s'agit que d'une instance simulée, vous n'avez donc pas besoin de pouvoir vous authentifier.

app/lib/user_authenticator.rb


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

Actuellement, il y a une erreur dans cette partie user_client.user. Ainsi, comment retourner lorsque user_client.user est terminé est reproduit avec un simulacre.

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

J'ai ajouté: utilisateur. Ajoutez ensuite la variable 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

Maintenant, le test s'exécute et réussit.

Assurez-vous également que les valeurs stockées sont correctes.

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

Ajoutez la ligne du bas.

Maintenant, exécutez le test et assurez-vous qu'il réussit.

Cependant, bien qu'un nouvel utilisateur soit créé à chaque fois, je souhaite réutiliser l'utilisateur une fois créé. Évidemment, c'est comme faire un nouvel enregistrement à chaque fois, donc c'est inefficace. J'écrirai donc le code pour qu'il puisse être utilisé.

Réutiliser l'utilisateur une fois enregistré

Tout d'abord, j'écrirai à partir du test.

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

Créez un utilisateur une fois et utilisez les mêmes user_data pour faire authentifier.perform. Ensuite, vérifiez si l'utilisateur créé par authentifier.perform et l'utilisateur créé par factorybot sont les mêmes.

Exécutez le test pour vous assurer qu'il échoue. Pour le moment, je ne le réutilise pas encore, mais je le crée à chaque fois. Alors, je vais le décrire pour qu'il puisse être utilisé.

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

Réécrivez comme ça. Si le même utilisateur existe, créez une branche qui utilise find_by.

L'exécution du test réussit.

Refactoring

Cependant, à ce stade, la quantité de description de la méthode perform est trop importante et la responsabilité de la méthode perform est ambiguë. Puisque la méthode perform a le sens de ce que l'on appelle l'exécution, il est préférable que ce soit une méthode uniquement pour l'exécution. Alors, écrivez la logique qui génère et arrange la valeur dans une autre méthode.

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

Supprimez grossièrement cette partie et déplacez-la vers un autre endroit. L'emplacement à déplacer est défini par la méthode privée. La raison en est qu'il définit une valeur qui n'a pas besoin d'être appelée depuis une classe externe.

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

Écrivez-le comme ça. La structure est telle que la méthode inférieure appelle la méthode supérieure et les responsabilités sont clairement séparées.

Exécutez maintenant le test pour vous assurer qu'il n'échoue pas.

C'est la fin du refactoring.

Suivant.

Générer un jeton pour l'authentification de l'utilisateur

Ensuite, je vais créer un access_token pour railsapi que je crée maintenant. Le jeton obtenu à l'aide de la méthode exchange_code_for_token n'est qu'un jeton pour accéder à l'API github et obtenir des informations utilisateur.Il ne peut donc pas être utilisé pour authentifier la demande d'API rails que nous faisons.

À partir de maintenant, je vais créer un jeton pour l'authentification de la demande de l'API rails que je crée maintenant. Ce jeton est nécessaire lors de l'exécution d'une action de création ou d'une action de suppression. Au contraire, lors de l'exécution d'une action d'index ou d'une action show, acceptez la demande même s'il n'y a pas de jeton. Mais cela dépend de l'application.

Test de génération de jetons

Ensuite, je vais créer le jeton, mais je vais d'abord l'écrire à partir du test.

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

Ajout de ce test à la fin.

Puis, après cela, modifiez la méthode 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

De cette manière, le jeton est défini comme l'attribut de l'instance.

app/lib/user_authenticator.rb


attr_reader :user, :access_token

De plus, permettez d'appeler access_token. Pour le moment, l'explication sera expliquée en détail plus loin.

Génération de modèle AccessToken

$ rails g model access_token token user:references

Pour le moment, créez un modèle access_token. Cela crée un modèle access_token avec appartient_to: utilisateur.

Définissez également l'association pour le modèle utilisateur.

app/models/user.rb


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

  has_one :access_token, dependent: :destroy #ajouter à
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

Vérifiez également le fichier de migration, ajoutez nill: false au jeton.

Exécutez rails db: migrate.

Ensuite, préparez-vous au test du jeton d'accès.

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

Maintenant que tout est prêt, lancez le test. $ rspec spec/lib/user_authenticator_spec.rb

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

Ensuite, un tel message est craché. Cette erreur semble se produire si vous avez null: false au niveau de la base de données, mais il est nul.

Ensuite, écrivez la logique pour générer le jeton afin qu'il ne soit pas nul. Faites un test avant cela.

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

Ajoutez ce code à la fin.

Le premier est de savoir si le jeton est correctement inclus lorsque le AccessToken est renouvelé. Je l'écrirai plus tard, mais je l'écrirai plus tard pour que le jeton soit automatiquement entré lorsqu'il est nouveau.

La seconde est de savoir si le nombre d'AccessToken augmente de 1. N'est-il pas pris dans la validation? Qu'il n'atteigne pas la validation ou non, je crée généralement un modèle, j'utilise la première valeur de la seconde, je construis et je vérifie si cela réussit correctement la validation, mais cette fois un peu Étant donné que le jeton est généré automatiquement lorsque vous créez un nouveau jeton spécial, vous ne pouvez pas le tester. Parce que vous ne pouvez pas spécifier un argument comme AccessToken.new (old_token). Avec AccessToken.new, le jeton est automatique.

implémentation de la logique de génération de jeton

Écrivons maintenant la logique pour générer le jeton.

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

La méthode spécifiée par after_inialize est exécutée lors de la création du modèle.

Je le tourne en boucle parce que je veux créer un jeton autant de fois que je le souhaite, à moins que les conditions spécifiées par break ne soient remplies. Générez un jeton à l'aide de la classe SecureRandom. Les valeurs sont créées de manière aléatoire, il n'est donc pas garanti que les mêmes valeurs seront générées exactement. Alors faisons une boucle. La condition de rupture a une valeur en jeton. Et la même valeur n'existe pas dans la base de données. Faites une boucle autant de fois que vous le souhaitez, à moins que ce ne soit le cas. Habituellement, il se brise une fois qu'il tourne.

Exécutez le test. $ rspec spec/models/access_token_spec.rb $ rspec spec/lib/user_authenticator_spec.rb

Assurez-vous que ce test réussit.

Au fait, ʻuser.create_access_tokendans user_authenticator.rb Cette méthode n'est pas définie quelque part, elle est automatiquement générée par les rails. Le sens reste le même, mais si vous le remplacez de manière facile à comprendre,AccessToken.create(user_id: user.id)` Cela a la même signification que cela.

Maintenant que la logique de génération de jetons est terminée, passons à autre chose.

Fonction de connexion

Ensuite, nous allons implémenter l'image globale de la fonction de connexion. Actuellement, un mécanisme pour générer un jeton a été établi, mais une fonction de connexion utilisant ce jeton n'a pas encore été implémentée. Je vais donc mettre en œuvre ce domaine.

Test des points finaux

Mais écrivez d'abord à partir du test. Je n'ai pas encore terminé le routage, je vais donc commencer par le test de routage. Il n'y a pas de fichier à décrire, alors créez-le.

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

L'explication de la description est omise.

Lorsque j'exécute le test, il ne dit pas de correspondance / connexion de route, alors modifiez routes.rb.

config/routes.rb


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

Essai.

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

On dit qu'il n'y a pas de contrôleur, alors je vais en faire un.

génération 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

Relancez le test. Le test réussit. Ceci termine l'installation du point de terminaison de connexion.

Test de access_tokens_controller

Maintenant, testons le contrôleur. Créez et décrivez le fichier suivant.

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

Je m'attends à ce que 401 soit retourné sans authentification. 401 n'est pas autorisé, mais il est sémantiquement non authentifié, il est donc souvent utilisé comme réponse lorsqu'il n'est pas authentifié.

Bien que ce soit encore nouveau, lorsque le contrôleur rails g est utilisé, des fichiers tels que requests / access_tokens_request_spec.rb sont automatiquement générés. C'est le successeur du test du contrôleur, mais la façon dont il est écrit est légèrement différente de controller_spec, donc cette fois j'ai créé et décrit le fichier moi-même à dessein. À l'origine, il est recommandé d'écrire dans request_spec.

Exécutez le test.

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

Puisque l'action de création n'est pas définie, écrivez-la.

app/controllers/access_tokens_controller.rb


class AccessTokensController < ApplicationController
  def create

  end
end

Exécutez le test. J'attends 401, mais 204 est de retour. 204 signifie: no_content.

Donc pour le moment, je vais l'écrire dans le contrôleur pour passer le test.

créer la mise en œuvre

app/controllers/access_tokens_controller.rb


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

Exécutez le test pour vous assurer qu'il réussit.

J'ajouterai plus de tests.

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

Attendez-vous à ce qu'une erreur res correcte soit renvoyée dans le cas de 401. La déclaration d'erreur est modifiée et utilisée en la copiant à partir du site suivant. https://jsonapi.org/examples/

Exécutez ensuite le test.

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

Puisque nil est renvoyé, écrivez un processus qui renvoie correctement une erreur du côté de la commande.

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

Assurez-vous que cela passe le test.

Actuellement, lorsque l'action de création est appelée, elle donne une erreur dans tout, mais corrige-la.

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

J'édite également le code pour le refactoring. À ce stade, nous écrivons finalement ʻUserAuthenticator.new (params [: code]) `. La logique pour créer un utilisateur en échangeant du code et des jetons, que j'ai écrits tout le temps, est écrite dans UserAuthenticator, mais je l'appelle ici.

Puis exécutez-le avec perform.

Le corps de l'erreur 401 est écrit dans la méthode. L'erreur renvoyée à ce stade est ʻUserAuthenticator :: AuthenticationError`, donc rescue_from la sauvera. Puisqu'il est écrit dans la méthode, il peut être appelé avec rescue_from.

Après cela, dans UserAuthenticator :: AuthenticationError, je souhaite émettre la même erreur même lorsque le code est vide. Au fait, j'ai besoin de refactoriser.

Refactoring et réparation

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

Vous pouvez maintenant obtenir une erreur lorsque le code est vide.

Pour récapituler, le code est le jeton envoyé depuis le frontal. Le frontal récupère le jeton de github et l'envoie à l'api. C'est du code (github_access_code). L'API reçoit le code et communique avec GitHub pour échanger le code contre un jeton (par la méthode exchange_code_for_token). Avec ce jeton, les informations utilisateur github peuvent être obtenues à partir de l'API github.

Sur cette base, il est possible que le code soit suffisamment vide, alors préparez une erreur.

Exécutez le test pour vous assurer qu'il réussit.

Refactor supplémentaire.

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

Laissez complètement authentification_error à l'application_controller afin que tous les contrôleurs puissent détecter cette erreur. La raison en est que des erreurs d'authentification peuvent survenir sur n'importe quel contrôleur.

Exécutez le test pour vous assurer que rien n'a changé.

Et il est encore mieux d'utiliser cette implémentation dans les tests. Le code va coller toutes les modifications pour l'instant car la description sera longue

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

J'aimerais que vous lisiez attentivement le code pour voir ce que vous faites, mais ici, nous utilisons deux tests avec shared_examples_for. should return 401 status code should return proper error body

Ces deux tests seront souvent réutilisés à l'avenir. Vous pouvez également appeler shared_examples_for en utilisant it_behaves_like. En utilisant le sujet et en le réglant sur DRY, vous pouvez entrer librement une valeur pour chaque sujet.

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

Aussi, concernant cette partie, cette description a été utilisée dans le test avant, et elle est reproduite avec simulacre sans se connecter directement à l'API github. Cela vous permet de reproduire l'API github sans vous connecter réellement à github.

Ensuite, j'écrirai un test lorsque le code est correct.

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

Il s'agit simplement d'un simulacre de manipulation du fait que le code est correct ou incorrect. Je m'attends simplement à ce que 201 soit renvoyé si le code est correct.

Exécutez le test.

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

Ce message s'affiche Donc, éditez le contrôleur de sorte que 201 soit renvoyé en réponse.

app/controlers/access_token_controller.rb


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

    render json: {}, status: :created
  end

Ajouter le rendu et le retour créés.

Maintenant, exécutez à nouveau le test et assurez-vous qu'il réussit.

Ensuite, je veux l'implémenter afin qu'il renvoie une réponse fermement. Je vais donc écrire à partir du test.

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

Ajoutez ce test à la fin. Quant au contenu du test, comme dans le cas de l'article, recevez la valeur avec json_data ['attributes'] et vérifiez si le contenu est correct. Puisque l'utilisateur récupéré par User.find_by est décrit par mock à l'aide de user_data décrit précédemment, le test que la valeur et la valeur renvoyée en tant que réponse sont les mêmes.

Cependant, même si j'exécute le test, je ne peux pas le récupérer avec json_data car je n'utilise pas de sérialiseur et json.data n'existe pas. Donc, nous allons introduire le sérialiseur pour faire une réponse dans un format soigné.

génération de sérialiseur

$ rails g serializer access_token

Cela sera décrit dans le fichier créé.

app/serializers/access_token_serializer.rb


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

Ajoutez la description du jeton. Cela permet à la réponse d'inclure un jeton.

Et spécifiez également la valeur à renvoyer par le rendu dans le contrôleur.

access_tokens_controller.rb


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

Cela vous permet de renvoyer une réponse bien formée au lieu d'un hachage de.

Exécutez le test. Puis un message apparaît.

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

Quelque part, le jeton a été recréé et la valeur a été changée. C'est un bogue qui crée des jetons à chaque fois que vous rechargez.

Donc, je vais écrire un test pour corriger le bogue.

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

Tout d'abord, lancez un test pour voir si le bogue peut être reproduit.

expected: "3afe2f824789a229014c" got: "c5e04c73aa7ff89fd0a1"

J'ai pu le reproduire correctement, j'ai donc reçu un message.

Améliorons-nous. Jetons d'abord un coup d'œil à la méthode buggy 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 

Il y a quelque chose qui ne va pas ici, le problème était que la condition de rupture n'était pas bonne. break if token.present? && !AccessToken.exists?(token: token) Cette condition a une valeur solide en jeton. Et le jeton n'existe pas dans la base de données. Cela devient une condition. Mais ce serait un peu une contradiction. L'existence du jeton signifie qu'il est stocké dans la base de données, donc cette expression conditionnelle ne peut pas être satisfaite. Par conséquent, il est condition qu'il n'y ait aucun jeton autre que le jeton spécifié qui a le même jeton.

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)

De cette manière, il est possible de créer une condition appelée jeton autre que le jeton actuellement spécifié. Maintenant, exécutez le test et assurez-vous qu'il réussit.

Fonction de déconnexion

Tests supplémentaires des points de terminaison

Maintenant, implémentons la fonction de déconnexion.

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

Écrivez un test de routage.

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

Ajout d'une ligne de déconnexion.

Le test réussit.

la mise en oeuvre

Ensuite, j'écrirai un test pour le contrôleur.

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

Initialement traité comme une erreur 403, mais renommé pour clarifier le rôle. Ensuite, j'écrirai tout un test dédié à détruire. Continuez à lire le contenu.

La notation de @@ est un code qui indique le nombre de lignes décrites, et il n'est pas nécessaire de l'écrire réellement.

Ensuite, implémentez le contrôleur.

app/controllers/access_tokens_controller.rb


  def destroy
    raise AuthorizationError
  end

Définissez la méthode de destruction. Tout d'abord, pour réussir le test de réponse d'erreur, augmentez AuthorizationError et définissez réellement l'état réel de l'erreur dans 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

Le contenu de l'erreur est le même que ce qui a été écrit dans le test.

Maintenant, exécutez le test et assurez-vous qu'il réussit.

Cependant, comme il y a une description légèrement dupliquée, je vais la rendre sèche.

spec/controllers/access_tokens_controller_spec.rb


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

Tout d'abord, utilisez shared_examples_for sous describe pour résumer la description.

La description suivante est incluse dans 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

Combinez les tests que vous avez écrits jusqu'à présent en un seul.

spec/controllers/access_tokens_controller_spec.rb


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

Et puisque c'est la description que it_behaves_likes appelle shared_expample_for, il appelle les requêtes interdites spécifiées précédemment dans la chaîne de caractères.

Maintenant que nous avons créé le même environnement qu'auparavant, exécutez-le à nouveau et assurez-vous que le test réussit.

Ensuite, nous allons combiner ces shared_example_for dans un seul fichier afin qu'ils puissent être utilisés. Il y a deux shared_example_for dans le fichier ʻaccess_tokens_controller_spec.rb` actuel, alors mettez-les ensemble dans le même fichier.

Créez spec / support / shared / json_errors.rb

Mettez-y la description de 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

Ensuite, toute la description de la source de découpe est supprimée.

spec/controllers/access_tokens_controller_spec.rb


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

Augmentez d'une étape l'imbrication de la définition du sujet. Ajoutez ensuite deux tests.

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

Dans ce test, le sujet n'est pas écrit car le sujet est déjà écrit dans shared_example_for, donc subject {delete: destroy} est automatiquement appelé. Et si vous utilisez avant, vous pouvez modifier le contenu de la demande. Cette fois, en mettant Invalid_token dans token, nous allons créer un utilisateur qui n'a pas été authentifié. Bien sûr, une erreur d'authentification se produira, donc un test qui s'y attend.

Exécutez maintenant le test pour vous assurer qu'il réussit.

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

Ensuite, écrivez un test pour quand la demande est valide. Pour envoyer la requête correcte, vous devez d'abord mettre le jeton dans headers ['authorisation'] et transmettre les permissions. Le porteur est l'authentification du porteur, et cette fois nous l'utiliserons.

Les tests s'attendent à ce que le modèle AccessToken soit réduit de un à partir de la base de données.

Assurez-vous maintenant que le test échoue correctement. Ici, une faute de frappe est souvent trouvée si vous confirmez qu'elle échoue correctement.

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



 Lorsque j'exécute le test, je reçois un message comme celui-ci:

 Forbidden est renvoyé car il est décrit de sorte que l'action de destruction renvoie toujours une erreur.

 Nous allons donc mettre en œuvre l'action de destruction.


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

  def destroy
    raise AuthorizationError
  end

Tout d'abord, ce que vous voulez faire avec cette destruction est de détruire le access_token de l'utilisateur qui a envoyé la requête. Alors écrivez comme suit.

app/controllers/access_tokens_controller.rb


  def destroy
    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

current_user fait référence à l'utilisateur actuellement connecté. Réfléchissez à la façon d'apporter current_user.

current_user ne peut pas être obtenu à partir de la requête à la fois. Cependant, si vous utilisez request.authorization, le Bearer xxxxxxxxxxxxxxxxxxxxx que vous avez envoyé dans le test plus tôt

Vous pouvez obtenir un jeton comme celui-ci. Utilisez donc ce jeton pour obtenir le 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

Tout d'abord, afin d'obtenir le jeton avec request.authorization et de rechercher le jeton dans la base de données, la méthode gsub est utilisée pour le couper avec une expression régulière. Si vous ne pouvez récupérer que la partie numérique du jeton, recherchez avec AccessToken.find_by et récupérez-la. Et si vous utilisez ce access_token.user, vous pouvez récupérer l'utilisateur qui a envoyé la demande. Et détruisez ce jeton Ensuite, la déconnexion est terminée.

La description de & . est appelée un opérateur bocce, et si vous l'ajoutez à une méthode dont vous savez à l'avance que nil peut revenir et devenir comme une méthode undifind, une erreur se produira dans le cas de nil. N'apparaît pas et nil est renvoyé comme valeur de retour telle quelle, donc aucune erreur ne se produit. quelque chose comme. Cette fois, Invalid_token peut être mélangé dans la requête, donc dans ce cas nil sera retourné, donc une erreur se produira à moins que l'opérateur Bocchi ne soit utilisé.

Exécutez maintenant le test et assurez-vous qu'il réussit tous les tests.

Ensuite, nous refactoriserons ce code.

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

Tout d'abord, découpez la description comme ceci. Ensuite, déplacez la description vers application_controller.rb. La raison en est que la logique qui reçoit cette requête et génère le current_user est une description que tout contrôleur souhaite utiliser.

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

Ensuite, écrivez la méthode comme celle-ci en privé. La méthode authorize! Donne une erreur 401 lorsque current_user n'est pas inclus. Utilisez la méthode access_token pour récupérer le bon access_token La méthode current_user récupère l'utilisateur pour ce jeton. La raison pour laquelle access_token et current_user sont séparés ici est de clarifier leurs rôles et leurs responsabilités distinctes.

Et enfin, écrivez pour que la méthode authorize! Définie puisse toujours être appelée.

app/controllers/access_tokens_controller.rb


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

La situation est toujours appelée par before_action. La raison pour laquelle seul destroy est spécifié est que s'il est appelé pendant l'action de création, ce sera une méthode qui ne peut pas être appelée.

Ces approches sont courantes, mais vous oubliez d'écrire before_action ou en écrivez trop. Alors, utilisez skip_before_action et spécifiez la méthode à sauter au contraire. En gros, en ce qui concerne la méthode authorize!, Il semble bon d'ignorer même créer.

app/controllers/application_controller.rb


  before_action :authorize!

  private

Ajout d'une description qui est toujours appelée ci-dessus privé.

app/controllers/access_tokens_controller.rb


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

Changez before_action et method.

app/controllers/articles_controller.rb


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

Et n'oubliez pas de sauter article_controller. Je veux faire l'indexation et montrer sans authentification.

Exécutez maintenant le test pour voir si vous obtenez les mêmes résultats qu'avant le refactoring.

$ bundle exec rspec

Exécutez tous les tests et assurez-vous qu'ils sont tous verts.

finalement

Je vous remercie pour votre travail acharné. Avec cela, nous avons pu implémenter la fonction d'authentification des utilisateurs qui était notre objectif initial. Celles-ci peuvent être remplacées par l'utilisation d'un joyau appelé «dispositif», mais selon que vous connaissez le mécanisme ou non, la réponse aux problèmes d'authentification des utilisateurs changera et je pense que le degré de compréhension est complètement différent. .. La zone autour du jeton est très difficile à imaginer, et lors de l'utilisation de oauth, il y a encore des gemmes qui remplacent tout, donc le mécanisme a tendance à être en boîte noire. Donc, cette fois, j'ai utilisé l'authentification utilisateur comme celle-ci.

Suite ajoutée

J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part3

Recommended Posts

J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2 -authentification de l'utilisateur-
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part3-Implémentation d'action avec authentification-
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part1-Implémentation de l'action sans authentification-
[Rails] Test avec RSpec
J'ai créé un domaine api avec Spring Framework. Partie 1
Construire une API de tableau d'affichage avec certification et autorisation avec Rails 6 # 18 ・ Implémentation du contrôleur de l'utilisateur final
Créez une API de tableau d'affichage avec certification et autorisation avec Rails 6 # 3 RSpec, FactoryBot introduit et post-modèle
Créer une API de tableau d'affichage avec autorisation dans Rails 6 # 12 Association d'utilisateur et de publication
[Rails] J'ai implémenté le message d'erreur de validation avec une communication asynchrone!
[Ruby on Rails] Implémenter la fonction de connexion par add_token_to_users avec l'API
Ce à quoi j'étais accro lors de la mise en œuvre de l'authentification Google avec des rails
Mise en œuvre de la fonction d'authentification avec Spring Security ②
Implémentez la fonction d'authentification avec Spring Security ③
Test de l'API REST à l'aide de REST Assured Part 2
[rails] Faisons un test unitaire avec Rspec!
# 16 paramètre de stratégie pour créer une API de tableau d'affichage avec autorisation de certification dans Rails 6
Implémentation n ° 8 pour créer une API de tableau d'affichage avec autorisation de certification dans Rails 6
Créez une API de tableau d'affichage avec certification et autorisation avec Rails 6 # 1 Construction de l'environnement
Créer une API de tableau d'affichage avec autorisation de certification dans Rails 6 # 13 Accorder l'en-tête d'authentification
Présentation du sérialiseur n ° 9 pour créer une API de tableau d'affichage avec certification et autorisation dans Rails 6