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.
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.