In the last lesson you have learned to:
It's your product manager again! As usual, he comes up with more requests and requirements. This time around he wants you to create a User model that satisfies the following rules:
You know the drill, try writing the specs.
First, we only define specs for username. Specs for password will be a little bit different.
require 'rails_helper'
describe User do
it "has a valid factory" do
expect(build(:user)).to be_valid
end
it "is valid with a username" do
expect(build(:user)).to be_valid
end
it "is invalid without a username" do
user = build(:user, username: nil)
user.valid?
expect(user.errors[:username]).to include("can't be blank")
end
it "is invalid with duplicate username" do
user1 = create(:user, username: "User 1")
user2 = build(:user, username: "User 1")
user2.valid?
expect(user2.errors[:username]).to include("has already been taken")
end
# -cut-
end
For password, remember that we always prompt users with two fields: password and password_confirmation.
describe User do
# -cut-
context "on a new user" do
it "is invalid without a password" do
user = build(:user, password: nil, password_confirmation: nil)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
it "is invalid with less than 8 characters password" do
user = build(:user, password: "short", password_confirmation: "short")
user.valid?
expect(user.errors[:password]).to include("is too short (minimum is 8 characters)")
end
it "is invalid with a confirmation mismatch" do
user = FactoryGirl.build(:user, password: "longpassword", password_confirmation: "longpassword1")
user.valid?
expect(user.errors[:password_confirmation]).to include("doesn't match Password")
end
end
# -cut-
end
When updating an existing user, don't check for password's length again because it's already tested.
describe User do
# -cut-
context "on an existing user" do
before :each do
@user = create(:user)
end
it "is valid with no changes" do
expect(@user.valid?).to eq(true)
end
it "is invalid with an empty password" do
@user.password_digest = ""
@user.valid?
expect(@user.errors[:password]).to include("can't be blank")
end
it "is valid with a new (valid) password" do
@user.password = "newlongpassword"
@user.password_confirmation = "newlongpassword"
expect(@user.valid?).to eq(true)
end
end
end
For User factory, we don't use Faker to generate password to ensure the password_confirmation matches the password.
FactoryGirl.define do
factory :user do
username { Faker::Internet.unique.user_name }
password "longpassword"
password_confirmation "longpassword"
end
end
Here, you will also learn new Rails features.
rails generate model User username:string password:digest
When you define a password as a digest type, Rails will equip it with ActiveModel's method called has_secure_password like this:
class User < ApplicationRecord
has_secure_password
end
Run rails db:migrate.
When you finished running rails db:migrate, you will notice that your schema.rb will look like this.
# -cut-
create_table "users", force: :cascade do |t|
t.string "username"
t.string "password_digest"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
# -cut-
ActiveModel's has_secure_password makes sure that all passwords stored in "password_digest" is stored in encrypted format rather than plain password. It also ensures that we can pass "password" and "password_confirmation" in User model and controller.
To make use password digest to the fullest, we need to install Bcrypt gem. Simply uncomment this line in your Gemfile.
gem 'bcrypt', '~> 3.1.7'
Then run bundle install.
Then we will add validations to our User model.
class User < ApplicationRecord
has_secure_password
validates :username, presence: true, uniqueness: true
validates :password, presence: true, on: :create
validates :password, length: { minimum: 8 }, allow_blank: true
end
ActiveModel's has_secure password already handles several validations for our password field. We just need to validate that password should be present on creation and its length should be 8 characters minimum. Allowing blank is somehow needed to make it works when we updating a User instance without modifying its password at all.
With this set up, you should pass your model specs. Don't forget to commit your progress so far.
Next, we will write our Users controller specs. Our Users controller will be a very simple CRUD. The only difference is when testing for update method, there is a specific way to test for password changes. You will be guided writing test for update, but by now you should be able to write the rest of the specs by yourself.
# -cut-
describe UsersController do
describe 'GET #index' do
it "populates an array of all users" do
user1 = create(:user, username: "user1")
user2 = create(:user, username: "user2")
get :index
expect(assigns(:users)).to match_array([user1, user2])
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
describe 'GET #show' do
it "assigns the requested user to @user" do
user = create(:user)
get :show, params: { id: user }
expect(assigns(:user)).to eq user
end
it "renders the :show template" do
user = create(:user)
get :show, params: { id: user }
expect(response).to render_template :show
end
end
# -cut-
end
describe UsersController do
# -cut-
describe 'GET #new' do
it "assigns a new User to @user" do
get :new
expect(assigns(:user)).to be_a_new(User)
end
it "renders the :new template" do
get :new
expect(:response).to render_template :new
end
end
describe 'GET #edit' do
it "assigns the requested user to @user" do
user = create(:user)
get :edit, params: { id: user }
expect(assigns(:user)).to eq user
end
it "renders the :edit template" do
user = create(:user)
get :edit, params: { id: user }
expect(response).to render_template :edit
end
end
# -cut-
end
describe UsersController do
# -cut-
describe 'POST #create' do
context "with valid attributes" do
it "saves the new user in the database" do
expect{
post :create, params: { user: attributes_for(:user) }
}.to change(User, :count).by(1)
end
it "redirects to users#index" do
post :create, params: { user: attributes_for(:user) }
expect(response).to redirect_to users_url
end
end
context "with invalid attributes" do
it "does not save the new user in the database" do
expect{
post :create, params: { user: attributes_for(:invalid_user) }
}.not_to change(User, :count)
end
it "re-renders the :new template" do
post :create, params: { user: attributes_for(:invalid_user) }
expect(response).to render_template :new
end
end
end
# -cut-
end
describe UsersController do
# -cut-
describe 'PATCH #update' do
before :each do
@user = create(:user, password: 'oldpassword', password_confirmation: 'oldpassword')
end
# -cut-
end
# -cut-
end
For update, first we create an object @user with a password and a password_confirmation.
describe UsersController do
# -cut-
describe 'PATCH #update' do
# -cut-
context "with valid attributes" do
it "locates the requested @user" do
patch :update, params: { id: @user, user: attributes_for(:user) }
expect(assigns(:user)).to eq @user
end
it "saves new password" do
patch :update, params: { id: @user, user: attributes_for(:user, password: 'newlongpassword', password_confirmation: 'newlongpassword') }
@user.reload
expect(@user.authenticate('newlongpassword')).to eq(@user)
end
it "redirects to users#index" do
patch :update, params: { id: @user, user: attributes_for(:user) }
expect(response).to redirect_to users_url
end
it "disables login with old password" do
patch :update, params: { id: @user, user: attributes_for(:user, password: 'newlongpassword', password_confirmation: 'newlongpassword') }
@user.reload
expect(@user.authenticate('oldpassword')).to eq(false)
end
end
# -cut-
end
# -cut-
end
describe UsersController do
# -cut-
describe 'PATCH #update' do
# -cut-
context "with invalid attributes" do
it "does not update the user in the database" do
patch :update, params: { id: @user, user: attributes_for(:user, password: nil, password_confirmation: nil) }
@user.reload
expect(@user.authenticate(nil)).to eq(false)
end
it "re-renders the :edit template" do
patch :update, params: { id: @user, user: attributes_for(:invalid_user) }
expect(response).to render_template :edit
end
end
end
# -cut-
end
As you can see here and in the previous slide, we test password changes using "@user.authenticate" method instead of testing the value of @user.password.
describe UsersController do
# -cut-
describe 'DELETE #destroy' do
before :each do
@user = create(:user)
end
it "deletes the user from the database" do
expect{
delete :destroy, params: { id: @user }
}.to change(User, :count).by(-1)
end
it "redirects to users#index" do
delete :destroy, params: { id: @user }
expect(response).to redirect_to users_url
end
end
end
Now try to write the Users controller. This is a standard CRUD controller, you should be able to write it just fine.
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# -cut-
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:username, :password, :password_confirmation)
end
end
Make sure that we allow parameters password and password_confirmation in our Users controller.
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /users
# GET /users.json
def index
@users = User.all
end
# GET /users/1
# GET /users/1.json
def show
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1/edit
def edit
end
# -cut-
end
class UsersController < ApplicationController
# -cut-
# POST /users
# POST /users.json
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to users_url, notice: 'User was successfully created.' }
format.json { render :show, status: :created, location: @user }
else
format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# -cut-
end
Please note that we change create method to redirect to Users index page when it is finished.
class UsersController < ApplicationController
# -cut-
# PATCH/PUT /users/1
# PATCH/PUT /users/1.json
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to users_url, notice: 'User was successfully updated.' }
format.json { render :show, status: :ok, location: @user }
@users = User.all
else
format.html { render :edit }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# -cut-
end
We also change update method to redirect to Users index page when it is finished.
class UsersController < ApplicationController
# -cut-
# DELETE /users/1
# DELETE /users/1.json
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
format.json { head :no_content }
end
end
# -cut-
end
Rails.application.routes.draw do
get 'home/hello'
root 'store#index', as: 'store_index'
resources :carts
resources :foods
resources :line_items
resources :orders
resources :users
end
Don't forget to add users routes.
Next, try to write the views. Just as the controller, there is not much different in user views than the regular views. The only difference will be in the form.
In our User form, we use "password_field" for the first time.
<%= form_for(user) do |f| %>
<% if user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<fieldset>
<legend>Enter User Details</legend>
<div class="field">
<%= f.label :username %>
<%= f.text_field :username, size: 40 %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password, size: 40 %>
</div>
<div class="field">
<%= f.label :password_confirmation, 'Confirm:' %>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions">
<%= f.submit %>
</div>
</fieldset>
<% end %>
Everything else is just a standard CRUD view.
json.extract! user, :id, :name, :description, :image_url, :price, :created_at, :updated_at
json.url user_url(user, format: :json)
_user.json.jbuilder
<h1>Editing User</h1>
<%= render 'form', user: @user %>
<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>
edit.html.erb
<h1>New User</h1>
<%= render 'form', user: @user %>
<%= link_to 'Back', users_path %>
new.html.erb
<p id="notice"><%= notice %></p>
<p>
<strong>Username:</strong>
<%= @user.username %>
</p>
<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>
show.html.erb
<p id="notice"><%= notice %></p>
<h1>Users</h1>
<table>
<% @users.each do |user| %>
<tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
<td><%= user.username %></td>
<td>
<%= link_to 'Show', user %>
<%= link_to 'Edit', edit_user_path(user) %>
<%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</table>
<br/>
<%= link_to 'New User', new_user_path %>
index.html.erb
json.array! @users, partial: 'users/user', as: :user
index.json.jbuilder
json.partial! "users/user", user: @user
show.json.jbuilder
With this set up, you should pass your Users controller specs. Try adding, editing, and deleting users. Let at least one user remains so you can use it for login and logout later.
Don't forget to commit your progress so far.
We have created our User MVC. Next, we are going to use it for authentication. For this, we will create a new controller called Sessions controller.
Sessions controller only has three methods:
Since its unique nature, you will be guided to write Sessions controller specs.
require 'rails_helper'
describe SessionsController do
describe "GET new" do
it "renders the :new template" do
get :new
expect(:response).to render_template :new
end
end
# -cut-
end
First, we only define new method to render the login template (:new session).
describe SessionsController do
# -cut-
describe "POST create" do
before :each do
@user = create(:user, username: 'user1', password: 'longpassword', password_confirmation: 'longpassword')
end
context "with valid username and password" do
it "assigns user_id to session variables" do
post :create, params: { username: 'user1', password: 'longpassword' }
expect(session[:user_id]).to eq(@user.id)
end
it "redirects to admin index page" do
post :create, params: { username: 'user1', password: 'longpassword' }
expect(response).to redirect_to admin_url
end
end
context "with invalid username and password" do
it "redirects to login page" do
post :create, params: { username: 'user1', password: 'wrongpassword' }
expect(response).to redirect_to login_url
end
end
end
# -cut-
end
require 'rails_helper'
describe SessionsController do
# -cut-
describe "DELETE destroy" do
before :each do
@user = create(:user)
end
it "removes user_id from session variables" do
delete :destroy, params: { id: @user }
expect(session[:user_id]).to eq(nil)
end
it "redirects to store index page" do
delete :destroy, params: { id: @user }
expect(response).to redirect_to store_index_url
end
end
end
This is how your Sessions controller should look like.
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:username])
if user.try(:authenticate, params[:password])
session[:user_id] = user.id
redirect_to admin_url
else
redirect_to login_url, alert: "Invalid user/password combination"
end
end
def destroy
session[:user_id] = nil
redirect_to store_index_url, notice: "Logged out"
end
end
We are going to use special routes for our Sessions controller.
Rails.application.routes.draw do
get 'home/hello'
root 'store#index', as: 'store_index'
controller :sessions do
get 'login' => :new
post 'login' => :create
delete 'logout' => :destroy
end
resources :carts
resources :foods
resources :line_items
resources :orders
resources :users
end
Although we define three methods for Sessions controller, we only need one view (the login form) in Sessions' new.html.erb.
<div class="go_food_form">
<% if flash[:alert] %>
<p id="notice"><%= flash[:alert] %></p>
<% end %>
<%= form_tag do %>
<fieldset>
<legend>Please Log In</legend>
<div>
<%= label_tag :username, 'Userame:' %>
<%= text_field_tag :username, params[:username] %>
</div>
<div>
<%= label_tag :password, 'Password:' %>
<%= password_field_tag :password, params[:password] %>
</div>
<div>
<%= submit_tag "Login" %>
</div>
</fieldset>
<% end %>
</div>
To make our Sessions controller spec pass, we need to generate Admin controller.
rails generate controller Admin index
With this set up, you should pass your Sessions controller specs. Don't forget to commit your progress so far.
We need to make our login/logout button visible from any page in our app. Let's modify our application.html.erb file.
<!DOCTYPE html>
<html>
<!-- cut -->
<body class="<%= controller.controller_name %>">
<!-- cut -->
<div id="columns">
<div id="side">
<!-- cut -->
<% if session[:user_id] %>
<ul>
<li><%= link_to 'Orders', orders_path %></li>
<li><%= link_to 'Foods', foods_path %></li>
<li><%= link_to 'Users', users_path %></li>
<li> </li>
<li><%= button_to 'Logout', logout_path, method: :delete %></li>
</ul>
<% else %>
<ul>
<li><%= link_to 'Login', login_path %></li>
</ul>
<% end %>
</div>
<!-- cut -->
</div>
</body>
</html>
Next, we are going to limit access to parts our controllers only for logged in users. First, let's modify our Application controller.
class ApplicationController < ActionController::Base
before_action :authorize
protect_from_forgery with: :exception
protected
def authorize
unless User.find_by(id: session[:user_id])
redirect_to login_url, notice: 'Please Login'
end
end
end
This will ensure that every controller (which is the children of Application controller) will check for users' credential and redirect them to login page if they don't pass the check.
Then, we are going to skip "before_action :authorize" for several parts of our controllers that should be accessible by everyone. Add these lines in their respective files.
class CartsController < ApplicationController
skip_before_action :authorize, only: [:create, :update, :destroy]
end
class LineItemsController < ApplicationController
skip_before_action :authorize, only: :create
end
class OrdersController < ApplicationController
skip_before_action :authorize, only: [:new, :create]
end
class SessionsController < ApplicationController
skip_before_action :authorize
end
class StoreController < ApplicationController
skip_before_action :authorize
end
Now you can try login and logout and accessing different pages in your app to see if the authorization actually works. Afterward, don't forget to commit your progress.