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| "[email protected]"}
          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


    * (update password_reset_controller.rb)
def create
               user = User.find_by(email: params[:email])
               if user
                    user.generate_password_reset_token!
                    Notifier.password_reset(user).deliver
                    flash[:success] ="Password reset instructions sent! Please check your Email"
                    redirect_to login_path
               else
                    flash.now[:notice] ="Email not found"
                    render action: 'new'
               end
          end
    * go to forgot password and get the link from the server log
http://localhost:3000/password_resets/ruTOwCmrVEcSqghK7M7kY1sHfSfs5PDMjsqmlbnxVqN-U1SXU-h6zkVcifre6Vuj/edit

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