Allow asynchronous deferral in NavigationSimulator

This patch adds functionality to the testing class NavigationSimulator,
to allow for NavigationThrottles to defer and cancel the navigation.

The new functionality is tested by new NavigationSimulator unit tests,
as well as new subresource filter unit tests exercising the untested
SubframeNavigationFilteringThrottle.

BUG=637415
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:linux_site_isolation

Review-Url: https://codereview.chromium.org/2698393002
Cr-Commit-Position: refs/heads/master@{#455752}
diff --git a/components/subresource_filter/content/browser/BUILD.gn b/components/subresource_filter/content/browser/BUILD.gn
index 84c7b2d..c3c4429 100644
--- a/components/subresource_filter/content/browser/BUILD.gn
+++ b/components/subresource_filter/content/browser/BUILD.gn
@@ -41,6 +41,7 @@
     "async_document_subresource_filter_unittest.cc",
     "content_ruleset_service_delegate_unittest.cc",
     "content_subresource_filter_driver_factory_unittest.cc",
+    "subframe_navigation_filtering_throttle_unittest.cc",
     "verified_ruleset_dealer_unittest.cc",
   ]
   deps = [
diff --git a/components/subresource_filter/content/browser/subframe_navigation_filtering_throttle_unittest.cc b/components/subresource_filter/content/browser/subframe_navigation_filtering_throttle_unittest.cc
new file mode 100644
index 0000000..84b1aa55
--- /dev/null
+++ b/components/subresource_filter/content/browser/subframe_navigation_filtering_throttle_unittest.cc
@@ -0,0 +1,192 @@
+// Copyright 2017 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 "components/subresource_filter/content/browser/subframe_navigation_filtering_throttle.h"
+
+#include <memory>
+
+#include "base/callback.h"
+#include "base/memory/ptr_util.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "components/subresource_filter/content/browser/async_document_subresource_filter.h"
+#include "components/subresource_filter/core/common/activation_level.h"
+#include "components/subresource_filter/core/common/activation_state.h"
+#include "components/subresource_filter/core/common/test_ruleset_creator.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace subresource_filter {
+
+class SubframeNavigationFilteringThrottleTest
+    : public content::RenderViewHostTestHarness,
+      public content::WebContentsObserver {
+ public:
+  SubframeNavigationFilteringThrottleTest() {}
+  ~SubframeNavigationFilteringThrottleTest() override {}
+
+  void SetUp() override {
+    content::RenderViewHostTestHarness::SetUp();
+    NavigateAndCommit(GURL("https://example.test"));
+    Observe(RenderViewHostTestHarness::web_contents());
+  }
+
+  void TearDown() override {
+    dealer_handle_.reset();
+    ruleset_handle_.reset();
+    parent_filter_.reset();
+    RunUntilIdle();
+    content::RenderViewHostTestHarness::TearDown();
+  }
+
+  // content::WebContentsObserver:
+  void DidStartNavigation(
+      content::NavigationHandle* navigation_handle) override {
+    ASSERT_FALSE(navigation_handle->IsInMainFrame());
+    // The |parent_filter_| is the parent frame's filter. Do not register a
+    // throttle if the parent is not activated with a valid filter.
+    if (parent_filter_) {
+      navigation_handle->RegisterThrottleForTesting(
+          base::MakeUnique<SubframeNavigationFilteringThrottle>(
+              navigation_handle, parent_filter_.get()));
+    }
+  }
+
+  void InitializeDocumentSubresourceFilter(const GURL& document_url) {
+    ASSERT_NO_FATAL_FAILURE(
+        test_ruleset_creator_.CreateRulesetToDisallowURLsWithPathSuffix(
+            "disallowed.html", &test_ruleset_pair_));
+
+    // Make the blocking task runner run on the current task runner for the
+    // tests, to ensure that the NavigationSimulator properly runs all necessary
+    // tasks while waiting for throttle checks to finish.
+    dealer_handle_ = base::MakeUnique<VerifiedRulesetDealer::Handle>(
+        base::MessageLoop::current()->task_runner());
+    dealer_handle_->SetRulesetFile(
+        testing::TestRuleset::Open(test_ruleset_pair_.indexed));
+    ruleset_handle_ =
+        base::MakeUnique<VerifiedRuleset::Handle>(dealer_handle_.get());
+
+    parent_filter_ = base::MakeUnique<AsyncDocumentSubresourceFilter>(
+        ruleset_handle_.get(),
+        AsyncDocumentSubresourceFilter::InitializationParams(
+            document_url, ActivationLevel::ENABLED,
+            false /* measure_performance */),
+        base::Bind([](ActivationState state) {
+          EXPECT_EQ(ActivationLevel::ENABLED, state.activation_level);
+        }),
+        base::OnceClosure());
+    RunUntilIdle();
+  }
+
+  void RunUntilIdle() { base::RunLoop().RunUntilIdle(); }
+
+  void CreateTestSubframeAndInitNavigation(const GURL& first_url,
+                                           content::RenderFrameHost* parent) {
+    content::RenderFrameHost* render_frame =
+        content::RenderFrameHostTester::For(parent)->AppendChild(
+            base::StringPrintf("subframe-%s", first_url.spec().c_str()));
+    navigation_simulator_ =
+        content::NavigationSimulator::CreateRendererInitiated(first_url,
+                                                              render_frame);
+  }
+
+  void SimulateStartAndExpectResult(
+      content::NavigationThrottle::ThrottleCheckResult expect_result) {
+    navigation_simulator_->Start();
+    EXPECT_EQ(expect_result,
+              navigation_simulator_->GetLastThrottleCheckResult());
+  }
+
+  void SimulateRedirectAndExpectResult(
+      const GURL& new_url,
+      content::NavigationThrottle::ThrottleCheckResult expect_result) {
+    navigation_simulator_->Redirect(new_url);
+    EXPECT_EQ(expect_result,
+              navigation_simulator_->GetLastThrottleCheckResult());
+  }
+
+  void SimulateCommitAndExpectResult(
+      content::NavigationThrottle::ThrottleCheckResult expect_result) {
+    navigation_simulator_->Commit();
+    EXPECT_EQ(expect_result,
+              navigation_simulator_->GetLastThrottleCheckResult());
+  }
+
+ private:
+  testing::TestRulesetCreator test_ruleset_creator_;
+  testing::TestRulesetPair test_ruleset_pair_;
+
+  std::unique_ptr<VerifiedRulesetDealer::Handle> dealer_handle_;
+  std::unique_ptr<VerifiedRuleset::Handle> ruleset_handle_;
+
+  std::unique_ptr<AsyncDocumentSubresourceFilter> parent_filter_;
+
+  std::unique_ptr<content::NavigationSimulator> navigation_simulator_;
+
+  DISALLOW_COPY_AND_ASSIGN(SubframeNavigationFilteringThrottleTest);
+};
+
+TEST_F(SubframeNavigationFilteringThrottleTest, FilterOnStart) {
+  InitializeDocumentSubresourceFilter(GURL("https://example.test"));
+  CreateTestSubframeAndInitNavigation(
+      GURL("https://example.test/disallowed.html"), main_rfh());
+  SimulateStartAndExpectResult(content::NavigationThrottle::CANCEL);
+}
+
+TEST_F(SubframeNavigationFilteringThrottleTest, FilterOnRedirect) {
+  InitializeDocumentSubresourceFilter(GURL("https://example.test"));
+  CreateTestSubframeAndInitNavigation(GURL("https://example.test/allowed.html"),
+                                      main_rfh());
+
+  SimulateStartAndExpectResult(content::NavigationThrottle::PROCEED);
+  SimulateRedirectAndExpectResult(GURL("https://example.test/disallowed.html"),
+                                  content::NavigationThrottle::CANCEL);
+}
+
+TEST_F(SubframeNavigationFilteringThrottleTest, FilterOnSecondRedirect) {
+  InitializeDocumentSubresourceFilter(GURL("https://example.test"));
+  CreateTestSubframeAndInitNavigation(GURL("https://example.test/allowed.html"),
+                                      main_rfh());
+
+  SimulateStartAndExpectResult(content::NavigationThrottle::PROCEED);
+  SimulateRedirectAndExpectResult(GURL("https://example.test/allowed2.html"),
+                                  content::NavigationThrottle::PROCEED);
+  SimulateRedirectAndExpectResult(GURL("https://example.test/disallowed.html"),
+                                  content::NavigationThrottle::CANCEL);
+}
+
+TEST_F(SubframeNavigationFilteringThrottleTest, NeverFilterNonMatchingRule) {
+  InitializeDocumentSubresourceFilter(GURL("https://example.test"));
+  CreateTestSubframeAndInitNavigation(GURL("https://example.test/allowed.html"),
+                                      main_rfh());
+
+  SimulateStartAndExpectResult(content::NavigationThrottle::PROCEED);
+  SimulateRedirectAndExpectResult(GURL("https://example.test/allowed2.html"),
+                                  content::NavigationThrottle::PROCEED);
+  SimulateCommitAndExpectResult(content::NavigationThrottle::PROCEED);
+}
+
+TEST_F(SubframeNavigationFilteringThrottleTest, FilterSubsubframe) {
+  // Fake an activation of the subframe.
+  content::RenderFrameHost* parent_subframe =
+      content::RenderFrameHostTester::For(main_rfh())
+          ->AppendChild("parent-sub");
+  GURL test_url = GURL("https://example.test");
+  content::RenderFrameHostTester::For(parent_subframe)
+      ->SimulateNavigationStart(test_url);
+  InitializeDocumentSubresourceFilter(GURL("https://example.test"));
+  content::RenderFrameHostTester::For(parent_subframe)
+      ->SimulateNavigationCommit(test_url);
+
+  CreateTestSubframeAndInitNavigation(
+      GURL("https://example.test/disallowed.html"), parent_subframe);
+  SimulateStartAndExpectResult(content::NavigationThrottle::CANCEL);
+}
+
+}  // namespace subresource_filter
diff --git a/content/browser/frame_host/navigation_handle_impl.cc b/content/browser/frame_host/navigation_handle_impl.cc
index 68d8d05..8a45b47 100644
--- a/content/browser/frame_host/navigation_handle_impl.cc
+++ b/content/browser/frame_host/navigation_handle_impl.cc
@@ -880,6 +880,11 @@
   ThrottleChecksFinishedCallback callback = complete_callback_;
   complete_callback_.Reset();
 
+  if (!complete_callback_for_testing_.is_null()) {
+    complete_callback_for_testing_.Run(result);
+    complete_callback_for_testing_.Reset();
+  }
+
   if (!callback.is_null())
     callback.Run(result);
 
diff --git a/content/browser/frame_host/navigation_handle_impl.h b/content/browser/frame_host/navigation_handle_impl.h
index 275c667..853c2fb 100644
--- a/content/browser/frame_host/navigation_handle_impl.h
+++ b/content/browser/frame_host/navigation_handle_impl.h
@@ -346,6 +346,11 @@
     response_headers_ = response_headers;
   }
 
+  void set_complete_callback_for_testing(
+      const ThrottleChecksFinishedCallback& callback) {
+    complete_callback_for_testing_ = callback;
+  }
+
  private:
   friend class NavigationHandleImplTest;
 
@@ -447,9 +452,16 @@
   // The mixed content context type for potential mixed content checks.
   blink::WebMixedContentContextType mixed_content_context_type_;
 
-  // This callback will be run when all throttle checks have been performed.
+  // This callback will be run when all throttle checks have been performed. Be
+  // careful about relying on it as the member may be removed as part of the
+  // PlzNavigate refactoring.
   ThrottleChecksFinishedCallback complete_callback_;
 
+  // This test-only callback will be run when all throttle checks have been
+  // performed.
+  // TODO(clamy): Revisit the unit test architecture when PlzNavigate ships.
+  ThrottleChecksFinishedCallback complete_callback_for_testing_;
+
   // PlzNavigate
   // Manages the lifetime of a pre-created ServiceWorkerProviderHost until a
   // corresponding ServiceWorkerNetworkProvider is created in the renderer.
diff --git a/content/public/test/navigation_simulator.cc b/content/public/test/navigation_simulator.cc
index 9cc1399..daf4cfb 100644
--- a/content/public/test/navigation_simulator.cc
+++ b/content/public/test/navigation_simulator.cc
@@ -4,6 +4,9 @@
 
 #include "content/public/test/navigation_simulator.h"
 
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
 #include "content/browser/frame_host/navigation_handle_impl.h"
 #include "content/browser/frame_host/navigation_request.h"
 #include "content/common/frame_messages.h"
@@ -123,7 +126,10 @@
         render_frame_host_->GetRoutingID(), common_params, begin_params));
     NavigationRequest* request =
         render_frame_host_->frame_tree_node()->navigation_request();
-    DCHECK(request);
+
+    // The request failed synchronously.
+    if (!request)
+      return;
     DCHECK_EQ(handle_, request->navigation_handle());
   } else {
     render_frame_host_->OnMessageReceived(
@@ -141,14 +147,16 @@
   }
 
   CHECK(handle_);
-
-  // Make sure all NavigationThrottles have run.
-  // TODO(clamy): provide a non auto-advance mode if needed.
-  while (handle_->state_for_testing() == NavigationHandleImpl::DEFERRING_START)
-    handle_->Resume();
+  WaitForThrottleChecksComplete();
 
   CHECK_EQ(1, num_did_start_navigation_called_);
-  CHECK_EQ(1, num_will_start_request_called_);
+  if (GetLastThrottleCheckResult() == NavigationThrottle::PROCEED) {
+    CHECK_EQ(1, num_will_start_request_called_);
+  } else {
+    // TODO(clamy): Add error handling code based on the
+    // NavigationThrottleCheckResult here and in other methods.
+    state_ = FAILED;
+  }
 }
 
 void NavigationSimulator::Redirect(const GURL& new_url) {
@@ -168,6 +176,7 @@
   int previous_did_redirect_navigation_called =
       num_did_redirect_navigation_called_;
 
+  PrepareCompleteCallbackOnHandle();
   if (IsBrowserSideNavigationEnabled()) {
     NavigationRequest* request =
         render_frame_host_->frame_tree_node()->navigation_request();
@@ -194,17 +203,16 @@
         base::Callback<void(NavigationThrottle::ThrottleCheckResult)>());
   }
 
-  // Make sure all NavigationThrottles have run.
-  // TODO(clamy): provide a non auto-advance mode if needed.
-  while (handle_->state_for_testing() ==
-         NavigationHandleImpl::DEFERRING_REDIRECT) {
-    handle_->Resume();
-  }
+  WaitForThrottleChecksComplete();
 
-  CHECK_EQ(previous_num_will_redirect_request_called + 1,
-           num_will_redirect_request_called_);
-  CHECK_EQ(previous_did_redirect_navigation_called + 1,
-           num_did_redirect_navigation_called_);
+  if (GetLastThrottleCheckResult() == NavigationThrottle::PROCEED) {
+    CHECK_EQ(previous_num_will_redirect_request_called + 1,
+             num_will_redirect_request_called_);
+    CHECK_EQ(previous_did_redirect_navigation_called + 1,
+             num_did_redirect_navigation_called_);
+  } else {
+    state_ = FAILED;
+  }
 }
 
 void NavigationSimulator::Commit() {
@@ -218,14 +226,16 @@
   if (state_ == INITIALIZATION)
     Start();
 
+  PrepareCompleteCallbackOnHandle();
   if (IsBrowserSideNavigationEnabled() &&
       render_frame_host_->frame_tree_node()->navigation_request()) {
     render_frame_host_->PrepareForCommit();
   }
 
   // Call NavigationHandle::WillProcessResponse if needed.
-  if (handle_->state_for_testing() <
-      NavigationHandleImpl::WILL_PROCESS_RESPONSE) {
+  // Note that the handle's state can be CANCELING if a throttle cancelled it
+  // synchronously in PrepareForCommit.
+  if (handle_->state_for_testing() < NavigationHandleImpl::CANCELING) {
     handle_->WillProcessResponse(
         render_frame_host_, scoped_refptr<net::HttpResponseHeaders>(),
         net::HttpResponseInfo::ConnectionInfo(), SSLStatus(), GlobalRequestID(),
@@ -234,11 +244,11 @@
         base::Callback<void(NavigationThrottle::ThrottleCheckResult)>());
   }
 
-  // Make sure all NavigationThrottles have run.
-  // TODO(clamy): provide a non auto-advance mode if needed.
-  while (handle_->state_for_testing() ==
-         NavigationHandleImpl::DEFERRING_RESPONSE) {
-    handle_->Resume();
+  WaitForThrottleChecksComplete();
+
+  if (GetLastThrottleCheckResult() != NavigationThrottle::PROCEED) {
+    state_ = FAILED;
+    return;
   }
 
   CHECK_EQ(1, num_will_process_response_called_);
@@ -446,6 +456,11 @@
   referrer_ = referrer;
 }
 
+NavigationThrottle::ThrottleCheckResult
+NavigationSimulator::GetLastThrottleCheckResult() {
+  return last_throttle_check_result_.value();
+}
+
 void NavigationSimulator::DidStartNavigation(
     NavigationHandle* navigation_handle) {
   // Check if this navigation is the one we're simulating.
@@ -475,6 +490,8 @@
                      weak_factory_.GetWeakPtr()),
           base::Bind(&NavigationSimulator::OnWillProcessResponse,
                      weak_factory_.GetWeakPtr())));
+
+  PrepareCompleteCallbackOnHandle();
 }
 
 void NavigationSimulator::DidRedirectNavigation(
@@ -507,4 +524,31 @@
   num_will_process_response_called_++;
 }
 
+void NavigationSimulator::WaitForThrottleChecksComplete() {
+  // If last_throttle_check_result_ is set, then throttle checks completed
+  // synchronously.
+  if (last_throttle_check_result_)
+    return;
+
+  base::RunLoop run_loop;
+  throttle_checks_wait_closure_ = run_loop.QuitClosure();
+  run_loop.Run();
+  throttle_checks_wait_closure_.Reset();
+}
+
+void NavigationSimulator::OnThrottleChecksComplete(
+    NavigationThrottle::ThrottleCheckResult result) {
+  DCHECK(!last_throttle_check_result_);
+  last_throttle_check_result_ = result;
+  if (throttle_checks_wait_closure_)
+    throttle_checks_wait_closure_.Run();
+}
+
+void NavigationSimulator::PrepareCompleteCallbackOnHandle() {
+  last_throttle_check_result_.reset();
+  handle_->set_complete_callback_for_testing(
+      base::Bind(&NavigationSimulator::OnThrottleChecksComplete,
+                 weak_factory_.GetWeakPtr()));
+}
+
 }  // namespace content
diff --git a/content/public/test/navigation_simulator.h b/content/public/test/navigation_simulator.h
index 8c37ab7a..13a90a0 100644
--- a/content/public/test/navigation_simulator.h
+++ b/content/public/test/navigation_simulator.h
@@ -7,6 +7,9 @@
 
 #include <memory>
 
+#include "base/callback.h"
+#include "base/optional.h"
+#include "content/public/browser/navigation_throttle.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "content/public/common/referrer.h"
 #include "content/public/test/navigation_simulator.h"
@@ -85,6 +88,18 @@
   //       NavigationSimulator::CreateRendererInitiated(
   //           original_url, render_frame_host);
   //   simulator->CommitSamePage();
+  //
+  // Example of usage for a renderer-initiated navigation which is cancelled by
+  // a throttle upon redirecting. Note that registering the throttle is done
+  // elsewhere:
+  //   unique_ptr<NavigationSimulator> simulator =
+  //       NavigationSimulator::CreateRendererInitiated(
+  //           original_url, render_frame_host);
+  //   simulator->SetTransition(ui::PAGE_TRANSITION_LINK);
+  //   simulator->Start();
+  //   simulator->Redirect(redirect_url);
+  //   EXPECT_EQ(NavigationThrottle::CANCEL,
+  //             simulator->GetLastThrottleCheckResult());
 
   // Simulates the start of the navigation.
   virtual void Start();
@@ -122,6 +137,10 @@
   // |Redirect|.
   virtual void SetReferrer(const Referrer& referrer);
 
+  // Gets the last throttle check result computed by the navigation throttles.
+  // It is an error to call this before Start() is called.
+  virtual NavigationThrottle::ThrottleCheckResult GetLastThrottleCheckResult();
+
  private:
   // WebContentsObserver:
   void DidStartNavigation(NavigationHandle* navigation_handle) override;
@@ -133,6 +152,17 @@
   void OnWillRedirectRequest();
   void OnWillProcessResponse();
 
+  // This method will block waiting for throttle checks to complete.
+  void WaitForThrottleChecksComplete();
+
+  // Sets |last_throttle_check_result_| and calls
+  // |throttle_checks_wait_closure_|.
+  void OnThrottleChecksComplete(NavigationThrottle::ThrottleCheckResult result);
+
+  // Helper method to set the OnThrottleChecksComplete callback on the
+  // NavigationHandle.
+  void PrepareCompleteCallbackOnHandle();
+
   enum State {
     INITIALIZATION,
     STARTED,
@@ -162,6 +192,15 @@
   int num_ready_to_commit_called_ = 0;
   int num_did_finish_navigation_called_ = 0;
 
+  // Holds the last ThrottleCheckResult calculated by the navigation's
+  // throttles. Will be unset before WillStartRequest is finished. Will be unset
+  // while throttles are being run, but before they finish.
+  base::Optional<NavigationThrottle::ThrottleCheckResult>
+      last_throttle_check_result_;
+
+  // Closure that is set when WaitForThrottleChecksComplete is called.
+  base::Closure throttle_checks_wait_closure_;
+
   base::WeakPtrFactory<NavigationSimulator> weak_factory_;
 };
 
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index c148904..8bdc92e9 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1412,6 +1412,7 @@
     "fileapi_test_file_set.h",
     "image_decoder_test.cc",
     "image_decoder_test.h",
+    "navigation_simulator_unittest.cc",
     "run_all_unittests.cc",
   ]
 
diff --git a/content/test/navigation_simulator_unittest.cc b/content/test/navigation_simulator_unittest.cc
new file mode 100644
index 0000000..27638b7
--- /dev/null
+++ b/content/test/navigation_simulator_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright 2017 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 "content/public/test/navigation_simulator.h"
+
+#include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/navigation_throttle.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_web_contents.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+enum CancelTime {
+  WILL_SEND_REQUEST,
+  WILL_REDIRECT_REQUEST,
+  WILL_PROCESS_RESPONSE,
+  NEVER,
+};
+
+enum ResultSynchrony {
+  SYNCHRONOUS,
+  ASYNCHRONOUS,
+};
+
+std::string CancelTimeToString(CancelTime cancel_time) {
+  switch (cancel_time) {
+    case WILL_SEND_REQUEST:
+      return "WILL_SEND_REQUEST";
+    case WILL_REDIRECT_REQUEST:
+      return "WILL_REDIRECT_REQUEST";
+    case WILL_PROCESS_RESPONSE:
+      return "WILL_PROCESS_RESPONSE";
+    case NEVER:
+      return "NEVER";
+  }
+  NOTREACHED();
+  return "";
+}
+
+std::string ResultSynchronyToString(ResultSynchrony sync) {
+  return sync == SYNCHRONOUS ? "SYNCHRONOUS" : "ASYNCHRONOUS";
+}
+
+class CancellingNavigationThrottle : public NavigationThrottle {
+ public:
+  CancellingNavigationThrottle(NavigationHandle* handle,
+                               CancelTime cancel_time,
+                               ResultSynchrony sync)
+      : NavigationThrottle(handle),
+        cancel_time_(cancel_time),
+        sync_(sync),
+        weak_ptr_factory_(this) {}
+
+  ~CancellingNavigationThrottle() override {}
+
+  NavigationThrottle::ThrottleCheckResult WillStartRequest() override {
+    return ProcessState(cancel_time_ == WILL_SEND_REQUEST);
+  }
+
+  NavigationThrottle::ThrottleCheckResult WillRedirectRequest() override {
+    return ProcessState(cancel_time_ == WILL_REDIRECT_REQUEST);
+  }
+
+  NavigationThrottle::ThrottleCheckResult WillProcessResponse() override {
+    return ProcessState(cancel_time_ == WILL_PROCESS_RESPONSE);
+  }
+
+  NavigationThrottle::ThrottleCheckResult ProcessState(bool should_cancel) {
+    if (sync_ == ASYNCHRONOUS) {
+      BrowserThread::PostTask(
+          BrowserThread::UI, FROM_HERE,
+          base::Bind(&CancellingNavigationThrottle::MaybeCancel,
+                     weak_ptr_factory_.GetWeakPtr(), should_cancel));
+      return NavigationThrottle::DEFER;
+    }
+    return should_cancel ? NavigationThrottle::CANCEL
+                         : NavigationThrottle::PROCEED;
+  }
+
+  void MaybeCancel(bool cancel) {
+    if (cancel)
+      navigation_handle()->CancelDeferredNavigation(NavigationThrottle::CANCEL);
+    else
+      navigation_handle()->Resume();
+  }
+
+  const CancelTime cancel_time_;
+  const ResultSynchrony sync_;
+  base::WeakPtrFactory<CancellingNavigationThrottle> weak_ptr_factory_;
+};
+
+class NavigationSimulatorTest : public RenderViewHostImplTestHarness,
+                                public WebContentsObserver,
+                                public testing::WithParamInterface<
+                                    std::tuple<CancelTime, ResultSynchrony>> {
+ public:
+  void SetUp() override {
+    RenderViewHostImplTestHarness::SetUp();
+    contents()->GetMainFrame()->InitializeRenderFrameIfNeeded();
+    Observe(RenderViewHostImplTestHarness::web_contents());
+    std::tie(cancel_time_, sync_) = GetParam();
+    simulator_ = NavigationSimulator::CreateRendererInitiated(
+        GURL("https://example.test"), main_rfh());
+  }
+
+  void DidStartNavigation(content::NavigationHandle* handle) override {
+    handle->RegisterThrottleForTesting(
+        base::MakeUnique<CancellingNavigationThrottle>(handle, cancel_time_,
+                                                       sync_));
+  }
+
+  CancelTime cancel_time_;
+  ResultSynchrony sync_;
+  std::unique_ptr<NavigationSimulator> simulator_;
+};
+
+// Stress test the navigation simulator by having a navigation throttle cancel
+// the navigation at various points in the flow, both synchronously and
+// asynchronously.
+TEST_P(NavigationSimulatorTest, Cancel) {
+  SCOPED_TRACE(::testing::Message()
+               << "CancelTime: " << CancelTimeToString(cancel_time_)
+               << " ResultSynchrony: " << ResultSynchronyToString(sync_));
+  simulator_->Start();
+  if (cancel_time_ == WILL_SEND_REQUEST) {
+    EXPECT_EQ(NavigationThrottle::CANCEL,
+              simulator_->GetLastThrottleCheckResult());
+    return;
+  }
+  EXPECT_EQ(NavigationThrottle::PROCEED,
+            simulator_->GetLastThrottleCheckResult());
+  simulator_->Redirect(GURL("https://example.redirect"));
+  if (cancel_time_ == WILL_REDIRECT_REQUEST) {
+    EXPECT_EQ(NavigationThrottle::CANCEL,
+              simulator_->GetLastThrottleCheckResult());
+    return;
+  }
+  EXPECT_EQ(NavigationThrottle::PROCEED,
+            simulator_->GetLastThrottleCheckResult());
+  simulator_->Commit();
+  if (cancel_time_ == WILL_PROCESS_RESPONSE) {
+    EXPECT_EQ(NavigationThrottle::CANCEL,
+              simulator_->GetLastThrottleCheckResult());
+    return;
+  }
+  EXPECT_EQ(NavigationThrottle::PROCEED,
+            simulator_->GetLastThrottleCheckResult());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    CancelMethod,
+    NavigationSimulatorTest,
+    ::testing::Values(std::make_tuple(WILL_SEND_REQUEST, SYNCHRONOUS),
+                      std::make_tuple(WILL_SEND_REQUEST, ASYNCHRONOUS),
+                      std::make_tuple(WILL_REDIRECT_REQUEST, SYNCHRONOUS),
+                      std::make_tuple(WILL_REDIRECT_REQUEST, ASYNCHRONOUS),
+                      std::make_tuple(WILL_PROCESS_RESPONSE, SYNCHRONOUS),
+                      std::make_tuple(WILL_PROCESS_RESPONSE, ASYNCHRONOUS),
+                      std::make_tuple(NEVER, SYNCHRONOUS),
+                      std::make_tuple(NEVER, ASYNCHRONOUS)));
+
+}  // namespace content