rails-controller
SKILL.md
Rails Controller Generator (TDD)
Creates RESTful controllers following project conventions with integration tests first.
Quick Start
- Write failing integration test in
test/controllers/ortest/integration/ - Run test to confirm RED
- Implement controller action
- Run test to confirm GREEN
- Refactor if needed
Project Conventions
This project uses:
- Pundit for authorization (
authorize @resource,policy_scope(Model)) - Pagy for pagination
- Presenters for view formatting
- Multi-tenancy via
current_account - Turbo Stream responses for dynamic updates
TDD Workflow
Step 1: Create Integration Test (RED)
# test/controllers/[resources]_controller_test.rb
require "test_helper"
class ResourcesControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:one)
@resource = resources(:one)
sign_in @user
end
# === INDEX ===
test "GET /resources returns success" do
get resources_path
assert_response :success
end
test "GET /resources shows only current_account resources (multi-tenant)" do
other_resource = resources(:other_account)
get resources_path
assert_includes response.body, @resource.name
assert_not_includes response.body, other_resource.name
end
test "GET /resources paginates results" do
get resources_path
assert_response :success
end
# === SHOW ===
test "GET /resources/:id returns success" do
get resource_path(@resource)
assert_response :success
end
test "GET /resources/:id returns 404 for other account" do
other_resource = resources(:other_account)
assert_raises(ActiveRecord::RecordNotFound) do
get resource_path(other_resource)
end
end
# === NEW ===
test "GET /resources/new returns success" do
get new_resource_path
assert_response :success
end
# === CREATE ===
test "POST /resources creates with valid params" do
assert_difference("Resource.count", 1) do
post resources_path, params: {
resource: { name: "New Resource", field1: "value" }
}
end
assert_redirected_to resources_path
assert_equal @user.account, Resource.last.account
end
test "POST /resources rejects invalid params" do
assert_no_difference("Resource.count") do
post resources_path, params: {
resource: { name: "" }
}
end
assert_response :unprocessable_entity
end
# === EDIT ===
test "GET /resources/:id/edit returns success" do
get edit_resource_path(@resource)
assert_response :success
end
# === UPDATE ===
test "PATCH /resources/:id updates with valid params" do
patch resource_path(@resource), params: {
resource: { name: "Updated Name" }
}
assert_redirected_to resource_path(@resource)
assert_equal "Updated Name", @resource.reload.name
end
test "PATCH /resources/:id rejects invalid params" do
patch resource_path(@resource), params: {
resource: { name: "" }
}
assert_response :unprocessable_entity
end
# === DESTROY ===
test "DELETE /resources/:id destroys resource" do
assert_difference("Resource.count", -1) do
delete resource_path(@resource)
end
assert_redirected_to resources_path
end
# === AUTHORIZATION ===
test "unauthenticated user is redirected" do
sign_out
get resources_path
assert_redirected_to new_session_path
end
end
Step 2: Run Test (Confirm RED)
bin/rails test test/controllers/resources_controller_test.rb
Step 3: Implement Controller (GREEN)
# app/controllers/[resources]_controller.rb
class ResourcesController < ApplicationController
before_action :set_resource, only: [:show, :edit, :update, :destroy]
def index
authorize Resource, :index?
@pagy, resources = pagy(policy_scope(Resource).order(created_at: :desc))
@resources = resources.map { |r| ResourcePresenter.new(r) }
end
def show
authorize @resource
@resource = ResourcePresenter.new(@resource)
end
def new
@resource = current_account.resources.build
authorize @resource
end
def create
@resource = current_account.resources.build(resource_params)
authorize @resource
if @resource.save
redirect_to resources_path, notice: "Resource created successfully"
else
render :new, status: :unprocessable_entity
end
end
def edit
authorize @resource
end
def update
authorize @resource
if @resource.update(resource_params)
redirect_to @resource, notice: "Resource updated successfully"
else
render :edit, status: :unprocessable_entity
end
end
def destroy
authorize @resource
@resource.destroy
redirect_to resources_path, notice: "Resource deleted successfully"
end
private
def set_resource
@resource = policy_scope(Resource).find(params[:id])
end
def resource_params
params.require(:resource).permit(:name, :field1, :field2)
end
end
Step 4: Run Test (Confirm GREEN)
bin/rails test test/controllers/resources_controller_test.rb
Test Helpers
Add to test/test_helper.rb:
class ActionDispatch::IntegrationTest
def sign_in(user)
session = user.identity.sessions.create!
cookies.signed[:session_token] = session.token
end
def sign_out
cookies.delete(:session_token)
end
end
Namespaced Controllers
For nested routes like settings/accounts:
# app/controllers/settings/accounts_controller.rb
module Settings
class AccountsController < ApplicationController
before_action :set_account
def show
authorize @account
end
private
def set_account
@account = current_account
end
end
end
Turbo Stream Response Pattern
def create
@resource = current_account.resources.build(resource_params)
authorize @resource
if @resource.save
respond_to do |format|
format.html { redirect_to resources_path, notice: "Created" }
format.turbo_stream do
flash.now[:notice] = "Created"
@pagy, @resources = pagy(policy_scope(Resource).order(created_at: :desc))
render turbo_stream: [
turbo_stream.replace("resources-list", partial: "resources/list"),
turbo_stream.update("modal", "")
]
end
end
else
render :new, status: :unprocessable_entity
end
end
Testing Turbo Streams
test "POST /resources with turbo_stream format" do
post resources_path, params: {
resource: { name: "Turbo Resource" }
}, as: :turbo_stream
assert_response :success
assert_includes response.body, "turbo-stream"
end
Full CRUD Fixture Setup
# test/fixtures/resources.yml
one:
name: "My Resource"
field1: "Value 1"
account: one
two:
name: "Another Resource"
field1: "Value 2"
account: one
other_account:
name: "Other Account Resource"
field1: "Value 3"
account: two
Checklist
- Integration test written first (RED)
- Multi-tenant isolation tested
- Authorization tested (redirect/404 for unauthorized)
- Controller uses
authorizeon every action - Controller uses
policy_scopefor queries - Presenter wraps models for views
- Strong parameters defined
- All 7 CRUD actions tested (index, show, new, create, edit, update, destroy)
- Invalid params tested (422 response)
- Turbo Stream responses tested (if applicable)
- All tests GREEN
Weekly Installs
2
Repository
dchuk/rails_ai_agentsFirst Seen
7 days ago
Security Audits
Installed on
gemini-cli2
opencode2
antigravity2
codex2
windsurf2
kiro-cli2