require 'spec_helper' describe Arachni::Browser do before( :all ) do @url = Arachni::Utilities.normalize_url( web_server_url_for( :browser ) ) end before( :each ) do clear_hit_count @browser = described_class.new end after( :each ) do Arachni::Options.reset Arachni::Framework.reset @browser.shutdown clear_hit_count end let(:subject) { @browser } let(:ua) { Arachni::Options.http.user_agent } def transitions_from_array( transitions ) transitions.map do |t| element, event = t.first.to_a options = {} if element == :page && event == :load options.merge!( url: @browser.watir.url, cookies: {} ) end if element.is_a? Hash element = described_class::ElementLocator.new( element ) end Arachni::Page::DOM::Transition.new( element, event, options ).complete end end def hit_count Typhoeus::Request.get( "#{@url}/hit-count" ).body.to_i end def clear_hit_count Typhoeus::Request.get( "#{@url}/clear-hit-count" ) end it 'supports HTTPS' do url = web_server_url_for( :browser_https ) @browser.start_capture pages = @browser.load( url ).flush_pages pages_should_have_form_with_input( pages, 'ajax-token' ) pages_should_have_form_with_input( pages, 'by-ajax' ) end describe '.has_executable?' do context 'when there is no executable browser' do it 'returns false' do Selenium::WebDriver::PhantomJS.stub(:path){ false } described_class.has_executable?.should be_false end end context 'when there is an executable browser' do it 'returns true' do Selenium::WebDriver::PhantomJS.stub(:path){ __FILE__ } described_class.has_executable?.should be_true end end end describe '.executable' do it 'returns the path to the browser executable' do stub = __FILE__ Selenium::WebDriver::PhantomJS.stub(:path){ stub } described_class.executable.should == stub end end describe '#initialize' do describe :concurrency do it 'sets the HTTP request concurrency' end describe :ignore_scope do context true do it 'ignores scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: true ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' subject.to_page.should be_true end end context false do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' subject.to_page.should be_nil end end context :default do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) Arachni::Options.scope.exclude_path_patterns << /sleep/ subject.load @url + '/ajax_sleep' subject.to_page.should be_nil end end end describe :width do it 'sets the window width' do @browser.shutdown width = 100 @browser = described_class.new( width: width ) subject.javascript.run('return window.innerWidth').should == width end it 'defaults to 1600' do subject.javascript.run('return window.innerWidth').should == 1600 end end describe :height do it 'sets the window height' do @browser.shutdown height = 100 @browser = described_class.new( height: height ) subject.javascript.run('return window.innerHeight').should == height end it 'defaults to 1200' do subject.javascript.run('return window.innerHeight').should == 1200 end end describe :store_pages do describe 'default' do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new @browser.load( @url + '/explore' ).flush_pages.should be_any end it 'stores captured pages' do @browser.shutdown @browser = described_class.new @browser.start_capture @browser.load( @url + '/with-ajax' ).flush_pages.should be_any end end describe true do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: true ) @browser.load( @url + '/explore' ).trigger_events.flush_pages.should be_any end it 'stores captured pages' do @browser.shutdown @browser = described_class.new( store_pages: true ) @browser.start_capture @browser.load( @url + '/with-ajax' ).flush_pages.should be_any end end describe false do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: false ) @browser.load( @url + '/explore' ).trigger_events.flush_pages.should be_empty end it 'stores captured pages' do @browser.shutdown @browser = described_class.new( store_pages: false ) @browser.start_capture @browser.load( @url + '/with-ajax' ).flush_pages.should be_empty end end end context 'when browser process spawn fails' do it "raises #{described_class::Error::Spawn}" do described_class.any_instance.stub(:spawn_phantomjs) { nil } expect { described_class.new }.to raise_error described_class::Error::Spawn end end end describe '#source_with_line_numbers' do it 'prefixes each source code line with a number' do subject.load @url lines = subject.source.lines.to_a lines.should be_any subject.source_with_line_numbers.lines.each.with_index do |l, i| l.should == "#{i+1} - #{lines[i]}" end end end describe '#load_delay' do it 'returns nil' do subject.load @url subject.load_delay.should be_nil end context 'when the page has JS timeouts' do it 'returns the maximum time the browser should wait for the page based on Timeout' do subject.load( "#{@url}load_delay" ) subject.load_delay.should == 2000 end end end describe '#wait_for_timers' do it 'returns' do subject.load @url subject.wait_for_timers.should be_nil end context 'when the page has JS timeouts' do it 'waits for them to complete' do subject.load( "#{@url}load_delay" ) seconds = subject.load_delay / 1000 time = Time.now subject.wait_for_timers (Time.now - time).should > seconds end it "caps them at #{Arachni::OptionGroups::HTTP}#request_timeout" do subject.load( "#{@url}load_delay" ) Arachni::Options.http.request_timeout = 100 time = Time.now subject.wait_for_timers (Time.now - time).should < 0.2 end end end describe '#capture_snapshot' do let(:sink_url) do "#{@url}script_sink?input=#{@browser.javascript.log_execution_flow_sink_stub(1)}" end let(:ajax_url) do "#{@url}with-ajax" end let(:captured) { subject.capture_snapshot } context 'when a snapshot has not been previously seen' do before :each do subject.load( @url + '/with-ajax', take_snapshot: false ) end it 'calls #on_new_page callbacks' do received = [] subject.on_new_page do |page| received << page end captured.should == received end context '#store_pages?' do context true do subject { @browser.shutdown; @browser = described_class.new( store_pages: true )} it 'stores it in #page_snapshots' do captured = subject.capture_snapshot subject.page_snapshots.should == captured end it 'returns it' do captured.size.should == 1 captured.first.should == subject.to_page end end context false do subject { @browser.shutdown; @browser = described_class.new( store_pages: false ) } it 'does not store it' do subject.capture_snapshot subject.page_snapshots.should be_empty end it 'returns an empty array' do captured.should be_empty end end end end context 'when a snapshot has already been seen' do before :each do subject.load( @url + '/with-ajax', take_snapshot: false ) end it 'ignores it' do subject.capture_snapshot.should be_any subject.capture_snapshot.should be_empty end end context 'when a snapshot has sink data' do before :each do subject.load sink_url, take_snapshot: false end it 'calls #on_new_page_with_sink callbacks' do sinks = [] subject.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end subject.capture_snapshot sinks.size.should == 1 end context 'and has already been seen' do it 'calls #on_new_page_with_sink callbacks' do sinks = [] subject.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end subject.capture_snapshot subject.capture_snapshot sinks.size.should == 2 end end context '#store_pages?' do context true do subject { @browser.shutdown; @browser = described_class.new( store_pages: true )} it 'stores it in #page_snapshots_with_sinks' do subject.capture_snapshot subject.page_snapshots_with_sinks.should be_any end end context false do subject { @browser.shutdown; @browser = described_class.new( store_pages: false )} it 'does not store it in #page_snapshots_with_sinks' do subject.capture_snapshot subject.page_snapshots_with_sinks.should be_empty end end end end context 'when a transition has been given' do before :each do subject.load( ajax_url, take_snapshot: false ) end it 'pushes it to the existing transitions' do transition = { stuff: :here } captured = subject.capture_snapshot( stuff: :here ) captured.first.dom.transitions.should include transition end end context 'when there are multiple windows open' do before :each do subject.load( ajax_url, take_snapshot: false ) end it 'captures snapshots from all windows' do subject.javascript.run( 'window.open()' ) subject.watir.windows.last.use subject.load sink_url, take_snapshot: false subject.capture_snapshot.map(&:url).sort.should == [ajax_url, sink_url].sort end end context 'when an error occurs' do it 'ignores it' do subject.watir.stub(:windows) { raise } subject.capture_snapshot( blah: :stuff ).should be_empty end end end describe '#flush_page_snapshots_with_sinks' do it 'returns pages with data-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks).should == @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks) end it 'returns pages with execution-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks).should == @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks) end it 'empties the data-flow sink page buffer' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:data_flow_sinks) @browser.page_snapshots_with_sinks.should be_empty end it 'empties the execution-flow sink page buffer' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" @browser.explore_and_flush @browser.flush_page_snapshots_with_sinks.map(&:dom).map(&:execution_flow_sinks) @browser.page_snapshots_with_sinks.should be_empty end end describe '#on_new_page_with_sink' do it 'assigns blocks to handle each page with execution-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_execution_flow_sink_stub( function: { name: 'blah' } )}" sinks = [] @browser.on_new_page_with_sink do |page| sinks << page.dom.execution_flow_sinks end @browser.explore_and_flush sinks.size.should == 2 sinks.should == @browser.page_snapshots_with_sinks.map(&:dom). map(&:execution_flow_sinks) end it 'assigns blocks to handle each page with data-flow sink data' do @browser.load "#{@url}/lots_of_sinks?input=#{@browser.javascript.log_data_flow_sink_stub( function: { name: 'blah' } )}" sinks = [] @browser.on_new_page_with_sink do |page| sinks << page.dom.data_flow_sinks end @browser.explore_and_flush sinks.size.should == 2 sinks.should == @browser.page_snapshots_with_sinks.map(&:dom). map(&:data_flow_sinks) end end describe '#on_fire_event' do it 'gets called before each event is triggered' do @browser.load "#{@url}/trigger_events" calls = [] @browser.on_fire_event do |element, event| calls << [element.opening_tag, event] end @browser.fire_event @browser.watir.div( id: 'my-div' ), :click @browser.fire_event @browser.watir.div( id: 'my-div' ), :mouseover calls.should == [ [ "