sorcery icon indicating copy to clipboard operation
sorcery copied to clipboard

Capybara & Selenium system test support

Open kleinjm opened this issue 1 year ago • 0 comments

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,

  1. Setting a "global" login user in the test env when calling login
  2. 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!

kleinjm avatar Apr 14 '23 21:04 kleinjm