sorcery
sorcery copied to clipboard
Capybara & Selenium system test support
Answering the call here: https://github.com/Sorcery/sorcery/wiki/Testing-Rails#feature-tests-w-selenium-or-webkit
In system tests, we've been stubbing our SSO calls to WorkOS and then visiting our sso callback action. That action ends up calling auto_login
. This approach works well but ends up in an extra round trip to the server on every spec. With that in mind, I came up with a way to authenticate in the test env when the first "real" request to a page happens.
The approach is,
- Setting a "global" login user in the test env when calling
login
- Setting the same
session
keys, as Sorcery sets, in middleware (and clearing the login user variable)
This approach is inspired by Warden
's on_next_request hook and rack_session_access middleware.
I tried many many ways to set session before the request, use query params, set headers, page.driver.browser.execute_cdp
with "Network.setCookie"
, setting localstorage, etc. No luck on any of those approaches because capybara fires up the browser at data:,
and that url prevents setting anything (or persisting anything) on the actual domain the test is hitting.
The singleton approach is never ideal but in the test environment, it works well. It's a simple and easily manageable way to pass around the "global user" that should be logged in and easy to clear out before the next test. This saves us about ~0.3 seconds per test run by avoiding the server roundtrip.
In spec/support/auth_support.rb
# Singleton class to store the login user which is used in TestUserAuthMiddleware
# to set the current user in the request.
# In tests, this saves a full round trip to the server to authenticate the user.
class TestLoginUser
include Singleton
attr_accessor(:login_user)
def clear_user
@login_user = nil
end
end
module AuthSupport
def login(user = create(:user))
TestLoginUser.instance.login_user = user
end
end
RSpec.configure do |config|
config.include(AuthSupport, type: -> (type) { %i[system request].include?(type) })
# Clear the login user before each test
config.before(:each, type: -> (type) { %i[system request].include?(type) }) do
TestLoginUser.instance.clear_user
end
end
In lib/middleware/test_user_auth_middleware.rb
# Intercept the request and set the user_id in the session.
# In tests, this saves a full round trip to the server to authenticate the user.
class TestUserAuthMiddleware
attr_reader(:app)
def initialize(app)
@app = app
end
def call(env)
login_user = TestLoginUser.instance.login_user
if login_user.present?
env["rack.session"][:user_id] = login_user.id
TestLoginUser.instance.clear_user
end
app.call(env)
end
end
In config/environments/test.rb
require "middleware/test_user_auth_middleware"
Rails.application.configure do
config.middleware.use(TestUserAuthMiddleware)
end
Let me know what you think!