Sessions

Now that our users have the possibility to register and confirm on our page, we need to make it possible for our users to sign in. For handling login, we need to create a session controller:

$ padrino-gen controller Sessions new create destroy
  create  app/controllers/sessions.rb
  create  app/views/sessions
   apply  tests/rspec
  create  spec/app/controllers/sessions_controller_spec.rb
  create  app/helpers/sessions_helper.rb
   apply  tests/rspec
  create  spec/app/helpers/sessions_helper_spec.rb

We made a mistake during the generation - we forget to add the right action for our request. Before making the mistake to delete the generated files by hand with a couple of rm's, you can run a generator to destroy a controller:

$ padrino-gen controller Sessions -d
  remove  app/controllers/sessions.rb
  remove  app/views/sessions
   apply  tests/rspec
  remove  spec/app/controllers/sessions_controller_spec.rb
  remove  app/helpers/sessions_helper.rb
   apply  tests/rspec
  remove  spec/app/helpers/sessions_helper_spec.rb

And run the generate command with the correct actions:

$ padrino-gen controller Sessions get:new post:create get:destroy

Our session controller is naked:

# app/controllers/sessions_controller.rb

JobVacancy:App.controllers :sessions do

  # get :index, :map => '/foo/bar' do
  #   session[:foo] = 'bar'
  #   render 'index'
  # end

  # get :sample, :map => '/sample/url', :provides => [:any, :js] do
  #   case content_type
  #     when :js then ...
  #     else ...
  # end

  # get :foo, :with => :id do
  #   'Maps to url '/foo/#{params[:id]}''
  # end

  # get '/example' do
  #   'Hello world!'
  # end

  get :new do
  end

  post :create do
  end

  get :destroy do
  end
end

\begin{aside} \heading{Test-First development}

Is a term from Extreme Programming (XP) and means that you first write down your tests before writing any code to solve it. This forces you to really think about what you are going to do. These tests prevent you from over engineering a problem because you have to make these tests green.

\end{aside}

We write our tests first before the implementation:

# spec/app/controllers/sessions_controller_spec.rb

require 'spec_helper'


RSpec.describe "SessionsController" do
  describe "GET /login" do
    it "load the login page" do
      get "/login"
      expect(last_response).to be_ok
    end
  end

  describe "POST :create" do
    let(:user) { build(:user)}
    let(:params) { attributes_for(:user)}

    it "stay on page if user is not found" do
      expect(User).to receive(:find_by_email).and_return(false)
      post 'sessions/create'
      expect(last_response).to be_ok
    end

    it "stay on login page if user is not confirmed" do
      user.confirmation = false
      expect(User).to receive(:find_by_email).and_return(user)
      post 'sessions/create'
      expect(last_response).to be_ok
    end

    it "stay on login page if user has wrong password" do
      user.confirmation = true
      user.password = "fake"
      expect(User).to receive(:find_by_email).and_return(user)
      post 'sessions/create', {:password => 'correct'}
      expect(last_response).to be_ok
    end

    it "redirects to home for confirmed user and correct password" do
      user.confirmation = true
      user.password = 'real'
      expect(User).to receive(:find_by_email).and_return(user)
      post 'sessions/create', {:password => 'real', :remember_me => false}
      expect(last_response).to be_redirect
    end
  end

  describe "GET /logout" do
    xit "empty the current session"
    xit "redirect to homepage if user is logging out"
  end
end

We are using method stubs to make test what we want with the expect(User).to receive(:find_by_email).and_return(false) method. At first I was thinking at that mocking is something very difficult. Read it the method out loud ten times and you can guess whats going on. If our User object gets call from it's class method find_by_email it should return false. So we stimulate the actual application call find_by_email in our application and preventing our tests from hitting the database and making it faster. Beside we are using xit to temporarily disable tests.

Here is the code for our session controller to make the test "green":

# app/controllers/session.rb

JobVacancy::App.controllers :sessions do

  get :new, :map => "/login" do
    render 'new'
  end

  post :create do
    @user = User.find_by_email(params[:email])

    if @user && @user.confirmation && @user.password == params[:password]
      redirect '/'
    else
      render 'new'
    end
  end

  get :destroy, :map => '/logout' do
  end

end

When I started the tests I got some weird error messages of calling a method user.save on a nil object and spend one hour till I found the issue. Do you remember the UserObserver? Exactly, this tiny piece of code is also activated for our tests and since we disable sending mails with the set :delivery_method, :test settings in app.rb I never received an mails. The simple to this problem was to add an option to in the spec_helper.rb to disable the observers:

# spec/spec_helper.rb
...
RSpec.configure do |conf|
  conf.before do
    ActiveRecord::Base.observers.disable :all
  end
  ...
end

Running our tests:

$ rspec spec/app/controllers/sessions_controller_spec.rb


SessionsController
  GET /login
    load the login page
  POST :create
    stay on page if user is not found (FAILED - 1)
    stay on login page if user is not confirmed (FAILED - 2)
    stay on login page if user has wrong password (FAILED - 3)
    redirects to home for confirmed user and correct password (FAILED - 4)
    redirect if user is correct and has remember_me (FAILED - 5)
  GET /logout
    empty the current session (PENDING: Temporarily skipped with xit)
    redirect to homepage if user is logging out (PENDING: Temporarily skipped with xit)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) SessionsController GET /logout empty the current session
     # Temporarily skipped with xit
     # ./spec/app/controllers/sessions_controller_spec.rb:66

  2) SessionsController GET /logout redirect to homepage if user is logging out
     # Temporarily skipped with xit
     # ./spec/app/controllers/sessions_controller_spec.rb:71


Failures:

  1) SessionsController POST :create stay on page if user is not found
     Failure/Error: expect(last_response).to be_ok
       expected `#<Rack::MockResponse:0xacd2ddc @original_headers={"Content-Type"=>
       "text/plain", "X-Content-Type-Options"=>"nosniff", "Set-Cookie"=>"rack.
       session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTlkOWJjYWM3YmQ1MDg2ZmFmMzk3%
       0AMmNmZTE4M2IyMmUyYjQ5YzRiYzNmZjg4ODNmYjcwODZkMTc5NjM4NTJh
       M2MG%0AOwBGSSIJY3NyZgY7AEZJIiVhM2JhMWZmMjFkNjg1MDMzODczMjFjYWYxNTBi%
       0AOWVkOAY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBfVVNFUl9BR0VOVAY7
       %0AAFRJIi1kYTM5YTNlZTVlNmI0YjBkMzI1NWJmZWY5NTYwMTg5MGFmZDgwNzA5
       %0ABjsARkkiGUhUVFBfQUNDRVBUX0xBTkdVQUdFBjsAVEkiLWRhMzlhM2VlNWU2%
       0AYjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkODA3MDkGOwBG%0A--
       d6e98e46cbddb5ab1287ac6bc9fba47bcfb2724f; path=/; HttpOnly"},
       @errors="", @body_string=nil, @status=403, @header={"Content-Type"=>"text/plain"
       , "X-Content-Type-Options"=>"nosniff", "Set-Cookie"=>
       "rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTlkOWJjYWM3YmQ1MDg2ZmFmMzk3
       %0AMmNmZTE4M2IyMmUyYjQ5YzRiYzNmZjg4ODNmYjcwODZkMTc5NjM4NTJhM2MG
       %0AOwBGSSIJY3NyZgY7AEZJIiVhM2JhMWZmMjFkNjg1MDMzODczMjFjYWYxNTBi%
       0AOWVkOAY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBfVVNFUl9BR0VOVAY7
       %0AAFRJIi1kYTM5YTNlZTVlNmI0YjBkMzI1NWJmZWY5NTYwMTg5MGFmZDgwNzA5
       %0ABjsARkkiGUhUVFBfQUNDRVBUX0xBTkdVQUdFBjsAVEkiLWRhMzlhM2VlNWU2
       %0AYjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkODA3MDkGOwBG%0A--
       d6e98e46cbddb5ab1287ac6bc9fba47bcfb2724f; path=/; HttpOnly",
       "Content-Length"=>"9"}, @chunked=false,
       @writer=#<Proc:0xacd2cb0@/home/wm/.rvm/gems/ruby-2.2.1/gems
       /rack-1.5.5/lib/rack/response.rb:27 (lambda)>, @block=nil,
       @length=9, @body=["Forbidden"]>.ok?` to return true, got false
       # ./spec/app/controllers/sessions_controller_spec.rb:18:in `block
       (3 levels) in <top (required)>'
...

Finished in 0.38537 seconds (files took 0.74964 seconds to load)
8 examples, 5 failures, 2 pending

Failed examples:

rspec ./spec/app/controllers/sessions_controller_spec.rb:15 # SessionsController
  # POST :create stay on page if user is not found
rspec ./spec/app/controllers/sessions_controller_spec.rb:21 # SessionsController
  # POST :create stay on login page if user is not confirmed
rspec ./spec/app/controllers/sessions_controller_spec.rb:28 # SessionsController
  # POST :create stay on login page if user has wrong password
rspec ./spec/app/controllers/sessions_controller_spec.rb:36 # SessionsController
  # POST :create redirects to home for confirmed user and correct password
rspec ./spec/app/controllers/sessions_controller_spec.rb:44 # SessionsController
  # POST :create redirect if user is correct and has remember_me

The part of the tests with POST :create.to be_ok are failing because of Padrinos csrf token. To make the tests running, you need to disable them for the test environment:

# app/app.rb

module JobVacancy
  class App < Padrino::Application
  ...

  configure :test do
    set :protect_from_csrf, false
  end

  end
end

Before going on with implementing the logout action we need to think what happened after we login. We have to find a mechanism to enable the information of the logged in user in all our controllers and views. We will do it with sessions helper. Let's look into this file:

# app/helpers/sessions_helper.rb

# Helper methods defined here can be accessed in any controller or view in
# the application

JobVacancy::App.helpers do
  # def simple_helper_method
  #  ...
  # end
end

Yeah, Padrino prints the purpose of this new file and it says what we want to do. Let's implement the main features:

# app/helpers/session_helper.rb

JobVacancy::App.helpers do
  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user ||= User.find_by_id(session[:current_user])
  end

  def current_user?(user)
    user == current_user
  end

  def sign_in(user)
    session[:current_user] = user.id
    self.current_user = user
  end

  def sign_out
    session.delete(:current_user)
  end

  def signed_in?
    !current_user.nil?
  end
end

There's a lot of stuff going on in this helper:

  • current_user: Uses the ||= notation. If the left hand-side isn't initialized, initialize the left hand-side with the right hand-side.
  • current_user?: Checks if the passed in user is the currently logged in user.
  • sign_in: Uses the global session method use the user Id as login information
  • sign_out: Purges the :current_user field from our session.
  • signed_in?: We will use this small method within our whole application to display special actions which should only be available for authenticated users.

\begin{aside} \heading{Why Sessions and how does sign_out work?}

When you request an URL in your browser, you are using the HTTP/HTTPS protocol. This protocol is stateless that means that it doesn't save the state in which you are in your application. Web applications implement states with one of the following mechanisms: hidden variables in forms when sending data, cookies, or query strings (e.g. http://localhost:3000/login?user=test&password=test).

We are going to use cookies to save if a user is logged in and saving the user-Id in our session cookies under the current_user key.

What the delete method does is the following: It will look into the last request in your application inside the session information hash and delete the current_user key. If you want to explore more of the internal of an application I highly recommend you Pry. You can throw in at any part of your application binding.pry and have full access to all variables. \end{aside}

Now we are in a position to write tests for our :destroy action:

# spec/app/controller/sessions_spec.rb

require 'spec_helper'

describe "SessionsController" do
  ...
  describe "GET /logout" do
    it "empty the current session" do
      get '/logout'
      expect(last_request.env['rack.session'][:current_user]).to be_nil
    end

    it "redirect to homepage if user is logging out" do
      get '/logout'
      expect(last_response).to be_redirect
    end
  end
end

We use the last_request method to access to Rack's SessionHash information.

And finally the implementation of the code that it make our tests green:

# app/controllers/session.rb

JobVacancy::App.controllers :sessions do
  ...
  get :destroy, :map => '/logout' do
    sign_out
    flash[:notice] = "You have successfully logged out."
    redirect '/'
  end
end

What we forget due to this point is to make use of the sign_in(user) method. We need this during our session :create action:

# app/controller/session.rb

JobVacancy::App.controllers :sessions do
  ...
  post :create do
    ...
    if user && user.confirmation && user.password == params[:password]
      sign_in(user)
      redirect '/'
    else
      ...
    end
  end
end

Where can we test now our logic? The main application layout of our application should have a "Login" and "Logout" link according to the status of the user:

<%# app/views/application.rb %>

<!DOCTYPE html>
<html lang="en-US">
  <%= stylesheet_link_tag '../assets/application' %>
  <%= javascript_include_tag '../assets/application' %>
</head>
<body>
  <div class=="container">
    <div class="row">
        <nav id="navigation">
        ...
        <% if signed_in? %>
          <%= link_to 'Logout', url(:sessions, :destroy) %>
        <% else %>
        <div class="span2">
          <%= link_to 'Login', url(:sessions, :new) %>
        </div>
        <% end %>
        </nav>
      </div>
      ...
    </div>
  </div>
</body>

With the change above we changed the default "Registration" entry in our header navigation to "Login". We will add the link to the registration form now in the 'session/new' view:

<%# app/views/sessions/new.erb %>

<h1>Login</h1>

<% form_tag '/sessions/create' do %>

  <%= label_tag :email %>
  <%= text_field_tag :email %>

  <%= label_tag :password %>
  <%= password_field_tag :password %>
  <p>
  <%= submit_tag "Sign up", :class => "btn btn-primary" %>
  </p>
<% end %>

New on this platform? <%= link_to 'Register', url(:users, :new) %>

\begin{aside} \heading{No hard coded urls for controller routes}

The line above with <% form_tag '/sessions/create' do %> is not a good solution. If you are changing the mapping inside the controller, you have to change all the hard coded paths manually. A better approach is to reference the controller and action within the url method with url(:sessions, :create). Try it out and see if it's working. \end{aside}

Here we are using the form_tag instead of the form_for tag because we don't want to render information about a certain model. We want to use the information of the session form to find a user in our database. We can use the submitted inputs with params[:email] and params[:password] in the :create action in our sessions controller. The basic idea is to pass a variable to the rendering of method which says if we have an error or not and display the message accordingly. To handle this we are using the :locals option to create customized params for your views:

# app/controllers/sessions.rb

JobVacancy::App.controllers :sessions do
  get :new, :map => "/login" do
    render 'new', :locals => { :error => false }
  end

  post :create do
    @user = User.find_by_email(params[:email])

    if @user && @user.confirmation && @user.password == params[:password]
      sign_in(@user)
      redirect '/'
    else
      render 'new', :locals => { :error => true }
    end
  end
  ...
end

Now we can use the error variable in our view:

<%# app/views/sessions/new.erb %>

<h1>Login</h1>

<% form_tag url(:sessions, :create) do %>
  <% if error %>
    <div class="alert alert-error">
      <h4>Error</h4>
      Your Email and/or Password is wrong!
    </div>
  <% end %>
...
<% end %>

New on this platform? <%= link_to 'Register', url(:users, :new) %>

The last thing we want to is to give the user feedback about what the recently action. Like that it would be nice to give feedback of the success of the logged and logged out action. We can do this with short flash messages above our application which will fade away after a certain amount of time. To do this we can use Padrino's flash mechanism is build on Rails flash message implementation.

And here is the implementation of the code:

<%# app/views/application.erb %>

<!DOCTYPE html>
<html lang="en-US">
<head>
  <title>Job Vacancy - find the best jobs</title>
  <%= stylesheet_link_tag '../assets/application' %>
  <%= javascript_include_tag '../assets/application' %>
</head>
<body>
  <div class="container">
    <% if !flash.empty? %>
      <div class="row" id="flash">
      <% if flash.key[:notice] %>
        <div class="span9 offset3 alert alert-success">
          <%= flash[:notice] %>
        </div>
      <% end %>
      </div>
    <% end %>
  </div>
</body>

Next we need implement the flash messages in our session controller:

# app/controllers/sessions.rb

JobVacancy::App.controllers :sessions do
  ...
  post :create do
    user = User.find_by_email(params[:email])

    if user && user.confirmation && user.password == params[:password]
      flash[:notice] = "You have successfully logged out."
      sign_in(user)
      redirect '/'
    else
      render 'new', :locals => { :error => true }
    end
  end
  ...
end

If you now login successfully you will see the message but it will stay there forever. But we don't want to have this message displayed the whole time, we will use jQuery's fadeOut method to get rid of the message. Since we are first writing our own customized JavaScript, let's create the inline with the following content:

<%# app/views/application.erb %>

<!DOCTYPE html>
<html lang="en-US">
<head>
  <title>Job Vacancy - find the best jobs</title>
  <%= stylesheet_link_tag '../assets/application' %>
  <%= javascript_include_tag '../assets/application' %>
</head>
<body>
  <div class=="container">
    <% if flash[:notice] %>
      <div class="row" id="flash">
        <div class="span9 offset3 alert alert-success">
          <%= flash[:notice] %></p>
        </div>
        <script type="text/javascript">
          $(function(){
              $("#flash").fadeOut(2000);
          });
        </script>
      </div>
    <% end %>
  </div>
</body>