Notes Teamtreehouse Rails User Authentication

Learn a lot of Ruby and Rails from Teamtreehouse, Trying that Track on Rails to bad I didn't jot down from the beginning... Started from Basic HTML, JS, CSS, Database which was pretty damn long... From now on I will add all my followings tutorial to github and give links in this post.

Creating the User Model: Part 1 and Part 2

 * (add to gem) 
gem 'bcrypt-ruby', '~> 3.1.2'
* bundle
* bin/rails generate scaffold user first_name:string last_name:string email:string:index password_digest:string --skip-jbuilder --skip-assets
* (user_spec.rb)
require 'spec_helper'

describe User do
let(:valid_attributes){
{
first_name: "Jason",
last_name: "Seifer",
email: "[email protected]"
}
}
context "validations" do
let(:user) {User.new(valid_attributes)}

before do
User.create(valid_attributes)
end
it "requires an email" do
expect(user).to validate_presence_of(:email)
end

it "requires a unique email" do
expect(user).to validate_uniqueness_of(:email)
end

it "requires a unique email (case insensitive)" do
user.email = "[email protected]"
expect(user).to validate_uniqueness_of(:email)
end
end

describe "#downcase_email" do
it "makes the email attributes lower case" do
user = User.new(valid_attributes.merge(email: "[email protected]"))
expect{ user.downcase_email}.to change{user.email}.
from("[email protected]").
to("[email protected]")
end

it "downcases an email before saving" do
user = User.new(valid_attributes)
user.email = "[email protected]"
expect(user.save).to be_true
expect(user.email).to eq("[email protected]")
end
end

end

  • (user.rb)
    class User < ActiveRecord::Base
    validates :email, presence: true,
    uniqueness: true

before_save :downcase_email
def downcase_email
self.email = email.downcase
end
end

Using has_secure_password

 * (user.rb)
validates :email, presence: true,
uniqueness: true
format:{
with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]+\Z/
} 
  • (user_spec)
    it "requires the email address to look like an email" do
    user.email = "jason"
    expect(user).to_not be_valid
    end

  • add valid attributes in user_controller_spec
    let(:valid_attributes) { {
    "first_name" => "MyString" ,
    "last_name" => "LastName",
    "email" => "[email protected]",
    "password" => "password1234",
    "password_confirmation" => "password1234"
    } }

  • create new spec spec/features/users/registration_spec.rb

  • (registration_spec_rb)
    require "spec_helper"

describe "Signing up" do
it "allows a user to signup for the site and create the object in the database" do
expect(User.count).to eq(0)

visit "/"
expect(page).to have_content("Sign Up")
click_link "Sign Up"

fill_in "First Name", with: "Jason"
fill_in "Last Name", with: "Siefer"
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "treehouse1234"
fill_in "Password (again)", with: "treehouse1234"
click_button "Sign Up"

expect(User.count).to eq(1)
end
end

  • add new link to application.html.erb
    <li><%= link_to "Sign Up", new_user_path %></li>

Creating the Sessions Controller

  • rails generate controller user_sessions new create --skip-assets
  • rm app/views/user_sessions/create.html.erb
  • rm spec/views/user_sessions/create.html.erb_spec.rb
  • (user_sessions_controller_spec.rb)
    require 'spec_helper'

describe UserSessionsController do

describe "GET 'new'" do
it "returns http success" do
get 'new'
response.should be_success
end

it "renders the new template" do
get "new"
expect(response).to render_template("new")
end
end

describe "POST 'create'" do

context "with correct credentials" do

let!(:user) {User.create(first_name: "Jason", last_name: "Seifer", email: "[email protected]", password: "treehouse1", password_confirmation: "treehouse1")}

it "redirects to the todo list path" do
post :create, email: "[email protected]", password: "treehouse1"
expect(response).to be_redirect
expect(response).to redirect_to(todo_lists_path)
end

it "finds the user" do
expect(User).to receive(:find_by).with(email: "[email protected]").and_return(user)

post :create, email: "[email protected]", password: "treehouse1"
end

it "authenticate the use" do
User.stub(:find_by).and_return(user)
expect(user).to receive(:authenticate)
post :create, email: "[email protected]", password: "treehouse1"

end

end
end

end

  • user_sessions_controller.rb
    class UserSessionsController < ApplicationController
    def new
    end

def create
user = User.find_by(email: params[:email])
user.authenticate(params[:password])
redirect_to todo_lists_path
end
end

Testing session creation

 * (user_sessions_controller_spec.rb)
* require 'spec_helper'

describe UserSessionsController do

describe "GET 'new'" do
it "returns http success" do
get 'new'
response.should be_success
end

it "renders the new template" do
get "new"
expect(response).to render_template("new")
end
end

describe "POST 'create'" do

context "with correct credentials" do

let!(:user) {User.create(first_name: "Jason", last_name: "Seifer", email: "[email protected]", password: "treehouse1", password_confirmation: "treehouse1")}

it "redirects to the todo list path" do
post :create, email: "[email protected]", password: "treehouse1"
expect(response).to be_redirect
expect(response).to redirect_to(todo_lists_path)
end

it "finds the user" do
expect(User).to receive(:find_by).with(email: "[email protected]").and_return(user)

post :create, email: "[email protected]", password: "treehouse1"
end

it "authenticate the use" do
User.stub(:find_by).and_return(user)
expect(user).to receive(:authenticate)
post :create, email: "[email protected]", password: "treehouse1"

end

it "sets the user_id in the session" do
post :create, email: "[email protected]", password: "treehouse1"
expect(session[:user_id]).to eq(user.id)

end

it "sets the flash success message" do
post :create, email: "[email protected]", password: "treehouse1"
expect(flash[:success]).to eq("Thanks for logging in!")
end
end

shared_examples_for "denied login" do
it "renders the new template" do
post :create, email: email, password: password
expect(response).to render_template('new')
end

it "sets the flash error message" do
post :create, email: email, password: password
expect(flash[:error]).to eq("There was a problem logging in. Please check your email and password.")

end
end

context "with blank credentials" do
let(:email) {""}
let(:password) {""}
it_behaves_like "denied login"
end

context "with an incorrect password" do

let!(:user) {User.create(first_name: "Jason", last_name: "Seifer", email: "[email protected]", password: "treehouse1", password_confirmation: "treehouse1")}
let(:email) {user.email}
let(:password) {"incorrect"}
it_behaves_like "denied login"

end

context "with no email in existence" do
let(:email) {"[email protected]"}
let(:password) {"incorrect"}
it_behaves_like "denied login"
end

end

end

  • def create
    user = User.find_by(email: params[:email])
    if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    flash[:success] = "Thanks for logging in!"
    redirect_to todo_lists_path
    else
    render action: 'new'
    flash[:error] = "There was a problem logging in. Please check your email and password."
    end
    end

integration testing authentication

 * (add to route.rb)
resources :user_sessions, only: [:new, :create]
* (user_controller_spec.rb)
it "sets the session user_id to the created_user" do
post :create, email: "[email protected]", password: "treehouse1"
expect(session[:user_id]).to eq(User.find_by(email: valid_attributes["email"].id))
end
* (app/views/user_session)
<h1>Log In</h1>
<%= form_tag user_sessions_path do %>
<%= label_tag :email, "Email Address" %>
<%= text_field_tag :email, params[:email] %>

<%= label_tag :password, "Password" %>
<%= password_field_tag :password %>
<%= submit_tag "Log In" %>
<% end %>

  • create a new file (authentication_spec.rb)
    require "spec_helper"

describe "Logging in" do

it "logs the user in and goes to the todo lists" do
User.create(first_name: "Jason", last_name: "Seifer", email:"[email protected]", password: "treehouse1", password_confirmation: "treehouse1")
visit new_user_session_path

fill_in "Email Address", with: "[email protected]"
fill_in "Password", with: "treehouse1"
click_button "Log In"

expect(page).to have_content("Todo Lists")
expect(page).to have_content("Thanks for logging in!")
end

it "displays the email address in the event of a failed login " do
visit new_user_session_path
fill_in "Email Address", with: "[email protected]"
fill_in "Password", with: "incorrect"
click_button "Log In"

expect(page).to have_content("Please check your email and password.")
end
end

  • (user_sessions_controller.rb)
    class UserSessionsController < ApplicationController
    def new
    end

def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
flash[:success] = "Thanks for logging in!"
redirect_to todo_lists_path
else
redirect_to new_user_session_path
flash[:error] = "There was a problem logging in. Please check your email and password."
end
end
end

Requiring Login

 * (create todo_lists/index_spec.rb)
require 'spec_helper'

describe "Listing todo lists" do
it "requires login" do
visit "/todo_lists"
expect(page).to have_content("You must be logged in")
end
end

  • (add to application_controller.rb)
    def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
    end

def require_user
if current_user
true
else
redirect_to new_user_session_path, notice: "You must be logged in to access that page."
end
end

  • (add to todo_lists_controller_spec.rb)

before do
controller.stub(:current_user).and_return(User.new)
end

Adding test helper

 * (add to gemfile)
gem "factory_girl_rails", "~> 4.0"
* (spec_helper.rb)
config.include FactoryGirl::Syntax::Methods
* (create file spec/factories.rb)
FactoryGirl.define do
factory :user do
first_name "First"
last_name "Last"
sequence(:email) { |n| "user#{n}@odot.com"}
password "treehouse1"
password_confirmation "treehouse1"
end
end
* (authentication_helpers.rb)
module AuthenticationHelpers
def sign_in(user)
controller.stub(:current_user).and_return(user)
controller.stub(:user_id).and_return(user.id)
end
end
* (changed todo_lists_controller_spec.rb)
before do
controller.stub(:current_user).and_return(FactoryGirl.build_stubbed(:user))
end

Fixing Our Tests

 * (change spec_helper.rb)
config.include AuthenticationHelpers::Controller, type: :controller
config.include AuthenticationHelpers::Feature, type: :feature
* (update authentication_helper.rb)
module AuthenticationHelpers

module Controller
def sign_in(user)
controller.stub(:current_user).and_return(user)
controller.stub(:user_id).and_return(user.id)
end
end

module Feature
def sign_in(user, option={})
visit "/login"
fill_in "Email", with: user.email
fill_in "Password", with: options[:password]
click_button "Log In"
end
end
end

  • (add route.rb)
    get "/login" => "user_sessions#new", as: :login
    delete "/logout" => "user_sessions#destroy", as: :logout
  • (add create_spec.rb , edit_spec.rb, destroy_spec.rb)
    let(:user) {create(:user)}
    before do
    sign_in user, password: "treehouse1"
    end
  • (todo_items_controller.rb)
    before_action :require_user
  • (todo_items/index_spec.rb, edit_spec.rb, destroy_spec.rb)
    let(:user) {create(:user)}
    before {sign_in user, password: 'treehouse1'}

Adding the password reset token

 * rails generate migration add_password_reset_token_to_users
* class AddPasswordResetTokenToUsers < ActiveRecord::Migration
def change
add_column :users, :password_reset_token, :string
add_index :users, :password_reset_token
end
end
  • rake db:migrate
  • rake db:migrate RAILS_ENV=test
  • (model/user_spec.rb)
    describe "#generate_password_reset_token!" do
    let(:user) { create(:user)}
    it "changes the password_reset_token attributes" do
    expect{ user.generate_password_reset_token!}.to change{user.password_reset_token}
    end

it "calls SecureRandom.urlsafe_base64 to generate_password_reset_token" do
expect(SecureRandom).to receive(:urlsafe_base64)
user.generate_password_reset_token!
end
end

  • (user.rb)
    def generate_password_reset_token!
    update_attribute(:password_reset_token, SecureRandom.urlsafe_base64(48) )
    end

Adding the Password Reset Controller

 * rails generate controller password_resets --skip-assets
* (add to route.rb)
resources :password_resets, only: [:new]
* (password_resets_controller_spec.rb)
require 'spec_helper'

describe PasswordResetsController do
describe "GET new" do
it "renders the new template" do
get :new
expect(response).to render_template("new")
end
end

describe "POST create" do
context "with a valid user and email" do

let(:user) {create(:user)}

it "finds the user" do
expect(User).to receive(:find_by).with(email: user.email).and_return(user)
post :create, email: user.email

end
it "generate a new password reset token" do
expect{ post :create, email: user.email; user.reload}.to change{user.password_reset_token}
end

it "sends a password reset email" do
expect{ post :create, email: user.email}.to change(ActionMailer::Base.deliveries, :size)
end
end
end
end

  • (password_resets_controller.rb)
    class PasswordResetsController < ApplicationController
    def new
    end

def create
user = User.find_by(email: params[:email])
user.generate_password_reset_token!
redirect_to login_path
end
end

  • (views/password_resets/new.html.erb)
    <h1>Reset your password</h1>
    <%= form_tag password_resets_path do %>
    <%= label_tag "Email" %>
    <%= text_field_tag :email %>
    <br />
    <%= submit_tag "Reset Password" %>
    <% end %>
  • (user.rb)
    def generate_password_reset_token!
    update_attribute(:password_reset_token, SecureRandom.urlsafe_base64(48) )
    end

Emailing Password Resets

 * rails generate mailer notifier
* (notifier.rb)
class Notifier < ActionMailer::Base
default_url_option[:host] = "localhost:3000"
default from: "[email protected]"
def password_reset(user)
@user = user
mail(to: "#{user.first_name} #{user.last_name} <#{user.email}>}",
subject: "Reset Your Password"
)
end
end
  • (create 2 files, password_resets.html.text.erb and password_resets.text.erb)
    Hi <%= @user.first_name %>,
    You can reset your password here:
    <%= edit_password_reset_url(@user.password_reset_token) %>

Hi <%= @user.first_name %>,
You can reset your password here:
<p><%= link_to edit_password_reset_url(@user.password_reset_token), edit_password_reset_url(@user.password_reset_token) %></p>

  • (routes.rb)
    resources :password_resets, only: [:new, :create, :edit]
  • (user_sessions/new.html.erb)
    <%= link_to "Forgot Password", new_password_reset_path
  • (password_reset_controller.rb)
    class PasswordResetsController < ApplicationController
    def new
    end

def create
user = User.find_by(email: params[:email])
user.generate_password_reset_token!
Notifier.password_reset(user).deliver
redirect_to login_path
end
end

Handling no email when resetting password

 * (add new context password_reset_controller_spec.rb)
context "with no user found" do 
it "renders the new page" do
post :create, email: '[email protected]'
expect(response).to render_template('new')
end

it "sets the flash message" do
post :create, email: '[email protected]'
expect(flash[:notice]).to match(/not found/)
end
end

  • (add flash with context valid email and password)

it "sets the flash success message" do
post :create, email:user.email
expect(flash[:success]).to match(/check your email/)
end

Displaying the change password form

 * (password_reset_controller_spec.rb)
describe "GET edit" do
context "with a valid password_reset_token" do
let(:user){create(:user)}
before { user.generate_password_reset_token!}

it "renders the edit template" do
get :edit, id: user.password_reset_token
expect(response).to render_template('edit')
end

it "assigns a user instance variable" do
get :edit, id: user.password_reset_token
expect(assigns(:user)).to eq(user)
end
end

context "with no password_reset_token" do
it "renders the 404 page" do
get:edit, id: "notfound"
expect(response.status).to eq(404)
expect(response).to render_template(file: "#{Rails.root}/public/404.html")
end

end
end

  • (create a new file password_resets/edit.html.erb)
    <h1>Change Your password</h1>
    <%= form_for @user,url: password_reset_path(@user.password_reset_token), html: {method: :path} do |form| %>

<%= form.label :password %>
<%= form.password_field :password %>
<br>

<%= form.label :password_confirmation , "Password (again)"%>
<%= form.password_field :password_confirm %>

<br>

<%= submit_tag "Change pasword" %>
<% end %>

  • (add functions PasswordResetsController)

  • (add routes)
    resources :password_resets, only: [:new, :create, :edit,:update]

  • (password_reset_controller.rb)
    def edit
    @user = User.find_by(password_reset_token: params[:id])
    if @user
    else
    render file: 'public/404.html', status: :not_found
    end
    end

Updating a user forgotten password

 * (password_resets_controller_spec.rb)
describe "PATCH update" do
context "with no token found" do
it "renders the edit page" do
patch :update, id: 'notfound',user: {password: 'newpassword1', password_confirmation: 'newpassword1'}
expect(response).to render_template('edit')
end

it "sets the flash message" do
patch :update, id: 'notfound',user: {password: 'newpassword1', password_confirmation: 'newpassword1'}
expect(flash[:notice]).to match(/not found/)
end
end

context "with a valid token" do
let(:user) {create(:user)}
before {user.generate_password_reset_token!}

it "updates the user's password" do
digest = user.password_digest

patch :update, id: user.password_reset_token, user: {password:'newpassword1', password_confirmation: 'newpassword1'}
user.reload
expect(user.password_digest).to_not eq(digest)
end

it "clears the password_reset_token" do
patch :update, id: user.password_reset_token, user: {password:'newpassword1', password_confirmation: 'newpassword1'}
user.reload
expect(user.password_reset_token).to be_blank
end

it "sets the session[:user_id] to the user's id" do
patch :update, id: user.password_reset_token, user: {password:'newpassword1', password_confirmation: 'newpassword1'}
expect(session[:user_id]).to eq(user.id)

end

it "sets the flash[:success] message" do
patch :update, id: user.password_reset_token, user: {password:'newpassword1', password_confirmation: 'newpassword1'}
expect(flash[:success]).to match(/Password Updated./)

end

it "redirects to the todo_lists page" do
patch :update, id: user.password_reset_token, user: {password:'newpassword1', password_confirmation: 'newpassword1'}
expect(response).to redirect_to(todo_lists_path)

end
end

end

  • (password_reset_controller.rb)
    def update
    @user = User.find_by(password_reset_token: params[:id])
    if @user && @user.update_attributes(user_params)
    @user.update_attribute(:password_reset_token, nil)
    session[:user_id] = @user.id
    redirect_to todo_lists_path, success: "Password Updated."
    else
    flash.now[:notice] = "Password reset token not found"
    render action: 'edit'
    end
    end

private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end

Integrating testing forgotten passwords

 * group :test do
gem 'capybara-email', '~>2.2.0'
end
* (spec_helper.rb)
require 'capybara/email/rspec
* (forgot_password_spec.rb)
require "spec_helper"
describe "Forgotten passwords" do
let!(:user) {create(:user)}

it "sends a user an email" do
visit login_path
click_link "Forgot Password"
fill_in "Email", with: user.email
expect{
click_button "Reset Password"
}.to change{ ActionMailer::Base.deliveries.size}.by(1)
end

it "resets a password when following the email link" do
visit login_path
click_link "Forgot Password"
fill_in "Email", with: user.email
click_button "Reset Password"
open_email(user.email)
current_email.click_link "http://"
expect(page).to have_content("Change Your Password")

fill_in "Password", with: "mynewpassword1"
fill_in "Password (again)", with: "mynewpassword1"
click_button "Change Password"
expect(page).to have_content("Password updated")
expect(page.current_path).to eq(todo_list_path)
end
end

Subscribe to You Live What You Learn

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe