Get a step-by-step tutorial on how to build a basic sign-in system using Ruby on Rails. Learn how to run tests and build new functionalities using test-driven development.
Key Insights
- The tutorial begins with setting up a test that ensures users only see their own to-dos in the task management app.
- A basic sign-in system based on the user's email is created for this purpose.
- Strategies such as redirecting to the homepage for the create action and associating to-dos with current_email are employed to pass the tests successfully.
- The tutorial uses Rails' session object to store values unique to each visitor to the site.
- Finally, the to-dos are displayed only for the signed-in user, ensuring that a random user's to-do created in the background does not appear.
- The tutorial concludes with the successful passing of all tests, marking the completion of a basic sign-in system with task management functionality.
Delve into the third tutorial on Ruby on Rails, where you'll be introduced to the concepts of setting up tests that build on previous features, with the key focus of ensuring that the user only views their own to-do lists.
This exercise is excerpted from Noble Desktop’s past web development training materials. Noble Desktop now teaches JavaScript and the MERN Stack in our Full Stack Development Certificate. To learn current skills in web development, check out our coding bootcamps in NYC and live online.
Topics Covered in This Ruby on Rails Tutorial:
Setting up a Test That Builds on Previous Features, Test #3: Ensuring the User Only Sees Their Own To-dos
Exercise Overview
In the previous exercise you created the Easels task management app, building new functionality using test driven development. Now that the user can see the homepage and add a to-do task to the list, we need to run another test that makes sure that a user can only see their respective to-do.
-
If you completed the previous exercises, you can skip the following sidebar. We recommend you finish the previous exercise (13A) before starting this one. If you haven’t finished it, do the following sidebar.
If You Did Not Do the Previous Exercise (13A)
- Close any files you may have open.
- Open the Finder and navigate to Class Files > yourname-Rails Class
- Open Terminal.
- Type
cd
and a single space (do NOT press Return yet). - Drag the yourname-Rails Class folder from the Finder to the Terminal window and press ENTER.
- Run
rm -rf easels
to delete your copy of the easels site. - Run
Git clone https://bitbucket.org/Noble Desktop/easels.Git
to copy the easels Git repository. - Type
cd easels
to enter the new directory. - Type
Git checkout 13A
to bring the site up to the end of the previous exercise. - Run
bundle
to install any necessary gems. - Run
yarn install—check-files
to install JavaScript dependencies.
Setting up a Test That Builds on Previous Features
Make sure Terminal is open. If you aren’t in the easels directory, type:
cd easels
Run
rails g rspec:feature user_can_see_own_todos
-
In the new file (spec > features > user_can_see_own_todos_spec), update as shown:
require "rails_helper" RSpec.feature "User Can See Own Todos", type: :feature do scenario "successfully" do end end
What steps does the user need to take to see their own to-dos? For one, they are going to need to create a new to-do, save it to the database, and see some CSS. That sounds familiar! To save time, let’s copy some steps from the previous test. Open: spec > features > user_creates_todo_spec.rb
-
Around lines 9–17, copy the last three comments and the code under them:
#user creates a to-do via a form
#user saves the to-do to the database
#verify that the page has our to-do
-
Back in user_can_see_own_todos_spec.rb, paste the code into the empty scenario:
scenario "successfully" do #user creates a to-do via a form click_on "Add a New To-Do" fill_in "Title", with: "paint house" #user saves the to-do to the database click_on "Add To-Do" #verify that the page has our to-do expect(page).to have_css ".todos li", text: "paint house" end
-
To ensure the user only sees their own to-do, we need the database to associate tasks with users. Let’s create a very basic sign in system based on the user’s email. Above the pasted code, type the two comments and code with Capybara syntax:
#create different user's to-do in background Todo.create(title: "buy milk", email: "somebody@example.com") #user signs in visit root_path fill_in "Email", with: "user@example.com" click_on "Sign In" #user creates a to-do via a form
Essentially we’re creating two to-dos at the same time. While our user is adding their
"
paint house"
task, a dummy"
buy milk"
to-do is created in the background. We’re going to make sure we only see one of them—the to-do which belongs to the signed in user. -
To ensure we see the logged in user’s to-do but not the dummy task, revise the final comment and add some more code under it, as indicated in bold:
#verify that the page has the signed in user's to-do, but not the to-do created in the background expect(page).to have_css ".todos li", text: "paint house" expect(page).not_to have_css ".todos li", text: "buy milk"
Save the file and switch to Terminal.
Test #3: Ensuring the User Only Sees Their Own To-Dos
-
Run the new feature test by entering the following (in Sublime Text you can copy the filename by CTRL–clicking or Right–clicking on it and choosing Copy Name):
rspec spec/features/user_can_see_own_todos_spec.rb
The test fails the first step: creating the dummy to-do of
"
buy milk"
. Rails doesn’t know anything about the Todo model’s'
email'
attribute. Back in your code editor, open db > schema.rb. This file shows the fields that are currently in our database.
It’s important not to edit this file, as we do that by running migrations. Around line 17, notice that we only have a
"
title"
field.-
In Terminal run a migration to add an email field (a string value) to each to-do:
rails g migration AddEmailToTodo email:string
-
To update the database, type
rails db:migrate
in the command line.NOTE: Feel free to look back at schema.rb to see that a new
"
email"
field was added. Now we should have enough to get around this test! -
To run the test again, enter the following command:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
(Remember that you can hit the Up Arrow key. We won’t remind you again.)This failure message does not mention the
"
buy milk"
dummy to-do, which means we were able to create it in the background! Now our browser simulator Capybara is trying to log in. So why can’t it find the"
Email"
field? Let’s go sleuthing. -
In your code editor, pull up the current test (user_can_see_own_todos_spec.rb).
Under the #user signs in comment (around line 9), notice that the user is going to the root_path to sign in.
-
Let’s see what we have on our homepage. Open: app > views > todos > index.html.erb
Hmm, all we have is an unordered list that shows all the to-dos, followed by the link that adds new to-dos to the database. This page doesn’t have a login form!
While we could add the login form to this page, it doesn’t make sense. Now that we’re associating tasks with logged in users, a user should log in before they can create to-dos. Let’s reconfigure our routes instead.
-
Open config > routes.rb.
In order to get around our current test failing, we’re going to create a sessions controller. After some configuration it will handle the sign in form we’ll create.
-
Add another set of resourceful routes by adding the bold code (around line 5):
root to: "todos#index" resources :todos resources :sessions
Save the file and switch to Terminal.
-
To see which new routes are now available, type:
rails routes
In addition to the todos paths, we now have paths for sessions! The new_session path (using the new action) adds a new session, routing the user to the sign in page. Come to think of it, we should make the user go to this path instead of the root path. Let’s revise our test.
Switch back to user_can_see_own_todos_spec.rb in your code editor.
-
Around line 10, modify the test as shown in bold below:
#user signs in visit new_session_path fill_in "Email", with: "user@example.com"
Save the file and switch to Terminal.
-
Run the test:
rspec spec/features/user_can_see_own_todos_spec.rb
As you may have expected, we get a RoutingError similar to what we saw in a previous test. The SessionsController doesn’t exist yet. Let’s initialize it so we can visit the new_session_path.
Run
rails g controller sessions
.-
Run the test:
rspec spec/features/user_can_see_own_todos_spec.rb
Just like anything else in programming, test driven development gets easier with practice. You may recognize this kind of error from previous tests. The last time a controller couldn’t find an action, we had to define it in our controller file.
-
In sessions_controller.rb create the new action as follows:
class SessionsController < ApplicationController def new end end
Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
It looks like we’ve previously seen this kind of failure as well. Now that our controller is working, we have a MissingTemplate. Let’s make a corresponding view.
In the app > views > sessions folder, create a new file called: new.html.erb
-
We know from experience that if we run the test now, the next failure will be for the form. Create the form by adding the following code to the empty view:
<%= form_with url: '/sessions', method: :post do |f| %> <%= f.label :email %> <%= f.text_field :email %> <%= f.submit "Sign In" %> <% end %>
-
Let’s break down that first line:
- We are using form_with url: instead of
form_with model:
. This is because we don’t have a session model (nor do we plan to create one at this time). - We have not declared a @session instance variable because we don’t have anywhere in the database to store the session information. We don’t need it for this simple app. Instead we’ll be temporarily holding onto the session info in the browser.
- We are using form_with url: instead of
Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
Making progress! Now it’s not able to click on
"
Sign In"
because the SessionsController has no create action. -
In sessions_controller.rb modify the code to add the create action as follows:
def new end def create end
Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
It’s trying to use the create action but we have a missing template in sessions/create. Remember that the create action doesn’t have a page that displays. Let’s add a redirect in our controller.
-
In sessions_controller.rb add the following bold code:
def create session[:current_email] = params[:email] redirect_to root_path and return end
Like we did to the TodosController in the previous exercise, the quick and easy way to get around the failure is to redirect to the homepage. However, this time we need to set our session equal to something so that we can hold the user’s session in the browser. The first line we added uses a special Rails feature called session object. session is a special variable in Rails that stores values unique to each visitor to the site. In this case, we set the :current_email key within the session object equal to the user typed into the new :session form.
Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
Finally it was able to successfully visit the new_session_path! Because we’re reusing so many steps which worked in our last feature test, our test failed on the very final step. The page should not have a list item with the
"
.todos li"
CSS and"
buy milk"
text, but it’s there. Our signed in user’s"
paint house"
task is here too. We need to restrict the to-dos that we display on our homepage. Switch back to your code editor and open: app > controllers > todos_controller.rb
-
The controller is currently pulling out all of the to-dos in the database and displaying them on the homepage. We want to redefine the @todos instance variable so it is equal to the signed in user’s to-dos. There are a few ways to do that—ideally, we’d create a full User model and authenticate it first. For now, we’ll do what is easiest, in keeping with our TDD best practices. Remove the existing line and replace it:
def index @todos = Todo.where(email: @current_email) end
Save the file.
We now need to define the @current_email instance variable in our ApplicationController so it can be used throughout the app. Open: app > controllers > application_controller.rb
-
Add the bold code:
class ApplicationController < ActionController::Base before_action :set_current_email private def set_current_email @current_email = session[:current_email] end end
We’re defining a @current_email instance variable. We are setting it equal to session[:current_email], as we previously used that code in our SessionsController. Feel free to go back to sessions_controller.rb to see that we defined it to be equal to the email address the user added in the new session form.
NOTE: You can also go back to todos_controller.rb to refresh yourself on how to-dos are associated with the current_email.
Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
Our test is still failing, but a step earlier. This time it couldn’t find the first to-do, the signed in user’s
"
paint house"
task. It looks like the to-dos are still not being associated with an email address. Let’s revise the code that successfully saved the to-dos to the database before we added user logins. Go to todos_controller.rb in your code editor.
-
In the create method, make the following bold code edits (around line 16):
#@todo = Todo.new(todo_params) @todo.email = @current_email @todo.save
We previously were creating a task with a title attribute but no email attribute. This updated code creates each to-do so it is associated with an email (the current_email that the user signed in with).
A few additional notes. First of all, we replaced
Todo.create
withTodo.new
. Create creates the new Todo and saves it all in one step. But, now we need to modify it before saving, so we usenew
instead.We could have just added
:email
to the list of permitted parameters below, in thetodo_params
method. We didn’t do that because that would allow troublemakers to push any email address through the form and create todos for other people. (This is exactly the scenario strong parameters exist to defend against.) Instead we are limiting it to the email they are authenticated with, by we explicitly setting it in the controller. Save the file and switch to Terminal.
-
Run the test:
bundle exec rspec spec/features/user_can_see_own_todos_spec.rb
Woo hoo, it passed! Now a user can sign in, add a new to-do, and save it to the database. Now that the database associates tasks with users, only the user’s own
"
paint house"
to-do is displaying, even though a random user’s"
buy milk"
to-do was created in the background. Very awesome! -
Run all the tests using
rake
to see that all three pass.Our app is working but it is a little brittle. If we go to the root_path of our application, we may not necessarily see any tasks because it requires the user to be signed in. We can also create tasks without an email address—that’s no good! We’ll change that in a later exercise.
-
We will continue with this app in the next exercise. To minimize confusion, leave the easels folder open in your code editor but close all the individual files that are open.
In the next exercise, we’ll create two more tests: one that ensures the user can mark a to-do complete, and one that makes sure they can mark a task incomplete.