910e62b5创建于 1月15日历史提交
// Copyright 2012 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/external_protocol/external_protocol_handler.h"

#include <memory>
#include <utility>

#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/jni_android.h"
#include "components/navigation_interception/intercept_navigation_delegate.h"
#else
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#endif  // BUILDFLAG(IS_ANDROID)

class FakeExternalProtocolHandlerWorker
    : public shell_integration::DefaultSchemeClientWorker {
 public:
  FakeExternalProtocolHandlerWorker(
      const GURL& url,
      shell_integration::DefaultWebClientState os_state,
      const std::u16string& program_name)
      : shell_integration::DefaultSchemeClientWorker(url),
        os_state_(os_state),
        program_name_(program_name) {}

 private:
  ~FakeExternalProtocolHandlerWorker() override = default;

  shell_integration::DefaultWebClientState CheckIsDefaultImpl() override {
    return os_state_;
  }

  std::u16string GetDefaultClientNameImpl() override { return program_name_; }

  void SetAsDefaultImpl(base::OnceClosure on_finished_callback) override {
    std::move(on_finished_callback).Run();
  }

  shell_integration::DefaultWebClientState os_state_;
  std::u16string program_name_;
};

class FakeExternalProtocolHandlerDelegate
    : public ExternalProtocolHandler::Delegate {
 public:
  explicit FakeExternalProtocolHandlerDelegate(base::OnceClosure on_complete)
      : block_state_(ExternalProtocolHandler::BLOCK),
        os_state_(shell_integration::UNKNOWN_DEFAULT),
        complete_on_launch_(false),
        has_launched_(false),
        has_prompted_(false),
        on_complete_(std::move(on_complete)),
        program_name_(u"") {}

  scoped_refptr<shell_integration::DefaultSchemeClientWorker> CreateShellWorker(
      const GURL& url) override {
    return new FakeExternalProtocolHandlerWorker(url, os_state_, program_name_);
  }

  ExternalProtocolHandler::BlockState GetBlockState(const std::string& scheme,
                                                    Profile* profile) override {
    return block_state_;
  }

  void BlockRequest() override {
    EXPECT_TRUE(block_state_ == ExternalProtocolHandler::BLOCK ||
                os_state_ == shell_integration::IS_DEFAULT);
    has_blocked_ = true;
    if (on_complete_)
      std::move(on_complete_).Run();
  }

  void RunExternalProtocolDialog(
      const GURL& url,
      content::WebContents* web_contents,
      ui::PageTransition page_transition,
      bool has_user_gesture,
      const std::optional<url::Origin>& initiating_origin,
      const std::u16string& program_name) override {
    EXPECT_EQ(block_state_, ExternalProtocolHandler::UNKNOWN);
    EXPECT_NE(os_state_, shell_integration::IS_DEFAULT);
    EXPECT_EQ(program_name_, program_name);
    has_prompted_ = true;
    launch_or_prompt_url_ = url;
    initiating_origin_ = initiating_origin;
  }

  void LaunchUrlWithoutSecurityCheck(
      const GURL& url,
      content::WebContents* web_contents) override {
    EXPECT_EQ(block_state_, ExternalProtocolHandler::DONT_BLOCK);
    EXPECT_NE(os_state_, shell_integration::IS_DEFAULT);
    has_launched_ = true;
    launch_or_prompt_url_ = url;
    if (complete_on_launch_ && on_complete_)
      std::move(on_complete_).Run();
  }

  void FinishedProcessingCheck() override {
    if (on_complete_)
      std::move(on_complete_).Run();
  }

  void ReportExternalAppRedirectToSafeBrowsing(
      const GURL& url,
      content::WebContents* web_contents) override {
    reported_to_safe_browsing_ = true;
  }

  void set_os_state(shell_integration::DefaultWebClientState value) {
    os_state_ = value;
  }

  void set_program_name(const std::u16string& value) { program_name_ = value; }

  void set_block_state(ExternalProtocolHandler::BlockState value) {
    block_state_ = value;
  }

  // Set this to true if you need the test to be completed upon calling into
  // LaunchUrlWithoutSecurityCheck which is the case when testing
  // ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck.
  void set_complete_on_launch(bool value) { complete_on_launch_ = value; }

  bool has_launched() { return has_launched_; }
  bool has_prompted() { return has_prompted_; }
  bool has_blocked() { return has_blocked_; }
  bool has_reported_to_safe_browsing() { return reported_to_safe_browsing_; }
  const std::optional<url::Origin>& initiating_origin() {
    return initiating_origin_;
  }

  const std::string& launch_or_prompt_url() {
    return launch_or_prompt_url_.spec();
  }

 private:
  ExternalProtocolHandler::BlockState block_state_;
  shell_integration::DefaultWebClientState os_state_;
  bool complete_on_launch_ = false;
  bool has_launched_ = false;
  bool has_prompted_ = false;
  bool has_blocked_ = false;
  bool reported_to_safe_browsing_ = false;
  GURL launch_or_prompt_url_;
  std::optional<url::Origin> initiating_origin_;
  base::OnceClosure on_complete_;
  std::u16string program_name_;
};

class ExternalProtocolHandlerTest : public testing::Test {
 public:
  content::WebContents* GetWebContents() const { return web_contents_.get(); }

 protected:
  ExternalProtocolHandlerTest() : delegate_(run_loop_.QuitClosure()) {}

  void SetUp() override {
    profile_ = std::make_unique<TestingProfile>();
    rvh_test_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();
    web_contents_ = content::WebContentsTester::CreateTestWebContents(
        profile_.get(), nullptr);
#if !BUILDFLAG(IS_ANDROID)
    web_modal::WebContentsModalDialogManager::CreateForWebContents(
        web_contents_.get());
#endif  // !BUILDFLAG(IS_ANDROID)
  }

  void TearDown() override {
    // Ensure that g_accept_requests gets set back to true after test execution.
    ExternalProtocolHandler::PermitLaunchUrl();
  }

  enum class Action { PROMPT, LAUNCH, BLOCK, NONE };

  void DoTest(ExternalProtocolHandler::BlockState block_state,
              shell_integration::DefaultWebClientState os_state,
              Action expected_action) {
    DoTest(block_state, os_state, expected_action, GURL("mailto:test@test.com"),
           url::Origin::Create(GURL("https://example.test")),
           url::Origin::Create(GURL("https://precursor.test")), u"TestApp");
  }

  // Launches |url| in the current WebContents and checks that the
  // ExternalProtocolHandler's delegate is called with the correct action (as
  // given in |expected_action|). |initiating_origin| is passed to the
  // ExternalProtocolHandler to attribute the request to launch the URL to a
  // particular site. If |initiating_origin| is opaque (in production, an
  // example would be a sandboxed iframe), then the delegate should be passed
  // the origin's precursor origin. The precursor origin is the origin that
  // created |initiating_origin|, and the expected precursor origin, if any, is
  // provided in |expected_initiating_precursor_origin|.
  void DoTest(ExternalProtocolHandler::BlockState block_state,
              shell_integration::DefaultWebClientState os_state,
              Action expected_action,
              const GURL& url,
              const url::Origin& initiating_origin,
              const url::Origin& expected_initiating_precursor_origin,
              const std::u16string& program_name) {
    EXPECT_FALSE(delegate_.has_prompted());
    EXPECT_FALSE(delegate_.has_launched());
    EXPECT_FALSE(delegate_.has_blocked());
    ExternalProtocolHandler::SetDelegateForTesting(&delegate_);
    delegate_.set_block_state(block_state);
    delegate_.set_os_state(os_state);
    delegate_.set_program_name(program_name);
    ExternalProtocolHandler::LaunchUrl(
        url,
        base::BindRepeating(&ExternalProtocolHandlerTest::GetWebContents,
                            base::Unretained(this)),
        ui::PAGE_TRANSITION_LINK, /*has_user_gesture=*/true,
        /*is_in_fenced_frame_tree=*/false, initiating_origin,
        content::WeakDocumentPtr()
#if BUILDFLAG(IS_ANDROID)
            ,
        nullptr
#endif
    );
    run_loop_.Run();
    ExternalProtocolHandler::SetDelegateForTesting(nullptr);

    EXPECT_EQ(expected_action == Action::PROMPT, delegate_.has_prompted());
    EXPECT_EQ(expected_action == Action::LAUNCH, delegate_.has_launched());
    EXPECT_EQ(expected_action == Action::LAUNCH,
              delegate_.has_reported_to_safe_browsing());
    EXPECT_EQ(expected_action == Action::BLOCK, delegate_.has_blocked());
    if (expected_action == Action::PROMPT) {
      ASSERT_TRUE(delegate_.initiating_origin().has_value());
      if (initiating_origin.opaque()) {
        EXPECT_EQ(expected_initiating_precursor_origin,
                  delegate_.initiating_origin().value());
      } else {
        EXPECT_EQ(initiating_origin, delegate_.initiating_origin().value());
      }
    } else {
      EXPECT_FALSE(delegate_.initiating_origin().has_value());
    }
  }

  content::BrowserTaskEnvironment task_environment_;

  base::RunLoop run_loop_;
  FakeExternalProtocolHandlerDelegate delegate_;

  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
  std::unique_ptr<content::WebContents> web_contents_;
};

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeBlockedChromeDefault) {
  DoTest(ExternalProtocolHandler::BLOCK, shell_integration::IS_DEFAULT,
         Action::BLOCK);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeBlockedChromeNotDefault) {
  DoTest(ExternalProtocolHandler::BLOCK, shell_integration::NOT_DEFAULT,
         Action::BLOCK);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeBlockedChromeUnknown) {
  DoTest(ExternalProtocolHandler::BLOCK, shell_integration::UNKNOWN_DEFAULT,
         Action::BLOCK);
}

TEST_F(ExternalProtocolHandlerTest,
       TestLaunchSchemeBlockedChromeOtherModeDefault) {
  DoTest(ExternalProtocolHandler::BLOCK,
         shell_integration::OTHER_MODE_IS_DEFAULT, Action::BLOCK);
}

// Android doesn't use the external protocol dialog.
#if !BUILDFLAG(IS_ANDROID)

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnBlockedChromeDefault) {
  DoTest(ExternalProtocolHandler::DONT_BLOCK, shell_integration::IS_DEFAULT,
         Action::BLOCK);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnBlockedChromeNotDefault) {
  DoTest(ExternalProtocolHandler::DONT_BLOCK, shell_integration::NOT_DEFAULT,
         Action::LAUNCH);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnBlockedChromeUnknown) {
  DoTest(ExternalProtocolHandler::DONT_BLOCK,
         shell_integration::UNKNOWN_DEFAULT, Action::LAUNCH);
}

TEST_F(ExternalProtocolHandlerTest,
       TestLaunchSchemeUnBlockedChromeOtherModeDefault) {
  DoTest(ExternalProtocolHandler::DONT_BLOCK,
         shell_integration::OTHER_MODE_IS_DEFAULT, Action::LAUNCH);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnknownChromeDefault) {
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::IS_DEFAULT,
         Action::BLOCK);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnknownChromeNotDefault) {
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::NOT_DEFAULT,
         Action::PROMPT);
}

TEST_F(ExternalProtocolHandlerTest, TestLaunchSchemeUnknownChromeUnknown) {
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::UNKNOWN_DEFAULT,
         Action::PROMPT);
}

TEST_F(ExternalProtocolHandlerTest,
       TestLaunchSchemeUnknownChromeOtherModeDefault) {
  DoTest(ExternalProtocolHandler::UNKNOWN,
         shell_integration::OTHER_MODE_IS_DEFAULT, Action::PROMPT);
}

TEST_F(ExternalProtocolHandlerTest, TestUrlEscape) {
  GURL url("alert:test message\" --bad%2B\r\n 文本 \"file");
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::NOT_DEFAULT,
         Action::PROMPT, url, url::Origin::Create(GURL("https://example.test")),
         url::Origin::Create(GURL("https://precursor.test")), u"TestApp");
  // Expect that the "\r\n" has been removed, and all other illegal URL
  // characters have been escaped.
  EXPECT_EQ("alert:test%20message%22%20--bad%2B%20%E6%96%87%E6%9C%AC%20%22file",
            delegate_.launch_or_prompt_url());
}

TEST_F(ExternalProtocolHandlerTest, TestNoDialogWithoutManager) {
  // WebContents without a dialog manager should not prompt crbug.com/40064553.
  GetWebContents()->SetUserData(
      web_modal::WebContentsModalDialogManager::UserDataKey(), nullptr);
  EXPECT_EQ(nullptr, web_modal::WebContentsModalDialogManager::FromWebContents(
                         GetWebContents()));
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::UNKNOWN_DEFAULT,
         Action::NONE);
}

#else  // if !BUILDFLAG(IS_ANDROID)

class MockInterceptNavigationDelegate
    : public navigation_interception::InterceptNavigationDelegate {
 public:
  MockInterceptNavigationDelegate()
      : InterceptNavigationDelegate(base::android::AttachCurrentThread(),
                                    nullptr) {}

  MOCK_METHOD5(HandleSubframeExternalProtocol,
               void(const GURL&,
                    ui::PageTransition,
                    bool,
                    const std::optional<url::Origin>&,
                    mojo::PendingRemote<network::mojom::URLLoaderFactory>*));
};

TEST_F(ExternalProtocolHandlerTest, TestUrlEscape_Android) {
  GURL url("alert:test message\" --bad%2B\r\n 文本 \"file");
  GURL escaped(
      "alert:test%20message%22%20--bad%2B%20%E6%96%87%E6%9C%AC%20%22file");

  auto delegate = std::make_unique<MockInterceptNavigationDelegate>();

  url::Origin precursor_origin =
      url::Origin::Create(GURL("https://precursor.test"));
  url::Origin opaque_origin =
      url::Origin::Resolve(GURL("data:text/html,hi"), precursor_origin);

  EXPECT_CALL(*delegate.get(),
              HandleSubframeExternalProtocol(testing::Eq(escaped), testing::_,
                                             true, testing::Eq(opaque_origin),
                                             testing::Eq(nullptr)));

  navigation_interception::InterceptNavigationDelegate::Associate(
      web_contents_.get(), std::move(delegate));

  ExternalProtocolHandler::LaunchUrl(
      url,
      base::BindRepeating(&ExternalProtocolHandlerTest::GetWebContents,
                          base::Unretained(this)),
      ui::PAGE_TRANSITION_LINK, /*has_user_gesture=*/true,
      /*is_in_fenced_frame_tree=*/false, opaque_origin,
      content::WeakDocumentPtr(), nullptr);
}

#endif  // if !BUILDFLAG(IS_ANDROID)

TEST_F(ExternalProtocolHandlerTest, TestUrlEscapeNoChecks) {
  GURL url("alert:test message\" --bad%2B\r\n 文本 \"file");

  EXPECT_FALSE(delegate_.has_prompted());
  EXPECT_FALSE(delegate_.has_launched());
  EXPECT_FALSE(delegate_.has_blocked());
  ExternalProtocolHandler::SetDelegateForTesting(&delegate_);
  delegate_.set_block_state(ExternalProtocolHandler::DONT_BLOCK);
  delegate_.set_os_state(shell_integration::NOT_DEFAULT);
  delegate_.set_complete_on_launch(true);
  ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(
      url, web_contents_.get(), content::WeakDocumentPtr());
  run_loop_.Run();
  ExternalProtocolHandler::SetDelegateForTesting(nullptr);

  EXPECT_FALSE(delegate_.has_prompted());
  EXPECT_TRUE(delegate_.has_launched());
  EXPECT_FALSE(delegate_.has_blocked());
  EXPECT_FALSE(delegate_.initiating_origin().has_value());

  // Expect that the "\r\n" has been removed, and all other illegal URL
  // characters have been escaped.
  EXPECT_EQ("alert:test%20message%22%20--bad%2B%20%E6%96%87%E6%9C%AC%20%22file",
            delegate_.launch_or_prompt_url());
}

TEST_F(ExternalProtocolHandlerTest, TestGetBlockStateUnknown) {
  base::HistogramTester histogram_tester;

  ExternalProtocolHandler::BlockState block_state =
      ExternalProtocolHandler::GetBlockState("tel", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state =
      ExternalProtocolHandler::GetBlockState("news", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state =
      ExternalProtocolHandler::GetBlockState("snews", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);

  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kPrompt, 3);
}

TEST_F(ExternalProtocolHandlerTest, TestGetBlockStateDefaultBlock) {
  base::HistogramTester histogram_tester;

  ExternalProtocolHandler::BlockState block_state =
      ExternalProtocolHandler::GetBlockState("afp", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::BLOCK, block_state);
  block_state =
      ExternalProtocolHandler::GetBlockState("res", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::BLOCK, block_state);
  block_state = ExternalProtocolHandler::GetBlockState("ie.http", nullptr,
                                                       profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::BLOCK, block_state);
  EXPECT_EQ("mk", GURL("mk:@FooBar:ie.http:res://foo.bar/baz").GetScheme());
  block_state =
      ExternalProtocolHandler::GetBlockState("mk", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::BLOCK, block_state);

  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kDeniedDefault, 4);
}

TEST_F(ExternalProtocolHandlerTest, TestGetBlockStateDefaultDontBlock) {
  base::HistogramTester histogram_tester;

  ExternalProtocolHandler::BlockState block_state =
      ExternalProtocolHandler::GetBlockState("mailto", nullptr, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::DONT_BLOCK, block_state);

  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kAllowedDefaultMail, 1);
}

TEST_F(ExternalProtocolHandlerTest, TestSetBlockState) {
  base::HistogramTester histogram_tester;

  const char kScheme_1[] = "custom1";
  const char kScheme_2[] = "custom2";
  url::Origin example_origin_1 =
      url::Origin::Create(GURL("https://example.test"));
  url::Origin example_origin_2 =
      url::Origin::Create(GURL("https://example2.test"));
  ExternalProtocolHandler::BlockState block_state =
      ExternalProtocolHandler::GetBlockState(kScheme_1, &example_origin_1,
                                             profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kPrompt, 4);

  // Set to DONT_BLOCK for {kScheme_1, example_origin_1}, and make sure it is
  // written to prefs.
  ExternalProtocolHandler::SetBlockState(kScheme_1, example_origin_1,
                                         ExternalProtocolHandler::DONT_BLOCK,
                                         profile_.get());
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::DONT_BLOCK, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kAllowedByPreference, 1);
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kPrompt, 7);

  // Set to DONT_BLOCK for {kScheme_2, example_origin_2}, and make sure it is
  // written to prefs independently of {kScheme_1, example_origin_1}.
  ExternalProtocolHandler::SetBlockState(kScheme_2, example_origin_2,
                                         ExternalProtocolHandler::DONT_BLOCK,
                                         profile_.get());
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::DONT_BLOCK, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::DONT_BLOCK, block_state);
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kAllowedByPreference, 3);
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kPrompt, 9);

  const base::Value::Dict& protocol_origin_pairs =
      profile_->GetPrefs()->GetDict(
          prefs::kProtocolHandlerPerOriginAllowedProtocols);
  base::Value::Dict expected_allowed_protocols_for_example_origin_1;
  expected_allowed_protocols_for_example_origin_1.Set(kScheme_1, true);
  const base::Value::Dict* allowed_protocols_for_example_origin_1 =
      protocol_origin_pairs.FindDict(example_origin_1.Serialize());
  EXPECT_EQ(expected_allowed_protocols_for_example_origin_1,
            *allowed_protocols_for_example_origin_1);
  base::Value::Dict expected_allowed_protocols_for_example_origin_2;
  expected_allowed_protocols_for_example_origin_2.Set(kScheme_2, true);
  const base::Value::Dict* allowed_protocols_for_example_origin_2 =
      protocol_origin_pairs.FindDict(example_origin_2.Serialize());
  EXPECT_EQ(expected_allowed_protocols_for_example_origin_2,
            *allowed_protocols_for_example_origin_2);

  // Note: BLOCK is no longer supported (it triggers a DCHECK in SetBlockState;
  // see https://crbug.com/724919).

  // Set back to UNKNOWN, and make sure this results in an empty dictionary.
  ExternalProtocolHandler::SetBlockState(kScheme_1, example_origin_1,
                                         ExternalProtocolHandler::UNKNOWN,
                                         profile_.get());
  ExternalProtocolHandler::SetBlockState(kScheme_2, example_origin_2,
                                         ExternalProtocolHandler::UNKNOWN,
                                         profile_.get());
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_1, &example_origin_1, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme_2, &example_origin_2, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kAllowedByPreference, 3);
  histogram_tester.ExpectBucketCount(
      ExternalProtocolHandler::kBlockStateMetric,
      ExternalProtocolHandler::BlockStateMetric::kPrompt, 11);
}

TEST_F(ExternalProtocolHandlerTest, TestSetBlockStateWithUntrustowrthyOrigin) {
  const char kScheme[] = "custom";
  // This origin is untrustworthy because it is "http://"
  url::Origin untrustworthy_origin =
      url::Origin::Create(GURL("http://example.test"));

  ExternalProtocolHandler::BlockState block_state =
      ExternalProtocolHandler::GetBlockState(kScheme, &untrustworthy_origin,
                                             profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());

  // Set to DONT_BLOCK for {kScheme, untrustworthy_origin}, and make sure it is
  // not written to prefs. Calling SetBlockState with a non-trustworthy origin
  // should not persist any state to prefs.
  ExternalProtocolHandler::SetBlockState(kScheme, untrustworthy_origin,
                                         ExternalProtocolHandler::DONT_BLOCK,
                                         profile_.get());
  block_state = ExternalProtocolHandler::GetBlockState(
      kScheme, &untrustworthy_origin, profile_.get());
  EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
  EXPECT_TRUE(profile_->GetPrefs()
                  ->GetDict(prefs::kProtocolHandlerPerOriginAllowedProtocols)
                  .empty());
}

#if !BUILDFLAG(IS_ANDROID)
// Test that an opaque initiating origin gets transformed to its precursor
// origin when the dialog is shown.
TEST_F(ExternalProtocolHandlerTest, TestOpaqueInitiatingOrigin) {
  url::Origin precursor_origin =
      url::Origin::Create(GURL("https://precursor.test"));
  url::Origin opaque_origin =
      url::Origin::Resolve(GURL("data:text/html,hi"), precursor_origin);
  DoTest(ExternalProtocolHandler::UNKNOWN, shell_integration::NOT_DEFAULT,
         Action::PROMPT, GURL("mailto:test@test.test"), opaque_origin,
         precursor_origin, u"TestApp");
}
#endif