La autenticación bien probada evita regresiones y errores silenciosos. Aquí se valida, paso a paso, cómo asegurar en Rails que solo usuarios autenticados puedan crear y actualizar, cómo responder con códigos correctos y cómo refactorizar la autenticación en un concern reutilizable. El foco está en pruebas claras, estados HTTP precisos y manejo explícito de excepciones.
¿Cómo validar la creación con autenticación en Rails?
Para creación se definen dos contextos: con autenticación válida y sin autenticación. Se envían parámetros anidados bajo la clave post, tal como exige el controlador en su método de parámetros fuertes. La verificación cubre el contenido del payload y el código de estado.
Con autenticación: se debe poder crear y responder con created.
Sin autenticación: no debe crear y debe responder unauthorized.
Los parámetros de creación incluyen: title, content y published.
El payload esperado incluye: id, title, content, published y author.
¿Qué parámetros y headers usar en el request?
Parám: enviar bajo la clave post.
Headers: usar auth headers válidos.
Método: POST a la ruta de posts.
# RSpeccontext 'with valid auth'do let(:create_params)do{post:{title:'title',content:'...',published:true}}end it 'crea el post y responde created'do post '/posts',params: create_params,headers: auth_headers
expect(payload).to include('id','title','content','published','author') expect(response).to have_http_status(:created)endendcontext 'without authentication'do it 'rechaza la creación con unauthorized'do post '/posts',params:{post:{title:'title',content:'...',published:true}} expect(payload).to include('error') expect(response).to have_http_status(:unauthorized)endend
¿Qué validar en el payload y response?
Contenido: incluir claves id, title, content, published y author.
Errores: incluir error si falta autenticación.
Estados: usar created y unauthorized según el caso.
¿Qué pruebas aseguran la actualización y control de acceso?
La actualización replica la estructura de creación, con énfasis en control de acceso. Con autenticación se puede actualizar el propio post; intentar actualizar el post de otro usuario debe responder not found. El estado esperado para éxito es ok.
Caso propio: actualizar vía PUT el recurso del usuario autenticado.
Caso ajeno: no permitir y responder not found.
Sin autenticación: ya cubierto por las validaciones de creación, evitar redundancia.
¿Cómo probar el update del propio post?
Ruta: PUT a /posts/:id del usuario autenticado.
Parámetros: usar update params con la misma estructura.
Validaciones: coincidir id y responder ok.
context "when updating user's post"do let(:update_params)do{post:{title:'new title',content:'...',published:true}}end it 'actualiza y responde ok'do put "/posts/#{user_post.id}",params: update_params,headers: auth_headers
expect(payload).to include('id','title','content','published','author') expect(payload['id']).to eq(user_post.id) expect(response).to have_http_status(:ok)endend
¿Cómo bloquear el update de otro usuario?
Ruta: PUT a /posts/:id de un tercero.
Resultado: payload con error y estado not found.
context "when updating other user's post"do let(:update_params)do{post:{title:'any',content:'...',published:true}}end it 'rechaza con not found'do put "/posts/#{other_user_post.id}",params: update_params,headers: auth_headers
expect(payload).to include('error') expect(response).to have_http_status(:not_found)endend
¿Cómo manejar excepciones y reutilizar autenticación con concerns?
Al buscar posts con find, si no existe el registro, Rails lanza ActiveRecord::RecordNotFound. Sin manejarla, un capturador genérico devuelve 500. Se debe capturar explícitamente y responder 404 not found. Además, para evitar duplicación, el método de autenticación se mueve a un concern llamado Secured y se incluye en los controladores que lo requieran.
Excepción: ActiveRecord::RecordNotFound debe mapear a not found.
Reutilización: extraer authenticate_user a un module compartido.
Limpieza: eliminar pruebas antiguas de creación y actualización sin autenticación, mantener listado y lectura de publicados.
¿Cómo capturar ActiveRecord::RecordNotFound y responder 404?
# ApplicationControllerrescue_from ActiveRecord::RecordNotFound do render json:{error:'Not found'},status::not_foundend
¿Cómo extraer authenticate_user a un concern reutilizable?
# app/controllers/concerns/secured.rbmoduleSecureddefauthenticate_user# implementación movida desde el controlador.endend# posts_controller.rbclassPostsController< ApplicationController
include Secured
# accionesend
¿Te gustaría compartir cómo estructuras tus contexts y expectativas para mantener las pruebas legibles y confiables?
Estuve un buen rato tratando de encontrar por qué me estaba fallando una prueba con el error:
Failure/Error: it { is_expected.tohave_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/
Gracias Diego!
Gracias my dude!
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.
Cual es la diferencia entre Request, payload y response?
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.toinclude(:id)} end
context 'response'do subject { response } #validation
it { is_expected.tohave_http_status(200)} end
end
context 'when post is arquived'dolet!(: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.toinclude(:error)} end
context 'response'do subject { response } it { is_expected.tohave_http_status(404)} end
end
end
end
describe 'POST /posts'do context 'with valid authentication'dolet!(: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.toinclude(:id,:title,:content,:status,:author)} end
context 'response'do subject { response } it { is_expected.tohave_http_status(201)} end
end
context 'without authentication'dolet!(:create_params){{'post'=>{'title'=>'title','content'=>'content','status'=>'published'}}} before { post "/posts",params: create_params } context 'payload'do subject { payload } it { is_expected.toinclude(:error)} end
context 'response'do subject { response } it { is_expected.tohave_http_status(401)} end
end
end
describe 'PUT /posts'do context 'with valid authentication'do context 'when updating users post'dolet!(: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.toinclude(:id,:title,:content,:status,:author)} it {expect(payload[:id]).toeq(user_post.id)} end
context 'response'do subject { response } it { is_expected.tohave_http_status(200)} end
end
context 'when updating other users post'dolet!(: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.toinclude(:error)} end
context 'response'do subject { response } it { is_expected.tohave_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:
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.toinclude(:id,:title,:content,:published,:author)} it {expect(payload).toinclude(:id,:title,:content,:published)} it {expect(payload[:id]).toeq(user_post.id)} end
context "response"do subject { response } it { is_expected.tohave_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.toinclude(:error)} end
context "response"do subject { response } it { is_expected.tohave_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)Postswith 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).toinclude(:id,:title,:content,:published,:author)} expected {"autor"=>{"email"=>"silas@schmidt.com","id"=>1,"name"=> "HerbertRodriguez"}, "content" => "content", "id" => 1, "published" => true, "title" => "title"} to include :author
Diff: @@ -1,5+1,9 @@
-[:id,:title,:content,:published,:author]+"autor"=>{"email"=>"silas@schmidt.com","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)>'
Finishedin1.8seconds(files took 4.19 seconds to load)5 examples,1 failure
Failed examples:rspec ./spec/requests/private_posts_spec.rb:94 # Postswith 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++
.....Finishedin2.17seconds(files took 5.37 seconds to load)5 examples,0 failures
estás mandando el parámetro 'autor', en vez de 'author' con h
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.
Extrano, no tiene sentido que guardar los headers en una variable de instancia en el controlador corrigiera el error, porque en el código de autenticaded_user! no la puedes referenciar. Lo más seguro es que haya sido alguna otra cosa.