Ruby
Article
By Glenn Goodrich

Rails Deep Dive: Loccasions, Authentication

By Glenn Goodrich

Loccasions

In the last post, we finished our first user story. That user story was pretty simple, but it flushed out the design of our home page. The next user story, As an administrator, I want to invite users to Loccasions is not quite so simple.

The implications from this user story are big: First, we have a new role, administrator, which brings our roles to two (unregistered user and administrator). Second, the administrator role brings out the idea of authorization, where functionality of the site is restricted based on the user’s role. Of course, this points us to authentication, because we need to know who the user is before we can figure out what the user can do. By the end of this user story, we should have authentication and authorization set up and ready to go.

Create a Branch

Something I neglected in the last post was to create a git branch for our new story. This keeps our code isolated to the branch and allows us to make unrelated changes to master if needed. Here is a good article on Git workflow. Git makes branching easy

--ADVERTISEMENT--

git checkout -b inviting_users

and we’re in our new branch, ready to go.

Write the Test

Continuing with our test-driven approach, let’s write a test for signing in to the application. Here, we will start to break our user story into smaller stories to facilitate testing. Breaking a problem or piece of functionality down into smaller parts makes the bigger problem easier to tackle. Here’s our first sub user story: As an administrator, I want to sign in to Loccasions.

Since we are writing specs from the user’s perspective, each story (and sub story) has implications. In this story, the act of “signing in” can mean many things, so how do we measure it to match what we/our client wants? In this case, I conferred with the client who wants a cool drop down to come down (a la Twitter) with a sign-in form. While I agreed that is cool, I talked the client into progressive enhancement, allowing us to develop a separate page for the sign-in form, for now, and return to make it sexier later.

With our client expectations in hand, we can make our sign-in form test. At first, we’ll just make sure the sign-in page has a form and a title.

require 'spec_helper'

feature 'Sign In', %q{
  As an administrator
  I want to sign in to Loccasions
} do
  background do
    visit "/"
  end
  scenario "Click Sign In" do
    click_link "Sign In"
    page.should have_selector("title", :text => "Loccasions: Sign In")
    page.should have_selector('form')
  end
end

(Remember to fire up mongodb before running your specs)
This spec fails, complaining about the title not matching. Also, I noticed I was getting the following message when I ran my specs:

NOTE: Gem.available? is deprecated, use Specification::find_by_name. It will be removed on or after 2011-11-01.
Gem.available? called from /Users/ggoodrich/.rvm/gems/ruby-1.9.2-p290@loccasions/gems/jasmine-1.0.2.1/lib/jasmine/base.rb:64.

HMMMM…I don’t like that. I quick trip the jasmine-gem github repo shows they are on version 1.1.0.rc3. For now, I will bump the version in my Gemfile and hope it works when we get to client-side testing. Bumping the version fixes that warning, so immediate needs met.

Here, I rely on experience to drive my next step. I am keen on using Devise for authentication, and I know it has it’s own views for signup, signin, etc. In other words, it’s time to setup Devise.

Setup Devise

Tipping my hat to the awesome RailsApps yet again, let’s prepare RSpec for Devise. Create a spec/support/devise.rb file with:

RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
end

Now, on to the typical Devise setup.

rails g devise:install

This creates config/initializers/devise.rb and config/locales/devise.en.yml. If we look in the initializer file, we can see require 'devise/orm/mongoid', so we know that Devise is aware of our choice to use Mongoid. The output of the devise:install generator gives some instructions:

  • Setup default_url_options for ActionMailer
  • Setup a default route (we have done this already)
  • Make sure we handle our flash/notice messages in our layout.

Let’s do this stuff while it’s fresh. I added

config.action_mailer.default_url_options = { :host => 'localhost:3000' }

to config/environments/development.rb Also, I added:

%p.notice= notice
%p.alert= alert

to app/views/layouts/application.html.haml just above the call to yield. It may not stay there, but we aren’t worried about that right now.
Devise will generate a User model for us:

rails g devise User

The output of this command shows that we get a model (User), a user_spec, and a new route (devise_for :users). If we do a quick rake routes at the command line, we see:

new_user_session GET    /users/sign_in(.:format)       {:action=>"new", :controller=>"devise/sessions"}

which is where we want our “Sign In” link to go. Let’s change it in the application layout.

#sign_in.sixteen.columns
  %a(href= new_user_session_path ) Sign In

Rerunning our spec, and we still have the same error. At this point, let’s fire up the server and see what is happening.

Sign Up page

Wow…that looks pretty good. However, the title isn’t what we want and it has a ‘Sign Up’ link, which we may want later, but not yet. We need to customize the Devise views and, thankfully, Devise gives us an easy way to do just that:

rails g devise:views

Output of Devise Views

That creates quite a few views, and they are all ERB not Haml…UGH. Googling around, I found this on the Devise wiki detailing how to get Haml views for Devise. So, rm -rf app/views/devise, add gem 'hpricot', '~> 0.8.4'
and gem 'ruby_parser', '~> 2.2.0' to the development group in your Gemfile, bundle install, and follow the instructions on the wiki. Blech. Not the end of the world, but not unicorns and rainbows, either.
First, let’s change the title. Since the Devise views will use the same application layout, we need a way to change the title for each page. Enter the ApplicationHelper. Add this method to app/helpers/application_helper.rb

def title(page_title)
  content_for(:title) { page_title }
end

Now, replace the title tag in the application layout with:

%title= "Loccasions: #{content_for?(:title) ? content_for(:title) : 'Home' }"

Finally, add this to the top of app/views/devise/session/new.html.haml:

- title('Sign In')

Now, the specs all pass.

Decision Point: User Names

After some deliberation with the client, we are going to add a name attribute to the users. Let’s add a test for our new attribute. Devise was kind enough to create a user_spec, so let’s create a test for name. (in spec/models/user_spec.rb)

describe User do
  it "should have a 'name' attribute" do
    user = User.new
    user.should respond_to(:name)
    user.should respond_to(:name=)
  end
end

That spec fails, as expected. We can make it pass by adding this to app/models/user.rb

field :name
attr_accessible :name

I want name to be unique and required. Tests (with a bit of refactoring):

describe User do
  describe "the 'Name' attribute" do
    before(:each) do
      @user = Factory.build(:user)
    end
    it "should exist on the User model" do
      @user.should respond_to(:name)
      @user.should respond_to(:name=)
    end
    it "should be unique" do
      @user.save
      user2 = Factory.build(:user, :email=>'diff@example.com')
      user2.valid?.should be_false
      user2.errors[:name].should include("is already taken")
    end
    it "should be required" do
      @user.name=nil
      @user.valid?.should be_false
      @user.errors[:name].should include("can't be blank")
    end
  end
end

The Alert Reader has notice the calls to Factory in the refactored spec. We need a user for this test, and we’ll turn to Factory Girl to get one. Add the file spec/factories.rb with:

require 'factory_girl'

FactoryGirl.define do
  factory :user do
    name 'Testy'
    email 'testy@test.com'
    password 'password'
  end
end

Running the spec gives us 2 failures:

User Name Spec Fails

Add some quick validation to our name field,

validates :name, :presence => true, :uniqueness => true

and all our specs pass. We can now move on to actually testing sign in.

Test Sign In

The first item to determine for our sign-in test is, what happens when a user successfully signs in?
The customer thinks that the user should be redirected to their individual “home” page. What is on the user home page, then? We know our main business objects are Event and Occasion, and that Occasions live inside Events. The user home page, then, should probably list the user’s events, to start. The spec, then, should fill out and submit the form, then redirect to the user home page.

Before we write this spec, I want to make the spec task the default Rake task (I am tired of typing rake spec) so add this to the bottom of your Rakefile

Rake::Task[:default].prerequisites.clear
task :default => [:spec]

Now, we can just type rake and our specs will run. AAAAAH, that’s better.

Here is our sign-in spec:

scenario "Successful Sign In" do
  click_sign_in
  fill_in 'Email', :with => 'testy@test.com'
  fill_in 'Password', :with => 'password'
  click_on('Sign in')
  current_path.should == user_root_path # this path is used by Devise
end

Notice the click_sign_in method? I made a quick helper (spec/support/request_helpers.rb) so I didn’t need to keep typing the lines to click get to the sign in page.

module RequestHelpers
  module Helpers
    def click_sign_in
      visit "/"
      click_link "Sign In"
    end
  end
end
RSpec.configure.include RequestHelpers::Helpers, :type => :acceptance, :example_group => {
 :file_path => config.escaped_path(%w[spec acceptance])
}

This will only include our helper in the acceptance tests, meaning, any specs in spec/acceptance (Note: RSpec defines a bunch of spec “types”, such as request, controller, models, etc. Here, we are just adding acceptance)

Running rake (Yay! Isn’t that better?) and we get an expected error about user_root_path being undefined. Just to get the test passing, add this to config/routes.rb

match 'events' => 'home#index', :as => :user_root

We’ll call the route /events, since we know events will be the main course of the user home page. The spec now fails because the URLs don’t match. After we submit the form, the URL is unchanged. This is because we have no users in the database. Add this to the “Successful Sign In” scenario, just after click_sign_in

FactoryGirl.create(:user)

Yay! The spec now passes. The user_root route, by the way, is a Devise convention to override where the user is redirected after successful sign in. We haven’t fully tested authentication, but it’s working. For completeness sake, let’s make sure a bad login fails. Add this under the “Successful Sign In” scenario:

scenario "Unsuccessful Sign In" do
  click_sign_in
  fill_in 'Email', :with => 'hacker@getyou.com'
  fill_in 'Password', :with => 'badpassword'
  click_on 'Sign in'
  current_path.should == user_session_path
  page.should have_content("Invalid email or password")
end

Yup, the new scenario passes, as expected. Let’s go ahead and push this to github.

git add .
git commit -m "Basic authentication"
git checkout master
git merge inviting_users

Run your specs here, just to make sure everything is OK, then

git push origin master
git branch -d inviting_users

Well, That Took Longer Than I Expected

This article is getting to be a bit too long, so we’ll stop there and pick up with our user events page in the next article. As always, your comments on how Loccasions is progressing are welcome.

More:
Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account