blob: fd6f9daecbaa836c85cbc15b5886dc24d331ce69 [file] [log] [blame]
// 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