🔧 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.