Reputation: 2021
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:
routes.rb
, I have some constraints depending on whether or not a subdomain is present (looks to see if the request.subdomain.present?
). If there is not a subdomain present, the only routes available are resources :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.
Upvotes: 1
Views: 745
Reputation: 49880
From your log it looks like your request to "http://subdomain6.lvh.me:31234/users/sign_in" is redirecting to "http://lvh.me:31234/" which would happen if the account didn't exist. I'm guessing you haven't disabled transactional testing which would mean the app can't actually see the records created in your test thread. See - https://github.com/jnicklas/capybara#transactions-and-database-setup and https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example
Upvotes: 1