Trinculo
Trinculo

Reputation: 2021

Rails integration tests with subdomains, RSpec and capybara-webkit (for JavaScript)

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:

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

Answers (1)

Thomas Walpole
Thomas Walpole

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

Related Questions