[RUBY] Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil3-Aktionsimplementierung mit Authentifizierung

Einführung

Dieser Artikel wird Teil 3 sein. Wenn Sie Teil1 und Teil2 nicht gesehen haben, sehen Sie bitte dort. (Sehr lang)

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

In diesem Teil3 verwenden wir die in Teil2 implementierte Benutzerauthentifizierung, um Aktionen zu implementieren, die nur verwendet werden können, wenn eine Authentifizierung wie z. B. eine Erstellungsaktion ausgeführt wird. Dieses Mal ist das Ziel, Aktionen zum Erstellen, Aktualisieren und Zerstören zu implementieren. Lass uns zum ersten Mal gehen.

Aktion erstellen

create Endpunkt hinzufügen

Zuerst werden wir Endpunkte hinzufügen. Und schreiben Sie vorher einmal einen Test.

spec/routing/articles_spec.rb


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

Da die http-Anfrage post ist, wird die Aktion create mit post anstatt get geschrieben.

$ bundle exec rspec spec/routing/articles_spec.rb

No route matches "/articles"

Es wird so aussehen, also werde ich Routing hinzufügen

Endpunktimplementierung

config/routes.rb


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

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

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

Aktionsimplementierung erstellen

spec/controllers/articles_controller_spec.rb


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

Fügen Sie diese Beschreibung am Ende hinzu.

Und ich werde auch einen Test schreiben, wenn die Authentifizierung mit den in Teil2 definierten verbotenen Anforderungen nicht funktioniert

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

Diese verbotenen_Anfragen führen einen Test aus, bei dem erwartet wird, dass ein 403 zurückgegeben wird.

$ rspec spec/controllers/articles_controller_spec.rb

Dann wird die folgende Nachricht zurückgegeben The action 'create' could not be found for ArticlesController Es wird gesagt, dass die Erstellungsaktion nicht gefunden werden kann. Definieren wir sie also.

app/controllers/articles_controller.rb


  def create

  end

Führen Sie nun den Test erneut aus, um sicherzustellen, dass alles erfolgreich ist. Wenn der Test bestanden wird, bedeutet dies, dass die Zertifizierung ordnungsgemäß funktioniert.

Schreiben wir nun einen Test, um die Aktion "Erstellen" zu implementieren.

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

Test hinzugefügt. Ich habe viel auf einmal hinzugefügt, aber jedes hat viele Teile, die bereits angekommen sind und abgedeckt sind.

Der hinzugefügte Test ist "wenn autorisiert". Wenn die Authentifizierung erfolgreich ist, wird er getestet. Jeder zu testende Gegenstand when invalid parameters provided should return 422 status code should return proper error json

Wird hinzugefügt. Wenn der Parameter korrekt ist, werde ich ihn später schreiben.

Wenn der Parameter von stammt, kann ich davon ausgehen, dass kein Leerzeichen zurückgegeben werden kann. Der Quellzeiger zeigt an, wo der Fehler auftritt. Dieses Mal ist alles eine Zeichenfolge von, daher gehe ich davon aus, dass von allem nicht leer zurückgegeben werden kann.

Führen Sie den Test aus. Zwei Tests schlagen fehl. expected the response to have status code :unprocessable_entity (422) but it was :no_content (204)

Zuerst hoffe ich auf eine nicht verarbeitbare Antwort, aber no_content ist zurück. Ich möchte no_content zurückgeben, wenn createa erfolgreich ausgeführt wird, daher werde ich es später beheben.

unexpected token at ''

Der zweite ist der Fehler, da JSON.parse einen Fehler mit der Zeichenfolge von ausgibt.

Jetzt implementieren wir es auf dem Controller und beseitigen den Fehler.

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

Ich erstelle eine Instanz von ActionController :: Parameters, da ich damit StrongParameter verwenden kann. Sie können allow und require verwenden, die Instanzmethoden von ActionController :: Parameters sind. Wenn Sie allow oder require verwenden, können Sie den unnötigen Teil abschneiden, wenn er sich von dem unterscheidet, was Sie formal erwarten, oder wenn ein Parameter mit einem anderen Schlüssel gesendet wird.

Ich habe einen Adapter für das Rendern angegeben, der das Format angibt. Wenn Sie diesen Adapter nicht angeben, werden standardmäßig Attribute angegeben. Dieses Mal benutze ich eine Person namens json_api. Das Folgende zeigt den Unterschied als Beispiel. Ich habe es von Weitere Informationen zu Rails active_model_serializer_100DaysOfCode Challenge Day 10 (Tag_10: # 100DaysOfCode) kopiert.

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"
            }
        }
   ]
}

Dieses Mal werden wir json_api verwenden, das für API geeignet ist.

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

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

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

Sie haben das richtige Token und die richtigen Parameter eingegeben. Führen Sie nun den Test aus.

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

Ich denke, jeder der drei Tests wird auf diese Weise fehlschlagen. Da diese die richtigen Fehler machen, werden wir den Controller implementieren, wenn die Parameter tatsächlich korrekt sind.

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

Als nächstes bearbeiten Sie erstellen wie folgt. Wenn ein Fehler auftritt, wird die Rettung verwendet, um den Fehler beim Rendern zu überspringen.

Wenn Sie in article_params eine Bedingung festlegen, dass nur : title ,: content,: slug in: Attribute in: data erfasst wird, werden alle anderen als dieses angegebene Format abgespielt. Ich bin.

Wenn ich jetzt den Test durchführe, geht alles durch.

Ich werde noch ein Refactoring durchführen.

app/controllers/articles_controller.rb


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

Da dieser "ActiveModel :: Serializer :: ErrorSerializer" lang ist, werde ich ihn an eine andere Klasse an anderer Stelle erben, damit er kurz geschrieben werden kann.

Erstellen Sie app / serializers / error_serializer.rb

app/serializers/error_serializer.rb


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

Lass es so vererbt werden.

app/controllers/articles_controller.rb


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

Und Sie können die lange Beschreibung bereinigen. Führen Sie einen Test durch, um festzustellen, ob er fehlschlägt.

Damit ist die Implementierung der Aktion zum Erstellen eines Artikels abgeschlossen.

Aktion aktualisieren

update Endpunkt hinzufügen

Beginnen wir mit dem erneuten Hinzufügen von Endpunkten. Zunächst werde ich einen Test schreiben.

spec/routing/articles_spec.rb


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

Ich werde wie jedes Mal Endpunkttests schreiben. Da es sich bei der http-Anforderung um Patch oder Put handelt, verwendet die Show-Aktion eine von beiden.

Führen Sie den Test aus, um sicherzustellen, dass der Fehler korrekt angezeigt wird.

config/routes.rb


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

Fügen Sie ein Update hinzu, um sicherzustellen, dass der Test erfolgreich ist.

Aktualisierungsaktion hinzugefügt

Als nächstes schreiben wir einen Test für die Controller # Update-Aktion.

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

Der Unterschied zwischen der Aktualisierungsaktion und der Erstellungsaktion besteht in der Art der Anforderung und der Aktualisierung, die sich bereits in der Datenbank befindet. Da es nur die Situation gibt, dass ein Artikel als Ziel ausgewählt werden muss, habe ich nur den Test zum Erstellen kopiert, mit Ausnahme des Teils, in dem der Artikel zuerst erstellt wird, und des Teils, in dem die Anforderung definiert ist.

Führen Sie nun den Test aus.

The action 'update' could not be found for ArticlesController

Ich denke, Sie werden einen solchen Fehler bekommen. Definieren wir also das Update.

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

Es ist nichts Neues, also werde ich es nicht erklären.

Führen Sie nun den Test aus, um sicherzustellen, dass alles durchläuft. Wenn Sie den Unterschied zwischen Erstellen und Aktualisieren kennen, können Sie feststellen, dass es fast keinen Unterschied gibt. Und Sie können fast den gleichen Test wiederverwenden.

Hier gibt es jedoch ein kleines Problem. Es kann auf Anfrage für jeden Artikel aktualisiert werden. Ich möchte nicht, dass es ohne Erlaubnis aktualisiert wird. Also werde ich es reparieren.

Wie das Problem behoben werden kann, ist ein Problem, das auftritt, da Benutzer und Artikel derzeit nicht miteinander verbunden sind. Daher werden wir Benutzer und Artikel mit einer Zuordnung versehen.

Stellen Sie zuvor die Zuordnung ein und testen Sie, ob der erwartete Wert zurückgegeben wird.

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

Ich habe den Test so hinzugefügt. Wir erstellen Artikel, die sich mit Benutzern verbinden und diese sogar authentifizieren.

Mit dem neu hinzugefügten Testelement überprüfe ich, ob verbotene_Anfragen ordnungsgemäß zurückgegeben werden, wenn versucht wird, den Artikel eines anderen Benutzers zu aktualisieren.

Nun, wenn Sie den Test ausführen

undefined method user=

Bei einer solchen Nachricht schlägt dies fehl. Dies ist ein Beweis dafür, dass der Verein nicht gegründet wurde, daher werden wir den Verein als nächstes festlegen.

app/models/article.rb


  belongs_to :user

app/models/user.rb


  has_many :articles, dependent: :destroy

Um die beiden Modelle zu verbinden, muss dem Artikelmodell eine Benutzer-ID zugewiesen werden. Fügen Sie diese also hinzu.

$ rails g migration AddUserToArticles user:references

$ rails db:migrate

Jetzt wurde der Verein selbst umgesetzt. Damit ändern wir die Beschreibung des Controllers.

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

Was sich in der Beschreibung geändert hat, ist, dass der zu findende Benutzer von current_user aufgerufen wird. Dies ermöglicht es Ihnen, nur vom angemeldeten Benutzer zu finden. Und wenn die angegebene ID nicht im Artikel von current_user enthalten ist, wird "ActiveRecord :: RecordNotFound" ausgelöst. Speichern Sie sie also wie folgt und geben Sie einen Autorisierungsfehler für die Authentifizierung aus.

Beschreiben Sie in create außerdem, wer den Artikel erstellen soll, und setzen Sie user_id auf article. Ich möchte es haben, also werde ich einige Änderungen vornehmen.

app/controllers/articles_controller.rb


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

Fügen Sie dann die Beschreibung der Zuordnung zum Factory-Bot hinzu.

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 Definiert automatisch die ID des Modells.

Wenn Sie den Test damit ausführen, besteht er. Als nächstes gehen wir zur Zerstörungsaktion über.

Aktion zerstören

Endpunkt zerstören hinzugefügt

Schreiben wir zunächst einen Test, um einen Endpunkt hinzuzufügen.

spec/routing/articles_spec.rb


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

Wenn ich den Test durchführe, wird die folgende Meldung angezeigt No route matches "/articles/1"

Bearbeiten wir also das Routing.

config/routes.rb


  resources :articles

Stellen Sie alles ein, ohne mit der einzigen Option anzugeben. Dies besteht den Routing-Test.

Fügen Sie als Nächstes einen Test für den Controller hinzu.

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

Der größte Teil des Codes für diesen Test ist eine Kopie des Aktualisierungstests. Der Inhalt ist nichts Neues. Führen Sie den Test aus.

The action 'destroy' could not be found for ArticlesController

Dieser Fehler ist korrekt, da wir die Zerstörungsaktion noch nicht definiert haben. Dann Controller Wird implementiert werden.

Zerstörungsaktion hinzugefügt

app/controllers/articles_controller.rb


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

Es zerstört einfach den angegebenen Artikel in current_user.

Führen Sie nun den Test aus.

Wenn Sie dies bestehen, ist alles erledigt. Vielen Dank, dass Sie lange bei uns geblieben sind!

Recommended Posts

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-
Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2 -Benutzerauthentifizierung-
# 8 Seed-Implementierung zum Erstellen einer Bulletin Board-API mit Zertifizierungsautorisierung in Rails 6
Erstellen Sie eine Bulletin Board-API mit Zertifizierung und Autorisierung im Rails 6 # 5-Controller und leiten Sie die Implementierung weiter
Erstellen Sie eine Bulletin Board-API mit Zertifizierungsberechtigung im Rails 6 # 7-Update und zerstören Sie die Implementierung
Verwenden der PAY.JP-API mit Rails ~ Implementierungsvorbereitung ~ (payjp.js v2)
[Schienen] Test mit RSpec
# 4 nach Validierung und Testimplementierung zum Erstellen einer Bulletin Board-API mit Zertifizierung und Autorisierung in Rails 6
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
[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 ①
[Schienen] Machen wir einen Unit-Test mit Rspec!
# 16 Richtlinieneinstellung zum Erstellen einer Bulletin Board-API mit Zertifizierungsberechtigung 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