Introducción

1

Creación de APIs en Ruby on Rails: Pruebas y Autenticación

2

Verificación de Entorno para Desarrollo en Ruby y Rails

Proyecto

3

Creación de APIs con Rails: Proyecto Blog API paso a paso

4

Configuración de Gemas para Pruebas en Proyectos Rails

5

Configuración de Gemas en Proyectos Rails: Arspec, Factory Bot y Database Cleaner

6

Implementación de un Health Check Endpoint en API con RSpec

7

Diseño de Casos de Uso y Diagramas de Entidad para Aplicaciones

8

Diagrama de Entidad Relación para Modelos de Aplicación

9

Modelado de Aplicaciones con TDD en Rails

10

Validaciones y Pruebas TDD en Rails: Modelos USR y Post

11

Implementación de Endpoints para Listar y Mostrar Posts con TDD

12

Implementación de Pruebas y Controladores en Rails

13

Creación y Actualización de Posts con Pruebas TDD

14

Implementación de Métodos y Manejo de Excepciones en Rails API

15

Serialización de Modelos en Rails con ActiveModelSerializer

16

Búsqueda y Filtrado de Posts por Título con TDD

17

Implementación de Búsqueda de Posts con Servicios en Rails

18

Problema N+1 en Rails: Detección y Solución Eficaz

19

Identificación y solución del problema N+1 en Rails

20

Flujo de Autenticación en APIs con Tokens y Proveedores

21

Pruebas de Autenticación en API con Test Driven Development

22

Autenticación con Tokens: Implementación en Rails API

23

Autenticación de Usuarios en Controladores Rails

24

Autenticación y Seguridad en CRUD de Posts en Rails

25

Pruebas de Creación y Actualización con Autenticación en Rails

26

Pruebas de API con Postman: Ejecución y Verificación de Respuestas

27

Caching en Aplicaciones Web: Funciones y Niveles

28

Aceleración de Búsquedas en Rails con Caching

29

Background Jobs en Rails: Conceptos y Funcionalidades

30

Procesamiento en Background y Envío de Correos con Rails

31

Envío de Correos en Rails con ActionMailer y Background Jobs

32

Autenticación y Autorización con JWT y Auth0 en Aplicaciones Web

Cierre

33

Creación de APIs con Rails: Buenas Prácticas y Features Avanzados

No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Pruebas de Creación y Actualización con Autenticación en Rails

25/33
Recursos

¿Cómo implementar pruebas unitarias para autenticación y actualización de datos?

En el desarrollo de aplicaciones, asegurar que los procesos de autenticación y manipulación de datos funcionan correctamente es esencial. A través de pruebas unitarias, podemos garantizar que los métodos de crear y actualizar se ejecuten con la seguridad y restricciones previstas. Este artículo examinará cómo implementar estas pruebas, paso a paso, usando un enfoque en el contexto de Rails.

¿Qué pruebas unitarias son necesarias?

Antes de implementar las pruebas, es crucial listar las diferentes situaciones a verificar:

  1. Creación de un post:

    • Se puede crear con autenticación.
    • No se puede crear sin autenticación (esperar un error 401).
  2. Actualización de un post:

    • Se puede actualizar un post propio con autenticación.
    • No se puede actualizar un post de otro usuario (se espera un error 401).
    • No se puede actualizar sin autenticación (se espera también un error 401).

¿Cómo crear las pruebas para la creación de un post?

Utilizando la autenticación adecuada, primero configuramos nuestros parámetros y encabezados para asegurarnos que las pruebas pasan los requisitos necesarios.

create_params = {
  title: 'title',
  content: 'some content',
  published: true
}

# Request con autenticación
response = post '/posts', params: create_params, headers: auth_headers
expect(response.status).to eq(201)

# Request sin autenticación
response = post '/posts', params: create_params
expect(response.status).to eq(401)

En el bloque de código anterior, definimos una estructura básica para crear un post, verificando si las respuestas contienen el estado esperado.

¿Qué debemos considerar al actualizar un post?

Cuando probamos actualizaciones, nos interesa tanto la propiedad del usuario sobre el post como la respuesta del sistema cuando se cambia uno que no le pertenece.

# Actualización de un post propio
update_params = { title: 'new title' }
response = put "/posts/#{user_post.id}", params: update_params, headers: auth_headers
expect(response.status).to eq(200)

# Intentar actualizar un post de otro usuario
response = put "/posts/#{other_user_post.id}", params: update_params, headers: auth_headers
expect(response.status).to eq(404)

Implementamos un test similar al anterior, pero para verificar que los posts solo puedan ser actualizados por su propietario. Cuando intentamos modificar el post de otro usuario, el sistema deberá devolver un error.

¿Cómo gestionar errores en las pruebas?

Es común que las pruebas arrojen errores inesperados como un "500 Internal Server Error" debido a excepciones mal manejadas en nuestro código. En tales casos, podemos ajustar el controlador de excepciones para manejar errores más específicos.

rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

def record_not_found
  render json: { error: 'Not Found' }, status: :not_found
end

Al manejar correctamente estas excepciones, garantizamos que el test muestre un "404 Not Found" en lugar de un error de servidor genérico.

¿Es posible mejorar y reutilizar el código de autenticación?

Al manejar el método de autenticación, es una buena práctica refactorizar su ubicación para que sea reutilizable en otros controles:

module Secured
  def authenticate_user
    # lógica de autenticación
  end
end

# Incluyendo en otros controladores
class PostsController < ApplicationController
  include Secured
  before_action :authenticate_user
end

Este enfoque ayuda a mantener el código limpio y disminuye la redundancia.

¿Por qué limpiar y revisar las pruebas futuras?

Una vez implementadas las pruebas para creación y actualización, es importante eliminar pruebas antiguas que ya no sean necesarias o que se hayan reemplazado por las nuevas, asegurando así un código más eficiente y menos propenso a errores.

Aportes 10

Preguntas 2

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Estuve un buen rato tratando de encontrar por qué me estaba fallando una prueba con el error:

Failure/Error: it { is_expected.to have_http_status(:unauthorized)}
       expected the response to have status code :unauthorized (401) but it was :internal_server_error (500)

Y tenía mal escrita la palabra unauthorized en un status. Eso me hizo dar cuenta que esas palabras no las elige Simon aleatoriamente sino que corresponde a un http status code así que decidí buscar el listado de simbolos rails para esos status y aquí está el listado:
http://billpatrianakos.me/blog/2013/10/13/list-of-rails-status-code-symbols/

Como una pequeña critica, podria decirte que cuando borraas y scrolleas en el codigo, esperes aunque sea 2 segundos para hacerlo, es muy dificil seguirte el paso si vas a una velocidad y es bastante molesto tener que estar pausando constantemente el video.

Es una buena practica usar la tecnica de setup, act, validation en cada test, enacapsulando então cada situacion de forma que no sea necesario hacer scroll mientras se lee el test y aprovechando mejor el lazy lookup, pues se debe tener en cuenta que una parte muy importante de los test es que sirvan como documentación, entonces deben tener una legibilidade lo mas comoda y clara possible sin importar si repetimos codigo en varias partes del documento, pues, en el caso de los test documentar es prioritario.

Por exemplo, la siguiente seria una mejor version de private_post_spec.rb:

require 'rails_helper'

RSpec.describe 'Posts with authentication' do
  describe 'Get /posts/{id}' do
    context 'when requisting others author post' do
      context 'when post is public' do
	#setup
        let!(:user) { create(:user) }
        let!(:other_user_published_post) { create(:published_post) }
        let!(:auth_headers) { { 'Authorization' => "Bearer #{user.auth_token}" } }

	#act
        before { get "/posts/#{other_user_published_post.id}", headers: auth_headers }

        context 'payload' do
          subject { payload }
	#validation
          it { is_expected.to include(:id) }
        end

        context 'response' do
          subject { response }
	#validation
          it { is_expected.to have_http_status(200) }
        end
      end

      context 'when post is arquived' do
        let!(:user) { create(:user) }
        let!(:other_user_archived_post) { create(:archived_post) }
        let!(:auth_headers) { { 'Authorization' => "Bearer #{user.auth_token}" } }

        before { get "/posts/#{other_user_archived_post.id}", headers: auth_headers }
        
        context 'payload' do
          subject { payload }
          it { is_expected.to include(:error) }
        end

        context 'response' do
          subject { response }
          it { is_expected.to have_http_status(404) }
        end
      end
    end
  end

  describe 'POST /posts' do
    context 'with valid authentication' do
      let!(:user) { create(:user) }
      let!(:auth_headers) { { 'Authorization' => "Bearer #{user.auth_token}" } }
      let!(:create_params) { { 'post' => {'title' => 'title', 'content' => 'content', 'status' => 'published'} } }

      before { post "/posts", params: create_params, headers: auth_headers }
        
      context 'payload' do
        subject { payload }
        
        it { is_expected.to include(:id, :title, :content, :status, :author) }
      end

      context 'response' do
        subject { response }

        it { is_expected.to have_http_status(201) }
      end
    end

    context 'without authentication' do
      let!(:create_params) { { 'post' => {'title' => 'title', 'content' => 'content', 'status' => 'published'} } }

      before { post "/posts", params: create_params }
        
      context 'payload' do
        subject { payload }

        it { is_expected.to include(:error) }
      end

      context 'response' do
        subject { response }

        it { is_expected.to have_http_status(401) }
      end
    end
  end

  describe 'PUT /posts' do
    context 'with valid authentication' do
      context 'when updating users post' do
        let!(:user) { create(:user) }
        let!(:user_post) { create(:published_post, user_id: user.id) }
        let!(:auth_headers) { { 'Authorization' => "Bearer #{user.auth_token}" } }
        let!(:update_params) { { 'post' => {'title' => 'title', 'content' => 'content', 'status' => 'published'} } }

        before { put "/posts/#{user_post.id}", params: update_params, headers: auth_headers }
          
        context 'payload' do
          subject { payload }

          it { is_expected.to include(:id, :title, :content, :status, :author) }
          it { expect(payload[:id]).to eq(user_post.id) }
        end

        context 'response' do
          subject { response }

          it { is_expected.to have_http_status(200) }
        end
      end

      context 'when updating other users post' do
        let!(:user) { create(:user) }
        let!(:other_user_published_post) { create(:published_post) }
        let!(:auth_headers) { { 'Authorization' => "Bearer #{user.auth_token}" } }
        let!(:update_params) { { 'post' => {'title' => 'title', 'content' => 'content', 'status' => 'published'} } }

        before { put "/posts/#{other_user_published_post.id}", params: update_params, headers: auth_headers }
          
        context 'payload' do
          subject { payload }

          it { is_expected.to include(:error) }
        end

        context 'response' do
          subject { response }

          it { is_expected.to have_http_status(404) }
        end
      end
    end
  end

  def payload
    JSON.parse(response.body).with_indifferent_access
  end
end	

El codigo para autenticacion hasta este punto deja igual acceder a la accion del controlador en el caso de que header no este presente, y da error cuando llamamos Current.user, mi codigo es el siguiente:

def authenticate_user!
    # authenticate header
    headers = request.headers
    token_regex = /^Bearer (\w+)/

    if headers[:authorization].present? && headers[:Authorization].match(token_regex)
      token = headers[:Authorization].match(token_regex)[1]
      Current.user = User.find_by(auth_token: token)
    end

    render json: { error: "Unauthorized" }, status: :unauthorized if Current.user.nil?
  end
Finished in 1.99 seconds (files took 2 seconds to load)
13 examples, 0 failures
Finished in 0.70052 seconds (files took 0.95774 seconds to load)
22 examples, 0 failures

Concerns

El orden de

rescue_from

en posts_controller,rb importa. Uno debe escribir de la excepción más general a la más especifica, el último rescue_from tiene la mayor prioridad:
https://stackoverflow.com/questions/58634123/global-error-handling-and-the-order-of-included-rescue-from

Estimados al realizar las pruebas se presenta un problema con el parámetro :autor dentro del include, que creen que pueda realizar para solucionar este problema.

++Código de la prueba: ++

describe "3. PUT /posts" do
    # con auth -> 
      # actualizar un post nuestro
      # !actualizar un post de otro -> 401
    context "with valid auth" do
      context "when updating users's post" do
        before { put "/posts/#{user_post.id}", params: update_params, headers: auth_headers }
        context "payload" do
          subject { payload }
          #it { is_expected.to include(:id, :title, :content, :published, :author) }
          it { expect(payload).to include(:id, :title, :content, :published) }
          it { expect(payload[:id]).to eq(user_post.id) }
        end
        context "response" do
          subject { response }
          it { is_expected.to have_http_status(:ok) }
        end
      end

      context "when updating other users's post" do
        before { put "/posts/#{other_user_post.id}", params: update_params, headers: auth_headers }
        context "payload" do
          subject { payload }
          it { is_expected.to include(:error) }
        end
        context "response" do
          subject { response }
          it { is_expected.to have_http_status(:not_found) }
        end
      end
    end
  end

  private

  def payload
    JSON.parse(response.body).with_indifferent_access
    #JSON.parse(response.body)
  end

Mensaje de Error:

Failures:

  1) Posts with authentication 3. PUT /posts with valid auth when updating users's post payload is e
xpected to include :id, :title, :content, :published, and :author
     Failure/Error: it { expect(payload).to include(:id, :title, :content, :published, :author) }

       expected {"autor" => {"email" => "[email protected]", "id" => 1, "name" => "Herbert Rodriguez
"}, "content" => "content", "id" => 1, "published" => true, "title" => "title"} to include :author
       Diff:
       @@ -1,5 +1,9 @@
       -[:id, :title, :content, :published, :author]
       +"autor" => {"email"=>"[email protected]", "id"=>1, "name"=>"Herbert Rodriguez"},
       +"content" => "content",
       +"id" => 1,
       +"published" => true,
       +"title" => "title",

     # ./spec/requests/private_posts_spec.rb:94:in `block (6 levels) in <top (required)>'
     # ./spec/rails_helper.rb:59:in `block (3 levels) in <top (required)>'
     # ./spec/rails_helper.rb:58:in `block (2 levels) in <top (required)>'

Finished in 1.8 seconds (files took 4.19 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/requests/private_posts_spec.rb:94 # Posts with authentication 3. PUT /posts with valid
auth when updating users's post payload is expected to include :id, :title, :content, :published, an
d :author

Quitando el parámetro :autor

.....

Finished in 2.17 seconds (files took 5.37 seconds to load)
5 examples, 0 failures

Hola les comparto un par de modificaciones por si a alguien le pasa, el inconveniente era que no le llegaban las cabeceras definidas y enviadas desde las pruebas a mi controlador.

Lo que me toco hacer fue lo siguiente:

En el controlador definir este metodo:

    def initialize(headers = {})
      @headers = headers
    end

eso al parecer lo que hizo fue permitir sobre escribir o hacer set al header del request.

Lo otro fue en el archivo secured.rb

dejar la funcion de esta manera:

module Secured
  def authenticate_user!
    #Bearer xxxxxxx
    token_regex = /Bearer (\w+)/
    #leer HEADER de Auth
    headers=request.headers
    #verificar que sea valido
    if headers['HTTP_AUTHORIZATION'].present? && headers['HTTP_AUTHORIZATION'].match(token_regex)
      token=headers['HTTP_AUTHORIZATION'].match(token_regex)[1]
      #debemos verificar que el token corresponda a un user
      if (Current.user=User.find_by_user_reset_password_token(token))
            return
      end
    end
    render json: {error:'Unauthorized'}, status: :unauthorized
  end
end

Quien sabe la causa científica de esto y el porque la codificación del curso no me funciono?

Gracias.