Rails Deep Dive: Loccasions, Authentication

Glenn Goodrich
Ruby Editor
This entry is part 7 of 15 in the series Loccasions

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

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.

Loccasions

<< Rails Deep Dive: Loccasions, Home PageRails Deep Dive: Loccasions, Spork, Events and Authorization >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Erik

    Is it me or should the Name attribute not be in the “sign in” specs?

    • http://www.ruprict.net/ Glenn Goodrich

      Doh! I don’t think it’s just you…it’s most certainly me. Fixed.

      Thanks Erik!

  • http://blog.firsthand.ca Nicholas Henry

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

    - content_for(:title, ‘Sign In’)

    This should really be:

    - title ‘Sign In’

    so it utilizes the title helper you created in application_helper.rb

    • http://www.ruprict.net/ Glenn Goodrich

      Yup…typo. The code was right but my post was not. Nice catch.

  • http://blog.firsthand.ca Nicholas Henry

    The application.html.haml provided in the last post does not contain a yield statement: https://raw.github.com/ruprict/loccasions/f402862c053017ee923c4fcacf54e5175264c773/app/views/layouts/application.html.haml

    • http://www.ruprict.net/ Glenn Goodrich

      Yup, wrong link…fixed.

  • http://blog.firsthand.ca Nicholas Henry

    require ‘factory_girl’ is no longer required in factories.rb.

    • http://www.ruprict.net/ Glenn Goodrich

      Thanks

  • http://blog.firsthand.ca Nicholas Henry

    The code snippet in request helpers:

    config.include RequestHelpers::Helpers, :type => :acceptance, :example_group => {
    :file_path => config.escaped_path(%w[spec acceptance])
    }

    should be passed to the class method Rspec.configure

    • http://blog.firsthand.ca Nicholas Henry

      Sorry that should have been RSpec.configure not Rspec.configure (note the difference in letter case for “S”)

      • http://www.ruprict.net/ Glenn Goodrich

        Fixed.

        (This is great stuff…thanks again!)

        • Oranges

          I had an issue with the request_helpers.rb file:


          RSpec.configure.include RequestHelpers::Helpers, :type => :acceptance, :example_group => {
          :file_path => config.escaped_path(%w[spec acceptance])
          }

          I recieved undefined
          local variable or method `config' for main:Object (NameError)

          Should be changed to

          RSpec.configure do |config|
          config.include RequestHelpers::Helpers, :type => :acceptance, :example_group => {
          :file_path => config.escaped_path(%w[spec acceptance])
          }
          end

  • http://blog.firsthand.ca Nicholas Henry

    scenario “Successful Sign In” fails as click_on(‘Sign In’) needs to be scenario click_on(‘Sign in’) as the default view generated by Devise has a lower case “i” for the submit button.

    • http://www.ruprict.net/ Glenn Goodrich

      And fixed….

  • http://iWantToKeepAnon.blogspot.com/ iWantToKeepAnon

    I have been going through these and it’s great stuff. I have made sure my application always passes the specs b/f moving on. I am up to the “Making Events” post and there’s a problem I’ve been putting off … just thought it’d be fixed by following posts but hasn’t been. It’s an auth issue so I am commenting on this post.

    The problem is I can’t sign up and try this stuff out. The specs pass but if I try and use it myself in a browser it won’t let me in. :( If I click sign up and fill in the form it throws these errors back at me:

    3 errors prohibited this user from being saved:
    Email can’t be blank
    Password can’t be blank
    Name can’t be blank

    It’s like the sign up form doesn’t pass any of my info to devise (the auth authority right?). So the specs sign in by directly creating the testy user but I cannot sign up and try this stuff out (firefox, ie, and opera tried and failed).

    Any thoughts?

    • http://www.ruprict.net/ Glenn Goodrich

      Yup, sign up is broken…oops.

      To fix, use the changes in this commit. Basically, I added a name field to the sign_up form and then changed the user model to allow email, password, and password conf to be mass-assigned (using attr_accessible)

      Sorry about that…lemme know if that doesn’t get you going.

      • http://iWantToKeepAnon.blogspot.com/ iWantToKeepAnon

        Yes, that fixes it very nicely! I can see the create event input boxes … now on with the show. Thanks for the quick reply.

        • http://iWantToKeepAnon.blogspot.com/ iWantToKeepAnon

          1 more thought … a sign up spec seems in order :)

          • http://www.ruprict.net/ Glenn Goodrich

            Yes…however, my intention (initially) was to handle sign-ups my invitation only. That is gonna be a monster, though, so I am putting it off to a later post…..*coughs*

  • http://www.pixtur.org pixtur

    So far, I could follow your tutorial. But your the 2 lines in user.rb…

    field :name
    attr_accessible :name

    …didn’t work for me. (`method_missing’: undefined method `field’)

    • http://www.ruprict.net/ Glenn Goodrich

      the field method comes from the Mongoid gem. Make sure you have it the gem file.

  • http://www.bemonkey.net sandy

    Hello,

    When you write the test for “Unsuccessful Sign In” you said :

    fill_in ‘Name’, :with => “BadUser” if you do that the test will crash ???

    because we don’t have that field for sign in…

    peace
    ps: great tutorial :)

    • http://www.ruprict.net/ Glenn Goodrich

      Sandy…sho’ enuff. You’re right. Fixed.

      thanks!

  • mark

    Glenn, I’ve finally taken some time to follow along. This is a good series and my first with Mongo.

    I’m getting the following error from the feature/sign_in_spec (feature as using RSpec 2.12 and Capybara 2.0.2 – basically all the latest gems).

    1) Sign In
    As an administrator
    I want to sign in to Loccasions
    Successful Sign In
    Failure/Error: FactoryGirl.create(:user)
    Mongoid::Errors::Validations:

    Problem:
    Validation of User failed.
    Summary:
    The following errors were found: Name is already taken, Email is already taken
    Resolution:
    Try persisting the document with valid data or remove the validations.

    Here is my spec: scenario “Successful Sign In” do
    click_sign_in
    FactoryGirl.create(:user)
    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

    Heres my ‘spec_helper.rb’

    require ‘database_cleaner’
    config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.orm = “mongoid”
    end
    config.before(:each) do
    DatabaseCleaner.clean
    end
    config.after(:each) do
    DatabaseCleaner.clean
    end

    I get the feeling that the cleaner is not running successfully. Being new to mongo I’ve google’d around but cannot seem to find why this might be happening other than guessing that the user I created in ‘model/user_spec.rb’ is still hanging around, or that this user is in memory.

    Did anyone else get this issue?

    I’m moving on with the rest of the tutorial but its reared is head in the ‘features/user_events_spec.rb’ test.

    I cannot see anything different from your github code (which is hard to follow given you had a corruption and I’m working at points before that).

    The PDF you created is great

    • http://www.ruprict.net/ Glenn Goodrich

      Hey Mark,

      Sorry about the delayed reply. Did you fix this issue? If not, I can try and take a look.

  • bemonkey

    hi i i have i probleme
    1) Sign In As an administrator
    I want to sign in to MouvMan Successful Sign In
    Failure/Error: current_path.should == user_root_path
    NameError:
    undefined local variable or method `user_root_path’ for #

    my spec

    require ‘spec_helper’

    feature ‘Sign In’ , %q{
    As an administrator
    I want to sign in to MouvMan
    }do
    scenario “Click Sign In” do
    click_sign_in
    page.should have_selector(“title”, :text => “MouvMan: Sign In”)
    page.should have_selector(‘form’)
    end

    scenario “Successful Sign In” do
    click_sign_in

    fill_in ‘Email’, :with => ‘testy@test.com’
    fill_in ‘Password’, :with => ‘password’
    click_on(‘Sign in’)
    Factory.create(:user)
    current_path.should == user_root_path
    end

    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
    end

    Can somebody tell where i messed up??
    peace

    • bemonkey

      precision i change my route to match events to user_root like this

      match ‘events’ => ‘home#index’, :as => :user_root

      still not work..so i try in the broswer and i redirect to home page ans in the url i have events!! ..

      i think i missing a little thing but what!! heeelpp..lol

      • http://www.ruprict.net/ Glenn Goodrich

        Bemonkey, do you have a github repo I can look at?

      • bemonkey

        You can delete all thoses comments please i solve my problem .;i was an idiot on this one..lol

        • http://www.ruprict.net/ Glenn Goodrich

          Oh, great! Hope you’re getting something out of the series.