Rails Deep Dive: Loccasions, Authentication
- Rails Intro, Deep Dive: RVM
- Rails Intro, Deep Dive: Installing Rails, Part One
- Loccasions: Installing Rails Part 2
- Rails Intro, Deep Dive: App Generation
- Rails Deep Dive: Application Setup, Loccasions
- Rails Deep Dive: Loccasions, Home Page
- Rails Deep Dive: Loccasions, Authentication
- Rails Deep Dive: Loccasions, Spork, Events and Authorization
- Rails Deep Dive: Loccasions, Making Events
- Loccasions: Pair Programming
- Loccasions: Hiring a Foreman, Inheriting Resources, & Occasions
- Loccasions: Going Client-Side with Leaflet, Backbone, and Jasmine
- Loccasions: Getting to Occasions
- Loccasions: Bubbly Map Events
- Loccasions: Retrospective
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_optionsfor 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.
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
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:
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.



Thanks Erik!
- content_for(:title, 'Sign In')
This should really be:
- title 'Sign In'
so it utilizes the title helper you created in application_helper.rb
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
(This is great stuff...thanks again!)
RSpec.configure.include RequestHelpers::Helpers, :type => :acceptance, :example_group => {
:file_path => config.escaped_path(%w[spec acceptance])
}
I recieved
undefinedlocal 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
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?
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.
field :name
attr_accessible :name
...didn't work for me. (`method_missing': undefined method `field')
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 :)
thanks!
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
Sorry about the delayed reply. Did you fix this issue? If not, I can try and take a look.