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: "jason@teamtreehouse.com"
}
}
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 = "JASON@TEAMTREEHOUSE.COM"
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: "JASON@TEAMTREEHOUSE.COM"))
expect{ user.downcase_email}.to change{user.email}.
from("JASON@TEAMTREEHOUSE.COM").
to("jason@teamtreehouse.com")
end
it "downcases an email before saving" do
user = User.new(valid_attributes)
user.email = "MIKE@TEAMTREEHOUSE.COM"
expect(user.save).to be_true
expect(user.email).to eq("mike@teamtreehouse.com")
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@example.com",
"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: "jason@teamtreehouse.com"
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: "jason@teamtreehouse.com", password: "treehouse1", password_confirmation: "treehouse1")}
it "redirects to the todo list path" do
post :create, email: "jason@teamtreehouse.com", 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: "jason@teamtreehouse.com").and_return(user)
post :create, email: "jason@teamtreehouse.com", password: "treehouse1"
end
it "authenticate the use" do
User.stub(:find_by).and_return(user)
expect(user).to receive(:authenticate)
post :create, email: "jason@teamtreehouse.com", 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: "jason@teamtreehouse.com", password: "treehouse1", password_confirmation: "treehouse1")}
it "redirects to the todo list path" do
post :create, email: "jason@teamtreehouse.com", 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: "jason@teamtreehouse.com").and_return(user)
post :create, email: "jason@teamtreehouse.com", password: "treehouse1"
end
it "authenticate the use" do
User.stub(:find_by).and_return(user)
expect(user).to receive(:authenticate)
post :create, email: "jason@teamtreehouse.com", password: "treehouse1"
end
it "sets the user_id in the session" do
post :create, email: "jason@teamtreehouse.com", password: "treehouse1"
expect(session[:user_id]).to eq(user.id)
end
it "sets the flash success message" do
post :create, email: "jason@teamtreehouse.com", 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: "jason@teamtreehouse.com", 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) {"none@found.com"}
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: "jason@teamtreehouse.com", 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:"jason@teamtreehouse.com", password: "treehouse1", password_confirmation: "treehouse1")
visit new_user_session_path
fill_in "Email Address", with: "jason@teamtreehouse.com"
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: "jason@teamtreehouse.com"
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: "from@example.com"
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: 'none@found.com'
expect(response).to render_template('new')
end
it "sets the flash message" do
post :create, email: 'none@found.com'
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