spec/arachni/browser_spec.rb in arachni-1.3.2 vs spec/arachni/browser_spec.rb in arachni-1.4

- old
+ new

@@ -12,11 +12,12 @@ end after( :each ) do Arachni::Options.reset Arachni::Framework.reset - @browser.shutdown + @browser.shutdown if @browser + described_class.asset_domains.clear clear_hit_count end let(:subject) { @browser } let(:ua) { Arachni::Options.http.user_agent } @@ -25,11 +26,11 @@ transitions.map do |t| element, event = t.first.to_a options = {} if element == :page && event == :load - options.merge!( url: @browser.watir.url, cookies: {} ) + options.merge!( url: @browser.dom_url, cookies: {} ) end if element.is_a? Hash element = described_class::ElementLocator.new( element ) end @@ -58,10 +59,50 @@ pages_should_have_form_with_input( pages, 'ajax-token' ) pages_should_have_form_with_input( pages, 'by-ajax' ) end + context 'when the browser dies' do + it 'kills the lifeline too' do + Arachni::Processes::Manager.kill subject.browser_pid + expect(Arachni::Processes::Manager.alive?(subject.lifeline_pid)).to be_falsey + end + end + + context 'when the lifeline dies' do + it 'kills the browser too' do + Arachni::Processes::Manager.kill subject.lifeline_pid + expect(Arachni::Processes::Manager.alive?(subject.browser_pid)).to be_falsey + end + end + + describe '#alive?' do + context 'when the lifeline is alive' do + it 'returns true' do + expect(Arachni::Processes::Manager.alive?(subject.lifeline_pid)).to be_truthy + expect(subject).to be_alive + end + end + + context 'when the browser is dead' do + it 'returns false' do + Arachni::Processes::Manager.kill subject.browser_pid + + expect(subject).to_not be_alive + end + end + + context 'when the lifeline is dead' do + it 'returns false' do + Arachni::Processes::Manager << subject.browser_pid + Arachni::Processes::Manager.kill subject.lifeline_pid + + expect(subject).to_not be_alive + end + end + end + describe '.has_executable?' do context 'when there is no executable browser' do it 'returns false' do allow(Selenium::WebDriver::PhantomJS).to receive(:path){ false } expect(described_class.has_executable?).to be_falsey @@ -83,16 +124,16 @@ expect(described_class.executable).to eq(stub) end end describe '#initialize' do - describe :concurrency do + describe ':concurrency' do it 'sets the HTTP request concurrency' end - describe :ignore_scope do - context true do + describe ':ignore_scope' do + context 'true' do it 'ignores scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: true ) @@ -101,11 +142,11 @@ subject.load @url + '/ajax_sleep' expect(subject.to_page).to be_truthy end end - context false do + context 'false' do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) @@ -114,11 +155,11 @@ subject.load @url + '/ajax_sleep' expect(subject.to_page.code).to eq(0) end end - context :default do + context ':default' do it 'enforces scope restrictions' do @browser.shutdown @browser = described_class.new( ignore_scope: false ) @@ -128,39 +169,49 @@ expect(subject.to_page.code).to eq(0) end end end - describe :width do + describe ':width' do it 'sets the window width' do @browser.shutdown width = 100 @browser = described_class.new( width: width ) + + subject.load @url + expect(subject.javascript.run('return window.innerWidth')).to eq(width) end it 'defaults to 1600' do + subject.load @url + expect(subject.javascript.run('return window.innerWidth')).to eq(1600) end end - describe :height do + describe ':height' do it 'sets the window height' do @browser.shutdown height = 100 @browser = described_class.new( height: height ) + + subject.load @url + expect(subject.javascript.run('return window.innerHeight')).to eq(height) end it 'defaults to 1200' do + subject.load @url + expect(subject.javascript.run('return window.innerHeight')).to eq(1200) end end - describe :store_pages do + describe ':store_pages' do describe 'default' do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new expect(@browser.load( @url + '/explore' ).flush_pages).to be_any @@ -172,11 +223,11 @@ @browser.start_capture expect(@browser.load( @url + '/with-ajax' ).flush_pages).to be_any end end - describe true do + describe 'true' do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: true ) expect(@browser.load( @url + '/explore' ).trigger_events.flush_pages).to be_any end @@ -187,11 +238,11 @@ @browser.start_capture expect(@browser.load( @url + '/with-ajax' ).flush_pages).to be_any end end - describe false do + describe 'false' do it 'stores snapshot pages' do @browser.shutdown @browser = described_class.new( store_pages: false ) expect(@browser.load( @url + '/explore' ).trigger_events.flush_pages).to be_empty end @@ -290,11 +341,11 @@ expect(captured).to eq(received) end context '#store_pages?' do - context true 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 @@ -305,11 +356,11 @@ expect(captured.size).to eq(1) expect(captured.first).to eq(subject.to_page) end end - context false do + context 'false' do subject { @browser.shutdown; @browser = described_class.new( store_pages: false ) } it 'does not store it' do subject.capture_snapshot @@ -363,20 +414,20 @@ expect(sinks.size).to eq(2) end end context '#store_pages?' do - context true 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 expect(subject.page_snapshots_with_sinks).to be_any end end - context false do + 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 expect(subject.page_snapshots_with_sinks).to be_empty @@ -397,28 +448,25 @@ expect(captured.first.dom.transitions).to 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 + url = "#{@url}open-new-window" + subject.load url, take_snapshot: false + expect(subject.capture_snapshot.map(&:url).sort).to eq( - [ajax_url, sink_url].sort + [url, "#{@url}with-ajax"].sort ) end end context 'when an error occurs' do it 'ignores it' do - allow(subject.watir).to receive(:windows) { raise } + allow(subject).to receive(:to_page) { raise } expect(subject.capture_snapshot( blah: :stuff )).to be_empty end end end @@ -494,12 +542,12 @@ 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 + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :mouseover expect(calls).to eq([ [ "<div id=\"my-div\" onclick=\"addForm();\">", :click ], [ "<div id=\"my-div\" onclick=\"addForm();\">", :mouseover ] ]) @@ -588,11 +636,11 @@ { { tag_name: 'a', attributes: { 'onmouseover' => 'writeButton();', - 'href' => 'javascript:level3();' + 'href' => '#' } } => :mouseover } ], [ @@ -601,15 +649,15 @@ { "#{@url}level2" => :request }, { { tag_name: 'a', attributes: { - 'onmouseover' => 'writeButton();', 'href' => 'javascript:level3();' } } => :click }, + { "#{@url}level4" => :request }, { "#{@url}level4" => :request } ], [ { :page => :load }, { "#{@url}deep-dom" => :request }, @@ -617,11 +665,11 @@ { { tag_name: 'a', attributes: { 'onmouseover' => 'writeButton();', - 'href' => 'javascript:level3();' + 'href' => '#' } } => :mouseover }, { { @@ -638,16 +686,16 @@ { "#{@url}level2" => :request }, { { tag_name: 'a', attributes: { - 'onmouseover' => 'writeButton();', 'href' => 'javascript:level3();' } } => :click }, { "#{@url}level4" => :request }, + { "#{@url}level4" => :request }, { { tag_name: 'div', attributes: { 'onclick' => 'level6();', @@ -678,11 +726,11 @@ { { tag_name: 'a', attributes: { 'onmouseover' => 'writeButton();', - 'href' => 'javascript:level3();' + 'href' => '#' } } => :mouseover } ], [ @@ -691,15 +739,15 @@ { "#{@url}level2" => :request }, { { tag_name: 'a', attributes: { - 'onmouseover' => 'writeButton();', 'href' => 'javascript:level3();' } } => :click }, + { "#{@url}level4" => :request }, { "#{@url}level4" => :request } ] ].map { |transitions| transitions_from_array( transitions ) }) end end @@ -989,19 +1037,66 @@ context "when the response takes more than #{Arachni::OptionGroups::HTTP}#request_timeout" do it 'returns nil' end - context 'when the resource is out-of-scope' do + context 'when the resource is out of scope' do it 'returns nil' do Arachni::Options.url = @url - @browser.load 'http://google.com/' + @browser.load @url + + subject.javascript.run( 'window.location = "http://google.com/";' ) + sleep 1 + expect(@browser.response).to be_nil end end end + describe '#state' do + it 'returns a Page::DOM with enough info to reproduce the current state' do + @browser.load "#{web_server_url_for( :taint_tracer )}/debug" << + "?input=#{@browser.javascript.log_execution_flow_sink_stub(1)}" + + dom = subject.to_page.dom + state = subject.state + + expect(state.page).to be_nil + expect(state.url).to eq dom.url + expect(state.digest).to eq dom.digest + expect(state.transitions).to eq dom.transitions + expect(state.skip_states).to eq dom.skip_states + expect(state.data_flow_sinks).to be_empty + expect(state.execution_flow_sinks).to be_empty + end + + context 'when the URL is about:blank' do + it 'returns nil' do + Arachni::Options.url = @url + subject.load @url + + subject.javascript.run( 'window.location = "about:blank";' ) + sleep 1 + + expect(subject.state).to be_nil + end + end + + context 'when the resource is out-of-scope' do + it 'returns an empty page' do + Arachni::Options.url = @url + subject.load @url + + subject.javascript.run( 'window.location = "http://google.com/";' ) + sleep 1 + + expect(subject.state).to be_nil + end + end + + end + describe '#to_page' do it "converts the working window to an #{Arachni::Page}" do ua = Arachni::Options.http.user_agent @browser.load( @url ) @@ -1015,14 +1110,16 @@ end it "assigns the proper #{Arachni::Page::DOM}#digest" do @browser.load( @url ) expect(@browser.to_page.dom.instance_variable_get(:@digest)).to eq( - '<HTML><HEAD><SCRIPT src=http://javascript.browser.arachni/' << - 'taint_tracer.js><SCRIPT src=http://javascript.' << - 'browser.arachni/dom_monitor.js><SCRIPT><TITLE><BODY><' << - 'DIV><SCRIPT type=text/javascript><SCRIPT type=text/javascript>' + '<HTML><HEAD><SCRIPT src=http://' << + 'javascript.browser.arachni/polyfills.js><SCRIPT src=http://' << + 'javascript.browser.arachni/' << + 'taint_tracer.js><SCRIPT src=http://javascript.' << + 'browser.arachni/dom_monitor.js><SCRIPT><TITLE><BODY><' << + 'DIV><SCRIPT type=text/javascript><SCRIPT type=text/javascript>' ) end it "assigns the proper #{Arachni::Page::DOM}#transitions" do @browser.load( @url ) @@ -1076,11 +1173,11 @@ end context 'when the page has' do context "#{Arachni::Element::UIForm} elements" do context "and #{Arachni::OptionGroups::Audit}#inputs is" do - context true do + context 'true' do before do Arachni::Options.audit.elements :ui_forms end context '<input> button' do @@ -1124,11 +1221,11 @@ end end end end - context false do + context 'false' do before do Arachni::Options.audit.skip_elements :ui_forms end it 'ignores them' do @@ -1139,11 +1236,11 @@ end end context "#{Arachni::Element::UIInput} elements" do context "and #{Arachni::OptionGroups::Audit}#inputs is" do - context true do + context 'true' do before do Arachni::Options.audit.elements :ui_inputs end context '<input>' do @@ -1187,11 +1284,11 @@ end end end end - context false do + context 'false' do before do Arachni::Options.audit.skip_elements :ui_inputs end it 'ignores them' do @@ -1202,11 +1299,11 @@ end end context "#{Arachni::Element::Form::DOM} elements" do context "and #{Arachni::OptionGroups::Audit}#forms is" do - context true do + context 'true' do before do Arachni::Options.audit.elements :forms end context 'and JavaScript action' do @@ -1229,11 +1326,11 @@ expect(@browser.to_page.forms.first.skip_dom).to be_truthy end end end - context false do + context 'false' do before do Arachni::Options.audit.skip_elements :forms end it 'does not set #skip_dom' do @@ -1246,11 +1343,11 @@ context "#{Arachni::Element::Cookie::DOM} elements" do let(:cookies) { @browser.to_page.cookies } context "and #{Arachni::OptionGroups::Audit}#cookies is" do - context true do + context 'true' do before do Arachni::Options.audit.elements :cookies @browser.load "#{@url}/#{page}" @browser.load "#{@url}/#{page}" @@ -1259,45 +1356,61 @@ context 'with DOM processing of cookie' do context 'names' do let(:page) { 'dom-cookies-names' } it 'does not set #skip_dom' do - expect(cookies.find { |c| c.name == 'my-cookie' }.skip_dom).to be_nil - expect(cookies.find { |c| c.name == 'my-cookie2' }.skip_dom).to be_nil + expect(cookies.find { |c| c.name == 'js_cookie1' }.skip_dom).to be_nil + expect(cookies.find { |c| c.name == 'js_cookie2' }.skip_dom).to be_nil end + + it 'does not track HTTP-only cookies' do + expect(cookies.find { |c| c.name == 'http_only_cookie' }.skip_dom).to be true + end + + it 'does not track cookies for other paths' do + expect(cookies.find { |c| c.name == 'other_path' }.skip_dom).to be true + end end context 'values' do let(:page) { 'dom-cookies-values' } it 'does not set #skip_dom' do - expect(cookies.find { |c| c.name == 'my-cookie' }.skip_dom).to be_nil - expect(cookies.find { |c| c.name == 'my-cookie2' }.skip_dom).to be_nil + expect(cookies.find { |c| c.name == 'js_cookie1' }.skip_dom).to be_nil + expect(cookies.find { |c| c.name == 'js_cookie2' }.skip_dom).to be_nil end + + it 'does not track HTTP-only cookies' do + expect(cookies.find { |c| c.name == 'http_only_cookie' }.skip_dom).to be true + end + + it 'does not track cookies for other paths' do + expect(cookies.find { |c| c.name == 'other_path' }.skip_dom).to be true + end end end context 'without DOM processing of cookie' do context 'names' do let(:page) { 'dom-cookies-names' } it 'does not set #skip_dom' do - expect(cookies.find { |c| c.name == 'my-cookie3' }.skip_dom).to be_truthy + expect(cookies.find { |c| c.name == 'js_cookie3' }.skip_dom).to be_truthy end end context 'values' do let(:page) { 'dom-cookies-values' } it 'does not set #skip_dom' do - expect(cookies.find { |c| c.name == 'my-cookie3' }.skip_dom).to be_truthy + expect(cookies.find { |c| c.name == 'js_cookie3' }.skip_dom).to be_truthy end end end end - context false do + context 'false' do before do Arachni::Options.audit.skip_elements :cookies @browser.load "#{@url}/#{page}" @browser.load "#{@url}/#{page}" @@ -1317,17 +1430,21 @@ end context 'when the resource is out-of-scope' do it 'returns an empty page' do Arachni::Options.url = @url - subject.load 'http://google.com/' + subject.load @url + + subject.javascript.run( 'window.location = "http://google.com/";' ) + sleep 1 + page = subject.to_page expect(page.code).to eq(0) - expect(page.url).to eq(subject.url) + expect(page.url).to eq('http://google.com/') expect(page.body).to be_empty - expect(page.dom.url).to eq(subject.watir.url) + expect(page.dom.url).to eq('http://google.com/') end end end describe '#fire_event' do @@ -1335,41 +1452,80 @@ before(:each) do @browser.load url end it 'fires the given event' do - @browser.fire_event @browser.watir.div( id: 'my-div' ), :click + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click pages_should_have_form_with_input [@browser.to_page], 'by-ajax' end it 'accepts events without the "on" prefix' do pages_should_not_have_form_with_input [@browser.to_page], 'by-ajax' - @browser.fire_event @browser.watir.div( id: 'my-div' ), :onclick + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :onclick pages_should_have_form_with_input [@browser.to_page], 'by-ajax' - @browser.fire_event @browser.watir.div( id: 'my-div' ), :click + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click pages_should_have_form_with_input [@browser.to_page], 'by-ajax' end it 'returns a playable transition' do - transition = @browser.fire_event @browser.watir.div( id: 'my-div' ), :click + transition = @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click pages_should_have_form_with_input [@browser.to_page], 'by-ajax' @browser.load( url ).start_capture pages_should_not_have_form_with_input [@browser.to_page], 'by-ajax' transition.play @browser pages_should_have_form_with_input [@browser.to_page], 'by-ajax' end - context 'when the element is not visible' do - it 'returns nil' do - element = @browser.watir.div( id: 'my-div' ) + context 'when new timers are introduced' do + let(:url) { "#{@url}/trigger_events/with_new_timers/3000" } - allow(element).to receive(:visible?) { false } + it 'waits for them' do + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click + pages_should_have_form_with_input [@browser.to_page], 'by-ajax' + end + context 'when a new timer exceeds Options.http.request_timeout' do + let(:url) { "#{@url}/trigger_events/with_new_timers/#{Arachni::Options.http.request_timeout + 5000}" } + + it 'waits for Options.http.request_timeout' do + t = Time.now + + @browser.fire_event @browser.selenium.find_element( id: 'my-div' ), :click + pages_should_not_have_form_with_input [@browser.to_page], 'by-ajax' + + expect(Time.now - t).to be <= Arachni::Options.http.request_timeout + end + end + end + + context 'when cookies are set' do + let(:url) { @url + '/each_element_with_events/set-cookie' } + + it 'sets them globally' do + expect(Arachni::HTTP::Client.cookies).to be_empty + + @browser.fire_event described_class::ElementLocator.new( + tag_name: :button, + attributes: { + onclick: 'setCookie()' + } + ), :click + + cookie = Arachni::HTTP::Client.cookies.first + expect(cookie.name).to eq 'cookie_name' + expect(cookie.value).to eq 'cookie value' + end + end + + context 'when the element is not visible' do + it 'returns nil' do + @browser.goto "#{url}/invisible-div" + element = @browser.selenium.find_element( id: 'invisible-div' ) expect(@browser.fire_event( element, :click )).to be_nil end end context "when the element is an #{described_class::ElementLocator}" do @@ -1380,61 +1536,49 @@ attributes: { 'id' => 'blahblah' } ) allow(element).to receive(:locate){ raise Selenium::WebDriver::Error::WebDriverError } expect(@browser.fire_event( element, :click )).to be_nil - - allow(element).to receive(:locate){ raise Watir::Exception::Error } - expect(@browser.fire_event( element, :click )).to be_nil end end end - context 'when the element never appears' do - it 'returns nil' do - element = @browser.watir.div( id: 'my-div' ) - - allow(element).to receive(:exists?) { false } - - expect(@browser.fire_event( element, :click )).to be_nil - end - end - context 'when the trigger fails with' do - let(:element) { @browser.watir.div( id: 'my-div' ) } + let(:element) { @browser.selenium.find_element( id: 'my-div' ) } - context Selenium::WebDriver::Error::WebDriverError do + context 'Selenium::WebDriver::Error::WebDriverError' do it 'returns nil' do - allow(element).to receive(:fire_event){ raise Selenium::WebDriver::Error::WebDriverError } - expect(@browser.fire_event( element, :click )).to be_nil - end - end + allow(@browser).to receive(:wait_for_pending_requests) do + raise Selenium::WebDriver::Error::WebDriverError + end - context Watir::Exception::Error do - it 'returns nil' do - allow(element).to receive(:fire_event){ raise Watir::Exception::Error } expect(@browser.fire_event( element, :click )).to be_nil end end end context 'form' do - context :submit do + context ':submit' do let(:url) { "#{@url}/fire_event/form/onsubmit" } context 'when option' do - describe :inputs do + describe ':inputs' do + + def element + @browser.selenium.find_element(:tag_name, :form) + end + context 'is given' do let(:inputs) do { - name: "The Dude", + name: 'The Dude', email: 'the.dude@abides.com' } end before(:each) do - @browser.fire_event @browser.watir.form, :submit, inputs: inputs + @browser.fire_event element, :submit, inputs: inputs end it 'fills in its inputs with the given values' do expect(@browser.watir.div( id: 'container-name' ).text).to eq( inputs[:name] @@ -1445,11 +1589,11 @@ end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.form, :submit, inputs: inputs + transition = @browser.fire_event element, :submit, inputs: inputs @browser.load url expect(@browser.watir.div( id: 'container-name' ).text).to be_empty expect(@browser.watir.div( id: 'container-email' ).text).to be_empty @@ -1511,11 +1655,11 @@ expect(@browser.watir.div( id: 'container-email' ).text).to be_empty end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.form, :submit, inputs: inputs + transition = @browser.fire_event element, :submit, inputs: inputs @browser.load url expect(@browser.watir.div( id: 'container-name' ).text).to be_empty expect(@browser.watir.div( id: 'container-email' ).text).to be_empty @@ -1539,11 +1683,11 @@ expect(@browser.watir.div( id: 'container-email' ).text).to be_empty end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.form, :submit, inputs: inputs + transition = @browser.fire_event element, :submit, inputs: inputs @browser.load url expect(@browser.watir.div( id: 'container-name' ).text).to be_empty expect(@browser.watir.div( id: 'container-email' ).text).to be_empty @@ -1568,11 +1712,11 @@ end context 'is not given' do it 'fills in its inputs with sample values' do @browser.load url - @browser.fire_event @browser.watir.form, :submit + @browser.fire_event element, :submit expect(@browser.watir.div( id: 'container-name' ).text).to eq( Arachni::Options.input.value_for_name( 'name' ) ) expect(@browser.watir.div( id: 'container-email' ).text).to eq( @@ -1580,11 +1724,11 @@ ) end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.form, :submit + transition = @browser.fire_event element, :submit @browser.load url expect(@browser.watir.div( id: 'container-name' ).text).to be_empty expect(@browser.watir.div( id: 'container-email' ).text).to be_empty @@ -1601,11 +1745,11 @@ context 'and has disabled inputs' do let(:url) { "#{@url}/fire_event/form/disabled_inputs" } it 'is skips those inputs' do - @browser.fire_event @browser.watir.form, :submit + @browser.fire_event element, :submit expect(@browser.watir.div( id: 'container-name' ).text).to eq( Arachni::Options.input.value_for_name( 'name' ) ) expect(@browser.watir.div( id: 'container-email' ).text).to be_empty @@ -1615,25 +1759,29 @@ end end end context 'image button' do - context :click do + context ':click' do before( :each ) { @browser.start_capture } let(:url) { "#{@url}fire_event/form/image-input" } + def element + @browser.selenium.find_element( :xpath, '//input[@type="image"]') + end + it 'submits the form with x, y coordinates' do @browser.load( url ) - @browser.fire_event @browser.watir.input( type: 'image'), :click + @browser.fire_event element, :click pages_should_have_form_with_input @browser.captured_pages, 'myImageButton.x' pages_should_have_form_with_input @browser.captured_pages, 'myImageButton.y' end it 'returns a playable transition' do @browser.load( url ) - transition = @browser.fire_event @browser.watir.input( type: 'image'), :click + transition = @browser.fire_event element, :click captured_pages = @browser.flush_pages pages_should_have_form_with_input captured_pages, 'myImageButton.x' pages_should_have_form_with_input captured_pages, 'myImageButton.y' @browser.shutdown @@ -1656,33 +1804,37 @@ calculate_expectation = proc do |string| [:onkeypress, :onkeydown].include?( event ) ? string[0...-1] : string end - context event do + context event.to_s do let( :url ) { "#{@url}/fire_event/input/#{event}" } context 'when option' do - describe :inputs do + describe ':inputs' do + def element + @browser.selenium.find_element(:tag_name, :input) + end + context 'is given' do let(:value) do 'The Dude' end before(:each) do - @browser.fire_event @browser.watir.input, event, value: value + @browser.fire_event element, event, value: value end it 'fills in its inputs with the given values' do expect(@browser.watir.div( id: 'container' ).text).to eq( calculate_expectation.call( value ) ) end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.input, event, value: value + transition = @browser.fire_event element, event, value: value @browser.load url expect(@browser.watir.div( id: 'container' ).text).to be_empty transition.play @browser @@ -1700,11 +1852,11 @@ expect(@browser.watir.div( id: 'container' ).text).to be_empty end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.input, event, value: value + transition = @browser.fire_event element, event, value: value @browser.load url expect(@browser.watir.div( id: 'container' ).text).to be_empty transition.play @browser @@ -1713,20 +1865,20 @@ end end context 'is not given' do it 'fills in a sample value' do - @browser.fire_event @browser.watir.input, event + @browser.fire_event element, event expect(@browser.watir.div( id: 'container' ).text).to eq( calculate_expectation.call( Arachni::Options.input.value_for_name( 'name' ) ) ) end it 'returns a playable transition' do @browser.load url - transition = @browser.fire_event @browser.watir.input, event + transition = @browser.fire_event element, event @browser.load url expect(@browser.watir.div( id: 'container' ).text).to be_empty transition.play @browser @@ -1740,10 +1892,48 @@ end end end end + describe '#elements_with_events' do + before :each do + @browser.load url + end + + let(:elements_with_events) do + elements_with_events = {} + @browser.each_element_with_events do |locator, events| + elements_with_events[locator] = events + end + elements_with_events + end + + let(:url) { @url + '/trigger_events' } + + it 'returns all elements with associated events' do + expect(subject.elements_with_events.to_s).to eq elements_with_events.to_s + end + + it 'caches results' do + expect(subject).to receive(:each_element_with_events) + subject.elements_with_events + + expect(subject).to_not receive(:each_element_with_events) + subject.elements_with_events + end + + context 'when passed true' do + it 'clears the cache' do + expect(subject).to receive(:each_element_with_events) + subject.elements_with_events + + expect(subject).to receive(:each_element_with_events) + subject.elements_with_events( true ) + end + end + end + describe '#each_element_with_events' do before :each do @browser.load url end let(:elements_with_events) do @@ -1760,23 +1950,23 @@ [ described_class::ElementLocator.new( tag_name: 'body', attributes: { 'onmouseover' => 'makePOST();' } ), - [[:onmouseover, 'makePOST();']] + { onmouseover: ['makePOST();'] } ], [ described_class::ElementLocator.new( tag_name: 'div', attributes: { 'id' => 'my-div', 'onclick' => 'addForm();' } ), - [[:onclick, 'addForm();']] + { onclick: ['addForm();']} ] ]) end - context :a do + context ':a' do context 'and the href is not empty' do context 'and it starts with javascript:' do let(:url) { @url + '/each_element_with_events/a/href/javascript' } it 'includes the :click event' do @@ -1784,11 +1974,11 @@ [ described_class::ElementLocator.new( tag_name: 'a', attributes: { 'href' => 'javascript:doStuff()' } ), - [[:click, 'javascript:doStuff()']] + {click: [ 'javascript:doStuff()']} ] ]) end end @@ -1808,12 +1998,12 @@ end end end end - context :form do - context :input do + context ':form' do + context ':input' do context 'of type "image"' do let(:url) { @url + '/each_element_with_events/form/input/image' } it 'includes the :click event' do expect(elements_with_events).to eq([ @@ -1824,11 +2014,11 @@ 'type' => 'image', 'name' => 'myImageButton', 'src' => '/__sinatra__/404.png' } ), - [[:click, 'image']] + {click: ['image']} ] ]) end end end @@ -1844,28 +2034,28 @@ tag_name: 'form', attributes: { 'action' => 'javascript:doStuff()' } ), - [[:submit, 'javascript:doStuff()']] + {submit: ['javascript:doStuff()']} ] ]) end end context 'and it does not start with javascript:' do let(:url) { @url + '/each_element_with_events/form/action/regular' } - it 'is ignored'do + it 'is ignored' do expect(elements_with_events).to be_empty end end context 'and is out of scope' do let(:url) { @url + '/each_element_with_events/form/action/out-of-scope' } - it 'is ignored'do + it 'is ignored' do expect(elements_with_events).to be_empty end end end end @@ -1874,11 +2064,11 @@ describe '#trigger_event' do it 'triggers the given event on the given tag and captures snapshots' do @browser.load( @url + '/trigger_events' ).start_capture locators = [] - @browser.watir.elements.each do |element| + @browser.selenium.find_elements(:css, '*').each do |element| begin locators << described_class::ElementLocator.from_html( element.opening_tag ) rescue end end @@ -1897,10 +2087,14 @@ pages_should_have_form_with_input @browser.captured_pages, 'ajax-token' end end describe '#trigger_events' do + it 'returns self' do + expect(@browser.load( @url + '/explore' ).trigger_events).to eq(@browser) + end + it 'waits for AJAX requests to complete' do @browser.load( @url + '/trigger_events-wait-for-ajax' ).start_capture.trigger_events pages_should_have_form_with_input @browser.captured_pages, 'ajax-token' pages_should_have_form_with_input @browser.page_snapshots, 'by-ajax' @@ -1914,10 +2108,11 @@ pages_should_have_form_with_input @browser.captured_pages, 'post-name' end it 'assigns the proper page transitions' do pages = @browser.load( @url + '/explore' ).trigger_events.page_snapshots + expect(pages.map(&:dom).map(&:transitions)).to eq([ [ { :page => :load }, { "#{@url}explore" => :request } ], @@ -1931,11 +2126,12 @@ 'id' => 'my-div', 'onclick' => 'addForm();' } } => :click }, - { "#{@url}get-ajax?ajax-token=my-token" => :request } + { "#{@url}get-ajax?ajax-token=my-token" => :request }, + { "#{@url}post-ajax" => :request } ], [ { :page => :load }, { "#{@url}explore" => :request }, { @@ -1945,10 +2141,12 @@ 'href' => 'javascript:inHref();' } } => :click }, { "#{@url}href-ajax" => :request }, + { "#{@url}post-ajax" => :request }, + { "#{@url}href-ajax" => :request } ] ].map { |transitions| transitions_from_array( transitions ) }) end it 'follows all javascript links' do @@ -1973,14 +2171,10 @@ @browser.load( "#{@url}form-with-image-button" ).start_capture.trigger_events pages_should_have_form_with_input @browser.captured_pages, 'myImageButton.x' pages_should_have_form_with_input @browser.captured_pages, 'myImageButton.y' end end - - it 'returns self' do - expect(@browser.load( @url + '/explore' ).trigger_events).to eq(@browser) - end end describe '#source' do it 'returns the evaluated HTML source' do @browser.load @url @@ -2188,11 +2382,11 @@ expect(@browser.watir.element( css: '#matchThis' ).tag_name).to eq('button') end it "waits a maximum of #{Arachni::OptionGroups::BrowserCluster}#job_timeout" do - Arachni::Options.browser_cluster.job_timeout = 4 + Arachni::Options.browser_cluster.job_timeout = 2 t = Time.now @browser.goto( @url + '/wait_for_elements#stuff/here' ) expect(Time.now - t).to be < 5 @@ -2214,11 +2408,11 @@ end end end context "#{Arachni::OptionGroups::BrowserCluster}#ignore_images" do - context true do + context 'true' do it 'does not load images' do Arachni::Options.browser_cluster.ignore_images = true @browser.shutdown @browser = described_class.new( disk_cache: false ) @@ -2226,11 +2420,11 @@ expect(image_hit_count).to eq(0) end end - context false do + context 'false' do it 'loads images' do Arachni::Options.browser_cluster.ignore_images = false @browser.shutdown @browser = described_class.new( disk_cache: false ) @@ -2264,45 +2458,35 @@ end context "with #{Arachni::OptionGroups::Scope}#auto_redundant_paths has bee configured" do it 'respects scope restrictions' do Arachni::Options.scope.auto_redundant_paths = 0 - expect(@browser.load( @url + '/explore?test=1&test2=2' ).response.code).to eq(0) + expect(@browser.load( @url + '/explore?test=1&test2=2' ).response).to be_nil end end - describe :cookies do + describe ':cookies' do it 'loads the given cookies' do cookie = { 'myname' => 'myvalue' } @browser.goto @url, cookies: cookie - expect(@browser.cookies.find { |c| c.name == cookie.keys.first }.inputs).to eq(cookie) + cookie_data = @browser.cookies. + find { |c| c.name == cookie.keys.first }.inputs + + expect(cookie_data).to eq(cookie) end it 'includes them in the transition' do cookie = { 'myname' => 'myvalue' } transition = @browser.goto( @url, cookies: cookie ) expect(transition.options[:cookies]).to eq(cookie) end - - context 'when auditing existing cookies' do - it 'preserves the HttpOnly attribute' do - @browser.goto( @url ) - expect(@browser.cookies.size).to eq(1) - - cookies = { @browser.cookies.first.name => 'updated' } - @browser.goto( @url, cookies: cookies ) - - @browser.cookies.first.value == 'updated' - expect(@browser.cookies.first).to be_http_only - end - end end - describe :take_snapshot do - describe true do + describe ':take_snapshot' do + describe 'true' do it 'captures a snapshot of the loaded page' do @browser.goto @url, take_snapshot: true pages = @browser.page_snapshots expect(pages.size).to eq(1) @@ -2311,11 +2495,11 @@ { @url => :request } ])) end end - describe false do + describe 'false' do it 'does not capture a snapshot of the loaded page' do @browser.goto @url, take_snapshot: false expect(@browser.page_snapshots).to be_empty end end @@ -2332,19 +2516,19 @@ ])) end end end - describe :update_transitions do - describe true do + describe ':update_transitions' do + describe 'true' do it 'pushes the page load to the transitions' do t = @browser.goto( @url, update_transitions: true ) expect(@browser.to_page.dom.transitions).to include t end end - describe false do + describe 'false' do it 'does not push the page load to the transitions' do t = @browser.goto( @url, update_transitions: false ) expect(@browser.to_page.dom.transitions).to be_empty end end @@ -2361,21 +2545,30 @@ describe '#load' do it 'returns self' do expect(@browser.load( @url )).to eq(@browser) end - describe :cookies do + it 'updates the global cookie-jar' do + @browser.load @url + + cookie = Arachni::HTTP::Client.cookies.find(&:http_only?) + + expect(cookie.name).to eq('This name should be updated; and properly escaped') + expect(cookie.value).to eq('This value should be updated; and properly escaped') + end + + describe ':cookies' do it 'loads the given cookies' do cookie = { 'myname' => 'myvalue' } @browser.load @url, cookies: cookie expect(@browser.cookies.find { |c| c.name == cookie.keys.first }.inputs).to eq(cookie) end end - describe :take_snapshot do - describe true do + describe ':take_snapshot' do + describe 'true' do it 'captures a snapshot of the loaded page' do @browser.load @url, take_snapshot: true pages = @browser.page_snapshots expect(pages.size).to eq(1) @@ -2384,11 +2577,11 @@ { @url => :request } ])) end end - describe false do + describe 'false' do it 'does not capture a snapshot of the loaded page' do @browser.load @url, take_snapshot: false expect(@browser.page_snapshots).to be_empty end end @@ -2406,11 +2599,11 @@ end end end context 'when given a' do - describe String do + describe 'String' do it 'treats it as a URL' do expect(hit_count).to eq(0) @browser.load @url expect(@browser.source).to include( ua ) @@ -2418,11 +2611,11 @@ expect(hit_count).to eq(1) end end - describe Arachni::HTTP::Response do + describe 'Arachni::HTTP::Response' do it 'loads it' do expect(hit_count).to eq(0) @browser.load Arachni::HTTP::Client.get( @url, mode: :sync ) expect(@browser.source).to include( ua ) @@ -2430,38 +2623,93 @@ expect(hit_count).to eq(1) end end - describe Arachni::Page do + describe 'Arachni::Page::DOM' do it 'loads it' do expect(hit_count).to eq(0) - @browser.load Arachni::HTTP::Client.get( @url, mode: :sync ).to_page + page = Arachni::HTTP::Client.get( @url, mode: :sync ).to_page + + expect(hit_count).to eq(1) + + @browser.load page.dom + expect(@browser.source).to include( ua ) expect(@browser.preloads).not_to include( @url ) + expect(hit_count).to eq(2) + end + + it 'replays its #transitions' do + @browser.load "#{@url}play-transitions" + page = @browser.explore_and_flush.last + expect(page.body).to include ua + + @browser.load page.dom + expect(@browser.source).to include ua + + page.dom.transitions.clear + @browser.load page.dom + expect(@browser.source).not_to include ua + end + + it 'loads its #skip_states' do + @browser.load( @url ) + pages = @browser.load( @url + '/explore' ).trigger_events. + page_snapshots + + page = pages.last + expect(page.dom.skip_states).to be_subset @browser.skip_states + + token = @browser.generate_token + + dpage = page.dup + dpage.dom.skip_states << token + + @browser.load dpage.dom + expect(@browser.skip_states).to include token + end + end + + describe 'Arachni::Page' do + it 'loads it' do + expect(hit_count).to eq(0) + + page = Arachni::HTTP::Client.get( @url, mode: :sync ).to_page + expect(hit_count).to eq(1) + + @browser.load page + + expect(@browser.source).to include( ua ) + expect(@browser.preloads).not_to include( @url ) + + expect(hit_count).to eq(2) end it 'uses its #cookie_jar' do expect(@browser.cookies).to be_empty + cookie = Arachni::Cookie.new( + url: @url, + inputs: { + 'my-name' => 'my-value' + } + ) + page = Arachni::Page.from_data( url: @url, - cookie_jar: [ - Arachni::Cookie.new( - url: @url, - inputs: { - 'my-name' => 'my-value' - } - ) - ] + cookie_jar: [ cookie ] ) + expect(@browser.cookies).to_not include cookie + @browser.load( page ) - expect(@browser.cookies).to eq(page.cookie_jar) + + expect(@browser.cookies).to include cookie end it 'replays its DOM#transitions' do @browser.load "#{@url}play-transitions" page = @browser.explore_and_flush.last @@ -2489,11 +2737,10 @@ dpage.dom.skip_states << token @browser.load dpage expect(@browser.skip_states).to include token end - end describe 'other' do it 'raises Arachni::Browser::Error::Load' do expect { @browser.load [] }.to raise_error Arachni::Browser::Error::Load @@ -2532,11 +2779,11 @@ @browser.load response.url expect(@browser.source).to include( ua ) end context 'when given a' do - describe Arachni::HTTP::Response do + describe 'Arachni::HTTP::Response' do it 'preloads it' do @browser.preload Arachni::HTTP::Client.get( @url, mode: :sync ) clear_hit_count expect(hit_count).to eq(0) @@ -2547,11 +2794,11 @@ expect(hit_count).to eq(0) end end - describe Arachni::Page do + describe 'Arachni::Page' do it 'preloads it' do @browser.preload Arachni::Page.from_url( @url ) clear_hit_count expect(hit_count).to eq(0) @@ -2603,11 +2850,11 @@ expect(@browser.source).to include( ua ) expect(@browser.cache).to include( response.url ) end context 'when given a' do - describe Arachni::HTTP::Response do + describe 'Arachni::HTTP::Response' do it 'caches it' do @browser.cache Arachni::HTTP::Client.get( @url, mode: :sync ) clear_hit_count expect(hit_count).to eq(0) @@ -2618,11 +2865,11 @@ expect(hit_count).to eq(0) end end - describe Arachni::Page do + describe 'Arachni::Page' do it 'caches it' do @browser.cache Arachni::Page.from_url( @url ) clear_hit_count expect(hit_count).to eq(0) @@ -2793,33 +3040,88 @@ end end end describe '#cookies' do - it 'returns the browser cookies' do + it 'returns cookies visible via JavaScript' do @browser.load @url - expect(@browser.cookies.size).to eq(1) + cookie = @browser.cookies.first + expect(cookie.name).to eq 'cookie_name' + expect(cookie.value).to eq 'cookie value' + expect(cookie.raw_name).to eq 'cookie_name' + expect(cookie.raw_value).to eq '"cookie value"' + end - expect(cookie).to be_kind_of Arachni::Cookie - expect(cookie.name).to eq('This name should be updated; and properly escaped') - expect(cookie.value).to eq('This value should be updated; and properly escaped') + it 'preserves expiration value' do + @browser.load "#{@url}/cookies/expires" + + cookie = @browser.cookies.first + expect(cookie.name).to eq 'without_expiration' + expect(cookie.value).to eq 'stuff' + expect(cookie.expires).to be_nil + + cookie = @browser.cookies.last + expect(cookie.name).to eq 'with_expiration' + expect(cookie.value).to eq 'bar' + expect(cookie.expires.to_s).to eq Time.parse( '2047-08-01 09:30:11 +0000' ).to_s end - it 'preserves the HttpOnly attribute' do - @browser.load @url - expect(@browser.cookies.first).to be_http_only + it 'preserves the domain' do + @browser.load "#{@url}/cookies/domains" + + cookies = @browser.cookies + + cookie = cookies.find { |c| c.name == 'include_subdomains' } + expect(cookie.name).to eq 'include_subdomains' + expect(cookie.value).to eq 'bar1' + expect(cookie.domain).to eq '.127.0.0.2' end + it 'ignores cookies for other domains' do + @browser.load "#{@url}/cookies/domains" + + cookies = @browser.cookies + expect(cookies.find { |c| c.name == 'other_domain' }).to be_nil + end + + it 'preserves the path' do + @browser.load "#{@url}/cookies/under/path" + + cookie = @browser.cookies.first + expect(cookie.name).to eq 'cookie_under_path' + expect(cookie.value).to eq 'value' + expect(cookie.path).to eq '/cookies/under/' + end + + it 'preserves httpOnly' do + @browser.load "#{@url}/cookies/under/path" + + cookie = @browser.cookies.first + expect(cookie.name).to eq 'cookie_under_path' + expect(cookie.value).to eq 'value' + expect(cookie.path).to eq '/cookies/under/' + expect(cookie).to_not be_http_only + + @browser.load "#{@url}/cookies/httpOnly" + + cookie = @browser.cookies.first + expect(cookie.name).to eq 'http_only' + expect(cookie.value).to eq 'stuff' + expect(cookie).to be_http_only + end + context 'when parsing v1 cookies' do it 'removes the quotes' do cookie = 'rsession="06142010_0%3Ae275d357943e9a2de0"' @browser.load @url @browser.javascript.run( "document.cookie = '#{cookie}';" ) - expect(@browser.cookies.first.value).to eq('06142010_0:e275d357943e9a2de0') + cookie = @browser.cookies.find { |c| c.name == 'rsession' } + expect(cookie.value).to eq('06142010_0:e275d357943e9a2de0') + expect(cookie.raw_value).to eq('"06142010_0%3Ae275d357943e9a2de0"') end end context 'when no page is available' do it 'returns an empty Array' do @@ -2827,27 +3129,48 @@ end end end describe '#snapshot_id' do - before(:each) { Arachni::Options.url = @url } + before(:each) do + Arachni::Options.url = @url + @empty_snapshot_id ||= @browser.load( empty_snapshot_id_url ).snapshot_id + + @snapshot_id = @browser.load( url ).snapshot_id + end + let(:empty_snapshot_id_url) { @url + '/snapshot_id/default' } let(:empty_snapshot_id) do - @browser.load( empty_snapshot_id_url ).snapshot_id + @empty_snapshot_id end let(:snapshot_id) do - @browser.load( url ).snapshot_id + @snapshot_id end let(:url) { @url + '/trigger_events' } it 'returns a DOM digest' do expect(snapshot_id).to eq(@browser.load( url ).snapshot_id) end - context :a do + context 'when there are new cookies' do + let(:url) { @url + '/each_element_with_events/set-cookie' } + + it 'takes them into account' do + @browser.fire_event described_class::ElementLocator.new( + tag_name: :button, + attributes: { + onclick: 'setCookie()' + } + ), :click + + expect(@browser.snapshot_id).not_to eq(snapshot_id) + end + end + + context ':a' do context 'and the href is not empty' do context 'and it starts with javascript:' do let(:url) { @url + '/each_element_with_events/a/href/javascript' } it 'takes it into account' do @@ -2879,13 +3202,13 @@ expect(snapshot_id).not_to eq(empty_snapshot_id) end end end - context :form do + context ':form' do let(:empty_snapshot_id_url) { @url + '/snapshot_id/form/default' } - context :input do + context ':input' do context 'of type "image"' do let(:url) { @url + '/each_element_with_events/form/input/image' } it 'takes it into account' do expect(snapshot_id).not_to eq(empty_snapshot_id)