0

Obviously Rails has deprecated ControllerTest in favour of IntegrationTests. I want to test behaviour based on logged in users. I can't set the current user directly and must use the app's login flow first before testing the route & behaviour of interest. That's fine, but after the controller responsible for handling the login and setting the details in the session has done its thing, the session seems to vanish!

As a simple test I've got a basic Rails app with a WelcomeController and its show view will display "Logged In" or "Logged Out" depending on the user's current status (determined whether the session has a user_id set and if that ID corresponds to a record in the DB).

The login system is handled by OmniAuth. This is easily mocked, and I create a user that is returned by the OmniAuth handler. Log in details is handled by a SessionsController which will display a form that sends an email and password by post to SessionsController.create (the route for this login action is /auth/identity/callback).

# Highly simplified logic for the login handler
class SessionsController < ApplicationController
  def create
    auth_hash = request.env['omniauth.auth']
    user = User.find_with_omniauth(auth_hash)
    session[:user_id] = user.id
    Rails.logger.debug(session.inspect)
    redirect_to root_path
  end 
end

class WelcomeController < ApplicationController
  def show
    Rails.logger.debug(session.inspect)
  end
end

class ApplicationController < ActionController::Base
  def current_user
    @current_user ||= if session.key?(:user_id)
                        User.find_by(id: session[:user_id])
                      else
                        nil
                      end
  end
  
  def logged_in?
    current_user.present?
  end
end

Now for the test:

require 'test_helper'

class WelcomeControllerTest < ActionDispatch::IntegrationTest
  include FactoryBot::Syntax::Methods

  setup do
    OmniAuth.config.test_mode = true
  end

  teardown do
    OmniAuth.config.test_mode = false
    OmniAuth.config.mock_auth[:identity] = nil
  end

  def manual_identity_log_in(user_with_identity)
    OmniAuth.config.mock_auth[:identity] =  OmniAuth::AuthHash.new(
      {
        provider: 'identity',
        uid: user_with_identity.id,
        info: {
          email: user_with_identity.email,
          first_name: user_with_identity.first_name,
          last_name: user_with_identity.last_name,
          name: user_with_identity.full_name
        }
      })

    post '/auth/identity/callback', params: {email: user_with_identity.email, password: ''}
  end

  test "can see the landing page for logged out users" do
    get "/"
    assert_select( "h1", "Logged Out")
  end

  test "can see the welcome page for logged in users" do
    current_user = create(:user_with_identity)
    manual_identity_log_in(current_user)
    get "/"
    assert_select( "h1", "Logged In")
  end
end

The test fails - it always shows the "Logged Out" message.

Note: if I print the session out to the log at the end of SessionsController.create I can everything I expect - the login has worked and is all set up. But the session instance logged within the WelcomeController, triggered by the subsequent get "/" call in the test, is #<ActionDispatch::Request::Session:0x3b880 not yet loaded>. To me that is clearly a failure due to the session not being persisted between the two requests. The WelcomeController, in the context if this test will always determine the user to be logged out.

Despite endless searching and reading, it seems to me that I must be missing something very obvious because this is the intended approach of IntegrationTests.

TIA

andyroberts
  • 3,458
  • 2
  • 37
  • 40

1 Answers1

0

I've found the solution and it relates to the session store config. This may be a bit of a "gotcha"

When developing Rails apps and running in development mode, I still use a self-signed cert in order to run localhost over https. And of course when the app is deployed to staging and production it's running via https too. Hence I had an a common initializer that was used for all environments that had session secured for https only:

# config/initializers/session_store.rb
Rails.application.config.session_store(
  :cookie_store,
  key: '_myapp_session',
  httponly: true,
  secure: true  # <--- the problem
)

But the tests were not using https, so this cookie config wasn't going to persist the session via the cookie at all.

My solution was to set secure to false for test mode:

# config/initializers/session_store.rb
Rails.application.config.session_store(
  :cookie_store,
  key: '_myapp_session',
  httponly: true,
  secure: !Rails.env.test?
)

And now the session is persisted across the two separate requests and the test completes.

andyroberts
  • 3,458
  • 2
  • 37
  • 40