Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using proxy server instead of normal HTTP server #1212

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4144786
check correct version string for rspec composable
twalpole Aug 14, 2014
05ffa1c
Fix a typo
bbatsov Jul 15, 2014
2e995b8
Increase wait times for window_opened_by spec due to slow Travis CI
abotalov Aug 24, 2014
8091a8d
Test should be tagged as :windows also
route Sep 4, 2014
1e6663f
Update History.md
takashi Sep 18, 2014
1272176
Use Rack::Lock to prevent concurrency when eager_load is false
twalpole Sep 18, 2014
b82ae1d
Update visit's documentation to indicate that it requires something t…
shepmaster Jul 8, 2014
4e43981
Always convert visit's URL to a String before manipulation
shepmaster Jul 8, 2014
3e48371
Update fixes in 2.4.2
twalpole Sep 19, 2014
2b8e08c
Merge pull request #1380 from jnicklas/composable_version_test
twalpole Sep 19, 2014
4756d1e
Update fixes in 2.4.2 [ci-skip]
twalpole Sep 19, 2014
1d79355
tweak test timings for window API to improve reliability on travis
twalpole Sep 19, 2014
4d19182
prepare for 2.4.2 release
twalpole Sep 20, 2014
d18047d
tagged 2.4.2
twalpole Sep 20, 2014
c47b5d3
Also check config.allow_concurrency to match Rails 4.2 behavior for R…
twalpole Sep 22, 2014
58add00
tagged 2.4.3
twalpole Sep 22, 2014
5dbb480
Remove insertion of Rack::Lock middleware in favor of a note in the R…
twalpole Oct 13, 2014
f9077ed
Replace hardcoded `localhost` in spec helper.
phillbaker Oct 5, 2014
eabbd85
Update History
twalpole Oct 13, 2014
120daaa
tagged 2.4.4
twalpole Oct 13, 2014
052cc93
added proxy server implementation
mreinsch Jul 28, 2015
9b94465
use TLSv1 insteadl of older SSL
mreinsch Jul 30, 2015
7058350
don't print stacktraces on errors
mreinsch Jul 30, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 25 additions & 2 deletions History.md
@@ -1,6 +1,29 @@
#Version 2.4.4
Release data: 2014-10-13

###Fixed
* Test for visit behavior updated [Phil Baker]
* Removed concurrency prevention in favor of a note in the README - due to load order issues

# Version 2.4.3
Relase date: 2014-09-21

###Fixed
* Update concurrency prevention to match Rails 4.2 behavior

# Version 2.4.2
Release date: 2014-09-20

### Fixed
* Prevent concurrency issue when testing Rails app with default test environment [Thomas Walpole]
* Tags for windows API tests fixed [Dmitry Vorotilin]
* Documentation Fixes [Andrey Botalov]
* Always convert visit url to string, fixes issue with visit when always_include_port was enabled [Jake Goulding]
* Check correct rspec version before including ::RSpec::Matchers::Composable in Capybara RSpec matchers [Thomas Walpole, Justin Ko]

# Version 2.4.1

Release data: 2014-07-03
Release date: 2014-07-03

### Added

Expand Down Expand Up @@ -76,7 +99,7 @@ Release date: 2013-11-21
instead of `about:blank`, fixing hanging issues in JRuby. [Jonas Nicklas]
* Fixed cookies not being set when path is blank under RackTest [Thomas Walpole]
* Clearing fields now correctly causes change events [Jonas Nicklas]
* Navigating to an absolut URI without trailing slash now works as expected
* Navigating to an absolute URI without trailing slash now works as expected
under RackTest [Jonas Nicklas]
* Checkboxes without assigned value default to `on` under RackTest [Nigel Sheridan-Smith]
* Clicks on buttons with no form associated with them are ignored in RackTest
Expand Down
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -36,6 +36,9 @@ If the application that you are testing is a Rails app, add this line to your te
require 'capybara/rails'
```

Note: In rails 4.0/4.1 The default rails test environment ( config/environments/test.rb ) is not threadsafe - see https://github.com/rails/rails/issues/15089
If you experience random errors about missing constants, adding config.allow_concurrency = false to config/environements/test.rb should solve the issue.

If the application that you are testing is a Rack app, but not Rails, set Capybara.app to your Rack app:

```ruby
Expand Down
35 changes: 34 additions & 1 deletion lib/capybara.rb
Expand Up @@ -22,7 +22,7 @@ class << self
attr_accessor :asset_host, :app_host, :run_server, :default_host, :always_include_port
attr_accessor :server_port, :exact, :match, :exact_options, :visible_text_only
attr_accessor :default_selector, :default_wait_time, :ignore_hidden_elements
attr_accessor :save_and_open_page_path, :automatic_reload, :raise_server_errors
attr_accessor :save_and_open_page_path, :automatic_reload, :raise_server_errors, :use_proxy_protocol
attr_writer :default_driver, :current_driver, :javascript_driver, :session_name, :server_host
attr_accessor :app

Expand All @@ -46,6 +46,7 @@ class << self
# [ignore_hidden_elements = Boolean] Whether to ignore hidden elements on the page (Default: true)
# [automatic_reload = Boolean] Whether to automatically reload elements as Capybara is waiting (Default: true)
# [save_and_open_page_path = String] Where to put pages saved through save_and_open_page (Default: Dir.pwd)
# [use_proxy_protocol = Boolean] Use the proxy protocol to send requests to capybara's proxy server instead of normal web server. Using the proxy server stubs all requests and allows you to use subdomains (Default: false)
#
# === DSL Options
#
Expand Down Expand Up @@ -132,6 +133,23 @@ def server(&block)
end
end

##
#
# Register a proc that Capybara will use to start a proxy server which will receive any requests from the browser.
# The proxy method is used in case the browser supports proxy servers.
#
# By default, Capybara will setup it's internal proxy.
#
# @yield [app, port] This block receives a rack app and port and should run a Rack handler
#
def proxy_server(&block)
if block_given?
@proxy_server = block
else
@proxy_server
end
end

##
#
# Wraps the given string, which should contain an HTML document or fragment
Expand Down Expand Up @@ -173,6 +191,19 @@ def run_default_server(app, port)
Rack::Handler::WEBrick.run(app, :Host => server_host, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
end

##
#
# Runs Capybara's default proxy server for the given application and port
# under most circumstances you should not have to call this method
# manually.
#
# @param [Rack Application] app The rack application to run
# @param [Fixnum] port The port to run the application on
#
def run_default_proxy_server(app, port)
Capybara::ProxyServer.run(app, :Host => server_host, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
end

##
#
# @return [Symbol] The name of the driver to use by default
Expand Down Expand Up @@ -322,6 +353,7 @@ module Selenium; end
require 'capybara/dsl'
require 'capybara/window'
require 'capybara/server'
require 'capybara/proxy_server'
require 'capybara/selector'
require 'capybara/result'
require 'capybara/version'
Expand Down Expand Up @@ -357,6 +389,7 @@ module Selenium; end
config.always_include_port = false
config.run_server = true
config.server {|app, port| Capybara.run_default_server(app, port)}
config.proxy_server {|app, port| Capybara.run_default_proxy_server(app, port)}
config.default_selector = :css
config.default_wait_time = 2
config.ignore_hidden_elements = true
Expand Down
4 changes: 4 additions & 0 deletions lib/capybara/driver/base.rb
Expand Up @@ -133,4 +133,8 @@ def reset!
def needs_server?
false
end

def supports_proxy_protocol?
false
end
end
112 changes: 112 additions & 0 deletions lib/capybara/proxy_server.rb
@@ -0,0 +1,112 @@
require 'webrick'
require 'webrick/httpproxy'
require 'webrick/https'
require 'rack/test'

module Capybara
class ProxyServer < WEBrick::HTTPProxyServer

class SSLHandler < WEBrick::HTTPServer
def initialize(app, config)
super(config.merge(:Port => 443, :DoNotListen => true))
@rack_handler = Rack::Handler::WEBrick.new(self, app)
end

def service(req, res)
if (host_handler = Capybara::ProxyServer.host_mapping[req.host])
Rack::Handler::WEBrick.new(self, host_handler).service(req, res)
else
@rack_handler.service(req, res)
end
rescue => err
$stderr.puts err
end
end

DEFAULT_404_HANDLER = Proc.new {|env| [404, {}, []] }

class << self
attr_accessor :host_mapping

##
#
# Configure Capybara ProxyServer to suit your needs.
#
# Capybara::ProxyServer.configure do |config|
# config.route "i.kissmetrics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER
# end
#
def configure
yield self
end

##
#
# Route a request to a specific host to a separate Rack application.
# This allows you to mock external services without actually doing external requests.
#
# You can use the Capybara::ProxyServer::DEFAULT_404_HANDLER to simply return a 404 error code, or create your own rack applications.
#
def route(host_map)
@host_mapping ||= {}
@host_mapping.merge!(host_map)
end

def run(app, config)
server = new(app, config.merge(:OutputBufferSize => 5))
server.start
server
end
end

def initialize(app, config)
super(config, WEBrick::Config::HTTP)
@rack_handler = Rack::Handler::WEBrick.new(self, app)
@ssl_context = generate_ssl_context
@ssl_server = SSLHandler.new(app, config)
end

def service(req, res)
if req.request_method == "CONNECT"
host, port = req.unparsed_uri.split(":", 2)
if port == '443'
ua = Thread.current[:WEBrickSocket]
res.status = WEBrick::HTTPStatus::RC_OK
res.send_response(ua)
req.parse(NullReader) rescue nil
ssl = OpenSSL::SSL::SSLSocket.new(ua, @ssl_context)
ssl.sync_close = true
ssl.accept
@ssl_server.run(ssl)
else
res.status = 501
end
elsif (host_handler = Capybara::ProxyServer.host_mapping[req.host])
Rack::Handler::WEBrick.new(self, host_handler).service(req, res)
else
@rack_handler.service(req, res)
end
rescue => err
$stderr.puts err
end

private

def generate_ssl_context
OpenSSL::SSL::SSLContext.new(:TLSv1_server).tap do |ssl_context|
self_signed_cert, self_signed_cert_key =
WEBrick::Utils.create_self_signed_cert(1024, [ [ "CN", "localhost" ] ], "Generated by Ruby/OpenSSL")
ssl_context.cert = self_signed_cert
ssl_context.key = self_signed_cert_key
end
end
end
end

Capybara::ProxyServer.configure do |config|
config.route "www.google-analytics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"ssl.google-analytics.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"fonts.googleapis.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"connect.facebook.net" => Capybara::ProxyServer::DEFAULT_404_HANDLER,
"platform.twitter.com" => Capybara::ProxyServer::DEFAULT_404_HANDLER
end
2 changes: 1 addition & 1 deletion lib/capybara/rspec/matchers.rb
@@ -1,7 +1,7 @@
module Capybara
module RSpecMatchers
class Matcher
include ::RSpec::Matchers::Composable if defined?(::RSpec::Version) && ::RSpec::Version::STRING.to_f >= 3.0
include ::RSpec::Matchers::Composable if defined?(::RSpec::Expectations::Version) && RSpec::Expectations::Version::STRING.to_f >= 3.0

def wrap(actual)
if actual.respond_to?("has_selector?")
Expand Down
8 changes: 7 additions & 1 deletion lib/capybara/selenium/driver.rb
Expand Up @@ -33,7 +33,6 @@ def initialize(app, options={})
raise e
end
end

@app = app
@browser = nil
@exit_status = nil
Expand Down Expand Up @@ -75,6 +74,13 @@ def find_css(selector)

def wait?; true; end
def needs_server?; true; end
def supports_proxy_protocol?; options[:browser] == :firefox; end

def setup_proxy_host(host, port)
proxy_server = "#{host}:#{port}"
options[:profile] ||= Selenium::WebDriver::Firefox::Profile.new
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selenium supports more than just Firefox - how will this work if the user is using selenium with chrome, ie, etc?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question, I suppose all browsers will somehow support setting up a proxy. If they don't, we can have the supports_proxy_protocol return false for now.

options[:profile].proxy = Selenium::WebDriver::Proxy.new(:http => proxy_server, :ssl => proxy_server)
end

def execute_script(script)
browser.execute_script script
Expand Down
11 changes: 9 additions & 2 deletions lib/capybara/server.rb
Expand Up @@ -33,8 +33,9 @@ def ports

attr_reader :app, :port, :host

def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
def initialize(app, driver, port=Capybara.server_port, host=Capybara.server_host)
@app = app
@driver = driver
@middleware = Middleware.new(@app)
@server_thread = nil # suppress warnings
@host, @port = host, port
Expand Down Expand Up @@ -65,9 +66,15 @@ def responsive?
def boot
unless responsive?
Capybara::Server.ports[@app.object_id] = @port
if Capybara.use_proxy_protocol && @driver.supports_proxy_protocol?
server_proc = Capybara.proxy_server
@driver.setup_proxy_host(@host, @port)
else
server_proc = Capybara.server
end

@server_thread = Thread.new do
Capybara.server.call(@middleware, @port)
server_proc.call(@middleware, @port)
end

Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
Expand Down
11 changes: 6 additions & 5 deletions lib/capybara/session.rb
Expand Up @@ -63,7 +63,7 @@ def initialize(mode, app=nil)
@mode = mode
@app = app
if Capybara.run_server and @app and driver.needs_server?
@server = Capybara::Server.new(@app).boot
@server = Capybara::Server.new(@app, driver).boot
else
@server = nil
end
Expand Down Expand Up @@ -199,22 +199,23 @@ def current_url
#
# Will actually navigate to `http://google.com:4567/test`.
#
# @param [String] url The URL to navigate to
# @param [#to_s] url The URL to navigate to. The parameter will be cast to a String.
#
def visit(url)
raise_server_error!

url = url.to_s
@touched = true

url_relative = URI.parse(url.to_s).scheme.nil?
url_relative = URI.parse(url).scheme.nil?

if url_relative && Capybara.app_host
url = Capybara.app_host + url.to_s
url = Capybara.app_host + url
url_relative = false
end

if @server
url = "http://#{@server.host}:#{@server.port}" + url.to_s if url_relative
url = "http://#{@server.host}:#{@server.port}" + url if url_relative

if Capybara.always_include_port
uri = URI.parse(url)
Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/spec/session/current_url_spec.rb
@@ -1,6 +1,6 @@
Capybara::SpecHelper.spec '#current_url, #current_path, #current_host' do
before :all do
@servers = 2.times.map { Capybara::Server.new(TestApp.clone).boot }
@servers = 2.times.map { Capybara::Server.new(TestApp.clone, nil).boot }
# sanity check
expect(@servers[0].port).not_to eq(@servers[1].port)
expect(@servers.map { |s| s.port }).not_to include 80
Expand Down
2 changes: 1 addition & 1 deletion lib/capybara/spec/session/visit_spec.rb
Expand Up @@ -22,7 +22,7 @@
@session.visit('/foo/bar')
root_uri = URI.parse(@session.current_url)

@session.visit("http://localhost:#{root_uri.port}")
@session.visit("http://#{root_uri.host}:#{root_uri.port}")
expect(@session).to have_content('Hello world!')
end

Expand Down
6 changes: 3 additions & 3 deletions lib/capybara/spec/session/window/become_closed_spec.rb
Expand Up @@ -8,7 +8,7 @@
end

after(:each) do
@session.document.synchronize(3, errors: [Capybara::CapybaraError]) do
@session.document.synchronize(5, errors: [Capybara::CapybaraError]) do
raise Capybara::CapybaraError if @session.windows.size != 1
end
@session.switch_to_window(@window)
Expand All @@ -20,7 +20,7 @@
@session.execute_script('setTimeout(function(){ window.close(); }, 500);')
end
Capybara.using_wait_time 0.1 do
expect(@other_window).to become_closed(wait: 0.7)
expect(@other_window).to become_closed(wait: 2)
end
end

Expand All @@ -41,7 +41,7 @@
@session.within_window @other_window do
@session.execute_script('setTimeout(function(){ window.close(); }, 500);')
end
Capybara.using_wait_time 0.7 do
Capybara.using_wait_time 1.5 do
expect(@other_window).to become_closed
end
end
Expand Down