Chapter 6. Authentication and basic authorization

This chapter covers

  • Working with engine code and generators
  • Building an authentication system with an engine
  • Implementing basic authorization checking

You’ve now created two resources for your Ticketee application: projects and tickets. Now you’ll use a gem called Devise, which provides authentication, to let users sign in to your application. With this feature, you can track which tickets were created by which users. A little later, you’ll use these user records to allow and deny access to certain parts of the application.

The general idea behind having users for this application is that some users are in charge of creating projects (project owners) and others use whatever the projects provide. If they find something wrong with it or wish to suggest an improvement, filing a ticket is a great way to inform the project owner of their request. You don’t want absolutely everybody creating or modifying projects, so you’ll learn to restrict project creation to a certain subset of users.

To round out the chapter, you’ll create another CRUD interface, this time for the users resource, but with a twist.

Before you start, you must set up Devise!

6.1. What Devise does

Devise is an authentication gem that provides a lot of the common functionality for user management, such as letting users sign in and sign up, in the form of a Rails engine. An engine can be thought of as a miniature application that provides a small subset of controllers, models, views, and additional functionality in one neat little package. You can override the controllers, models, and views if you wish, though, by placing identically named files inside your application. This works because Rails looks for a file in your application first before diving into the gems and plugins of the application, which can speed up the application’s execution time.

6.1.1. Installing Devise

To install Devise, first add the following line to the Gemfile, right after the end for the :test group:

gem 'devise', '~> 1.4.3'

To install the Devise gem, run bundle. Once Devise is installed, you need to run the generator, but how do you know the name of it? Simple! You can run the generate command with no additional arguments to list all the generators:

rails generate

In this output, you’ll see devise:install listed. Hey, that’ll probably help you get things installed! Let’s try it:

rails g devise:install

This code generates two files, config/initializers/devise.rb and config/locales/devise.en.yml:

  • config/initializers/devise.rb sets up Devise for your application and is the source for all configuration settings for Devise.
  • config/locales/devise.en.yml contains the English translations for Devise and is loaded by the internationalization (I18n) part of Rails. You can learn about internationalization in Rails by reading the official I18n guide: http://guides.rubyonrails.org/i18n.html.

The code also gives you three setup tips to follow. The first setup tip tells you to set up some Action Mailer settings, which you can place in your development environment’s configuration at config/environments/development.rb and in your test environment’s configuration at config/environments/test.rb:

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

The files in config/environments are used for environment-specific settings, like the one you just added. If you wish to configure something across all environments for your application, you put it in config/application.rb.

The second tip tells you to set up a root route, which you have done already. The third tip tells you to add displays for notice and alert in your application layout, which you’ve also done, except with different code than what it suggests.

The next step to get Devise going is to run this command:

rails g devise user

This generator generates a model for your user and adds the following line to your config/routes.rb file:

devise_for :users

By default, this one simple line adds routes for user registration, signup, editing and confirmation, and password retrieval. The magic for this line comes from inside the User model that was generated, which contains the code from the following listing.

Listing 6.1. app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password,
                  :password_confirmation, :remember_me
end

The devise method here comes from the Devise gem and configures the gem to provide the specified functions. These modules are shown in the following two tables. Table 6.1 shows default functions, and table 6.2 shows the optional functions.

Table 6.1. Devise default modules

Module

Provides

:database_authenticatable Adds the ability to authenticate via an email and password field in the database.
:registerable Provides the functionality to let a user sign up.
:recoverable Adds functionality to let the user recover their password if they ever lose it.
:rememberable Provides a check box for users to check if they want their session to be remembered. If they close their browser and revisit the application, they are automatically signed in on their return.
:trackable Adds functionality to track users, such as how many times they sign in, when they last signed in, and the current and last IPs they signed in from.
:validatable Validates the user has entered correct data, such as a valid email address and password.
Table 6.2. Devise optional modules (off by default)

Module

Provides

:token_authenticatable Lets the user authenticate via a token; can be used in conjunction with :database_authenticatable
:encryptable Adds support for other methods of encrypting passwords; by default, Devise uses bcrypt
:confirmable When users register, sends them an email with a link they click to confirm they’re a real person (you’ll switch on this module shortly because it’s one step to prevent automated signups)
:lockable Locks the user out for a specific amount of time after a specific number of retries (configurable in the initializer); default is a lockout time of 1 hour after 20 retries
:timeoutable If users have no activity in their session for a specified period of time, they are automatically signed out; useful for sites that may be used by multiple people on the same computer, such as email or banking sites
:omniauthable Adds support for the OmniAuth gem, which allows for alternative authentication methods using services such as OAuth and OpenID

The devise call is followed by a call to attr_accessible. This method defines fields that are accessible via attribute mass-assignment. Attribute mass-assignment happens when you pass a whole slew of attributes to a method such as create or update_attributes; because these methods take any and all parameters passed to them by default, users may attempt to hack the form and set an attribute they are not supposed to set, such as an admin boolean attribute. By using attr_accessible, you define a white list of fields you want the user to access. Any other fields passed through in an attribute mass-assignment are ignored.

The final step here is to run rake db:migrate to create the users table from the Devise-provided migration in your development database and run rake db:test:prepare so it’s created in the test database too.

6.2. User signup

With Devise set up, you’re ready to write a feature that allows users to sign up. The Devise gem provides this functionality, so this feature will act as a safeguard to ensure that if the functionality were ever changed, the feature would break.

To make sure this functionality is always available, you write a feature for it, using the following listing, and put it in a new file at features/signing_up.feature.

Listing 6.2. features/signing_up.feature
Feature: Signing up
In order to be attributed for my work
As a user
I want to be able to sign up

Scenario: Signing up
  Given I am on the homepage
  When I follow "Sign up"
  And I fill in "Email" with "[email protected]"
  And I fill in "Password" with "password"
  And I fill in "Password confirmation" with "password"
  And I press "Sign up"
  Then I should see "You have signed up successfully."

When you run this feature using bin/cucumber features/signing_up.feature, you’re told it can’t find a Sign Up link, probably because you haven’t added it yet:

no link with title, id or text 'Sign up' found

You should now add this link in a nav:[1]

1nav is an HTML5 tag and may not be supported by some browsers. As an alternative, you could put <div id='nav'> instead, in app/views/layouts/application.html.erb, directly underneath the h1 tag for your application’s title.

<nav>
<%= link_to "Sign up", new_user_registration_path %>
</nav>

You previously used the menu on the app/views/tickets/show.html.erb page to style the links there. Here you use a nav tag because this is a major navigation menu for the entire application, not just a single page’s navigation.

With this link now in place, the entire feature passes when you run it because Devise does all the heavy lifting for you:

1 scenario (1 passed)
7 steps (7 passed)

Because Devise has already implemented this functionality, you don’t need to write any code for it. This functionality could be overridden in your application, and this feature is insurance against anything changing for the worse.

With the signup feature implemented, this is a great point to see if everything else is working by running rake cucumber:ok spec. You should see this output:

14 scenarios (14 passed)
115 steps (115 passed)
# and
6 examples, 0 failures, 5 pending

Great! Commit that!

git add .
git commit -m "Added feature to ensure Devise signup is always working"
git push

In this section, you added a feature to make sure Devise is set up correctly for your application. When users sign up to your site, they’ll receive an email as long as you configured your Action Mailer settings correctly. The next section covers how Devise automatically signs in users who click the confirmation link provided in the email they’ve been sent.

6.3. Confirmation link sign-in

With users now able to sign up to your site, you should make sure they’re also able to sign in. When users are created, they should be sent a confirmation email in which they have to click a link to confirm their email address. You don’t want users signing up with fake email addresses! Once confirmed, the user is automatically signed in by Devise.

6.3.1. Testing email

First, you enable the confirmable module for Devise because (as you saw earlier) it’s one of the optional modules. With this module turned on, users will receive a confirmation email that contains a link for them to activate their account. You need to write a test that checks whether users receive a confirmation email when they sign up and can confirm their account by clicking a link inside that email.

For this test, you use another gem called email_spec. To install this gem, add the following line to your Gemfile inside the test group:

gem 'email_spec'

Now run bundle to install it.

Next, run the generator to get the steps for email_spec with the following command:

rails g email_spec:steps

This command generates steps in a file at features/step_definitions/email_steps.rb that you can use in your features to check whether a user received a specific email and more. This is precisely what you need to help you craft the next feature: signing in, receiving an email, and clicking the confirmation link inside it.

One additional piece you must set up is to require the specific files from the email_spec library. Create a new file at features/support/email.rb, which you use for requiring the email_spec files. Inside this file, put these lines:

# Email Spec helpers
require 'email_spec'
require 'email_spec/cucumber'

6.3.2. Confirming confirmation

With the email_spec gem now fully installed and set up, let’s write a feature for signing users in when they click the confirmation link they should receive in their email. Insert the following listing at features/signing_in.feature.

Listing 6.3. features/signing_in.feature
Feature: Signing in
  In order to use the site
  As a user
  I want to be able to sign in

  Scenario: Signing in via confirmation
    Given there are the following users:
      | email             | password |
      | [email protected] | password |
    And "[email protected]" opens the email with subject
"Confirmation instructions"
    And they click the first link in the email
    Then I should see "Your account was successfully confirmed"
    And I should see "Signed in as [email protected]"

With this scenario, you make sure that when users are created, they receive an email called “Confirmation instructions” that should contain confirmation instructions. This email will contain a link, and when users click it, they should see two things: a message saying “Your account was successfully confirmed” and notification that they are now “Signed in as [email protected]” where [email protected] represents the username.

The first step in this scenario is currently undefined, so when you run this feature using bin/cucumber features/signing_in.feature, it fails with the undefined step:

Given /^there are the following users:$/ do |table|
  pending # express the regexp above with the code you wish you had
end

This step definition allows you to create as many users as you wish using Cucumber’s table syntax. Put this step definition inside a new file called features/step_definitions/user_steps.rb, using the code from the following listing.

Listing 6.4. features/step_definitions/user_steps.rb
Given /^there are the following users:$/ do |table|
  table.hashes.each do |attributes|
    @user = User.create!(attributes)
  end
end

In this step definition, you use Cucumber’s table format again to specify more than one user to create. To get to these attributes, iterate through table.hashes, storing each set of attributes as attributes for each iteration. Inside the iteration, the create! method creates the user record using these attributes. All of this should look pretty familiar—you used it to create tickets for a project.

The second step in this scenario is provided by the email_spec gem, and it fails when you run bin/cucumber features/signing_in.feature again:

And "[email protected]" opens the email with subject "Confirmation
instructions"
Could not find email With subject "Confirmation instructions".
Found the following emails:

[]

This is email_spec telling you it can’t find an email with the title “Confirmation instructions,” which is what Devise would send out if you had told it you wanted users to be confirmable. You haven’t yet done this, so no emails are being sent.

To make users confirmable, add the :confirmable symbol at the end of the devise call in app/models/user.rb:

devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable,
       :validatable, :confirmable

Now Devise will send confirmation emails when users sign up. When you run bin/cucumber features/signing_in.feature again, your first step is failing:

Given there are the following users:
  | email             | password | unconfirmed |
  | [email protected] | password | true        |
  undefined local variable or method 'confirmed_at' for #<User:...>

The confirmed_at attribute is used by Devise to determine whether or not a user has confirmed their account. By default, this attribute is nil, indicating the user hasn’t confirmed yet. The attribute doesn’t exist at the moment, so you get this error.

You could add this attribute to the existing db/migrate/[timestamp]_devise_create_users.rb migration, but because you already pushed that migration, you should avoid changing it, as others will have to rerun the migration to get those changes. Even though it’s just you working on the project at the moment, it’s a good rule of thumb to not modify migrations that have already been pushed.

Instead, create a new migration to add the confirmed_at field and two others, confirmation_token and confirmation_sent_at. The confirmation_token is generated by Devise and used to identify users attempting to confirm their account when they click the confirmation link from the email. The confirmation_sent_at field is used also by Devise and tracks the time when the confirmation email was sent:

rails g migration add_confirmable_fields_to_users

Let’s now open this migration and put the code from the following listing inside it.

Listing 6.5. db/migrate/[timestamp]_add_confirmable_fields_to_users.rb
class AddConfirmableFieldsToUsers < ActiveRecord::Migration
  def change
    add_column :users, :confirmation_token, :string
    add_column :users, :confirmed_at, :datetime
    add_column :users, :confirmation_sent_at, :datetime
  end
end

This migration adds the specified columns to the users table when you run rake db:migrate or removes them when you run rake db:rollback.

When users sign up, a confirmation token is generated for them. An email with a link containing this token is sent (and the confirmation_sent_at field is set). When users click the link, their account is confirmed, and the confirmed_at field is set to the current time in the process.

You now need to run rake db:migrate and rake db:test:prepare to update your test database with this latest change. With these fields in place, you should be much closer to having your scenario pass. Let’s see with a quick run of bin/cucumber features/signing_in.feature:

Scenario: Signing in via confirmation
    Given there are the following users:
      | email             | password |
      | [email protected] | password |
    And "[email protected]" opens the email with subject "Confirmation
instructions"
    And they click the first link in the email
    Then I should see "Your account was successfully confirmed"
    Then I should see "Signed in as [email protected]"
      expected there to be content "Created by [email protected]" in "[text]"

Everything but the final step is passing. The final step checks for “Signed in as [email protected]” somewhere on the page, but it can’t find it. You must add it to your application’s layout, replacing the Sign Up link with “Signed in as [username]” so users don’t have the option to sign up if they’re already signed in!

Let’s open app/views/layouts/application.html.erb and change this line

<%= link_to "Sign up", new_user_registration_path %>

to the following:

<% if user_signed_in? %>
  Signed in as <%= current_user.email %>
<% else %>
  <%= link_to "Sign up", new_user_registration_path %>
<% end %>

The user_signed_in? and current_user methods are provided by Devise. The user_signed_in? method returns true if the user is signed in; otherwise it returns false. The current_user method returns a User object representing the current user, and from that object you can call the email method to display the user’s email.

When you run bin/cucumber features/signing_up.feature, the entire feature is passing:

1 scenario (1 passed)
5 steps (5 passed)

In addition to signing-up facilities, Devise handles all the signing-in facilities too. All you need to add is the :confirmable symbol to the devise call in the model, the confirmation fields to the users table, and the message “Signed in as [user]” in your application—three easy steps.

Does everything else pass? Run rake cucumber:ok spec, and you should see the following output:

15 scenarios (15 passed)
120 steps (120 passed)
# and
6 examples, 0 failures, 5 pending

Great! Push that!

git add .
git commit -m "Added feature for ensuring that a user is signed in
             via a confirmation email by using email spec"
git push

Now that users can sign up and confirm their email addresses, what should happen when they return to the site? They should be able to sign in! Let’s make sure this can happen.

6.4. Form sign-in

The previous story covered the automatic sign-in that happens when users follow the confirmation link from the email they receive when they sign up. Now you must write a story for users who have confirmed their account and are returning and need to sign in again.

Place the scenario in the following listing directly underneath the previous scenario in features/signing_in.feature.

Listing 6.6. features/signing_in.feature
Scenario: Signing in via form
Given there are the following users:
  | email             | password |
  | [email protected] | password |
And I am on the homepage
When I follow "Sign in"
And I fill in "Email" with "[email protected]"
And I fill in "Password" with "password"
And I press "Sign in"
Then I should see "Signed in successfully."

When you run bin/cucumber features/signing_in.feature, it complains about the missing Sign In link:

When I follow "Sign in"
no link with title, id or text 'Sign in' found (Capybara::ElementNotFound)

You should add the link directly under the Sign Up link in app/views/layouts/application.html.erb, as shown in the following listing.

Listing 6.7. app/views/layouts/application.html.erb
<%= link_to "Sign in", new_user_session_path %>

When you run the feature again, it still fails, but this time on the final step:

And I press "Sign in"
Then I should see "Signed in successfully."
  expected there to be content "Signed in successfully." in "[text]"

It fails because users who have not yet confirmed their account by clicking the link in the email can’t sign in. To fix this, you could confirm the users with the first step in this scenario, but it would break the first scenario because it requires users to be unconfirmed.

To fix this, alter the first scenario in this feature to contain the following as its first step:

Given there are the following users:
| email             | password | unconfirmed |
| [email protected] | password | true        |

With this small change, the step now has an additional key available in the attributes hash in the step definition. If this step is called with an unconfirmed user, it doesn’t confirm the user; otherwise it does. Let’s alter this step definition in features/step_definitions/user_steps.rb:

Given /^there are the following users:$/ do |table|
  table.hashes.each do |attributes|
    unconfirmed = attributes.delete("unconfirmed") == "true"
    @user = User.create!(attributes)
    @user.confirm! unless unconfirmed
  end
end

At the top of the iteration over table.hashes, you now call attributes.delete("unconfirmed"), which removes the unconfirmed key from the attributes hash, returning its value in the process. If that value is equal to true then unconfirmed is also set to true. If that’s the case, the final line in the iterator isn’t called and the user isn’t confirmed. Otherwise, as in the case in the second scenario of the feature, the user is confirmed and allowed to sign in.

When you run bin/cucumber features/signing_in.feature again, both scenarios pass:

2 scenarios (2 passed)
12 steps (12 passed)

Run your tests again before you commit these changes with rake cucumber:ok spec. Even though you didn’t change much code in this section, it’s still a good habit to run your tests before every commit to stop unintentional regressions. You should see the following summaries:

16 scenarios (16 passed)
127 steps (127 passed)
# and
6 examples, 0 failures, 5 pending

Great, let’s commit and push:

git add .
git commit -m "Added feature for signing in via the Devise-provided form"
git push

6.5. Linking tickets to users

Now that users can sign in and sign up to your application, it’s time to link a ticket with a user when it’s created automatically, clearly defining which user created the ticket. You also want to ensure that the user who created the ticket gets attribution on the ticket page.

That part is easy: you need a “Created by [user]” message displayed on the ticket page. The setup before it is a little more difficult, but you’ll get through it.

You can test for this functionality by amending the Creating a Ticket scenario inside features/creating_tickets.feature to have the following line as the final line for the scenario:

Then I should see "Created by [email protected]"

When you run the feature using bin/cucumber features/creating_tickets.feature, it fails on this new step because it isn’t on the ticket show template:

expected #has_content?("Created by [email protected]")
to return true, got false ...

You need to make sure the user is signed in before they can create a ticket; otherwise, you won’t know who to make the owner of that ticket. When users go to a project page and click the New Ticket link, they should be redirected to the sign-in page and asked to sign in. Once they’re signed in, they should be able to create the ticket. Change the Background of the Feature in features/creating_tickets.feature to ensure this process happens, using the code from the following listing.

Listing 6.8. features/creating_tickets.feature
Given there is a project called "Internet Explorer"
And there are the following users:
| email             | password |
| [email protected] | password |
And I am on the homepage
When I follow "Internet Explorer"
And I follow "New Ticket"
Then I should see "You need to sign in or sign up before continuing."
When I fill in "Email" with "[email protected]"
And I fill in "Password" with "password"
And I press "Sign in"
Then I should see "New Ticket"

The step that checks for the text “You need to sign in or sign up before continuing” fails because you’re not ensuring the user is signed in before the new action in the TicketsController.

To do so, you can use the Devise-provided method authenticate_user! as a before_filter. Put this method directly underneath the class definition for TicketsController inside app/controllers/tickets_controller.rb. The placement of this before_filter ensures that if it fails, the other two before_filters underneath it will not needlessly run, eventually saving valuable CPU cycles.

The line you put in the TicketsController is

before_filter :authenticate_user!, :except => [:index, :show]

This line ensures that users are authenticated before they go to any action in the controller that isn’t the index or show, including the new and create actions.

By ensuring this authentication, you’ll know which user created a ticket during the creation process, so let’s link tickets to users.

6.5.1. Attributing tickets to users

To link tickets to specific users, you alter the build line in your create action in TicketsController from this line

@ticket = @project.tickets.build(params[:ticket])

to this:

@ticket = @project.tickets.build(params[:ticket].merge!(:user => current_user))

The merge! method here is a Hash and HashWithIndifferentAccess method, which merges the provided keys into the hash and overrides any keys already specified.[2] When you run the feature again using bin/cucumber features/creating_tickets.feature, it complains about an unknown attribute in all three scenarios:

2 Which could happen if someone hacked the form and attempted to pass their own user attribute.

unknown attribute: user (ActiveRecord::UnknownAttributeError)

This error occurs because you haven’t added a belongs_to association between the Ticket and User. Let’s open app/models/ticket.rb and add this line directly under the belongs_to :project line:

belongs_to :user

The belongs_to method defines methods for accessing the association, as has_many does, except here you retrieve only one record. Active Record knows which record to retrieve when you call either project or user on a Ticket object because it intelligently uses the name of the belongs_to association to imply that the fields are project_id and user_id, respectively. When looking up a user, Active Record performs a query like this:

SELECT * FROM users WHERE id = #{@ticket.user_id}

This query then returns a row from the database that matches the ID (if there is one), and Active Record creates a new User object from this result.

With these associations set up, you now need to add a field on the tickets table to store the ID of the user that a ticket links to. Run this command:

rails g migration add_user_id_to_tickets user_id:integer

Based solely on how you wrote the name of this feature, Rails will understand that you want to add a particular column to the tickets table. You specify the name and type of the column after the migration name, and Rails creates the migration with the field prefilled for you.

If you open the new migration file (it’s the last one in the db/migrate directory), you’ll see the output in the following listing.

Listing 6.9. db/migrate/[timestamp]_add_user_id_to_tickets.rb
class AddUserIdToTickets < ActiveRecord::Migration
  def change
    add_column :tickets, :user_id, :integer
  end
end

It’s all done for you! You can close this file and then run the migration with rake db:migrate and prepare the test database by using rake db:test:prepare.

Let’s rerun bin/cucumber features/creating_tickets.feature and see where it stands now:

Then I should see "Created by [email protected]"
  expected there to be content "Created by [email protected]" in "[text]"

 

Bash migrate alias

If you’re using bash as a shell (which is probably the case if you’re on a UNIX operating system), you could add an alias to your ~/.bashrc to do both of these steps for you rather than having to type them out:

alias migrate='rake db:migrate && rake db:test:prepare'

Then type source ~/.bashrc, and the alias will be available to you in your current terminal window. It’ll also be available in new terminal windows even if you didn’t use source, because this file is processed every time a new bash session is started. If you don’t like typing source, then . ~/.bashrc will do.

 

 

Make sure to run db:test:prepare

If you don’t prepare the test database, the following error occurs when you run the feature:

And I press "Create Ticket"
undefined method 'email' for nil:NilClass (ActionView::Template::Error)

Watch out for that one.

 

Only one scenario fails now. We’re right back to the missing “Created by [email protected]” text. Open app/views/tickets/show.html.erb and, above the ticket description, put the following line:

<br><small>Created by <%= @ticket.user.email %></small>

This line adds the text the feature needs to pass, so when you run bin/cucumber features/creating_tickets.feature, you get the following output:

3 scenarios (3 passed)
44 steps (44 passed)

You should run rake cucumber:ok spec as usual to ensure you haven’t broken anything:

Failing Scenarios:
cucumber features/deleting_tickets.feature:15
cucumber features/editing_tickets.feature:16
cucumber features/viewing_tickets.feature:18

Oops, it looks like you did! If you didn’t have these tests in place, you wouldn’t have known about this breakage unless you tested the application manually or guessed (or somehow knew) that your changes would break the application in this way. Let’s see if you can fix it.

6.5.2. We broke something!

Luckily, all the failed tests have the same error

When I follow "Make it shiny!"
  undefined method 'email' for nil:NilClass (ActionView::Template::Error)
  ...
  ./app/views/tickets/show.html.erb:4:in ...

Whatever is causing this error is on line 4 of app/views/tickets/show.html.erb:

Created by <%= @ticket.user.email %>

Aha! The error is undefined method 'email' for nil:NilClass, and the only place you call email on this line is on the user object from @ticket, so you can determine that user must be nil. But why? Let’s have a look at how to set up the data in the features/viewing_tickets.feature feature, as shown in the following listing.

Listing 6.10. features/viewing_tickets.feature
Given there is a project called "TextMate 2"
And that project has a ticket:
  | title           | description                   |
  | Make it shiny! | Gradients! Starbursts! Oh my! |

No user is assigned to this ticket for the second step, and that’s why user is nil. You should rewrite this feature to make it create a ticket and link it to a specific user.

6.5.3. Fixing the Viewing Tickets feature

The first step is to create a user you can link to, so change the first lines of the Background to this:

Given there are the following users:
  | email             | password |
  | [email protected] | password |
And there is a project called "TextMate 2"
And that project has a ticket:
  | title           | description                   |
  |  Make it shiny! | Gradients! Starbursts! Oh my! |

Next, change the third step a little so it creates a ticket with a user:

And "[email protected]" has created a ticket for this project:
  | title           | description                   |
  |  Make it shiny! | Gradients! Starbursts! Oh my! |

Also be sure to change the other ticket-creation lines further down:

And "[email protected]" has created a ticket for this project:
  | title                | description   |
  | Standards compliance | Isn't a joke. |

When you run bin/cucumber features/viewing_tickets.feature, you get the new version of this step definition:

Given /^"([^"]*)" has created a ticket for this project:$/ do |arg1, table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

Copy the first line of this step definition, open features/step_definitions/ticket_steps.rb, and replace the first line in the file with this new line. Then replace arg1 with email, making the entire step definition

Given /^"([^"]*)" has created a ticket for this project:$/ do |email, table|
  table.hashes.each do |attributes|
    @project.tickets.create!(attributes)
  end
end

Next, link this new ticket to the user who has the email you pass in. Change the step definition as follows:

Given /^"([^"]*)" has created a ticket for this project:$/ do |email, table|
  table.hashes.each do |attributes|
  attributes = attributes.merge!(:user => User.find_by_email!(email))
    @project.tickets.create!(attributes)
  end
end

With this step definition in place, the feature should pass. Let’s do another run of bin/cucumber features/viewing_tickets.feature:

1 scenario (1 passed)
19 steps (20 passed)

Let’s now fix up the other two, beginning with the Editing Tickets feature.

6.5.4. Fixing the Editing Tickets feature

You can re-use the step definition you created in the previous section in the features/editing_tickets.feature by changing the first few lines of the Background to be identical to the next listing.

Listing 6.11. features/editing_tickets.feature
Given there are the following users:
  | email             | password |
  | [email protected] | password |
Given there is a project called "TextMate 2"
And "[email protected]" has created a ticket for this project:
  | title           | description                   |
  |  Make it shiny! | Gradients! Starbursts! Oh my! |

When you run the feature—unlike the Viewing Tickets feature—it doesn’t pass, complaining that it can’t find the field called Title. Uh oh:

cannot fill in, no text field, text area or password field with id,
name, or label 'Title' found (Capybara::ElementNotFound)

Back in the TicketsController, you restricted some of the actions by using the before_filter:

before_filter :authenticate_user!, :except => [:index, :show]

This before_filter restricts any access to the edit action for people who are not signed in. In this feature then, you should sign in as the user you create so you can edit this ticket. Change the first line of the Background to sign in as that user:

Background:
  Given there are the following users:
    | email             | password |
    | [email protected] | password |
  And I am signed in as them

When you run this feature, you see the last step in the example is undefined. You must define this new step so you can sign in as the user set up in the first step of the Background. Because you assigned @user in the there are the following users step, you can reference this variable in the new step. Define this new step at the bottom of features/step_definitions/user_steps.rb by copying the lines from features/signing_in.feature and doing a couple of replacements, as shown in the following listing.

Listing 6.12. features/user_steps.rb
Given /^I am signed in as them$/ do
  steps(%Q{
    Given I am on the homepage
    When I follow "Sign in"
    And I fill in "Email" with "#{@user.email}"
    And I fill in "Password" with "password"
    And I press "Sign in"
    Then I should see "Signed in successfully."
  })
end

In this step definition, you use a method called steps. Because step definitions are written in Ruby, you can’t use step definitions as you do in Cucumber features. To get around this restriction, use the steps method and specify each step you want to call inside %Q{}, a kind of super-String that allows you to use double and single quotes inside. The steps method then takes each of these steps and runs them as if they were inside a feature.

Because this step is essentially a duplicate of what’s already in features/signing_in.feature, you can remove the similar lines and turn the Signing in via Form scenario into what’s shown in the following listing.

Listing 6.13. features/signing_in.feature
Scenario: Signing in via form
Given there are the following users:
  | email             | password |
  | [email protected] | password |
  And I am signed in as them

Much simpler!

Now if you run bin/cucumber features/editing_tickets.feature, this feature passes because you’re signing in as a user before attempting to edit a ticket!

2 scenarios (2 passed)
27 steps (27 passed)

One more feature to go: the Deleting Tickets feature.

6.5.5. Fixing the Deleting Tickets feature

To fix the Deleting Tickets feature, take the first couple of lines from features/editing_tickets.feature and put them into features/deleting_tickets.feature so that the first few lines of the Background for this feature look like the following listing.

Listing 6.14. features/deleting_tickets.feature
Given there are the following users:
  | email             | password |
  | [email protected] | password |
And I am signed in as them
Given there is a project called "TextMate 2"
And "[email protected]" has created a ticket for this project:
  | title           | description                   |
  |  Make it shiny! | Gradients! Starbursts! Oh my! |

When you run bin/cucumber features/deleting_tickets.feature, this feature passes once again:

1 scenario (1 passed)
11 steps (11 passed)

There! The last of the broken features is fixed.

Now that the known failing scenarios are working, let’s check for any other breakages with rake cucumber:ok spec. You should see this output:

16 scenarios (16 passed)
148 steps (148 passed)
# and
6 examples, 0 failures, 4 pending

Great! Let’s commit and push that to GitHub now:

git add .
git commit -m "When creating tickets, attribute them to the creator."
git push

You’ve added the feature to add attribution to the tickets so that when a ticket is created, you know who created it. You’ve also restricted certain actions in the TicketsController on the basis of whether or not a user is signed in.

6.6. Summary

This chapter covered how to set up authentication so that users can sign up and sign in to your application to accomplish certain tasks.

We began with Devise, a gem that provides the signing up and signing in capabilities right out of the box by way of being a Rails engine. Using Devise, you tested the functionality provided by the gem in the same way you tested functionality you wrote yourself: by writing Cucumber features to go with it.

Then you moved into testing whether emails were sent out to the right people by using another gem called email_spec. The gem allows you to click a link in an email to confirm a user’s account and then have Devise automatically sign in the user.

Then came linking tickets to users, so you can track which user created which ticket. This was done by using the setter method provided by the belongs_to method’s presence on the Ticket class. You were also able to use Hash’s lovely merge! method in the TicketsController’s create action to link any ticket that was being created to the currently signed-in user.

In the next chapter, we look at restricting certain actions to only users who are signed in or who have a special attribute set on them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.91.47