Módulo 13-14: Rails Intermedio
Funcionalidades avanzadas y testing en Rails
🔧 Módulo 13-14: Rails Intermedio
Duración: Semanas 17 y 18
Objetivos del Módulo
- Implementar autenticación y autorización robusta
- Dominar testing en aplicaciones Rails
- Configurar jobs en background
- Crear APIs REST profesionales
- Aplicar patrones de diseño en Rails
Contenido Teórico
1. Autenticación con Devise
Devise es la gema más popular para autenticación en Rails:
# Gemfile
gem 'devise'
# Instalación
rails generate devise:install
rails generate devise User
rails db:migrate
Configuración básica
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts, dependent: :destroy
def full_name
"#{first_name} #{last_name}"
end
end
Controladores con autenticación
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = current_user.posts
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: 'Post created successfully.'
else
render :new
end
end
private
def post_params
params.require(:post).permit(:title, :content, :published)
end
end
2. Autorización con CanCanCan
# Gemfile
gem 'cancancan'
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.admin?
can :manage, :all
else
can :read, Post, published: true
can :manage, Post, user: user
end
end
end
Uso en controladores
class PostsController < ApplicationController
load_and_authorize_resource
def index
# @posts ya está cargado y filtrado por CanCanCan
end
end
3. Testing con RSpec
# Gemfile (grupo test)
group :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'capybara'
gem 'selenium-webdriver'
end
Model specs
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
it 'requires an email' do
user = User.new(email: '')
expect(user).not_to be_valid
expect(user.errors[:email]).to include("can't be blank")
end
end
describe 'associations' do
it 'has many posts' do
association = described_class.reflect_on_association(:posts)
expect(association.macro).to eq :has_many
end
end
describe '#full_name' do
it 'returns the full name' do
user = User.new(first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq 'John Doe'
end
end
end
Controller specs
# spec/controllers/posts_controller_spec.rb
require 'rails_helper'
RSpec.describe PostsController, type: :controller do
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
before { sign_in user }
describe 'GET #index' do
it 'returns a success response' do
get :index
expect(response).to be_successful
end
it 'assigns user posts' do
get :index
expect(assigns(:posts)).to eq(user.posts)
end
end
end
Feature specs con Capybara
# spec/features/user_posts_spec.rb
require 'rails_helper'
RSpec.feature 'User Posts', type: :feature do
let(:user) { create(:user) }
before { login_as user }
scenario 'User creates a new post' do
visit posts_path
click_link 'New Post'
fill_in 'Title', with: 'My First Post'
fill_in 'Content', with: 'This is the content'
click_button 'Create Post'
expect(page).to have_content 'Post created successfully'
expect(page).to have_content 'My First Post'
end
end
4. Jobs en Background con Sidekiq
# Gemfile
gem 'sidekiq'
gem 'redis'
# config/application.rb
config.active_job.queue_adapter = :sidekiq
Crear un job
# app/jobs/email_notification_job.rb
class EmailNotificationJob < ApplicationJob
queue_as :default
def perform(user_id, post_id)
user = User.find(user_id)
post = Post.find(post_id)
UserMailer.new_post_notification(user, post).deliver_now
end
end
Usar el job
class PostsController < ApplicationController
def create
@post = current_user.posts.build(post_params)
if @post.save
# Enviar notificación en background
EmailNotificationJob.perform_later(current_user.id, @post.id)
redirect_to @post, notice: 'Post created successfully.'
else
render :new
end
end
end
5. APIs REST
Configurar API
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create, :update, :destroy]
end
end
end
Controlador API
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :update, :destroy]
def index
@posts = current_user.posts
render json: @posts
end
def show
render json: @post
end
def create
@post = current_user.posts.build(post_params)
if @post.save
render json: @post, status: :created
else
render json: { errors: @post.errors }, status: :unprocessable_entity
end
end
private
def set_post
@post = current_user.posts.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :content, :published)
end
end
6. Serializers con Active Model Serializers
# Gemfile
gem 'active_model_serializers'
# app/serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :published, :created_at
belongs_to :user
def user
{
id: object.user.id,
name: object.user.full_name,
email: object.user.email
}
end
end
7. Validaciones Avanzadas
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
validates :content, presence: true, length: { minimum: 10 }
validates :published, inclusion: { in: [true, false] }
# Validación personalizada
validate :title_not_in_blacklist
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
private
def title_not_in_blacklist
blacklisted_words = ['spam', 'fake', 'scam']
if blacklisted_words.any? { |word| title&.downcase&.include?(word) }
errors.add(:title, 'contains inappropriate content')
end
end
end
Ejercicios Prácticos
Ve al archivo ejercicios.md para practicar los conceptos aprendidos.
Proyecto del Módulo
Ve al archivo proyecto_blog_completo.md para completar el proyecto de este módulo.