| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "components/navigation_interception/intercept_navigation_resource_throttle.h" |
| #include "components/navigation_interception/navigation_params.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/resource_context.h" |
| #include "content/public/browser/resource_controller.h" |
| #include "content/public/browser/resource_dispatcher_host.h" |
| #include "content/public/browser/resource_dispatcher_host_delegate.h" |
| #include "content/public/browser/resource_request_info.h" |
| #include "content/public/browser/resource_throttle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "content/public/common/page_transition_types.h" |
| #include "content/public/test/mock_resource_context.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "net/base/request_priority.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_response_info.h" |
| #include "net/url_request/url_request.h" |
| #include "net/url_request/url_request_test_util.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::Eq; |
| using testing::Ne; |
| using testing::Property; |
| using testing::Return; |
| |
| namespace navigation_interception { |
| |
| namespace { |
| |
| const char kTestUrl[] = "http://www.test.com/"; |
| const char kUnsafeTestUrl[] = "about:crash"; |
| |
| // The MS C++ compiler complains about not being able to resolve which url() |
| // method (const or non-const) to use if we use the Property matcher to check |
| // the return value of the NavigationParams::url() method. |
| // It is possible to suppress the error by specifying the types directly but |
| // that results in very ugly syntax, which is why these custom matchers are |
| // used instead. |
| MATCHER(NavigationParamsUrlIsTest, "") { |
| return arg.url() == GURL(kTestUrl); |
| } |
| |
| MATCHER(NavigationParamsUrlIsSafe, "") { |
| return arg.url() != GURL(kUnsafeTestUrl); |
| } |
| |
| } // namespace |
| |
| |
| // MockInterceptCallbackReceiver ---------------------------------------------- |
| |
| class MockInterceptCallbackReceiver { |
| public: |
| MOCK_METHOD2(ShouldIgnoreNavigation, |
| bool(content::WebContents* source, |
| const NavigationParams& navigation_params)); |
| }; |
| |
| // MockResourceController ----------------------------------------------------- |
| class MockResourceController : public content::ResourceController { |
| public: |
| enum Status { |
| UNKNOWN, |
| RESUMED, |
| CANCELLED |
| }; |
| |
| MockResourceController() |
| : status_(UNKNOWN) { |
| } |
| |
| Status status() const { return status_; } |
| |
| // ResourceController: |
| virtual void Cancel() OVERRIDE { |
| NOTREACHED(); |
| } |
| virtual void CancelAndIgnore() OVERRIDE { |
| status_ = CANCELLED; |
| } |
| virtual void CancelWithError(int error_code) OVERRIDE { |
| NOTREACHED(); |
| } |
| virtual void Resume() OVERRIDE { |
| DCHECK(status_ == UNKNOWN); |
| status_ = RESUMED; |
| } |
| |
| private: |
| Status status_; |
| }; |
| |
| // TestIOThreadState ---------------------------------------------------------- |
| |
| enum RedirectMode { |
| REDIRECT_MODE_NO_REDIRECT, |
| REDIRECT_MODE_302, |
| }; |
| |
| class TestIOThreadState { |
| public: |
| TestIOThreadState(const GURL& url, |
| int render_process_id, |
| int render_frame_id, |
| const std::string& request_method, |
| RedirectMode redirect_mode, |
| MockInterceptCallbackReceiver* callback_receiver) |
| : resource_context_(&test_url_request_context_), |
| request_(url, |
| net::DEFAULT_PRIORITY, |
| NULL, |
| resource_context_.GetRequestContext()) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); |
| if (render_process_id != MSG_ROUTING_NONE && |
| render_frame_id != MSG_ROUTING_NONE) { |
| content::ResourceRequestInfo::AllocateForTesting( |
| &request_, |
| ResourceType::MAIN_FRAME, |
| &resource_context_, |
| render_process_id, |
| MSG_ROUTING_NONE, |
| render_frame_id, |
| false); |
| } |
| throttle_.reset(new InterceptNavigationResourceThrottle( |
| &request_, |
| base::Bind(&MockInterceptCallbackReceiver::ShouldIgnoreNavigation, |
| base::Unretained(callback_receiver)))); |
| throttle_->set_controller_for_testing(&throttle_controller_); |
| request_.set_method(request_method); |
| |
| if (redirect_mode == REDIRECT_MODE_302) { |
| net::HttpResponseInfo& response_info = |
| const_cast<net::HttpResponseInfo&>(request_.response_info()); |
| response_info.headers = new net::HttpResponseHeaders( |
| "Status: 302 Found\0\0"); |
| } |
| } |
| |
| void ThrottleWillStartRequest(bool* defer) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); |
| throttle_->WillStartRequest(defer); |
| } |
| |
| void ThrottleWillRedirectRequest(const GURL& new_url, bool* defer) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); |
| throttle_->WillRedirectRequest(new_url, defer); |
| } |
| |
| bool request_resumed() const { |
| return throttle_controller_.status() == |
| MockResourceController::RESUMED; |
| } |
| |
| bool request_cancelled() const { |
| return throttle_controller_.status() == |
| MockResourceController::CANCELLED; |
| } |
| |
| private: |
| net::TestURLRequestContext test_url_request_context_; |
| content::MockResourceContext resource_context_; |
| net::URLRequest request_; |
| scoped_ptr<InterceptNavigationResourceThrottle> throttle_; |
| MockResourceController throttle_controller_; |
| }; |
| |
| // InterceptNavigationResourceThrottleTest ------------------------------------ |
| |
| class InterceptNavigationResourceThrottleTest |
| : public content::RenderViewHostTestHarness { |
| public: |
| InterceptNavigationResourceThrottleTest() |
| : mock_callback_receiver_(new MockInterceptCallbackReceiver()), |
| io_thread_state_(NULL) { |
| } |
| |
| virtual void SetUp() OVERRIDE { |
| RenderViewHostTestHarness::SetUp(); |
| } |
| |
| virtual void TearDown() OVERRIDE { |
| if (web_contents()) |
| web_contents()->SetDelegate(NULL); |
| |
| content::BrowserThread::DeleteSoon( |
| content::BrowserThread::IO, FROM_HERE, io_thread_state_); |
| |
| RenderViewHostTestHarness::TearDown(); |
| } |
| |
| void SetIOThreadState(TestIOThreadState* io_thread_state) { |
| io_thread_state_ = io_thread_state; |
| } |
| |
| void RunThrottleWillStartRequestOnIOThread( |
| const GURL& url, |
| const std::string& request_method, |
| RedirectMode redirect_mode, |
| int render_process_id, |
| int render_frame_id, |
| bool* defer) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); |
| TestIOThreadState* io_thread_state = |
| new TestIOThreadState(url, render_process_id, render_frame_id, |
| request_method, redirect_mode, |
| mock_callback_receiver_.get()); |
| |
| SetIOThreadState(io_thread_state); |
| |
| if (redirect_mode == REDIRECT_MODE_NO_REDIRECT) |
| io_thread_state->ThrottleWillStartRequest(defer); |
| else |
| io_thread_state->ThrottleWillRedirectRequest(url, defer); |
| } |
| |
| protected: |
| enum ShouldIgnoreNavigationCallbackAction { |
| IgnoreNavigation, |
| DontIgnoreNavigation |
| }; |
| |
| void SetUpWebContentsDelegateAndDrainRunLoop( |
| ShouldIgnoreNavigationCallbackAction callback_action, |
| bool* defer) { |
| ON_CALL(*mock_callback_receiver_, ShouldIgnoreNavigation(_, _)) |
| .WillByDefault(Return(callback_action == IgnoreNavigation)); |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(web_contents(), |
| NavigationParamsUrlIsTest())) |
| .Times(1); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "GET", |
| REDIRECT_MODE_NO_REDIRECT, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void WaitForPreviouslyScheduledIoThreadWork() { |
| base::WaitableEvent io_thread_work_done(true, false); |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &base::WaitableEvent::Signal, |
| base::Unretained(&io_thread_work_done))); |
| io_thread_work_done.Wait(); |
| } |
| |
| scoped_ptr<MockInterceptCallbackReceiver> mock_callback_receiver_; |
| TestIOThreadState* io_thread_state_; |
| }; |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| RequestDeferredAndResumedIfNavigationNotIgnored) { |
| bool defer = false; |
| SetUpWebContentsDelegateAndDrainRunLoop(DontIgnoreNavigation, &defer); |
| |
| EXPECT_TRUE(defer); |
| ASSERT_TRUE(io_thread_state_); |
| EXPECT_TRUE(io_thread_state_->request_resumed()); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| RequestDeferredAndCancelledIfNavigationIgnored) { |
| bool defer = false; |
| SetUpWebContentsDelegateAndDrainRunLoop(IgnoreNavigation, &defer); |
| |
| EXPECT_TRUE(defer); |
| ASSERT_TRUE(io_thread_state_); |
| EXPECT_TRUE(io_thread_state_->request_cancelled()); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| NoCallbackMadeIfContentsDeletedWhileThrottleRunning) { |
| bool defer = false; |
| |
| // The tested scenario is when the WebContents is deleted after the |
| // ResourceThrottle has finished processing on the IO thread but before the |
| // UI thread callback has been processed. Since both threads in this test |
| // are serviced by one message loop, the post order is the execution order. |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, _)) |
| .Times(0); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "GET", |
| REDIRECT_MODE_NO_REDIRECT, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(&defer))); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, |
| FROM_HERE, |
| base::Bind( |
| &RenderViewHostTestHarness::DeleteContents, |
| base::Unretained(this))); |
| |
| // The WebContents will now be deleted and only after that will the UI-thread |
| // callback posted by the ResourceThrottle be executed. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(defer); |
| ASSERT_TRUE(io_thread_state_); |
| EXPECT_TRUE(io_thread_state_->request_resumed()); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| RequestNotDeferredForRequestNotAssociatedWithARenderView) { |
| bool defer = false; |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "GET", |
| REDIRECT_MODE_NO_REDIRECT, |
| MSG_ROUTING_NONE, |
| MSG_ROUTING_NONE, |
| base::Unretained(&defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(defer); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| CallbackCalledWithFilteredUrl) { |
| bool defer = false; |
| |
| ON_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, NavigationParamsUrlIsSafe())) |
| .WillByDefault(Return(false)); |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, NavigationParamsUrlIsSafe())) |
| .Times(1); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kUnsafeTestUrl), |
| "GET", |
| REDIRECT_MODE_NO_REDIRECT, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(&defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| CallbackIsPostFalseForGet) { |
| bool defer = false; |
| |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, AllOf( |
| NavigationParamsUrlIsSafe(), |
| Property(&NavigationParams::is_post, Eq(false))))) |
| .WillOnce(Return(false)); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "GET", |
| REDIRECT_MODE_NO_REDIRECT, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(&defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| CallbackIsPostTrueForPost) { |
| bool defer = false; |
| |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, AllOf( |
| NavigationParamsUrlIsSafe(), |
| Property(&NavigationParams::is_post, Eq(true))))) |
| .WillOnce(Return(false)); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "POST", |
| REDIRECT_MODE_NO_REDIRECT, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(&defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(InterceptNavigationResourceThrottleTest, |
| CallbackIsPostFalseForPostConvertedToGetBy302) { |
| bool defer = false; |
| |
| EXPECT_CALL(*mock_callback_receiver_, |
| ShouldIgnoreNavigation(_, AllOf( |
| NavigationParamsUrlIsSafe(), |
| Property(&NavigationParams::is_post, Eq(false))))) |
| .WillOnce(Return(false)); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::IO, |
| FROM_HERE, |
| base::Bind( |
| &InterceptNavigationResourceThrottleTest:: |
| RunThrottleWillStartRequestOnIOThread, |
| base::Unretained(this), |
| GURL(kTestUrl), |
| "POST", |
| REDIRECT_MODE_302, |
| web_contents()->GetRenderViewHost()->GetProcess()->GetID(), |
| web_contents()->GetMainFrame()->GetRoutingID(), |
| base::Unretained(&defer))); |
| |
| // Wait for the request to finish processing. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| } // namespace navigation_interception |