chromium/chrome/browser/dev_ui/android/dev_ui_loader_throttle_unittest.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/dev_ui/android/dev_ui_loader_throttle.h"

#include <deque>
#include <memory>
#include <utility>

#include "chrome/android/modules/dev_ui/provider/dev_ui_module_provider.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/test/mock_navigation_handle.h"
#include "net/base/net_errors.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace dev_ui {

namespace {

const char* const kNonDevUiUrls[] = {
    "https://example.com",
    "https://example.com/path?query#frag",
    "chrome://credits",
    "chrome://credits/path?query#frag",
};

const char* const kDevUiUrls[] = {
    "chrome://bluetooth-internals",
    "chrome://bluetooth-internals/path?query#frag",
};

/******** MockDevUiModuleProvider ********/

// Mock DevUiModuleProvider that overrides the main module install / load
// functions, and provides SimulateAsyncInstall().
class MockDevUiModuleProvider : public DevUiModuleProvider {
 public:
  MockDevUiModuleProvider() = default;
  ~MockDevUiModuleProvider() override = default;

  // DevUiModuleProvider:
  bool ModuleInstalled() override { return is_installed_; }

  // DevUiModuleProvider:
  void InstallModule(base::OnceCallback<void(bool)> on_complete) override {
    on_complete_queue_.emplace_back(std::move(on_complete));
  }

  // DevUiModuleProvider:
  void EnsureLoaded() override { is_loaded_ = true; }

  void Reset() {
    is_installed_ = false;
    is_loaded_ = false;
    on_complete_queue_.clear();
  }

  // Simulate DevUI DFN install, and dispatches all on-complete callbacks.
  void SimulateAsyncInstall(bool install_succeeds) {
    is_installed_ = is_installed_ || install_succeeds;
    while (!on_complete_queue_.empty()) {
      std::move(on_complete_queue_.front()).Run(install_succeeds);
      on_complete_queue_.pop_front();
    }
  }

  // Direct access of fake installation states.
  void SetIsInstalled(bool is_installed) { is_installed_ = is_installed; }
  bool GetIsInstalled() const { return is_installed_; }
  void SetIsLoaded(bool is_loaded) { is_loaded_ = is_loaded; }
  bool GetIsLoaded() const { return is_loaded_; }

 private:
  bool is_installed_ = false;
  bool is_loaded_ = false;
  std::deque<base::OnceCallback<void(bool)>> on_complete_queue_;
};

/******** TestDevUiLoaderThrottle ********/

// DevUiLoaderThrottle that overrides and captures Resume() and
// CancelDeferredNavigation(). This is needed because:
// * In actual usage, both functions can lead to throttle deletion, which is
//   problematic since the tests own the throttle instance.
// * We want to record results for verification.
// Note that this is instantiated directly, instead of using
// DevUiLoaderThrottle::MaybeCreateThrottleFor().
class TestDevUiLoaderThrottle : public DevUiLoaderThrottle {
 public:
  explicit TestDevUiLoaderThrottle(content::NavigationHandle* navigation_handle)
      : DevUiLoaderThrottle(navigation_handle), cancel_result(LAST) {}
  ~TestDevUiLoaderThrottle() override = default;
  TestDevUiLoaderThrottle(const TestDevUiLoaderThrottle&) = delete;
  const TestDevUiLoaderThrottle& operator=(const TestDevUiLoaderThrottle&) =
      delete;

  // content::NavigationThrottle:
  void Resume() override { called_resume = true; }

  // content::NavigationThrottle:
  void CancelDeferredNavigation(
      content::NavigationThrottle::ThrottleCheckResult result) override {
    called_cancel = true;
    cancel_result = std::move(result);
  }

  bool called_resume = false;
  bool called_cancel = false;
  content::NavigationThrottle::ThrottleCheckResult cancel_result;
};

/******** DevUiLoaderThrottleTest ********/

class DevUiLoaderThrottleTest : public ChromeRenderViewHostTestHarness {
 public:
  DevUiLoaderThrottleTest() = default;
  ~DevUiLoaderThrottleTest() override = default;
  DevUiLoaderThrottleTest(const DevUiLoaderThrottleTest&) = delete;
  const DevUiLoaderThrottleTest& operator=(const DevUiLoaderThrottleTest&) =
      delete;

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    DevUiModuleProvider::SetTestInstance(&mock_provider_);
  }

  void TearDown() override {
    mock_provider_.Reset();
    DevUiModuleProvider::SetTestInstance(nullptr);
    ChromeRenderViewHostTestHarness::TearDown();
  }

 protected:
  MockDevUiModuleProvider mock_provider_;
};

class DevUiLoaderThrottleFencedFrameTest : public DevUiLoaderThrottleTest {
 public:
  DevUiLoaderThrottleFencedFrameTest() {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

}  // namespace

TEST_F(DevUiLoaderThrottleTest, ShouldInstallDevUiDfm) {
  auto ShouldInstallDevUiDfm = DevUiLoaderThrottle::ShouldInstallDevUiDfm;
  for (const char* url_string : kNonDevUiUrls)
    EXPECT_FALSE(ShouldInstallDevUiDfm(GURL(url_string)));
  for (const char* url_string : kDevUiUrls)
    EXPECT_TRUE(ShouldInstallDevUiDfm(GURL(url_string)));
}

// Test to ensure that pages that are purposefully left in the base module are
// not accidentally moved to the DevUI DFM.
TEST_F(DevUiLoaderThrottleTest, PreventAccidentalInclusion) {
  auto ShouldInstallDevUiDfm = DevUiLoaderThrottle::ShouldInstallDevUiDfm;

  // Useful to have the catalog always.
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://chrome-urls")));
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://about")));
  // Inclusion in base module is mandatory.
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://credits")));
  // Well-loved game, and shown when there's no internet (cannot install DFM).
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://dino")));
  // chrome://flags has relatively high usage.
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://flags")));
  // Useful for filing bugs.
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://version")));
  // Used by Android WebView.
  EXPECT_FALSE(ShouldInstallDevUiDfm(GURL("chrome://safe-browsing")));
}

TEST_F(DevUiLoaderThrottleTest, MaybeCreateThrottleFor) {
  bool is_installed = false;
  auto creates_throttle = [&](const std::string& url_string) -> bool {
    mock_provider_.Reset();
    mock_provider_.SetIsInstalled(is_installed);
    content::MockNavigationHandle handle(GURL(url_string), main_rfh());
    std::unique_ptr<content::NavigationThrottle> throttle =
        DevUiLoaderThrottle::MaybeCreateThrottleFor(&handle);
    return throttle != nullptr;
  };

  // Case 1: DevUI DFM is not installed.
  is_installed = false;
  // Case 1a: Non DevUI URLs: Throttle not created.
  for (const char* url_string : kNonDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
  // Case 1b: DevUI URLs: Throttle created, but no load takes place.
  for (const char* url_string : kDevUiUrls) {
    EXPECT_TRUE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }

  // Case 2: DevUI DFM is installed.
  is_installed = true;
  // Case 2a: Non DevUI URLs: Throttle not created.
  for (const char* url_string : kNonDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
  // Case 2b: DevUI URLs: Throttle not created, but load takes place.
  for (const char* url_string : kDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_TRUE(mock_provider_.GetIsLoaded());
  }
}

TEST_F(DevUiLoaderThrottleFencedFrameTest, MaybeCreateThrottleFor) {
  bool is_installed = false;
  content::RenderFrameHost* render_frame_host = nullptr;
  auto creates_throttle = [&](const std::string& url_string) -> bool {
    mock_provider_.Reset();
    mock_provider_.SetIsInstalled(is_installed);
    content::MockNavigationHandle handle(GURL(url_string), render_frame_host);
    std::unique_ptr<content::NavigationThrottle> throttle =
        DevUiLoaderThrottle::MaybeCreateThrottleFor(&handle);
    return throttle != nullptr;
  };

  content::RenderFrameHostTester::For(main_rfh())
      ->InitializeRenderFrameIfNeeded();
  render_frame_host =
      content::RenderFrameHostTester::For(main_rfh())->AppendFencedFrame();
  EXPECT_TRUE(render_frame_host);

  // In any case, throttles should not be created in fenced frames.
  is_installed = false;
  for (const char* url_string : kNonDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
  for (const char* url_string : kDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
  is_installed = true;
  for (const char* url_string : kNonDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
  for (const char* url_string : kDevUiUrls) {
    EXPECT_FALSE(creates_throttle(url_string));
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
  }
}

TEST_F(DevUiLoaderThrottleTest, InstallSuccess) {
  for (const char* url_string : kDevUiUrls) {
    mock_provider_.Reset();
    content::MockNavigationHandle handle(GURL(url_string), main_rfh());
    auto throttle = std::make_unique<TestDevUiLoaderThrottle>(&handle);
    EXPECT_FALSE(throttle->called_resume);
    EXPECT_FALSE(throttle->called_cancel);
    EXPECT_FALSE(mock_provider_.GetIsInstalled());
    EXPECT_FALSE(mock_provider_.GetIsLoaded());

    // Simulate request start.
    EXPECT_EQ(content::NavigationThrottle::DEFER, throttle->WillStartRequest());
    EXPECT_FALSE(throttle->called_resume);
    EXPECT_FALSE(throttle->called_cancel);
    EXPECT_FALSE(mock_provider_.GetIsInstalled());
    EXPECT_FALSE(mock_provider_.GetIsLoaded());

    // Simulate actual install (success)
    mock_provider_.SimulateAsyncInstall(true);  // !
    EXPECT_TRUE(throttle->called_resume);
    EXPECT_FALSE(throttle->called_cancel);
    EXPECT_TRUE(mock_provider_.GetIsInstalled());
    EXPECT_TRUE(mock_provider_.GetIsLoaded());
  }
}

TEST_F(DevUiLoaderThrottleTest, InstallQueued) {
  mock_provider_.Reset();
  // Page 1 request.
  content::MockNavigationHandle handle1(GURL(kDevUiUrls[0]), main_rfh());
  auto throttle1 = std::make_unique<TestDevUiLoaderThrottle>(&handle1);
  // Page 2 request.
  content::MockNavigationHandle handle2(GURL(kDevUiUrls[1]), main_rfh());
  auto throttle2 = std::make_unique<TestDevUiLoaderThrottle>(&handle2);
  EXPECT_FALSE(mock_provider_.GetIsInstalled());
  EXPECT_FALSE(mock_provider_.GetIsLoaded());

  // Simulate request start of page 1. This triggers install.
  EXPECT_EQ(content::NavigationThrottle::DEFER, throttle1->WillStartRequest());

  // Simulate request start of page 2. This does not trigger install. However,
  // the on-complete callback is queued, and will be called.
  EXPECT_EQ(content::NavigationThrottle::DEFER, throttle2->WillStartRequest());

  // Simulate actual install.
  mock_provider_.SimulateAsyncInstall(true);  // !
  EXPECT_TRUE(throttle1->called_resume);
  EXPECT_FALSE(throttle1->called_cancel);
  EXPECT_TRUE(throttle2->called_resume);
  EXPECT_FALSE(throttle2->called_cancel);
  EXPECT_TRUE(mock_provider_.GetIsInstalled());
  EXPECT_TRUE(mock_provider_.GetIsLoaded());
}

TEST_F(DevUiLoaderThrottleTest, InstallRedundant) {
  mock_provider_.Reset();
  // Page 1 request.
  content::MockNavigationHandle handle1(GURL(kDevUiUrls[0]), main_rfh());
  auto throttle1 = std::make_unique<TestDevUiLoaderThrottle>(&handle1);
  // Page 2 request.
  content::MockNavigationHandle handle2(GURL(kDevUiUrls[1]), main_rfh());
  auto throttle2 = std::make_unique<TestDevUiLoaderThrottle>(&handle2);
  EXPECT_FALSE(mock_provider_.GetIsInstalled());
  EXPECT_FALSE(mock_provider_.GetIsLoaded());

  // Simulate request start of page 1.
  EXPECT_EQ(content::NavigationThrottle::DEFER, throttle1->WillStartRequest());

  // Simulate actual install.
  mock_provider_.SimulateAsyncInstall(true);  // !
  EXPECT_TRUE(throttle1->called_resume);
  EXPECT_FALSE(throttle1->called_cancel);
  EXPECT_TRUE(mock_provider_.GetIsInstalled());
  EXPECT_TRUE(mock_provider_.GetIsLoaded());

  // Simulate request start of page 2. The request was made before install
  // completes, but request start occurs after install completes. In this case,
  // simply proceed.
  EXPECT_EQ(content::NavigationThrottle::PROCEED,
            throttle2->WillStartRequest());
  EXPECT_FALSE(throttle2->called_resume);
  EXPECT_FALSE(throttle2->called_cancel);
  EXPECT_TRUE(mock_provider_.GetIsInstalled());
  EXPECT_TRUE(mock_provider_.GetIsLoaded());
}

TEST_F(DevUiLoaderThrottleTest, InstallFailure) {
  for (const char* url_string : kDevUiUrls) {
    mock_provider_.Reset();
    content::MockNavigationHandle handle(GURL(url_string), main_rfh());
    auto throttle = std::make_unique<TestDevUiLoaderThrottle>(&handle);
    EXPECT_FALSE(throttle->called_resume);
    EXPECT_FALSE(throttle->called_cancel);
    EXPECT_FALSE(mock_provider_.GetIsInstalled());
    EXPECT_FALSE(mock_provider_.GetIsLoaded());

    // Simulate request start.
    EXPECT_EQ(content::NavigationThrottle::DEFER, throttle->WillStartRequest());
    EXPECT_FALSE(throttle->called_resume);
    EXPECT_FALSE(throttle->called_cancel);
    EXPECT_FALSE(mock_provider_.GetIsInstalled());
    EXPECT_FALSE(mock_provider_.GetIsLoaded());

    // Simulate actual install (failure)
    mock_provider_.SimulateAsyncInstall(false);  // !
    EXPECT_FALSE(throttle->called_resume);
    EXPECT_TRUE(throttle->called_cancel);
    EXPECT_FALSE(mock_provider_.GetIsInstalled());
    EXPECT_FALSE(mock_provider_.GetIsLoaded());
    EXPECT_EQ(content::NavigationThrottle::CANCEL,
              throttle->cancel_result.action());
    EXPECT_EQ(net::ERR_CONNECTION_FAILED,
              throttle->cancel_result.net_error_code());
    EXPECT_TRUE(throttle->cancel_result.error_page_content().has_value());
    std::string page_content =
        throttle->cancel_result.error_page_content().value();
    EXPECT_NE(std::string::npos, page_content.find("<!DOCTYPE html"));
  }
}

}  // namespace dev_ui