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