I am having a problem getting my integration tests to pass with capybara-webkit. The problem is directly related to using subdomains.
All of my normal (non JS) integration tests are working, but I can't quite figure out how to get it to work for a multitenanted app. I am using the apartment gem for multitenancy which uses PostgreSQL schemas to segment the user data. Users can sign up, choose a subdomain, and then access the application.
The application is set up as follows:
- In
routes.rb
, I have some constraints depending on whether or not a subdomain is present (looks to see if therequest.subdomain.present?
). If there is not a subdomain present, the only routes available areresources :accounts, only: [:new, :create]
, otherwise the rest of the application routes are available.
I have a before_action on my accounts_controller to call the private method load_schema to switch to the current subdomain or redirect to the root_url.
def load_schema
Apartment::Tenant.switch!('public')
return unless request.subdomain.present?
if current_account
Apartment::Tenant.switch!(current_account.subdomain)
else
redirect_to root_url(subdomain: false)
end
end
def current_account
@current_account ||= Account.find_by(subdomain: request.subdomain)
end
helper_method :current_account
In all of my RSpec feature specs (that are currently passing that do not utilize JavaScript), I run a sign_user_in method to sign the user in:
def sign_user_in(user, opts={})
if opts[:subdomain]
visit new_user_session_url(subdomain: opts[:subdomain])
else
visit new_user_session_path
end
fill_in 'user[email]', with: user.email
fill_in 'user[password]', with: (opts[:password] || user.password)
click_button 'Log in'
end
All of my specs pass that utilize this method.
However, when I run a spec with :js on, it is never able to find 'user[email]'. The view uses a bootstrap modal to pop up a create or edit action and then AJAX (via remote: true) to add or update the newly created resource on the page. One easy feature spec I am currently trying to get to pass is this:
require 'rails_helper'
feature 'user creates product' do
let(:user) { build(:user) }
let(:account) { create(:account_with_schema, owner: user) }
scenario 'successfully', :js do
product = build_stubbed(:product, name: 'Test Product', amazon_sku: 'test_sku', price: 9.99)
sign_user_in(user, subdomain: account.subdomain)
click_on 'Products'
click_on 'New Product'
fill_in 'product[name]', with: product.name
fill_in 'product[amazon_sku]', with: product.amazon_sku
fill_in 'product[price]', with: product.price
click_on 'Create Product'
expect(page).to have_content('Test Product')
end
end
In my log, it shows that it is trying to connect to subdomain1.example.com/users/sign_in:
Received "Visit(http://subdomain1.example.com/users/sign_in)"
Started "Visit(http://subdomain1.example.com/users/sign_in)"
Load started
"Visit(http://subdomain1.example.com/users/sign_in)" started page load
Started request to "http://subdomain1.example.com/users/sign_in"
Finished "Visit(http://subdomain1.example.com/users/sign_in)" with response "Success()"
Received 200 from "http://subdomain1.example.com/users/sign_in"
This subdomain1 is based on a sequence I have for my account factory e.g.:
factory :account do
sequence(:subdomain) { |n| "subdomain#{n}" }
...
Of course this fails as it cannot connect to subdomain1.example.com:
Failures:
1) user creates product successfully
Failure/Error: fill_in 'user[email]', with: user.email
Capybara::ElementNotFound:
Unable to find field "user[email]"
When I am testing locally, I am using lvh.me to test the subdomains locally (since you cannot do subdomains on localhost).
Since I saw these errors, I added the following to my rails_helper.rb:
Capybara::Webkit.configure do |config|
config.debug = true
config.allow_unknown_urls
config.allow_url("lvh.me")
config.allow_url("*.lvh.me")
end
But this still results in the same thing. After some Googling, I found an issue mentioning using "path" vs "url". For example, in my sign_user_in helper
Instead of:
if opts[:subdomain]
visit new_user_session_url(subdomain: opts[:subdomain])
else
to use:
if opts[:subdomain]
visit new_user_session_path(subdomain: opts[:subdomain])
else
When I do this, I seem to get closer as the log now shows this:
Received "AllowUrl(*.lvh.me)"
Started "AllowUrl(*.lvh.me)"
Finished "AllowUrl(*.lvh.me)" with response "Success()"
Wrote response true ""
Received "Visit(http://lvh.me:3000/users/sign_in)"
Started "Visit(http://lvh.me:3000/users/sign_in)"
Cool, progress. Although it is not including the subdomain even though it is being passed in. Additionally, this causes 5 other tests to fail all with a RoutingError:
For example:
5) user authentication does not allow user from one subdomain to sign in on another subdomain
Failure/Error: visit new_user_session_path(subdomain: opts[:subdomain])
ActionController::RoutingError:
No route matches [GET] "/users/sign_in"
Even though the subdomain is being passed in, it is being ignored and that route is not available unless there is a subdomain.
After some Googling and figuring out how to set up the config.allow_url section for lvh.me, I also found I should add this to my development.rb file so that the port is known:
Capybara.always_include_port = true
This is working as the above log output shows it is using port 3000.
Next, I changed the sign_user_in method to use visit new_user_session_url(subdomain: opts[:subdomain]) again (so my previous specs would still pass). Following the advise of this SO thread: Capybara with subdomains - default_host I made a tweak to my spec:
before(:each) do
set_host "lvh.me:3000"
end
def set_host (host)
default_url_options[:host] = host
Capybara.app_host = "http://" + host
end
Now when I run the spec, I see the following in my log:
Received "AllowUrl(*.lvh.me)"
Started "AllowUrl(*.lvh.me)"
Finished "AllowUrl(*.lvh.me)" with response "Success()"
Wrote response true ""
Received "Visit(http://subdomain1.lvh.me:3000/users/sign_in)"
Started "Visit(http://subdomain1.lvh.me:3000/users/sign_in)"
Load started
"Visit(http://subdomain1.lvh.me:3000/users/sign_in)" started page load
Started request to "http://subdomain1.lvh.me:3000/users/sign_in"
Finished "Visit(http://subdomain1.lvh.me:3000/users/sign_in)" with response "Success()"
Started request to "http://lvh.me:3000/"
Received 302 from "http://subdomain1.lvh.me:3000/users/sign_in"
Started request to "http://lvh.me:3000/assets/application.self-e7adbbd6d89b36b8d2524d4a3bbcb85ee152c7a2641271423c86da07df306565.css?body=1"
Started request to "http://lvh.me:3000/assets/jquery.self-660adc51e0224b731d29f575a6f1ec167ba08ad06ed5deca4f1e8654c135bf4c.js?body=1"
Started request to "http://lvh.me:3000/assets/bootstrap/transition.self-6ad2488465135ab731a045a8ebbe3ea2fc501aed286042496eda1664fdd07ba9.js?body=1"
More progress! It is now including the subdomain, port, and pointing to port 3000. I still get the same error though:
1) user creates product successfully
Failure/Error: fill_in 'user[email]', with: user.email
Capybara::ElementNotFound:
Unable to find field "user[email]"
One of the comments on that SO thread says:
"this works perfectly. Also, if you're using a public domain like lvh.me you can set the port automatically using Capybara.server_port = 31234, and then set_host "lvh.me:31234"
So in my rails_helper, I added the server_port just below setting the app_host:
Capybara.app_host = 'http://lvh.me/'
Capybara.server_port = 31234
And changed the before(:each)
to use port 31234. Same result:
"Visit(http://subdomain6.lvh.me:31234/users/sign_in)" started page load
Started request to "http://subdomain6.lvh.me:31234/users/sign_in"
Finished "Visit(http://subdomain6.lvh.me:31234/users/sign_in)" with response "Success()"
Started request to "http://lvh.me:31234/"
Received 302 from "http://subdomain6.lvh.me:31234/users/sign_in"
Started request to "http://lvh.me:31234/assets/application-2f17abe5cd0f04e7f5455c4ae0a6e536b5d84dd05e600178874c6a5938ac0804.css"
Started request to "http://lvh.me:31234/assets/application-8e1c2330cf761b5bfefcaa648b8994224c7c6a87b2f76475831c76474ddca9d1.js"
Received 200 from "http://lvh.me:31234/"
Received 200 from "http://lvh.me:31234/assets/application-8e1c2330cf761b5bfefcaa648b8994224c7c6a87b2f76475831c76474ddca9d1.js"
Received 200 from "http://lvh.me:31234/assets/application-2f17abe5cd0f04e7f5455c4ae0a6e536b5d84dd05e600178874c6a5938ac0804.css"
...
Also see this repeated about 100 times (in this and in all previous examples):
Wrote response true ""
Received "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])"
Started "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])"
Finished "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[normalize-space(string(.)) = 'user[email]']/@for)] | .//label[normalize-space(string(.)) = 'user[email]']//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" with response "Success()"
Wrote response true ""
Received "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])"
Started "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])"
Finished "FindXpath(.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = 'user[email]' or ./@name = 'user[email]') or ./@placeholder = 'user[email]') or ./@id = //label[contains(normalize-space(string(.)), 'user[email]')]/@for)] | .//label[contains(normalize-space(string(.)), 'user[email]')]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')])" with response "Success()"
...
But alas, get the same result:
Failures:
1) user creates product successfully
Failure/Error: fill_in 'user[email]', with: user.email
Capybara::ElementNotFound:
Unable to find field "user[email]"
It seems like it should be working, but there must be something I am missing. Any help would be greatly appreciated.