// 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/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/redirect_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 content::ResourceType; 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: void Cancel() override { NOTREACHED(); } void CancelAndIgnore() override { status_ = CANCELLED; } void CancelWithError(int error_code) override { NOTREACHED(); } 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_(resource_context_.GetRequestContext()->CreateRequest( url, net::DEFAULT_PRIORITY, NULL /* delegate */, NULL /* cookie_store */)) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); if (render_process_id != MSG_ROUTING_NONE && render_frame_id != MSG_ROUTING_NONE) { content::ResourceRequestInfo::AllocateForTesting( request_.get(), content::RESOURCE_TYPE_MAIN_FRAME, &resource_context_, render_process_id, MSG_ROUTING_NONE, render_frame_id, true, // is_main_frame false, // parent_is_main_frame true, // allow_download false); // is_async } throttle_.reset(new InterceptNavigationResourceThrottle( request_.get(), 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(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 net::RedirectInfo& redirect_info, bool* defer) { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)); throttle_->WillRedirectRequest(redirect_info, 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_; scoped_ptr request_; scoped_ptr throttle_; MockResourceController throttle_controller_; }; // InterceptNavigationResourceThrottleTest ------------------------------------ class InterceptNavigationResourceThrottleTest : public content::RenderViewHostTestHarness { public: InterceptNavigationResourceThrottleTest() : mock_callback_receiver_(new MockInterceptCallbackReceiver()), io_thread_state_(NULL) { } void SetUp() override { RenderViewHostTestHarness::SetUp(); } 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 { // 302 redirects convert POSTs to gets. net::RedirectInfo redirect_info; redirect_info.new_url = url; redirect_info.new_method = "GET"; io_thread_state->ThrottleWillRedirectRequest(redirect_info, 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 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