Rails Project Time, Seeds and Testing
For my final project in rails, I put into practice the more complicated aspects of user authentication and authorization. It’s a simple recipe tracker, but under the hood it is doing some awesome stuff. Watch this video to get an idea of the web application and then read below to understand how it’s built.
Authentication with Devise and Omniauth
The user sign up process is built with Devise, which by default only asks for a user’s email and password. I added a name and a user’s role (like an administrator, moderator or user) to the User table in the database. I also used enum to create user roles in the User model, setting the default to ‘user’ after initializing a new User. This also gives me methods like User.role
and User.admin?
, which returns true if the user is an admin.
class User < ActiveRecord::Base
enum role: [:user, :moderator, :admin]
after_initialize :set_default_user_role
def set_default_user_role
self.role = :user unless self.role
end
end
I had to modify Devise’s default controllers to allow :name
and :role
in the strong params of the Users::RegistrationsController.
class Users::RegistrationsController < Devise::RegistrationsController
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up){ |u| u.permit(:name, :role, :email,:password,:password_confirmation) }
end
# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update){ |u| u.permit(:name, :role, :email,:password,:password_confirmation) }
end
I also needed to modify the views for users to show links to delete comments conditionally, based on their role.
#Comments.html.erb
...
<% if current_user.admin? || comment.user_id == current_user.id %>
<%= link_to "Delete", recipe_comment_path(@recipe, comment.id), method: :delete %>
<% end %>
...
Authorization with Pundit
The Pundit gem sets up simple policies that verify whether a user has a particular role before authorizing them to do that action. So if only administrators and moderators can edit comments the CommentPolicy.rb
looks like:
class CommentPolicy < ApplicationPolicy
def edit?
user.admin? || user.moderator?
end
end
It is called by simply adding authorize @comment
into comments_controller.rb. I also added a custom rescue for Pundit errors in the ApplicationController.
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "Access denied."
redirect_to(request.referrer || root_path)
end
end
So if a user tries to do something they shouldn’t be able to do, through some internet wizardry, a notification pops up and says:
Building Seed Data
To work with the web application in my development environment, I built my own seed data using the faker gem. So to create 10 fake users:
#db/seeds.rb
10.times do
User.create(
name: Faker::Name.name,
email: Faker::Internet.email,
password: "testtest"
)
end
Then when I run rake db:seed
I have some users set up to test out permissions with. They’ve got great names too, like Constantin Rath V. I also made seed data for recipes, ingredients and comments.
Writing Tests
My favorite part of this project was learning the importance of testing organically. I was trying to build the various user roles and found myself manually signing up users over and over, trying to test the role function. In a railscast I watched, Ryan Bates mentioned that if you’re testing things in the browser, you should probably write a test for that.
So I used Capybara and Rspec to write a test that signs up a user for each role and then checks if they are signed up in that role. Here’s the test for an admin:
# spec/features
it "signs up an Admin" do
visit('/users/sign_up')
fill_in('Name', :with => 'John Admin')
fill_in('Email', :with => 'John@inter.net')
fill_in('Password', :with => 'testtest')
fill_in('Password confirmation', :with => 'testtest')
select('admin', :from => 'Role')
click_button('Sign up')
expect(page).to have_content 'You have signed up successfully'
visit('/about')
expect(page).to have_content 'admin'
end
That allowed me to just run the test while I was figuring out the sign up process. When the test passed, I knew it worked without having to manually sign up a new user in all three roles. This is a simple test and I admire the more complex tests used in Test Drive Development. I’ve learned the importance of writing and having quality tests.
In Summation
I learned a lot during this process and did many a first-pump when things started to function as I invisioned them. The biggest lesson I’ve learned from this project is about the importance and practicality of Test Driven Development (TDD). That’s how Learn is built and how I most enjoy writing programs.
Please check out the code on github and comment where you see something you like, or that could be improved.