Testing multidomain Rails 3 app with Capybara - ruby-on-rails

I want to test my multidomain RoR3 App.
Here's my test_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'capybara/rails'
require 'blueprints'
class ActiveSupport::TestCase
end
class ActionDispatch::IntegrationTest
include Capybara
def host
"http://#{subdomain}.lvh.me:3000"
end
def subdomain
#subdomain ? #subdomain : 'demostore'
end
def visit(url)
super("http://#{subdomain}.lvh.me:3000#{url}")
end
end
And my integration test:
require 'test_helper'
class ProductsTest < ActionDispatch::IntegrationTest
def setup
#subdomain = 'demostore'
# creating stuff
end
def teardown
# deleting stuff
end
test "user views product list" do
visit('/')
assert page.has_css?('ul.product-listing')
assert page.has_xpath?("//ul[#class='product-listing']/li", :count => 12)
end
test "user views product page" do
product = Product.first
visit('/')
find(:xpath, "//ul[#class='product-listing']/li/a[1]").click
save_and_open_page
end
end
And I'm sure the link exists. There is problem with clicking and filling stuff.
click_link('Existent link title')
doesn't work too.
I think the default Capybara's driver Rack::Test could have problems with this multidomain stuff?

In your setup, call this rack::test function, which will change your host's value. Well, it changes the host that gets returned about the fake web request.
host! "#{store.subdomain}.example.com"

The problem was that i'm using multidomain stuff so I had to use lvh.me which resolves localhost. You can do the same think by setting in Your /etc/hosts
127.0.0.1 subdomain.yourapp.local
and then use this domain.
I've overwritten Capybara's visit method with sth like that:
def visit(link)
super("mysubdomain.lvh.me:3000#{link}")
end
but problem persisted because when Capybara clicked for example link, the visit method was not used and my host was not requested. Which was? I don't know - probably the default one.
So solution is to set host and port in Capybara settings:
class ActionDispatch::IntegrationTest
include Capybara
Capybara.default_host = "subdomain.yourapp.local"
Capybara.server_port = 3000
# ... rest of stuff here
end

Apparently it's a problem with rack-test.
But there is a fork of it by hassox that just solved it for me.
It's just a couple of commits that really matter, in case you want to check what the changes are.
This is how my Gemfile looks:
group :test, :cucumber do
gem 'rack-test', :git => "https://github.com/hassox/rack-test.git"
gem 'capybara', '= 0.4.1.2'
gem 'capybara-envjs', '= 0.4.0'
gem 'cucumber-rails', '>= 0.3.2'
gem 'pickle', '>= 0.3.4'
end
And then I just make sure to
visit('http://my_subdomain.example.com')
in my steps. Now I'm trying to understand what would make url helpers work with subdomains.

Here's a quick setup that may help you out...
rails 3.2+ testing custom subdomains using cucumber capybara with pow setup:
https://gist.github.com/4465773

I'd like to share what I found to be a great solution for this problem. It involves creating a helper method to prepend URLs with the desired subdomain, doesn't overwrite any Capybara methods, and works with the Rack::Test and capybara-webkit drivers. In fact, it will even work in specs which do not even use Capybara. (source: http://minimul.com/capybara-and-subdomains.html)
The Spec Helper Method
# spec/support/misc.helpers.rb
def hosted_domain(options = {})
path = options[:path] || "/" # use root path by default
subdomain = options[:subdomain] || 'www'
if example.metadata[:js]
port = Capybara.current_session.driver.server_port
url = "http://#{ subdomain }.lvh.me:#{ port }#{ path }"
else
url = "http://#{ subdomain }.example.com#{ path }"
end
end
And to illustrate it's use, here are two examples:
Used in a Feature Spec (with Capybara)
require 'spec_helper'
describe "Accounts" do
# Creates an account using a factory which sequences
# account subdomain names
# Additionally creates users associated with the account
# using FactoryGirl's after callbacks (see FactoryGir docs)
let (:account) { FactoryGirl.create(:account_with_users) })
it "allows users to sign in" do
visit hosted_domain(path: new_sessions_path, subdomain: account.subdomain)
user = account.users.first
fill_in "email", with: user.email
fill_in "password", with: user.password
click_button "commit"
# ... the rest of your specs
end
end
Used in a Request Spec (without Capybara)
#spec/requests/account_management_spec.rb
require "spec_helper"
describe "Account management" do
# creates an account using a factory which sequences
# account subdomain names
let (:account) { FactoryGirl.create(:account) })
it "shows the login page" do
get hosted_domain(path: "/login", subdomain: account.subdomain)
expect(response).to render_template("sessions/new")
end
end

A simple and clean solution is to override the urls you provide to Capybara's visit method. It works well with *.lvh.me domains, which will redirect you to localhost:
describe "Something" do
def with_subdomain(link)
"http://subdomain.lvh.me:3000#{link}"
end
it "should do something" do
visit with_subdomain(some_path)
end
end
Or you could do the same by redefining app_host before a spec:
Capybara.app_host = 'http://sudbomain.lvh.me:3000'
..
visit(some_path)

Related

RSpec, Capybara: redirect_to not working in create/post specs

I set up a simple controller with its according feature_spec:
stamps_controller.rb
class StampsController < ApplicationController
def new
#stamp = Stamp.new
end
def create
#stamp = Stamp.new(stamp_params)
if #stamp.save
redirect_to(stamp_url(#stamp.id), status: 201)
else
render 'new'
end
end
def show
#stamp = Stamp.find(params[:id])
end
private
def stamp_params
params.require(:stamp).permit(::percentage)
end
end
specs/requests/stamps_request_spec.rb
RSpec.describe 'stamp requests', type: :request do
describe 'stamp creation', js: true do
before do
FactoryBot.create_list(:domain, 2)
FactoryBot.create_list(:label, 2)
end
it 'allows users to create new stamps' do
visit new_stamp_path
expect(page).to have_content('Percentage')
find('#stamp_percentage').set('20')
click_button 'Create'
expect(current_path).to eq(stamp_path(Stamp.first.id))
end
end
end
According to the capybara docs:
Capybara automatically follows any redirects, and submits forms associated with buttons.
But this does not happen in the test, instead it throws an error:
expected: "/stamps/1
got: "/stamps"
The results are obvious: it successfully creates the stamp but fails to redirect to the new stamp. I also confirmed this by using binding.pry.
Why wouldn't capybara follow the redirect as described in the docs?
Sidenotes:
it even fails if I use the normal driver instead of js
I've looked into lots of SO questions and docs, finding nothing useful. One potential attempt I was unable to grasp was an answer with no specifics of how to implement it.
my configs:
support/capybara.rb
require 'capybara/rails'
require 'capybara/rspec'
Capybara.server = :puma
Capybara.register_driver :selenium do |app|
Capybara::Selenium::Driver.new(app, browser: :firefox, marionette: true)
end
Capybara.javascript_driver = :selenium
RSpec.configure do |config|
config.include Capybara::DSL
end
spec_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'
require 'factory_bot_rails'
require 'pundit/matchers'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
# .
# unrelated stuff
# .
end
You have a number of issues in your test.
First, Capybara is not meant to be used in request specs - https://relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec - but should instead be used with feature/system tests. Once you've fixed that you should no longer need to include Capybara into every RSpec test and should remove the config.include Capybara::DSL from you config (when you require capybara/rspec it includes Capybara into the test types it should be included in - https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rspec.rb#L10)
Second, click_button is not guaranteed to wait for any actions it triggers to complete. Because of that you need to wait for a visual page change before attempting to access any database objects that would be created by that action (technically you really shouldn't be doing direct DB access in feature specs at all but if you're going to ...)
click_button 'Create'
expect(page).to have_text('Stamp created!') # Whatever message is shown after creation
# Now you can safely access the DB for the created stamp
Third, as pointed out by #chumakoff you should not be using static matchers with Capybara and should instead be using the matchers provided by Capybara
click_button 'Create'
expect(page).to have_text('Stamp created!') # Whatever message is shown after creation
expect(page).to have_path(stamp_path(Stamp.first.id))
Finally, you should look at your test.log and use save_and_open_screenshot to see what your controllers actually did - It's and there's actually an error being raised on creation which is causing your app to redirect to /stamps and display the error message (would also imply your test DB isn't actually being reset between tests, or the factories you show are creating nested records, etc).
Update: After rereading your controller code I noticed the that you're passing a 201 status code to redirect_to. 201 won't actually do a redirect - From the redirect_to docs - https://api.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to
Note that the status code must be a 3xx HTTP code, or redirection will
not occur.
The problem might be that it takes some time for current_path to change after the form is submitted. Your code would work if you put sleep(x) before expect(current_path).
Instead, you should use methods that have so-called "waiting behaviour", such as has_current_path?, have_current_path, assert_current_path:
expect(page).to have_current_path(stamp_path(Stamp.first.id))
or
expect(page.has_current_path?(stamp_path(Stamp.first.id))).to eq true
For anyone else coming here, another possible solution is to increase your wait time. Either globally or per click
# Globally
Capybara.default_max_wait_time = 5
# Per Click
find("#my-button").click(wait: 5)

Puffing Billy redirect not working with Webmock and Capybara Webkit

I use an external site to handle my authentication. In my main controller, I do this by redirecting away to the external site which in turn redirects back to my site's callback url once the user authenticates.
I'm trying to stub this behavior in RSpec using Puffing Billy.
Here's my controller
class AuthController < ApplicationController
# `root_path` routes to here
def connect
some_external_url = "https://partner.co/some/path?foo=bar"
redirect_to(some_external_url)
end
# `auth_callback_path` routes to here
# The external site will also send over some data in the params,
# which will need to be stubbed as well in the specs below
def callback
data = params[:data]
binding.pry
# do stuff
end
end
My spec_helper has Webmock and Capybara configured as follows
# Set webmock and capybara drivers
WebMock.disable_net_connect!(allow_localhost: true)
Capybara.current_driver = :webkit_billy
Capybara.javascript_driver = :webkit_billy
And finally, the feature spec itself does the stubbing and testing
before(:each) do
# Stub the external url so that it redirects to the `callback` action above
proxy.stub("https://partner.co/some/path").and_return(Proc.new { |params|
data = params["foo"] == "bar" ? "good data" : "bad data"
url = auth_callback_url(data: data)
{ redirect_to: url }
})
end
it "should work" do
visit root_path
puts current_url #=> "https://partner.co/some/path?foo=bar"
end
The problem I'm seeing is that the redirect itself isn't working. After visiting the root path and getting redirected to the partner's site, it never redirects back to my app, even though I have that configured in the Proc object.
Could this be some interference from Webmock? Or did I misconfigure the stub?
Thanks!
EDIT:
After various iterations of testing above, decided to simplify and see if using the proxy was working as expected when visiting some plain old static page.
Ran the blow code snippet:
# spec/spec_helper.rb
RSpec.configure do |config|
config.before(:each, :use_puffing_billy) do
Capybara.default_driver = :webkit_billy
Capybara.javascript_driver = :webkit_billy
end
end
# spec/feature/my_spec.rb
it "can do stuff", :use_puffing_billy, js: true do
#
# `/contact` is a standard "Contact Us" static page. It requires no
# authentication, has no redirects, etc... making it useful for simply
# testing whether the page loads or not
#
# Capybara.app_host => nil
# Billy.config.proxy_host => "localhost"
visit contact_url
binding.pry
#
# current_url => "http://www.example.com:51984/contact"
# page.body.present? => false
visit contact_path
binding.pry
#
# current_url => "http://127.0.0.1:51984/contact"
# page.body.present? => true
end
It appears that current_url does not work but current_path does, likely because they use different hosts.

Devise and OmniAuth twitter integration testing with rspec

I am trying to write a integration test for signing in with twitter using OmniAuth and Devise. I am having trouble getting the request variable to be set. It works in the controller test but not the integration test which leads me to think that I am not configuring the spec helper properly. I have looked around, but I can't seem to find a working solution. Here is what I have so far:
# spec/integrations/session_spec.rb
require 'spec_helper'
describe "signing in" do
before do
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
visit new_user_session_path
click_link "Sign in with twitter"
end
it "should sign in the user with the authentication" do
(1+1).should == 3
end
end
This spec raies a error before it can get to the test and I am not quite sure where the request variable needs to be initialized. The error is:
Failure/Error: request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
NoMethodError:
undefined method `env' for nil:NilClass
Now I use the request variable in my controller spec and the test pass but it is not being initialized for the integration tests.
# spec/spec_helper.rb
Dir[Rails.root.join("spec/support/*.rb")].each {|f| require f}
...
# spec/support/devise.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end
Thanks for the help!
Capybara README says "Access to session and request is not possible from the test", so I gave up to configure in test and decided to write a helper method in application_controller.rb.
before_filter :set_request_env
def set_request_env
if ENV["RAILS_ENV"] == 'test'
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
end
end
The Devise test helpers are only meant to be used in controller specs not integration specs. In capybara there is no request object so setting it won't work.
What you should do instead is scope loading of Devise test helpers to your controller specs, something like this:
class ActionController::TestCase
include Devise::TestHelpers
end
and use the warden helper for capybara specs as suggested in this guide: https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara
For a more detailed discussion look at this github issue page: https://github.com/nbudin/devise_cas_authenticatable/issues/36
This one works for me during test using rspec + devise + omniauth + omniauth-google-apps. No doubt the twitter solution will be very similar:
# use this method in request specs to sign in as the given user.
def login(user)
OmniAuth.config.test_mode = true
hash = OmniAuth::AuthHash.new
hash[:info] = {email: user.email, name: user.name}
OmniAuth.config.mock_auth[:google_apps] = hash
visit new_user_session_path
click_link "Sign in with Google Apps"
end
When using request specs with newer versions of RSpec, which do not allow access to the request object:
before do
Rails.application.env_config["devise.mapping"] = Devise.mappings[:user] # If using Devise
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
end

Setting up one time login with minitest/capybara for running rails tests

I'm using capybara with minitest on Rails 2.3.14. Like most applications, this one also requires login to do anything inside the site. I'd like to be able to login once per test-suite and use that session throughout all tests that are run. How do I refactor that to the minitest_helper? Right now my helper looks something like this:
#!/usr/bin/env ruby
ENV['RAILS_ENV'] = 'test'
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
gem 'minitest'
gem 'capybara_minitest_spec'
require 'minitest/unit'
require 'minitest/spec'
require 'minitest/mock'
require 'minitest/autorun'
require 'capybara/rails'
require 'factory_girl'
FactoryGirl.find_definitions
class MiniTest::Spec
include FactoryGirl::Syntax::Methods
include Capybara::DSL
include ActionController::URLWriter
before(:each) do
# .. misc global setup stuff, db cleanup, etc.
end
after(:each) do
# .. more misc stuff
end
end
thanks.
Here’s an example of multiple sessions and custom DSL in an integration test
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
fixtures :users
test "login and browse site" do
# User avs logs in
avs = login(:avs)
# User guest logs in
guest = login(:guest)
# Both are now available in different sessions
assert_equal 'Welcome avs!', avs.flash[:notice]
assert_equal 'Welcome guest!', guest.flash[:notice]
# User avs can browse site
avs.browses_site
# User guest can browse site as well
guest.browses_site
# Continue with other assertions
end
private
module CustomDsl
def browses_site
get "/products/all"
assert_response :success
assert assigns(:products)
end
end
def login(user)
open_session do |sess|
sess.extend(CustomDsl)
u = users(user)
sess.https!
sess.post "/login", :username => u.username, :password => u.password
assert_equal '/welcome', path
sess.https!(false)
end
end
end
Source : http://guides.rubyonrails.org/testing.html#helpers-available-for-integration-tests

Rails rspec set subdomain

I am using rSpec for testing my application. In my application controller I have a method like so:
def set_current_account
#current_account ||= Account.find_by_subdomain(request.subdomains.first)
end
Is it possible to set the request.subdomain in my spec? Maybe in the before block? I am new to rSpec so any advice on this would be great thanks.
Eef
I figured out how to sort this issue.
In my before block in my specs I simply added:
before(:each) do
#request.host = "#{mock_subdomain}.example.com"
end
This setups up the request.subdomains.first to be the value of the mock_subdomain.
Hope someone finds this useful as its not explained very well anywhere else on the net.
I know this is a relatively old question, but I've found that this depends on what kind of test you're running. I'm also running Rails 4 and RSpec 3.2, so I'm sure some things have changed since this question was asked.
Request Specs
before { host! "#{mock_subdomain}.example.com" }
Feature Specs with Capybara
before { Capybara.default_host = "http://#{mock_subdomain}.example.com" }
after { Capybara.default_host = "http://www.example.com" }
I usually create modules in spec/support that look something like this:
# spec/support/feature_subdomain_helpers.rb
module FeatureSubdomainHelpers
# Sets Capybara to use a given subdomain.
def within_subdomain(subdomain)
before { Capybara.default_host = "http://#{subdomain}.example.com" }
after { Capybara.default_host = "http://www.example.com" }
yield
end
end
# spec/support/request_subdomain_helpers.rb
module RequestSubdomainHelpers
# Sets host to use a given subdomain.
def within_subdomain(subdomain)
before { host! "#{subdomain}.example.com" }
after { host! "www.example.com" }
yield
end
end
Include in spec/rails_helper.rb:
RSpec.configure do |config|
# ...
# Extensions
config.extend FeatureSubdomainHelpers, type: :feature
config.extend RequestSubdomainHelpers, type: :request
end
Then you can call within your spec like so:
feature 'Admin signs in' do
given!(:admin) { FactoryGirl.create(:user, :admin) }
within_subdomain :admin do
scenario 'with valid credentials' do
# ...
end
scenario 'with invalid password' do
# ...
end
end
end
In rails 3 everything I tried to manually set the host didn't work, but looking the code I noticed how nicely they parsed the path you pass to the request helpers like get.
Sure enough if your controller goes and fetches the user mentioned in the subdomain and stores it as #king_of_the_castle
it "fetches the user of the subomain" do
get "http://#{mock_subdomain}.example.com/rest_of_the_path"
assigns[:king_of_the_castle].should eql(User.find_by_name mock_subdomain)
end
Rspec - 3.6.0
Capybara - 2.15.1
Chris Peters' answer worked for me for Request specs, but for Feature specs, I had to make the following changes:
rails_helper:
Capybara.app_host = 'http://lvh.me'
Capybara.always_include_port = true
feature_subdomain_helpers:
module FeatureSubdomainHelpers
def within_subdomain(subdomain)
before { Capybara.app_host = "http://#{subdomain}.lvh.me" }
after { Capybara.app_host = "http://lvh.me" }
yield
end
end

Resources