ruby-on-railsdeviseintegration-testingminitestwarden

Sessions not persisting in Rails 5.2 integration tests


I have a multi-tenant app that uses Apartment for postgreSQL schemas and Devise for user authentication. Everything is running smoothly until I attempt to write some integration tests.

Here is a stripped down version of what I have so far (feel free to ask for more information):

# test/support/sign_in_helper.rb
module SignInHelper
  def sign_in_as(name)
    sign_in users(name)
  end
end


# test/test_helper.rb
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

#...

class ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers
  include SignInHelper
end


# test/system/article/post_test.rb
require "application_system_test_case"

class Article::PostTest < ApplicationSystemTestCase
  test 'post a new document' do
    sign_in_as :will
    visit articles_path

    click_on 'Add New Article' # redirected to login page after clicking this button
    fill_in 'Name', with: 'Hello world!'
    fill_in 'Content', with: 'Yes yes'

    click_on 'Create Article'
    assert_select 'h1', /Hello world!/
  end
end

The articles_path requires an authenticated user, so I know the sign in helper works. Yet whenever I attempt to go to another link, suddenly the user is unauthenticated.

I monkey patched Devise's authenticate_user! method like so:

def authenticate_user!(*args)
  byebug
  super
end

and confirmed that warden.authenticated? returned true for the articles_path but false while subsequently attempting to navigate to the new_article_path.

I have noticed this behavior in both integration test types, controller and system. However, this is not an issue when using this app in the development environment.

The most frustrating part, is that I have a different app that seems to have an identical setup to this app, but this authentication issue does not occur while testing.

How can I go about debugging this issue?

System


Update 1 (04 February 2019)

Here is the Articles controller as requested by @BKSpurgeon

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :archive]

  def index
    @articles = Article.where(archived: false)
  end

  def show
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article, notice: 'Your article was successfully created.'
    else
      flash[:error] = @article.errors.full_messages.to_sentence
      render :new
    end
  end

  def edit
  end

  def update
    if @article.update(article_params)
      redirect_to articles_path, notice: 'Your article was successfully updated.'
    else
      flash[:error] = @article.errors.full_messages.to_sentence
      render :edit
    end
  end

  def archive
    if @article.archive!
      redirect_to articles_path, notice: 'Your article was successfully archived.'
    else
      render :edit
    end
  end

  private
    def set_article
      @article = Article.find(params[:id])
    end

    def article_params
      params.require(:article).permit(:name, :content, :archived)
    end
end

Update 2 (04 February 2019)

I wrote simple middleware and placed it before Warden to debug:

# lib/debug_warden_middleware.rb
class DebugWardenMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    @status, @headers, @response = @app.call(env)
    if env['warden'].present?
      puts "User (#{env['warden'].user.present?}), Class: #{@response.class.name}"
    end
    return [@status, @headers, @response]
  end
end

# config/application.rb
#...
module AppName
  class Application < Rails::Application
    # ...

    config.middleware.insert_before Warden::Manager, DebugWardenMiddleware
  end
end

And I noticed that warden seems to clear its user after every request, including requests for assets:

bin/rails test:system
Run options: --seed 39763

# Running:

Capybara starting Puma...
* Version 3.9.1 , codename: Private Caller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:57466
User (true), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: Sprockets::Asset
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: ActionDispatch::Response::RackBody
User (false), Uri: Sprockets::Asset
[Screenshot]: tmp/screenshots/failures_test_post_a_new_article.png
E

Error:
Agenda::PostTest#test_post_a_new_article:
Capybara::ElementNotFound: Unable to find field "Name"
    test/system/article/post_test.rb:9:in `block in <class:PostTest>'


bin/rails test test/system/article/post_test.rb:4

As a side note, I am using both Sprockets and Webpacker.


Solution

  • Alright, I stumbled upon the answer after both @BKSpurgeon (in a chat message) and @Old Pro recommended I try to recreate the problem in a separate app to share with you all.

    Because this app is multi-tenanted and using the Apartment gem, my testing setup was a little convoluted. Unfortunately, I did not include some of that setup, thinking it was irrelevant (big mistake, huge!). It wasn't until I tried to recreate my app that I notice the flaw. Here is my testing setup I was using:

    # test/test_helper.rb
    ENV['RAILS_ENV'] ||= 'test'
    require_relative '../config/environment'
    require 'rails/test_help'
    
    # support files
    Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
    
    # Apartment setup
    Apartment::Tenant.drop('sample') rescue nil
    Apartment::Tenant.create('sample') rescue nil
    Apartment::Tenant.switch! 'sample'
    
    # Capybara setup
    Capybara.configure do |config|
      config.default_host = 'http://lvh.me'
      config.always_include_port = true
    end
    
    class ActiveSupport::TestCase
      # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
      fixtures :all
    
      # Add more helper methods to be used by all tests here...
    end
    
    class ActionDispatch::IntegrationTest
      include Devise::Test::IntegrationHelpers
      include SubdomainHelper
      include SignInHelper # dependent on SubomainHelper
    
      setup do
        default_url_options[:host] = 'lvh.me'
        Apartment::Tenant.switch! 'public'
      end
    end
    

    ...

    # test/support/sign_in_helper.rb
    module SignInHelper
      def sign_in_as(name)
        # This right here was the problem code
        user = users(name)
        use_account user.account
        sign_in user
      end
    end
    

    ...

    # test/support/subdomain_helper.rb
    module SubdomainHelper
      def use_account(account)
        account.save unless account.persisted?
    
        default_url_options[:host] = "#{account.subdomain}.lvh.me"
        Capybara.app_host = "http://#{account.subdomain}.lvh.me"
        Apartment::Tenant.switch! account.subdomain
      end
    end
    

    ...

    # test/system/article/post_test.rb
    require "application_system_test_case"
    
    class Article::PostTest < ApplicationSystemTestCase
      test 'post a new document' do
        sign_in_as :will
        visit articles_path
    
        click_on 'Add New Article' # redirected to login page after clicking this button
        fill_in 'Name', with: 'Hello world!'
        fill_in 'Content', with: 'Yes yes'
    
        click_on 'Create Article'
        assert_select 'h1', /Hello world!/
      end
    end
    

    What ended up being the problem, was I was trying to grab the user in the SignInHelper before I had actually switched to the tenant containing that user. With that knowledge, I changed up my code to look like the following:

    # test/support/sign_in_helper.rb
    module SignInHelper
      def sign_in_as(name, account)
        use_account accounts(account)
        sign_in users(name)
      end
    end
    

    ...

    # test/system/article/post_test.rb
    require "application_system_test_case"
    
    class Article::PostTest < ApplicationSystemTestCase
      test 'post a new document' do
        sign_in_as :will, :sample
    
        # ...
      end
    end
    

    And now the sessions are persisting. One thing that still confuses me is, why was I able to log in at all? I have chalked it up to lingering test data, but that is more of a guess than anything else.