[RUBY] J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part3-Implémentation d'action avec authentification-

introduction

Cet article sera part3. Si vous n'avez pas vu part1 et part2, veuillez les consulter. (Très long)

↓part1 https://qiita.com/yoshi_4/items/6c9f3ced0eb20131903d ↓part2 https://qiita.com/yoshi_4/items/963bd1f5397caf8d7d67

Dans cette partie3, nous utiliserons l'authentification utilisateur implémentée dans la partie2 pour implémenter des actions qui ne peuvent être utilisées que lorsqu'une authentification telle qu'une action de création est effectuée. L'objectif cette fois est de mettre en œuvre des actions de création, de mise à jour et de destruction. Allons-y pour la première fois.

créer une action

créer Ajouter un point de terminaison

Tout d'abord, nous allons ajouter des points de terminaison. Et avant cela, faites un test une fois.

spec/routing/articles_spec.rb


  it 'should route articles create' do
    expect(post '/articles').to route_to('articles#create')
  end

Puisque la requête http est post, l'action create est écrite avec post au lieu de get.

$ bundle exec rspec spec/routing/articles_spec.rb

No route matches "/articles"

Cela ressemblera à ceci, donc j'ajouterai le routage

Implémentation des terminaux

config/routes.rb


  resources :articles, only: [:index, :show, :create]
$ bundle exec rspec spec/routing/articles_spec.rb

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

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

créer la mise en œuvre de l'action

spec/controllers/articles_controller_spec.rb


  describe '#create' do
    subject { post :create }
  end
end

Ajoutez cette description à la fin.

Et j'écrirai également un test lorsque l'authentification ne fonctionne pas en utilisant les interdits_requests définis dans la partie 2

spec/controllers/articles_controller_spec.rb


  describe '#create' do
    subject { post :create }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid parameters provided' do

    end
  end

Ceci interdit_rquests exécute un test qui s'attend à ce qu'un 403 soit renvoyé.

$ rspec spec/controllers/articles_controller_spec.rb

Ensuite, le message suivant sera retourné The action 'create' could not be found for ArticlesController On dit que l'action de création est introuvable, définissons-la.

app/controllers/articles_controller.rb


  def create

  end

Maintenant, relancez le test pour vous assurer que tout se passe bien. Si le test réussit, cela signifie que la certification fonctionne correctement.

Écrivons maintenant un test pour implémenter l'action de création.

spec/controllers/articles_controller_spec.rb


    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

        subject { post :create, params: invalid_attributes }

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

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do

      end
    end

Ajout d'un test. J'ai beaucoup ajouté à la fois, mais chacun a de nombreuses pièces déjà arrivées et couvertes.

Le test ajouté est «lorsqu'il est autorisé», donc si l'authentification réussit, il sera testé. Chaque article à tester when invalid parameters provided should return 422 status code should return proper error json

Est ajouté. Si le paramètre est correct, je l'écrirai plus tard.

Si le paramètre est de, je m'attends à ne pas pouvoir être retourné. Le pointeur source montre où l'erreur se produit. Cette fois, tout est une chaîne de, donc je suppose que ce qui ne peut pas être vide sera renvoyé de tout.

Exécutez le test. Deux tests échouent. expected the response to have status code :unprocessable_entity (422) but it was :no_content (204)

Tout d'abord, j'espère une réponse impossible à traiter, mais no_content est de retour. Je veux retourner no_content lorsque createa s'exécute avec succès, donc je vais le réparer plus tard.

unexpected token at ''

La seconde est l'erreur car JSON.parse donne une erreur avec la chaîne de caractères de.

Maintenant, implémentons-le sur le contrôleur et éliminons l'erreur.

app/controllers/articles_controller.rb


  def create
    article = Article.new(article_params)
    if article.valid?
      #we will figure that out
    else
      render json: article, adapter: :json_api,
        serializer: ActiveModel::Serializer::ErrorSerializer,
        status: :unprocessable_entity
    end
  end

  private

  def article_params
    ActionController::Parameters.new
  end

Je crée une instance d'ActionController :: Parameters car cela me permet d'utiliser StrongParameter. Vous pourrez utiliser permit et require, qui sont des méthodes d'instance d'ActionController :: Parameters. Si vous utilisez permit ou require, vous pouvez tronquer la partie inutile si elle est différente de ce que vous attendez formellement ou si un paramètre est envoyé avec une clé différente.

J'ai spécifié un adaptateur pour le rendu, qui spécifie le format. Si vous ne spécifiez pas cet adaptateur, les attributs sont spécifiés par défaut. Cette fois, j'utilise une personne appelée json_api. Ce qui suit montre la différence à titre d'exemple. Je l'ai copié à partir de Learn about Rails active_model_serializer_100DaysOfCode Challenge Day 10 (Day_10: # 100DaysOfCode).

attributes

[
    {
        "id": 1,
        "name": "Nakajima Hikari",
        "email": "[email protected]",
        "birthdate": "2016-05-02",
        "birthday": "2016/05/02"
    }
  ]
}

json_api

{
    "data": [
        {
            "id": "1",
            "type": "contacts",
            "attributes": {
                "name": "Nakajima Hikari",
                "email": "[email protected]",
                "birthdate": "2016-05-02",
                "birthday": "2016/05/02"
            }
        }
   ]
}

Cette fois, nous utiliserons json_api, qui convient aux api.

Exécutez le test et assurez-vous qu'il réussit.

Ensuite, j'écrirai un test lorsque le paramètre est correct.

spec/controllers/articles_controller_spec.rb


      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end

Vous avez entré le bon jeton et les bons paramètres. Maintenant, lancez le test.

expected the response to have status code :created (201) but it was :unprocessable_entity (422)

undefined method `[]' for nil:NilClass

`Article.count` to have changed by 1, but was changed by 0

Je pense que chacun des trois tests échouera de cette façon. Comme ils commettent les bonnes erreurs, nous implémenterons le contrôleur lorsque les paramètres seront réellement corrects.

app/controllers/articles_controller.rb


  def create
    article = Article.new(article_params)
    article.save!
    render json: article, status: :created
  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

  private

  def article_params
    params.requrie(:data).require(:attributes).
      permit(:title, :content, :slug) ||
    ActionController::Parameters.new
  end

Ensuite, modifiez créer comme ceci. Lorsqu'une erreur se produit, le sauvetage est utilisé pour ignorer l'erreur avec le rendu.

Dans article_params, en définissant une condition selon laquelle seul : title,: content ,: slug in: attributes dans: data est acquis, tous les autres que ce format spécifié seront lus. Je suis.

Maintenant, quand je lance le test, tout passe.

Je vais faire un refactoring de plus.

app/controllers/articles_controller.rb


  rescue
    render json: article, adapter: :json_api,
      serializer: ActiveModel::Serializer::ErrorSerializer,
      status: :unprocessable_entity
  end

Puisque ce ʻActiveModel :: Serializer :: ErrorSerializer, `est long, je vais l'hériter dans une classe différente ailleurs afin qu'il puisse être écrit court.

ʻCreate app / serializers / error_serializer.rb`

app/serializers/error_serializer.rb


class ErrorSerializer < ActiveModel::Serializer::ErrorSerializer; end

Qu'il soit hérité comme ça.

app/controllers/articles_controller.rb


  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

Et vous pouvez nettoyer la longue description. Exécutez un test pour voir s'il échoue.

Ceci termine la mise en œuvre de l'action pour créer un article.

action de mise à jour

mettre à jour Ajouter un point de terminaison

Commençons par ajouter à nouveau des points de terminaison. Tout d'abord, j'écrirai un test.

spec/routing/articles_spec.rb


  it 'should route articles show' do
    expect(patch '/articles/1').to route_to('articles#update', id: '1')
  end

J'écrirai des tests de point de terminaison comme à chaque fois. Puisque la requête http est patch ou put, l'action show utilise l'un ou l'autre.

Exécutez le test pour vous assurer d'obtenir l'erreur correctement.

config/routes.rb


  resources :articles, only: [:index, :show, :create, :update]

Ajoutez une mise à jour pour vous assurer que le test réussit.

action de mise à jour ajoutée

Ensuite, écrivons un test pour l'action de mise à jour du contrôleur.

spec/controllers/articles_controller_spec.rb


  describe '#update' do
    let(:article) { create :article }

    subject { patch :update, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when authorized' do
      let(:access_token) { create :access_token }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
      context 'when invalid parameters provided' do
        let(:invalid_attributes) do
          {
            data: {
              attributes: {
                title: '',
                content: '',
              }
            }
          }
        end

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

        it 'should return proper error json' do
          subject
          expect(json['errors']).to include(
            {
              "source" => { "pointer" => "/data/attributes/title" },
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/content"},
              "detail" => "can't be blank"
            },
            {
              "source" => {"pointer" => "/data/attributes/slug"},
              "detail" => "can't be blank"
            }
          )
        end
      end

      context 'when success request sent' do
        let(:access_token) { create :access_token }
        before { request.headers['authorization'] = "Bearer #{access_token.token}" }
        let(:valid_attributes) do
          {
            'data' => {
              'attributes' => {
                'title' => 'Awesome article',
                'content' => 'Super content',
                'slug' => 'awesome-article',
              }
            }
          }
        end

        subject { post :create, params: valid_attributes }

        it 'should have 201 status code' do
          subject
          expect(response).to have_http_status(:created)
        end

        it 'should have proper json body' do
          subject
          expect(json_data['attributes']).to include(
            valid_attributes['data']['attributes']
          )
        end

        it 'should create article' do
          expect { subject }.to change{ Article.count }.by(1)
        end
      end
    end
  end

La différence entre l'action de mise à jour et l'action de création réside dans le type de demande et la mise à jour déjà dans la base de données. Puisqu'il n'y a qu'une situation où il y a un article à cibler, je viens de copier le test de création sauf pour la partie où l'article est créé en premier et la partie où la demande est définie.

Maintenant, lancez le test.

The action 'update' could not be found for ArticlesController

Je pense que vous obtiendrez une erreur comme celle-ci. Alors, définissons réellement la mise à jour.

app/controllers/articles_controller.rb


  def update
    article = Article.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

Ce n'est pas nouveau, donc je ne vais pas l'expliquer.

Maintenant, exécutez le test pour vous assurer que tout se passe bien. Si vous connaissez la différence entre créer et mettre à jour, vous pouvez voir qu'il n'y a presque aucune différence. Et vous pouvez réutiliser presque le même test.

Cependant, il y a un léger problème ici. Il peut être mis à jour sur l'article de n'importe qui sur demande. Je ne veux pas qu'il soit mis à jour sans autorisation. Alors je vais le réparer.

Quant à la façon de le résoudre, c'est un problème qui se produit parce que l'utilisateur et l'article ne sont pas liés pour le moment, nous allons donc ajouter une association à l'utilisateur et à l'article.

Avant cela, définissez l'association et testez que la valeur attendue est renvoyée.

spec/controllers/articles_controller_spec.rb


   describe '#update' do
+    let(:user) { create :user }
     let(:article) { create :article }
+    let(:access_token) { user.create_access_token }

     subject { patch :update, params: { id: article.id } }

@ -140,8 +142,17 @@ describe ArticlesController do
       it_behaves_like 'forbidden_requests'
     end

+    context 'when trying to update not owned article' do
+      let(:other_user) { create :user }
+      let(:other_article) { create :article, user: other_user }
+
+      subject { patch :update, params: { id: other_article.id } }
+      before { request.headers['authorization'] = "Bearer #{access_token.token}" }
+
+      it_behaves_like 'forbidden_requests'
+    end

     context 'when authorized' do
-      let(:access_token) { create :access_token }
       before { request.headers['authorization'] = "Bearer #{access_token.token}" }

       context 'when invalid parameters provided' do
         let(:invalid_attributes) do

J'ai ajouté le test comme ça. Nous créons des articles qui se connectent avec les utilisateurs et même les authentifient.

Ce que je fais avec l'élément de test nouvellement ajouté est de vérifier si les interdictions_requests sont correctement renvoyées lorsque je tente de mettre à jour l'article d'un autre utilisateur.

Maintenant, lorsque vous exécutez le test

undefined method user=

Cela échouera avec un message comme celui-ci. Comme c'est la preuve que l'association n'a pas été établie, nous définirons l'association ensuite.

app/models/article.rb


  belongs_to :user

app/models/user.rb


  has_many :articles, dependent: :destroy

Et, pour connecter les deux modèles, il est nécessaire de donner au modèle d'article un user_id, alors ajoutez-le.

$ rails g migration AddUserToArticles user:references

$ rails db:migrate

Maintenant, l'association elle-même a été mise en place. Donc, en utilisant cela, nous modifierons la description du contrôleur.

app/controllers/articles_controller.rb


  def update
    article = current_user.articles.find(params[:id])
    article.update_attributes!(article_params)
    render json: article, status: :ok
  rescue ActiveRecord::RecordNotFound
    authorization_error
  rescue
    render json: article, adapter: :json_api,
      serializer: ErrorSerializer,
      status: :unprocessable_entity
  end

Ce qui a changé dans la description, c'est que l'utilisateur à rechercher est appelé par current_user. Cela vous permet de rechercher uniquement à partir de l'utilisateur connecté. Et si l'id spécifié n'est pas dans l'article de current_user, ʻActiveRecord :: RecordNotFound` sera déclenché, alors sauvez-le comme ça et émettez une autorisation_error dédiée à l'authentification.

En outre, dans create, décrivez qui est l'article à créer et définissez user_id sur article. Je veux l'avoir, alors je vais faire quelques changements.

app/controllers/articles_controller.rb


   def create
-    article = Article.new(article_params)
+    article = current_user.articles.build(article_params)

Ensuite, ajoutez la description de l'association au bot d'usine.

spec/factories/articles.rb


FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "My article #{n}"}
    sequence(:content) { |n| "The content of article #{n}"}
    sequence(:slug) { |n| "article-#{n}"}
    association :user
  end
end

association :model_name Définira automatiquement l'id du modèle.

Si vous exécutez le test avec ceci, il réussira. Ensuite, passons à l'action de destruction.

détruire l'action

destroy endpoint ajouté

Tout d'abord, écrivons un test pour ajouter un point de terminaison.

spec/routing/articles_spec.rb


  it 'should route articles destroy' do
    expect(delete '/articles/1').to route_to('articles#destroy', id: '1')
  end

Lorsque j'exécute le test, j'obtiens le message suivant No route matches "/articles/1"

Alors, éditons le routage.

config/routes.rb


  resources :articles

Réglez tout sans spécifier avec la seule option. Cela passera le test de routage.

Ensuite, ajoutez un test pour le contrôleur.

spec/controllers/articles_controller_spec.rb


  describe '#delete' do
    let(:user) { create :user }
    let(:article) { create :article, user_id: user.id }
    let(:access_token) { user.create_access_token }

    subject { delete :destroy, params: { id: article.id } }

    context 'when no code provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid code provided' do
      before { request.headers['authorization'] = 'Invalid token' }
      it_behaves_like 'forbidden_requests'
    end

    context 'when trying to remove not owned article' do
      let(:other_user) { create :user }
      let(:other_article) { create :article, user: other_user }

      subject { delete :destroy, params: { id: other_article.id } }
      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it_behaves_like 'forbidden_requests'
    end

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

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

      it 'should have empty json body' do
        subject
        expect(response.body).to be_blank
      end

      it 'should destroy the article' do
        article
        expect{ subject }.to change{ user.articles.count }.by(-1)
      end
    end
  end

La plupart du code de ce test est une copie du test de mise à jour. Le contenu n'a rien de nouveau. Exécutez le test.

The action 'destroy' could not be found for ArticlesController

Cette erreur est correcte car nous n'avons pas encore défini l'action de destruction. Puis contrôleur Sera mis en œuvre.

Ajout de l'action de destruction

app/controllers/articles_controller.rb


  def destroy
    article = current_user.articles.find(params[:id])
    article.destroy
    head :no_content
  rescue
    authorization_error
  end

Il détruit simplement l'article spécifié dans current_user.

Maintenant, lancez le test.

Si vous réussissez, tout est fait. Merci d'être resté longtemps avec nous!

Recommended Posts

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-
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2 -authentification de l'utilisateur-
Implémentation n ° 8 pour créer une API de tableau d'affichage avec autorisation de certification dans Rails 6
Construire une API de tableau d'affichage avec certification et autorisation dans le contrôleur Rails 6 # 5, implémentation des routes
Créer une API de tableau d'affichage avec autorisation de certification dans la mise à jour Rails 6 # 7, détruire l'implémentation
Utilisation de l'API PAY.JP avec Rails ~ Préparation de l'implémentation ~ (payjp.js v2)
[Rails] Test avec RSpec
# 4 post-validation et mise en œuvre de test pour créer une API de tableau d'affichage avec certification et autorisation dans Rails 6
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
[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
Implémentez la fonction d'authentification avec Spring Security ③
Mise en œuvre de la fonction d'authentification avec Spring Security ①
[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
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