// 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.
//
// This test creates a safebrowsing service using test safebrowsing database
// and a test protocol manager. It is used to test logics in safebrowsing
// service.

#include "chrome/browser/safe_browsing/safe_browsing_service.h"

#include <map>
#include <set>
#include <string>
#include <utility>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/sha1.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/field_trial.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/thread_test_helper.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/extensions/browsertest_util.h"
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/chrome_ping_manager_factory.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/browser/safe_browsing/v5_get_hash_protocol_manager_factory.h"
#include "chrome/browser/safe_browsing/v5_search_hashes_cache_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/error_page/content/browser/net_error_auto_reloader.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/content/browser/safe_browsing_blocking_page.h"
#include "components/safe_browsing/content/browser/ui_manager.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/db/sb_database.h"
#include "components/safe_browsing/core/browser/db/test_database_manager.h"
#include "components/safe_browsing/core/browser/db/util.h"
#include "components/safe_browsing/core/browser/db/v4_get_hash_protocol_manager.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/browser/db/v4_test_util.h"
#include "components/safe_browsing/core/browser/db/v5_get_hash_protocol_manager.h"
#include "components/safe_browsing/core/browser/db/v5_search_hashes_cache.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/security_interstitials/core/controller_client.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "crypto/sha2.h"
#include "net/cookies/cookie_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/websockets/websocket_handshake_challenge.h"
#include "net/websockets/websocket_handshake_constants.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "sql/database.h"
#include "sql/statement.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/url_canon.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_switches.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#endif

#if !BUILDFLAG(SAFE_BROWSING_DB_LOCAL)
#error This test requires SAFE_BROWSING_DB_LOCAL.
#endif

using content::WebContents;
using ::testing::_;
using ::testing::Mock;
using ::testing::StrictMock;

namespace safe_browsing {

namespace {

const char kEmptyPage[] = "/empty.html";
const char kMalwareFile[] = "/downloads/dangerous/dangerous.exe";
const char kMalwarePage[] = "/safe_browsing/malware.html";
const char kMalwareJsRequestPage[] = "/safe_browsing/malware_js_request.html";
const char kMalwareWebSocketPath[] = "/safe_browsing/malware-ws";
const char kPrefetchMalwarePage[] = "/safe_browsing/prefetch_malware.html";
const char kBillingInterstitialPage[] = "/safe_browsing/billing.html";

// This is not a proper WebSocket server. It does the minimum necessary to make
// the browser think the handshake succeeded.
// TODO(ricea): This could probably go in //net somewhere.
class QuasiWebSocketHttpResponse : public net::test_server::HttpResponse {
 public:
  explicit QuasiWebSocketHttpResponse(
      const net::test_server::HttpRequest& request) {
    const auto it = request.headers.find("Sec-WebSocket-Key");
    const std::string key =
        it == request.headers.end() ? std::string() : it->second;
    accept_hash_ = net::ComputeSecWebSocketAccept(key);
  }
  ~QuasiWebSocketHttpResponse() override = default;

  void SendResponse(
      base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
    base::StringPairs response_headers = {
        {"Upgrade", "WebSocket"},
        {"Connection", "Upgrade"},
        {"Sec-WebSocket-Accept", accept_hash_}};

    delegate->SendResponseHeaders(net::HTTP_SWITCHING_PROTOCOLS,
                                  "WebSocket Protocol Handshake",
                                  response_headers);
    // Never call FinishResponse(). The connection should stay open.
  }

 private:
  std::string accept_hash_;
};

std::unique_ptr<net::test_server::HttpResponse> HandleWebSocketRequests(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != kMalwareWebSocketPath) {
    return nullptr;
  }

  return std::make_unique<QuasiWebSocketHttpResponse>(request);
}

enum class ContextType { kWindow, kWorker, kSharedWorker, kServiceWorker };

enum class JsRequestType {
  kWebSocket,
  // Load a URL using the Fetch API.
  kFetch
};

struct JsRequestTestParam {
  JsRequestTestParam(ContextType in_context_type, JsRequestType in_request_type)
      : context_type(in_context_type), request_type(in_request_type) {}

  ContextType context_type;
  JsRequestType request_type;
};

std::string ContextTypeToString(ContextType context_type) {
  switch (context_type) {
    case ContextType::kWindow:
      return "window";
    case ContextType::kWorker:
      return "worker";
    case ContextType::kSharedWorker:
      return "shared-worker";
    case ContextType::kServiceWorker:
      return "service-worker";
  }

  NOTREACHED();
}

std::string JsRequestTypeToString(JsRequestType request_type) {
  switch (request_type) {
    case JsRequestType::kWebSocket:
      return "websocket";
    case JsRequestType::kFetch:
      return "fetch";
  }

  NOTREACHED();
}

// Return a new URL with ?contextType=<context_type>&requestType=<request_type>
// appended.
GURL AddJsRequestParam(const GURL& base_url, const JsRequestTestParam& param) {
  GURL::Replacements add_query;
  std::string query =
      "contextType=" + ContextTypeToString(param.context_type) +
      "&requestType=" + JsRequestTypeToString(param.request_type);
  add_query.SetQueryStr(query);
  return base_url.ReplaceComponents(add_query);
}

// Given the URL of the malware_js_request.html page, calculate the URL of the
// WebSocket it will fetch.
GURL ConstructWebSocketURL(const GURL& main_url) {
  // This constructs the URL with the same logic as malware_js_request.html.
  GURL resolved = main_url.Resolve(kMalwareWebSocketPath);
  GURL::Replacements replace_scheme;
  replace_scheme.SetSchemeStr("ws");
  return resolved.ReplaceComponents(replace_scheme);
}

GURL ConstructJsRequestURL(const GURL& base_url, JsRequestType request_type) {
  switch (request_type) {
    case JsRequestType::kWebSocket:
      return ConstructWebSocketURL(base_url);
    case JsRequestType::kFetch:
      return base_url.Resolve(kMalwarePage);
  }
  NOTREACHED();
}

// Navigate |browser| to |url| and wait for the title to change to "NOT BLOCKED"
// or "ERROR". This is specific to the tests using malware_js_request.html.
// Returns the new title.
std::string JsRequestTestNavigateAndWaitForTitle(
    BrowserWindowInterface* browser,
    const GURL& url) {
  std::u16string expected_title = u"ERROR";
  content::TitleWatcher title_watcher(
      browser->GetTabStripModel()->GetActiveWebContents(), expected_title);
  title_watcher.AlsoWaitForTitle(u"NOT BLOCKED");

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
  return base::UTF16ToUTF8(title_watcher.WaitAndGetTitle());
}

class FakeSafeBrowsingUIManager : public TestSafeBrowsingUIManager {
 public:
  void MaybeSendClientSafeBrowsingWarningShownReport(
      std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest> report,
      content::WebContents* web_contents) override {
    EXPECT_FALSE(got_warning_shown_report_);
    got_warning_shown_report_ = true;
    warning_shown_report_ = *(report.get());
    SafeBrowsingUIManager::MaybeSendClientSafeBrowsingWarningShownReport(
        std::move(report), web_contents);
  }

  bool got_warning_shown_report_ = false;
  safe_browsing::ClientSafeBrowsingReportRequest warning_shown_report_;

 private:
  ~FakeSafeBrowsingUIManager() override = default;
};

MATCHER_P(IsUnsafeResourceFor, url, "") {
  return (arg.url.spec() == url.spec() &&
          arg.threat_type != SBThreatType::SB_THREAT_TYPE_SAFE);
}

class ServiceEnabledHelper : public base::ThreadTestHelper {
 public:
  ServiceEnabledHelper(
      SafeBrowsingService* service,
      bool enabled,
      scoped_refptr<base::SingleThreadTaskRunner> target_thread)
      : base::ThreadTestHelper(target_thread),
        service_(service),
        expected_enabled_(enabled) {}

  void RunTest() override {
    set_test_result(service_->enabled() == expected_enabled_);
  }

 private:
  ~ServiceEnabledHelper() override = default;

  scoped_refptr<SafeBrowsingService> service_;
  const bool expected_enabled_;
};

class TestSBClient : public base::RefCountedThreadSafe<TestSBClient>,
                     public SafeBrowsingDatabaseManager::Client {
 public:
  TestSBClient() : TestSBClient(base::WeakPtr<V5GetHashProtocolManager>()) {}

  explicit TestSBClient(
      base::WeakPtr<V5GetHashProtocolManager> v5_protocol_manager)
      : SafeBrowsingDatabaseManager::Client(GetPassKeyForTesting()),
        threat_type_(SB_THREAT_TYPE_SAFE),
        safe_browsing_service_(g_browser_process->safe_browsing_service()),
        v5_protocol_manager_(v5_protocol_manager) {}

  TestSBClient(const TestSBClient&) = delete;
  TestSBClient& operator=(const TestSBClient&) = delete;

  base::WeakPtr<V5GetHashProtocolManager> GetV5GetHashProtocolManager()
      override {
    return v5_protocol_manager_;
  }

  SBThreatType GetThreatType() const { return threat_type_; }

  void CheckDownloadUrl(const std::vector<GURL>& url_chain) {
    base::RunLoop loop;
    bool synchronous_safe_signal =
        safe_browsing_service_->database_manager()->CheckDownloadUrl(url_chain,
                                                                     this);
    if (synchronous_safe_signal) {
      threat_type_ = SB_THREAT_TYPE_SAFE;
      content::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(&TestSBClient::CheckDone, this));
    }
    set_quit_closure(loop.QuitWhenIdleClosure());
    loop.Run();
  }

  void CheckBrowseUrl(const GURL& url) {
    base::RunLoop loop;
    SBThreatTypeSet threat_types = CreateSBThreatTypeSet(
        {SB_THREAT_TYPE_URL_PHISHING, SB_THREAT_TYPE_URL_MALWARE,
         SB_THREAT_TYPE_URL_UNWANTED, SB_THREAT_TYPE_BILLING});

    // The async CheckDone() hook will not be called when we have a synchronous
    // safe signal, handle it right away.
    bool synchronous_safe_signal =
        safe_browsing_service_->database_manager()->CheckBrowseUrl(
            url, threat_types, this, CheckBrowseUrlType::kHashDatabase);
    if (synchronous_safe_signal) {
      threat_type_ = SB_THREAT_TYPE_SAFE;
      content::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(&TestSBClient::CheckDone, this));
    }
    set_quit_closure(loop.QuitWhenIdleClosure());
    loop.Run();
  }

 private:
  using enum SBThreatType;

  friend class base::RefCountedThreadSafe<TestSBClient>;
  ~TestSBClient() override = default;

  void set_quit_closure(base::OnceClosure quit_closure) {
    quit_closure_ = std::move(quit_closure);
  }

  // Called when the result of checking a download URL is known.
  void OnCheckDownloadUrlResult(const std::vector<GURL>& /* url_chain */,
                                SBThreatType threat_type) override {
    threat_type_ = threat_type;
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&TestSBClient::CheckDone, this));
  }

  // Called when the result of checking a browse URL is known.
  void OnCheckBrowseUrlResult(const GURL& /* url */,
                              SBThreatType threat_type) override {
    threat_type_ = threat_type;
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&TestSBClient::CheckDone, this));
  }

  void CheckDone() { std::move(quit_closure_).Run(); }

  SBThreatType threat_type_;
  raw_ptr<SafeBrowsingService> safe_browsing_service_;
  base::OnceClosure quit_closure_;
  base::WeakPtr<V5GetHashProtocolManager> v5_protocol_manager_;
};

}  // namespace

// Tests the safe browsing blocking page in a browser.
// Base class for SafeBrowsingService tests supporting both v4 and v5 lists.
class SBSafeBrowsingServiceTestBase : public InProcessBrowserTest {
 public:
  explicit SBSafeBrowsingServiceTestBase(bool use_v5,
                                         bool enable_warning_shown_reports)
      : use_v5_(use_v5) {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;
    if (use_v5_) {
      enabled_features.push_back(safe_browsing::kLocalListsUseSBv5);
    } else {
      disabled_features.push_back(safe_browsing::kLocalListsUseSBv5);
    }
    if (enable_warning_shown_reports) {
      enabled_features.push_back(
          safe_browsing::kCreateWarningShownClientSafeBrowsingReports);
    } else {
      disabled_features.push_back(
          safe_browsing::kCreateWarningShownClientSafeBrowsingReports);
    }
    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }

  SBSafeBrowsingServiceTestBase(const SBSafeBrowsingServiceTestBase&) = delete;
  SBSafeBrowsingServiceTestBase& operator=(
      const SBSafeBrowsingServiceTestBase&) = delete;

  // Returns whether v5 local lists are enabled for this test.
  bool use_v5() const { return use_v5_; }

  void SetUp() override {
    sb_factory_ = std::make_unique<TestSafeBrowsingServiceFactory>();
    sb_factory_->SetTestUIManager(new FakeSafeBrowsingUIManager());
    sb_factory_->UseSBLocalDatabaseManager();
    SafeBrowsingService::RegisterFactory(sb_factory_.get());

    store_factory_ = new TestV4StoreFactory();
    SBDatabase::RegisterStoreFactoryForTest(
        base::WrapUnique(store_factory_.get()));

    sb_db_factory_ = new TestSBDatabaseFactory();
    SBDatabase::RegisterDatabaseFactoryForTest(
        base::WrapUnique(sb_db_factory_.get()));

    if (!use_v5_) {
      v4_get_hash_factory_ = new TestV4GetHashProtocolManagerFactory();
      V4GetHashProtocolManager::RegisterFactory(
          base::WrapUnique(v4_get_hash_factory_.get()));
    }

    InProcessBrowserTest::SetUp();
  }

  void TearDown() override {
    InProcessBrowserTest::TearDown();

    // Unregister test factories after InProcessBrowserTest::TearDown
    // (which destructs SafeBrowsingService).
    if (!use_v5_) {
      V4GetHashProtocolManager::RegisterFactory(nullptr);
    }
    SBDatabase::RegisterDatabaseFactoryForTest(nullptr);
    SBDatabase::RegisterStoreFactoryForTest(nullptr);
    SafeBrowsingService::RegisterFactory(nullptr);
  }

  base::WeakPtr<V5GetHashProtocolManager> GetV5ProtocolManager() {
    if (!use_v5_) {
      return nullptr;
    }
    return V5GetHashProtocolManagerFactory::GetForProfile(
               browser()->GetProfile())
        ->GetWeakPtr();
  }

  scoped_refptr<TestSBClient> CreateTestSBClient() {
    return new TestSBClient(GetV5ProtocolManager());
  }

  void MarkUrlForListIdUnexpired(const GURL& bad_url,
                                 const ListIdentifier& list_id,
                                 V5::ThreatType threat_type) {
    if (use_v5_) {
      FullHashStr full_hash = SBProtocolManagerUtil::GetFullHash(bad_url);
      while (!sb_db_factory_->IsReady()) {
        content::RunAllTasksUntilIdle();
      }
      sb_db_factory_->MarkPrefixAsBad(list_id, full_hash);

      auto* cache =
          V5SearchHashesCacheFactory::GetForProfile(browser()->GetProfile());
      CHECK(cache);
      cache->CacheArtificialV5SearchHashesLookupVerdict(bad_url, threat_type,
                                                        /*is_warn_only=*/false);
    } else {
      ThreatMetadata metadata;
      FullHashInfo full_hash_info =
          GetFullHashInfoWithMetadata(bad_url, list_id, metadata);
      while (!sb_db_factory_->IsReady()) {
        content::RunAllTasksUntilIdle();
      }
      sb_db_factory_->MarkPrefixAsBad(list_id, full_hash_info.full_hash);
      v4_get_hash_factory_->AddToFullHashCache(full_hash_info);
    }
  }

  // Sets up the prefix database and the full hash cache to match one of the
  // prefixes for the given URL and metadata.
  void MarkUrlForMalwareUnexpired(const GURL& bad_url) {
    MarkUrlForListIdUnexpired(bad_url, GetUrlMalwareId(),
                              V5::ThreatType::MALWARE);
  }

  // Sets up the prefix database and the full hash cache to match one of the
  // prefixes for the given URL in the UwS store.
  void MarkUrlForUwsUnexpired(const GURL& bad_url) {
    MarkUrlForListIdUnexpired(bad_url, GetUrlUwsId(),
                              V5::ThreatType::UNWANTED_SOFTWARE);
  }

  // Sets up the prefix database and the full hash cache to match one of the
  // prefixes for the given URL in the phishing store.
  void MarkUrlForPhishingUnexpired(const GURL& bad_url) {
    MarkUrlForListIdUnexpired(bad_url, GetUrlSocEngId(),
                              V5::ThreatType::SOCIAL_ENGINEERING);
  }

  // Sets up the prefix database and the full hash cache to match one of the
  // prefixes for the given URL in the malware binary store.
  void MarkUrlForMalwareBinaryUnexpired(const GURL& bad_url) {
    MarkUrlForListIdUnexpired(bad_url, GetUrlMalBinId(),
                              V5::ThreatType::MALICIOUS_BINARY);
  }

  // Sets up the prefix database and the full hash cache to match one of the
  // prefixes for the given URL in the Billing store.
  void MarkUrlForBillingUnexpired(const GURL& bad_url) {
    MarkUrlForListIdUnexpired(bad_url, GetUrlBillingId(),
                              V5::ThreatType::TRICK_TO_BILL);
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
#if BUILDFLAG(IS_CHROMEOS)
    command_line->AppendSwitch(
        ash::switches::kIgnoreUserProfileMappingForTests);
#endif
  }

  void SetUpInProcessBrowserTestFixture() override {
    base::FilePath test_data_dir;
    base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
    embedded_test_server()->RegisterRequestHandler(
        base::BindRepeating(&HandleWebSocketRequests));
    embedded_test_server()->ServeFilesFromDirectory(test_data_dir);
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  bool ShowingInterstitialPage(BrowserWindowInterface* browser) {
    WebContents* contents = browser->GetTabStripModel()->GetActiveWebContents();
    return chrome_browser_interstitials::IsShowingInterstitial(contents);
  }

  bool ShowingInterstitialPage() { return ShowingInterstitialPage(browser()); }

  void SetUpSendingNotificationsAcceptedCSBRR() {
    // Enable enhanced safe browsing for the profile.
    PrefService* prefs = browser()->GetProfile()->GetPrefs();
    prefs->SetBoolean(prefs::kSafeBrowsingEnabled, true);
    prefs->SetBoolean(prefs::kSafeBrowsingEnhanced, true);
    ASSERT_TRUE(IsExtendedReportingEnabled(*prefs));

    // Mark the permission_prompt_origin as allowlisted for the report to be
    // sent.
    ASSERT_TRUE(safe_browsing_service());
    safe_browsing_service()->SetUrlIsAllowlistedForTesting();
  }

  FakeSafeBrowsingUIManager* ui_manager() {
    return static_cast<FakeSafeBrowsingUIManager*>(
        g_browser_process->safe_browsing_service()->ui_manager().get());
  }
  bool got_warning_shown_report() {
    return ui_manager()->got_warning_shown_report_;
  }
  const safe_browsing::ClientSafeBrowsingReportRequest& report() {
    return ui_manager()->warning_shown_report_;
  }

  SafeBrowsingServiceImpl* safe_browsing_service() {
    return static_cast<SafeBrowsingServiceImpl*>(
        g_browser_process->safe_browsing_service());
  }

 protected:
  using enum SBThreatType;

 private:
  // Whether this test is running with v5 safe browsing lists enabled.
  const bool use_v5_;

  std::unique_ptr<TestSafeBrowsingServiceFactory> sb_factory_;
  // Owned by the SBDatabase.
  raw_ptr<TestSBDatabaseFactory, AcrossTasksDanglingUntriaged> sb_db_factory_;
  // Owned by the V4GetHashProtocolManager.
  raw_ptr<TestV4GetHashProtocolManagerFactory, AcrossTasksDanglingUntriaged>
      v4_get_hash_factory_ = nullptr;
  // Owned by the SBDatabase.
  raw_ptr<TestV4StoreFactory, AcrossTasksDanglingUntriaged> store_factory_;
  base::test::ScopedFeatureList scoped_feature_list_;

#if defined(ADDRESS_SANITIZER)
  // TODO(lukasza): https://crbug.com/41463664: Disallow renderer crashes once
  // the bug is fixed.
  content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes_;
#endif
};

// Parameterized fixture for SafeBrowsingService tests running with v4 and v5
// lists.
class SBSafeBrowsingServiceTest : public SBSafeBrowsingServiceTestBase,
                                  public ::testing::WithParamInterface<bool> {
 public:
  // Constructs the parameterized test fixture.
  SBSafeBrowsingServiceTest()
      : SBSafeBrowsingServiceTestBase(GetParam(),
                                      /*enable_warning_shown_reports=*/true) {}
};

// Proceeding through an interstitial should cause it to get allowlisted for
// that user.
IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, MalwareWithAllowlist) {
  GURL url = embedded_test_server()->GetURL(kEmptyPage);

  // After adding the URL to SafeBrowsing database and full hash cache, we
  // should see the interstitial page.
  MarkUrlForMalwareUnexpired(url);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  // There should be an InterstitialPage.
  WebContents* contents = browser()->GetTabStripModel()->GetActiveWebContents();

  security_interstitials::SecurityInterstitialTabHelper* helper =
      security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
          contents);
  security_interstitials::SecurityInterstitialPage* interstitial =
      helper->GetBlockingPageForCurrentlyCommittedNavigationForTesting();
  ASSERT_TRUE(helper);
  // TODO(carlosil): 1 is CMD_PROCEED, this should be changed to the enum
  // values once CommandReceived is changed to accept integers.
  content::TestNavigationObserver observer(contents);
  interstitial->CommandReceived("1");
  observer.WaitForNavigationFinished();
  EXPECT_FALSE(ShowingInterstitialPage());

  // Navigate to kEmptyPage again -- should hit the allowlist this time.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  EXPECT_FALSE(ShowingInterstitialPage());
}

// This test confirms that prefetches don't themselves get the interstitial
// treatment.
IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, Prefetch) {
  GURL url = embedded_test_server()->GetURL(kPrefetchMalwarePage);
  GURL malware_url = embedded_test_server()->GetURL(kMalwarePage);

  // Even though we have added this URI to the SafeBrowsing database and
  // full hash result, we should not see the interstitial page since the
  // only malware was a prefetch target.
  MarkUrlForMalwareUnexpired(malware_url);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  EXPECT_FALSE(ShowingInterstitialPage());
  EXPECT_FALSE(got_warning_shown_report());

  // However, when we navigate to the malware page, we should still get
  // the interstitial.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), malware_url));
  EXPECT_TRUE(ShowingInterstitialPage());
  EXPECT_TRUE(got_warning_shown_report());
}

// Ensure that the referrer information is preserved in the hit report.
IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, MainFrameHitWithReferrer) {
  GURL first_url = embedded_test_server()->GetURL(kEmptyPage);
  GURL bad_url = embedded_test_server()->GetURL(kMalwarePage);

  MarkUrlForMalwareUnexpired(bad_url);

  // Navigate to first, safe page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));
  EXPECT_FALSE(ShowingInterstitialPage());
  EXPECT_FALSE(got_warning_shown_report());

  // Navigate to malware page, should show interstitial and have first page in
  // referrer.
  NavigateParams params(browser(), bad_url, ui::PAGE_TRANSITION_LINK);
  params.referrer.url = first_url;
  ui_test_utils::NavigateToURL(&params);

  EXPECT_TRUE(ShowingInterstitialPage());
  EXPECT_TRUE(got_warning_shown_report());
  EXPECT_EQ(bad_url, report().url());
  EXPECT_EQ(bad_url, report().page_url());
  EXPECT_EQ(first_url, report().referrer_url());
}

///////////////////////////////////////////////////////////////////////////////
// START: These tests use SafeBrowsingService::Client to directly interact with
// SafeBrowsingService.
///////////////////////////////////////////////////////////////////////////////
IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, CheckDownloadUrl) {
  GURL badbin_url = embedded_test_server()->GetURL(kMalwareFile);
  std::vector<GURL> badbin_urls(1, badbin_url);

  scoped_refptr<TestSBClient> client(CreateTestSBClient());
  client->CheckDownloadUrl(badbin_urls);

  // Since badbin_url is not in database, it is considered to be safe.
  EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

  MarkUrlForMalwareBinaryUnexpired(badbin_url);

  client->CheckDownloadUrl(badbin_urls);

  // Now, the badbin_url is not safe since it is added to download database.
  EXPECT_EQ(SB_THREAT_TYPE_URL_BINARY_MALWARE, client->GetThreatType());
}

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, CheckUnwantedSoftwareUrl) {
  const GURL bad_url = embedded_test_server()->GetURL(kMalwareFile);
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());

    // Since bad_url is not in database, it is considered to be
    // safe.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

    MarkUrlForUwsUnexpired(bad_url);

    // Now, the bad_url is not safe since it is added to download
    // database.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_UNWANTED, client->GetThreatType());
  }

  // The unwantedness should survive across multiple clients.
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_UNWANTED, client->GetThreatType());
  }

  // An unwanted URL also marked as malware should be flagged as malware.
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());

    MarkUrlForMalwareUnexpired(bad_url);

    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_MALWARE, client->GetThreatType());
  }
}

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, CheckBrowseUrl) {
  const GURL bad_url = embedded_test_server()->GetURL(kMalwareFile);
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());

    // Since bad_url is not in database, it is considered to be
    // safe.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

    MarkUrlForMalwareUnexpired(bad_url);

    // Now, the bad_url is not safe since it is added to download
    // database.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_MALWARE, client->GetThreatType());
  }

  // The unwantedness should survive across multiple clients.
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_MALWARE, client->GetThreatType());
  }

  // Adding the unwanted state to an existing malware URL should have no impact
  // (i.e. a malware hit should still prevail).
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());

    MarkUrlForUwsUnexpired(bad_url);

    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_URL_MALWARE, client->GetThreatType());
  }
}

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, CheckBrowseUrlForBilling) {
  const GURL bad_url = embedded_test_server()->GetURL(kBillingInterstitialPage);
  {
    scoped_refptr<TestSBClient> client(CreateTestSBClient());

    // Since the feature isn't enabled and the URL isn't in the database, it is
    // considered to be safe.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

    // Since bad_url is not in database, it is considered to be
    // safe.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

    MarkUrlForBillingUnexpired(bad_url);

    // Now, the bad_url is not safe since it is added to the database.
    client->CheckBrowseUrl(bad_url);
    EXPECT_EQ(SB_THREAT_TYPE_BILLING, client->GetThreatType());
  }
}

// Parameterized fixture for auto reload tests across v4 and v5 lists.
class SafeBrowsingServiceWithAutoReloadTest : public SBSafeBrowsingServiceTest {
 public:
  SafeBrowsingServiceWithAutoReloadTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kEnableAutoReload);
    SBSafeBrowsingServiceTest::SetUpCommandLine(command_line);
  }
};

// SafeBrowsing interstitials should disable autoreload timer.
IN_PROC_BROWSER_TEST_P(SafeBrowsingServiceWithAutoReloadTest,
                       AutoReloadDisabled) {
  GURL url = embedded_test_server()->GetURL(kEmptyPage);
  MarkUrlForMalwareUnexpired(url);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  EXPECT_TRUE(ShowingInterstitialPage());
  WebContents* contents = browser()->GetTabStripModel()->GetActiveWebContents();
  auto* reloader = error_page::NetErrorAutoReloader::FromWebContents(contents);
  const std::optional<base::OneShotTimer>& timer =
      reloader->next_reload_timer_for_testing();
  EXPECT_EQ(std::nullopt, timer);
}

// Parameterized fixture for warning shown reports disabled across v4 and v5
// lists.
class SafeBrowsingServiceWarningShownCSBRRsDisabled
    : public SBSafeBrowsingServiceTestBase,
      public ::testing::WithParamInterface<bool> {
 public:
  // Constructs the test fixture with warning shown reports disabled.
  SafeBrowsingServiceWarningShownCSBRRsDisabled()
      : SBSafeBrowsingServiceTestBase(GetParam(),
                                      /*enable_warning_shown_reports=*/false) {}
};

IN_PROC_BROWSER_TEST_P(SafeBrowsingServiceWarningShownCSBRRsDisabled,
                       CheckWarningShownReportNotSent) {
  GURL bad_url = embedded_test_server()->GetURL(kMalwarePage);
  MarkUrlForMalwareUnexpired(bad_url);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), bad_url));
  EXPECT_TRUE(ShowingInterstitialPage());

  EXPECT_FALSE(got_warning_shown_report());
}

// Parameterised fixture to permit running the same test for Window and Worker
// scopes across v4 and v5 lists.
class SafeBrowsingServiceJsRequestNoInterstitialTest
    : public SBSafeBrowsingServiceTestBase,
      public ::testing::WithParamInterface<
          std::tuple<JsRequestTestParam, bool>> {
 public:
  // Constructs the parameterized test fixture.
  SafeBrowsingServiceJsRequestNoInterstitialTest()
      : SBSafeBrowsingServiceTestBase(std::get<1>(GetParam()),
                                      /*enable_warning_shown_reports=*/true) {}
};

IN_PROC_BROWSER_TEST_P(SafeBrowsingServiceJsRequestNoInterstitialTest,
                       MalwareNotBlocked) {
  GURL base_url = embedded_test_server()->GetURL(kMalwareJsRequestPage);
  JsRequestTestParam param = std::get<0>(GetParam());
  MarkUrlForMalwareUnexpired(
      ConstructJsRequestURL(base_url, param.request_type));

  // Load the parent page after marking the JS request as malware.
  auto new_title = JsRequestTestNavigateAndWaitForTitle(
      browser(), AddJsRequestParam(base_url, param));

  EXPECT_EQ("NOT BLOCKED", new_title);
  EXPECT_FALSE(ShowingInterstitialPage());
  EXPECT_FALSE(got_warning_shown_report());
}

INSTANTIATE_TEST_SUITE_P(
    All,
    SafeBrowsingServiceJsRequestNoInterstitialTest,
    ::testing::Combine(
        ::testing::Values(
            JsRequestTestParam(ContextType::kWindow, JsRequestType::kWebSocket),
            JsRequestTestParam(ContextType::kWorker, JsRequestType::kWebSocket),
            JsRequestTestParam(ContextType::kSharedWorker,
                               JsRequestType::kWebSocket),
            JsRequestTestParam(ContextType::kServiceWorker,
                               JsRequestType::kWebSocket),
            JsRequestTestParam(ContextType::kWindow, JsRequestType::kFetch),
            JsRequestTestParam(ContextType::kWorker, JsRequestType::kFetch),
            JsRequestTestParam(ContextType::kSharedWorker,
                               JsRequestType::kFetch),
            JsRequestTestParam(ContextType::kServiceWorker,
                               JsRequestType::kFetch)),
        ::testing::Bool()));

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest, CheckDownloadUrlRedirects) {
  GURL original_url = embedded_test_server()->GetURL(kEmptyPage);
  GURL badbin_url = embedded_test_server()->GetURL(kMalwareFile);
  GURL final_url = embedded_test_server()->GetURL(kEmptyPage);
  std::vector<GURL> badbin_urls;
  badbin_urls.push_back(original_url);
  badbin_urls.push_back(badbin_url);
  badbin_urls.push_back(final_url);

  scoped_refptr<TestSBClient> client(CreateTestSBClient());
  client->CheckDownloadUrl(badbin_urls);

  // Since badbin_url is not in database, it is considered to be safe.
  EXPECT_EQ(SB_THREAT_TYPE_SAFE, client->GetThreatType());

  MarkUrlForMalwareBinaryUnexpired(badbin_url);

  client->CheckDownloadUrl(badbin_urls);

  // Now, the badbin_url is not safe since it is added to download database.
  EXPECT_EQ(SB_THREAT_TYPE_URL_BINARY_MALWARE, client->GetThreatType());
}

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest,
                       NotificationsAcceptedReportSentWithCorrectOrigins) {
  SetUpSendingNotificationsAcceptedCSBRR();
  network::TestURLLoaderFactory test_url_loader_factory;
  ChromePingManagerFactory::GetForBrowserContext(browser()->GetProfile())
      ->SetURLLoaderFactoryForTesting(
          base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
              &test_url_loader_factory));

  // Define test URLs and duration.
  const GURL kUrl("https://example.com/page_that_requested_permission");
  const GURL kPageUrl("https://example.com/main_frame_url");
  const GURL kPermissionPromptOrigin(
      "https://prompt.example.com/origin_of_prompt");
  const base::TimeDelta kDisplayDuration = base::Seconds(25);

  // Setup report interceptor, which validates the contents of the report.
  base::RunLoop run_loop;
  bool report_sent_and_verified = false;
  test_url_loader_factory.SetInterceptor(base::BindLambdaForTesting(
      [&](const network::ResourceRequest& resource_request) {
        std::string upload_data = network::GetUploadData(resource_request);
        ClientSafeBrowsingReportRequest report;
        ASSERT_TRUE(report.ParseFromString(upload_data));

        // Check report contents.
        EXPECT_EQ(
            report.type(),
            ClientSafeBrowsingReportRequest::NOTIFICATION_PERMISSION_ACCEPTED);
        EXPECT_EQ(report.url(), kUrl.spec());
        EXPECT_EQ(report.page_url(), kPageUrl.spec());
        ASSERT_TRUE(report.has_permission_prompt_info());
        EXPECT_EQ(report.permission_prompt_info().origin(),
                  kPermissionPromptOrigin.spec());
        EXPECT_EQ(report.permission_prompt_info().display_duration_sec(),
                  kDisplayDuration.InSeconds());
        report_sent_and_verified = true;
        run_loop.Quit();
      }));

  // Call the `MaybeSendNotificationsAcceptedReport` method. Note
  // render_frame_host should be nullptr as we're not testing referrer chain
  // here.
  bool result = safe_browsing_service()->MaybeSendNotificationsAcceptedReport(
      /*render_frame_host=*/nullptr, browser()->GetProfile(), kUrl, kPageUrl,
      kPermissionPromptOrigin, kDisplayDuration);
  EXPECT_TRUE(result)
      << "MaybeSendNotificationsAcceptedReport should return true";
  EXPECT_TRUE(report_sent_and_verified)
      << "Report was not sent or not verified by the interceptor";
}

IN_PROC_BROWSER_TEST_P(SBSafeBrowsingServiceTest,
                       NotificationsAcceptedReportSentWithReferrerChain) {
  SetUpSendingNotificationsAcceptedCSBRR();
  network::TestURLLoaderFactory test_url_loader_factory;
  ChromePingManagerFactory::GetForBrowserContext(browser()->GetProfile())
      ->SetURLLoaderFactoryForTesting(
          base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
              &test_url_loader_factory));

  // Define URLs for navigation and reporting.
  GURL referrer_gurl = embedded_test_server()->GetURL(std::string(kEmptyPage) +
                                                      "?from=referrer");
  GURL landing_page_gurl = embedded_test_server()->GetURL(
      std::string(kEmptyPage) + "?from=landing_page");
  GURL permission_prompt_origin_gurl = landing_page_gurl;
  GURL report_resource_url = landing_page_gurl;
  const base::TimeDelta kDisplayDuration = base::Seconds(25);

  // Perform navigations to establish a referrer chain.
  WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), referrer_gurl));

  // Navigate from referrer_gurl to landing_page_gurl to create a referrer.
  content::TestNavigationObserver nav_observer(web_contents);
  ASSERT_TRUE(content::ExecJs(
      web_contents,
      "window.location.href = '" + landing_page_gurl.spec() + "';"));
  nav_observer.Wait();
  ASSERT_EQ(web_contents->GetLastCommittedURL(), landing_page_gurl);
  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
  ASSERT_TRUE(rfh);

  // Setup report interceptor, which validates the contents of the report.
  base::RunLoop run_loop;
  bool report_sent_and_verified = false;
  test_url_loader_factory.SetInterceptor(base::BindLambdaForTesting(
      [&](const network::ResourceRequest& resource_request) {
        std::string upload_data = network::GetUploadData(resource_request);
        ClientSafeBrowsingReportRequest report_proto;
        ASSERT_TRUE(report_proto.ParseFromString(upload_data));

        // Check standard report fields.
        EXPECT_EQ(
            report_proto.type(),
            ClientSafeBrowsingReportRequest::NOTIFICATION_PERMISSION_ACCEPTED);
        EXPECT_EQ(report_proto.url(), report_resource_url.spec());
        EXPECT_EQ(report_proto.page_url(), landing_page_gurl.spec());
        ASSERT_TRUE(report_proto.has_permission_prompt_info());
        EXPECT_EQ(report_proto.permission_prompt_info().origin(),
                  permission_prompt_origin_gurl.spec());
        EXPECT_EQ(report_proto.permission_prompt_info().display_duration_sec(),
                  kDisplayDuration.InSeconds());

        // Verify referrer chain.
        ASSERT_EQ(report_proto.referrer_chain_size(), 2)
            << "Referrer chain should have one entry for the direct referrer.";
        const auto& referrer_entry_0 = report_proto.referrer_chain(0);
        EXPECT_EQ(referrer_entry_0.url(), landing_page_gurl.spec());
        EXPECT_GT(referrer_entry_0.navigation_time_msec(), 0);
        const auto& referrer_entry_1 = report_proto.referrer_chain(1);
        EXPECT_EQ(referrer_entry_1.url(), referrer_gurl.spec());
        EXPECT_GT(referrer_entry_1.navigation_time_msec(), 0);

        report_sent_and_verified = true;
        run_loop.Quit();
      }));

  // Call the `MaybeSendNotificationsAcceptedReport` method.
  bool result = safe_browsing_service()->MaybeSendNotificationsAcceptedReport(
      rfh, browser()->GetProfile(), report_resource_url, landing_page_gurl,
      permission_prompt_origin_gurl, kDisplayDuration);
  EXPECT_TRUE(result)
      << "MaybeSendNotificationsAcceptedReport should return true.";
  EXPECT_TRUE(report_sent_and_verified)
      << "Report was not sent or not verified by the interceptor.";
}

///////////////////////////////////////////////////////////////////////////////
// END: These tests use SafeBrowsingService::Client to directly interact with
// SafeBrowsingService.
///////////////////////////////////////////////////////////////////////////////

INSTANTIATE_TEST_SUITE_P(All, SBSafeBrowsingServiceTest, ::testing::Bool());

INSTANTIATE_TEST_SUITE_P(All,
                         SafeBrowsingServiceWithAutoReloadTest,
                         ::testing::Bool());

INSTANTIATE_TEST_SUITE_P(All,
                         SafeBrowsingServiceWarningShownCSBRRsDisabled,
                         ::testing::Bool());

// TODO(vakh): Add test for UnwantedMainFrame.

}  // namespace safe_browsing
