// 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 <stddef.h>

#include <algorithm>
#include <memory>
#include <set>
#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_string_value_serializer.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/synchronization/lock.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/load_error_reporter.h"
#include "chrome/browser/net/profile_network_context_service.h"
#include "chrome/browser/net/profile_network_context_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/simple_url_loader_test_helper.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/rules_monitor_service.h"
#include "extensions/browser/api/declarative_net_request/ruleset_manager.h"
#include "extensions/browser/api/declarative_net_request/ruleset_matcher.h"
#include "extensions/browser/api/declarative_net_request/test_utils.h"
#include "extensions/browser/api/web_request/web_request_info.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/warning_service.h"
#include "extensions/browser/warning_set.h"
#include "extensions/common/api/declarative_net_request/constants.h"
#include "extensions/common/api/declarative_net_request/test_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/file_util.h"
#include "extensions/common/url_pattern.h"
#include "extensions/test/extension_test_message_listener.h"
#include "ipc/ipc_message.h"
#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "net/test/test_data_directory.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace extensions {
namespace declarative_net_request {
namespace {

constexpr char kJSONRulesFilename[] = "rules_file.json";
const base::FilePath::CharType kJSONRulesetFilepath[] =
    FILE_PATH_LITERAL("rules_file.json");

// Returns true if |window.scriptExecuted| is true for the given frame.
bool WasFrameWithScriptLoaded(content::RenderFrameHost* rfh) {
  if (!rfh)
    return false;
  bool script_resource_was_loaded = false;
  EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
      rfh, "domAutomationController.send(!!window.scriptExecuted)",
      &script_resource_was_loaded));
  return script_resource_was_loaded;
}

// Used to monitor requests that reach the RulesetManager.
class URLRequestMonitor : public RulesetManager::TestObserver {
 public:
  explicit URLRequestMonitor(GURL url) : url_(std::move(url)) {}

  // This is called from both the UI and IO thread. Clients should ensure
  // there's no race.
  bool GetAndResetRequestSeen(bool new_val) {
    bool return_val = request_seen_;
    request_seen_ = new_val;
    return return_val;
  }

 private:
  // RulesetManager::TestObserver implementation.
  void OnEvaluateRequest(const WebRequestInfo& request,
                         bool is_incognito_context) override {
    if (request.url == url_)
      GetAndResetRequestSeen(true);
  }

  GURL url_;
  bool request_seen_ = false;

  DISALLOW_COPY_AND_ASSIGN(URLRequestMonitor);
};

// Used to wait till the number of rulesets managed by the RulesetManager reach
// a certain count.
class RulesetCountWaiter : public RulesetManager::TestObserver {
 public:
  RulesetCountWaiter() = default;

  void WaitForRulesetCount(size_t count) {
    {
      base::AutoLock lock(lock_);
      ASSERT_FALSE(expected_count_);
      if (current_count_ == count)
        return;
      expected_count_ = count;
      run_loop_ = std::make_unique<base::RunLoop>();
    }

    run_loop_->Run();
  }

 private:
  // RulesetManager::TestObserver implementation.
  void OnRulesetCountChanged(size_t count) override {
    base::AutoLock lock(lock_);
    current_count_ = count;
    if (expected_count_ != count)
      return;

    ASSERT_TRUE(run_loop_.get());

    // The run-loop has either started or a task on the UI thread to start it is
    // underway. RunLoop::Quit is thread-safe and should post a task to the UI
    // thread to quit the run-loop.
    run_loop_->Quit();
    expected_count_.reset();
  }

  // Accessed on both the UI and IO threads. Access is synchronized using
  // |lock_|.
  size_t current_count_ = 0;
  base::Optional<size_t> expected_count_;
  std::unique_ptr<base::RunLoop> run_loop_;

  base::Lock lock_;

  DISALLOW_COPY_AND_ASSIGN(RulesetCountWaiter);
};

// Helper to set (and reset on destruction) the given
// RulesetManager::TestObserver on the IO thread. Lifetime of |observer| should
// be managed by clients.
class ScopedRulesetManagerTestObserver {
 public:
  ScopedRulesetManagerTestObserver(RulesetManager::TestObserver* observer,
                                   scoped_refptr<InfoMap> info_map)
      : info_map_(std::move(info_map)) {
    SetRulesetManagerTestObserver(observer);
  }

  ~ScopedRulesetManagerTestObserver() {
    SetRulesetManagerTestObserver(nullptr);
  }

 private:
  void SetRulesetManagerTestObserver(RulesetManager::TestObserver* observer) {
    content::BrowserThread::PostTask(
        content::BrowserThread::IO, FROM_HERE,
        base::BindOnce(
            [](RulesetManager::TestObserver* observer, InfoMap* info_map) {
              info_map->GetRulesetManager()->SetObserverForTest(observer);
            },
            observer, base::RetainedRef(info_map_)));
    content::RunAllTasksUntilIdle();
  }

  scoped_refptr<InfoMap> info_map_;

  DISALLOW_COPY_AND_ASSIGN(ScopedRulesetManagerTestObserver);
};

// Helper to wait for warnings thrown for a given extension.
class WarningServiceObserver : public WarningService::Observer {
 public:
  WarningServiceObserver(WarningService* warning_service,
                         const ExtensionId& extension_id)
      : observer_(this), extension_id_(extension_id) {
    observer_.Add(warning_service);
  }

  // Should only be called once per WarningServiceObserver lifetime.
  void WaitForWarning() { run_loop_.Run(); }

 private:
  // WarningService::TestObserver override:
  void ExtensionWarningsChanged(
      const ExtensionIdSet& affected_extensions) override {
    if (!base::ContainsKey(affected_extensions, extension_id_))
      return;

    run_loop_.Quit();
  }

  ScopedObserver<WarningService, WarningService::Observer> observer_;
  const ExtensionId extension_id_;
  base::RunLoop run_loop_;

  DISALLOW_COPY_AND_ASSIGN(WarningServiceObserver);
};

class DeclarativeNetRequestBrowserTest
    : public ExtensionBrowserTest,
      public ::testing::WithParamInterface<ExtensionLoadType> {
 public:
  DeclarativeNetRequestBrowserTest() {}

  // ExtensionBrowserTest overrides:
  void SetUpOnMainThread() override {
    ExtensionBrowserTest::SetUpOnMainThread();

    base::FilePath test_root_path;
    base::PathService::Get(chrome::DIR_TEST_DATA, &test_root_path);
    test_root_path = test_root_path.AppendASCII("extensions")
                         .AppendASCII("declarative_net_request");
    embedded_test_server()->ServeFilesFromDirectory(test_root_path);

    embedded_test_server()->RegisterRequestMonitor(
        base::BindRepeating(&DeclarativeNetRequestBrowserTest::MonitorRequest,
                            base::Unretained(this)));

    ASSERT_TRUE(embedded_test_server()->Start());

    // Map all hosts to localhost.
    host_resolver()->AddRule("*", "127.0.0.1");

    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  }
  void CreatedBrowserMainParts(
      content::BrowserMainParts* browser_main_parts) override {
    // At this point, the notification service is initialized but the profile
    // and extensions have not. Initialize |background_page_ready_listener_| to
    // listen for messages from extensions.
    CHECK(content::NotificationService::current());

    background_page_ready_listener_ =
        std::make_unique<ExtensionTestMessageListener>("ready",
                                                       false /*will_reply*/);

    ExtensionBrowserTest::CreatedBrowserMainParts(browser_main_parts);
  }

 protected:
  content::WebContents* web_contents(Browser* browser) const {
    return browser->tab_strip_model()->GetActiveWebContents();
  }

  content::WebContents* web_contents() const { return web_contents(browser()); }

  content::RenderFrameHost* GetMainFrame(Browser* browser) const {
    return web_contents(browser)->GetMainFrame();
  }

  content::RenderFrameHost* GetMainFrame() const {
    return GetMainFrame(browser());
  }

  content::RenderFrameHost* GetFrameByName(const std::string& name) const {
    return content::FrameMatchingPredicate(
        web_contents(), base::Bind(&content::FrameMatchesName, name));
  }

  content::PageType GetPageType(Browser* browser) const {
    return web_contents(browser)
        ->GetController()
        .GetLastCommittedEntry()
        ->GetPageType();
  }

  content::PageType GetPageType() const { return GetPageType(browser()); }

  // Sets whether the extension should have a background script.
  void set_has_background_script(bool has_background_script) {
    has_background_script_ = has_background_script;
  }

  // Loads an extension with the given declarative |rules| in the given
  // |directory|. Generates a fatal failure if the extension failed to load.
  // |hosts| specifies the host permissions, the extensions should
  // have.
  void LoadExtensionWithRules(const std::vector<TestRule>& rules,
                              const std::string& directory,
                              const std::vector<std::string>& hosts) {
    base::ScopedAllowBlockingForTesting scoped_allow_blocking;
    base::HistogramTester tester;

    base::FilePath extension_dir = temp_dir_.GetPath().AppendASCII(directory);
    EXPECT_TRUE(base::CreateDirectory(extension_dir));

    WriteManifestAndRuleset(extension_dir, kJSONRulesetFilepath,
                            kJSONRulesFilename, rules, hosts,
                            has_background_script_);

    background_page_ready_listener_->Reset();
    const Extension* extension = nullptr;
    switch (GetParam()) {
      case ExtensionLoadType::PACKED:
        extension = InstallExtensionWithPermissionsGranted(
            extension_dir, 1 /* expected_change */);
        break;
      case ExtensionLoadType::UNPACKED:
        extension = LoadExtension(extension_dir);
        break;
    }

    ASSERT_TRUE(extension);
    EXPECT_TRUE(HasValidIndexedRuleset(*extension, profile()));

    // Ensure the ruleset is also loaded on the IO thread.
    content::RunAllTasksUntilIdle();

    // Wait for the background page to load if needed.
    if (has_background_script_)
      WaitForBackgroundScriptToLoad(extension->id());

    // Ensure no load errors were reported.
    EXPECT_TRUE(LoadErrorReporter::GetInstance()->GetErrors()->empty());

    tester.ExpectTotalCount(kIndexRulesTimeHistogram, 1);
    tester.ExpectTotalCount(kIndexAndPersistRulesTimeHistogram, 1);
    tester.ExpectUniqueSample(kManifestRulesCountHistogram,
                              rules.size() /*sample*/, 1 /*count*/);
    tester.ExpectTotalCount(
        "Extensions.DeclarativeNetRequest.CreateVerifiedMatcherTime", 1);
    tester.ExpectUniqueSample(
        "Extensions.DeclarativeNetRequest.LoadRulesetResult",
        RulesetMatcher::kLoadSuccess /*sample*/, 1 /*count*/);
  }

  void LoadExtensionWithRules(const std::vector<TestRule>& rules) {
    LoadExtensionWithRules(rules, "test_extension",
                           {URLPattern::kAllUrlsPattern});
  }

  void WaitForBackgroundScriptToLoad(const ExtensionId& extension_id) {
    ASSERT_TRUE(background_page_ready_listener_->WaitUntilSatisfied());
    ASSERT_EQ(extension_id,
              background_page_ready_listener_->extension_id_for_message());
    background_page_ready_listener_->Reset();
  }

  void AddAllowedPages(const ExtensionId& extension_id,
                       const std::vector<std::string>& patterns) {
    UpdateAllowedPages(extension_id, patterns, "addAllowedPages");
  }

  void RemoveAllowedPages(const ExtensionId& extension_id,
                          const std::vector<std::string>& patterns) {
    UpdateAllowedPages(extension_id, patterns, "removeAllowedPages");
  }

  // Verifies that the result of getAllowedPages call is the same as
  // |expected_patterns|.
  void VerifyGetAllowedPages(const ExtensionId& extension_id,
                             const std::set<std::string>& expected_patterns) {
    static constexpr char kScript[] = R"(
      chrome.declarativeNetRequest.getAllowedPages(function(patterns) {
        window.domAutomationController.send(chrome.runtime.lastError
                                                ? 'error'
                                                : JSON.stringify(patterns));
      });
    )";

    const std::string result =
        ExecuteScriptInBackgroundPage(extension_id, kScript);
    ASSERT_NE("error", result);

    // Parse |result| as a list and deserialize it to a set of strings.
    std::unique_ptr<base::Value> value =
        JSONStringValueDeserializer(result).Deserialize(
            nullptr /*error_code*/, nullptr /*error_message*/);
    ASSERT_TRUE(value);
    ASSERT_TRUE(value->is_list());
    std::set<std::string> patterns;
    for (const auto& pattern_value : value->GetList()) {
      ASSERT_TRUE(pattern_value.is_string());
      patterns.insert(pattern_value.GetString());
    }

    EXPECT_EQ(expected_patterns, patterns);
  }

  std::set<GURL> GetAndResetRequestsToServer() {
    base::AutoLock lock(requests_to_server_lock_);
    std::set<GURL> results = requests_to_server_;
    requests_to_server_.clear();
    return results;
  }

 private:
  // Handler to monitor the requests which reach the EmbeddedTestServer. This
  // will be run on the EmbeddedTestServer's IO thread.
  void MonitorRequest(const net::test_server::HttpRequest& request) {
    base::AutoLock lock(requests_to_server_lock_);
    requests_to_server_.insert(request.GetURL());
  }

  void UpdateAllowedPages(const ExtensionId& extension_id,
                          const std::vector<std::string>& patterns,
                          const std::string& function_name) {
    static constexpr char kScript[] = R"(
      chrome.declarativeNetRequest.%s(%s, function() {
        window.domAutomationController.send(chrome.runtime.lastError
                                                ? 'error'
                                                : 'success');
      });
    )";

    // Serialize |patterns| to JSON.
    std::unique_ptr<base::ListValue> list = ToListValue(patterns);
    std::string json_string;
    ASSERT_TRUE(JSONStringValueSerializer(&json_string).Serialize(*list));

    EXPECT_EQ("success", ExecuteScriptInBackgroundPage(
                             extension_id,
                             base::StringPrintf(kScript, function_name.c_str(),
                                                json_string.c_str())));
  }

  base::ScopedTempDir temp_dir_;
  bool has_background_script_ = false;
  std::unique_ptr<ExtensionTestMessageListener> background_page_ready_listener_;

  // Requests observed by the EmbeddedTestServer. This is accessed on both the
  // UI and the EmbeddedTestServer's IO thread. Access is protected by
  // |requests_to_server_lock_|.
  std::set<GURL> requests_to_server_;

  base::Lock requests_to_server_lock_;

  DISALLOW_COPY_AND_ASSIGN(DeclarativeNetRequestBrowserTest);
};

using DeclarativeNetRequestBrowserTest_Packed =
    DeclarativeNetRequestBrowserTest;

// Tests the "urlFilter" property of a declarative rule condition.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_UrlFilter) {
  struct {
    std::string url_filter;
    int id;
  } rules_data[] = {
      {"pages_with_script/*ex", 1},
      {"||a.b.com", 2},
      {"|http://*.us", 3},
      {"pages_with_script/page2.html|", 4},
      {"|http://msn*/pages_with_script/page.html|", 5},
      {"%20", 6},  // Block any urls with space.
  };

  // Rule |i| is the rule with id |i|.
  struct {
    std::string hostname;
    std::string path;
    bool expect_main_frame_loaded;
  } test_cases[] = {
      {"example.com", "/pages_with_script/index.html", false},  // Rule 1
      {"example.com", "/pages_with_script/page.html", true},
      {"a.b.com", "/pages_with_script/page.html", false},    // Rule 2
      {"c.a.b.com", "/pages_with_script/page.html", false},  // Rule 2
      {"b.com", "/pages_with_script/page.html", true},
      {"example.us", "/pages_with_script/page.html", false},  // Rule 3
      {"example.jp", "/pages_with_script/page.html", true},
      {"example.jp", "/pages_with_script/page2.html", false},  // Rule 4
      {"example.jp", "/pages_with_script/page2.html?q=hello", true},
      {"msn.com", "/pages_with_script/page.html", false},  // Rule 5
      {"msn.com", "/pages_with_script/page.html?q=hello", true},
      {"a.msn.com", "/pages_with_script/page.html", true},
      {"abc.com", "/pages_with_script/page.html?q=hi bye", false},    // Rule 6
      {"abc.com", "/pages_with_script/page.html?q=hi%20bye", false},  // Rule 6
      {"abc.com", "/pages_with_script/page.html?q=hibye", true},
  };

  // Load the extension.
  std::vector<TestRule> rules;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.condition->resource_types = std::vector<std::string>({"main_frame"});
    rule.id = rule_data.id;
    rules.push_back(rule);
  }
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Verify that the extension correctly intercepts network requests.
  for (const auto& test_case : test_cases) {
    GURL url =
        embedded_test_server()->GetURL(test_case.hostname, test_case.path);
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(test_case.expect_main_frame_loaded,
              WasFrameWithScriptLoaded(GetMainFrame()));

    content::PageType expected_page_type = test_case.expect_main_frame_loaded
                                               ? content::PAGE_TYPE_NORMAL
                                               : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  }
}

// Tests the matching behavior of the separator ('^') character.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_Separator) {
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter =
      std::string("pages_with_script/page2.html?q=bye^");
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  // '^' (Separator character) matches anything except a letter, a digit or
  // one of the following: _ - . %.
  struct {
    std::string url_path;
    bool expect_main_frame_loaded;
  } test_cases[] = {
      {"/pages_with_script/page2.html?q=bye&x=1", false},
      {"/pages_with_script/page2.html?q=bye#x=1", false},
      {"/pages_with_script/page2.html?q=bye:", false},
      {"/pages_with_script/page2.html?q=bye3", true},
      {"/pages_with_script/page2.html?q=byea", true},
      {"/pages_with_script/page2.html?q=bye_", true},
      {"/pages_with_script/page2.html?q=bye-", true},
      {"/pages_with_script/page2.html?q=bye%", true},
      {"/pages_with_script/page2.html?q=bye.", true},
  };

  for (const auto& test_case : test_cases) {
    GURL url = embedded_test_server()->GetURL("google.com", test_case.url_path);
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(test_case.expect_main_frame_loaded,
              WasFrameWithScriptLoaded(GetMainFrame()));

    content::PageType expected_page_type = test_case.expect_main_frame_loaded
                                               ? content::PAGE_TYPE_NORMAL
                                               : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  }
}

// Tests that the separator character '^' correctly matches the end of the url.
// TODO(crbug.com/772260): Enable once the bug is fixed.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       DISABLED_BlockRequests_SeparatorMatchesEndOfURL) {
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("page2.html^");

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  GURL url = embedded_test_server()->GetURL("google.com",
                                            "/pages_with_script/page2.html");
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_EQ(content::PAGE_TYPE_ERROR, GetPageType());
}

// Tests case sensitive url filters.
// TODO(crbug.com/767605): Enable once case-insensitive matching is implemented
// in url_pattern_index.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       DISABLED_BlockRequests_IsUrlFilterCaseSensitive) {
  struct {
    std::string url_filter;
    size_t id;
    bool is_url_filter_case_sensitive;
  } rules_data[] = {{"pages_with_script/index.html?q=hello", 1, false},
                    {"pages_with_script/page.html?q=hello", 2, true}};

  std::vector<TestRule> rules;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.id = rule_data.id;
    rule.condition->is_url_filter_case_sensitive =
        rule_data.is_url_filter_case_sensitive;
    rules.push_back(rule);
  }
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Rule |i| is the rule with id |i|.
  struct {
    std::string hostname;
    std::string path;
    bool expect_main_frame_loaded;
  } test_cases[] = {
      {"example.com", "/pages_with_script/index.html?q=hello",
       false},  // Rule 1
      {"example.com", "/pages_with_script/index.html?q=HELLO",
       false},                                                         // Rule 1
      {"example.com", "/pages_with_script/page.html?q=hello", false},  // Rule 2
      {"example.com", "/pages_with_script/page.html?q=HELLO", true},
  };

  for (const auto& test_case : test_cases) {
    GURL url =
        embedded_test_server()->GetURL(test_case.hostname, test_case.path);
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(test_case.expect_main_frame_loaded,
              WasFrameWithScriptLoaded(GetMainFrame()));

    content::PageType expected_page_type = test_case.expect_main_frame_loaded
                                               ? content::PAGE_TYPE_NORMAL
                                               : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  }
}

// Tests the "domains" and "excludedDomains" property of a declarative rule
// condition.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_Domains) {
  struct {
    std::string url_filter;
    size_t id;
    std::vector<std::string> domains;
    std::vector<std::string> excluded_domains;
  } rules_data[] = {{"child_frame.html?frame=1", 1, {"x.com"}, {"a.x.com"}},
                    {"child_frame.html?frame=2", 2, {}, {"a.y.com"}}};

  std::vector<TestRule> rules;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.id = rule_data.id;

    // An empty list is not allowed for the "domains" property.
    if (!rule_data.domains.empty())
      rule.condition->domains = rule_data.domains;

    rule.condition->excluded_domains = rule_data.excluded_domains;
    rule.condition->resource_types = std::vector<std::string>({"sub_frame"});
    rules.push_back(rule);
  }
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Rule |i| is the rule with id |i|.
  struct {
    std::string main_frame_hostname;
    bool expect_frame_1_loaded;
    bool expect_frame_2_loaded;
  } test_cases[] = {
      {"x.com", false /* Rule 1 */, false /* Rule 2 */},
      {"b.x.com", false /* Rule 1 */, false /* Rule 2 */},
      {"a.x.com", true, false /* Rule 2 */},
      {"b.a.x.com", true, false /* Rule 2 */},
      {"y.com", true, false /* Rule 2*/},
      {"a.y.com", true, true},
      {"b.a.y.com", true, true},
      {"google.com", true, false /* Rule 2 */},
  };

  for (const auto& test_case : test_cases) {
    GURL url = embedded_test_server()->GetURL(test_case.main_frame_hostname,
                                              "/page_with_two_frames.html");
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    content::RenderFrameHost* main_frame = GetMainFrame();
    EXPECT_TRUE(WasFrameWithScriptLoaded(main_frame));
    EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());

    content::RenderFrameHost* child_1 = content::ChildFrameAt(main_frame, 0);
    content::RenderFrameHost* child_2 = content::ChildFrameAt(main_frame, 1);

    EXPECT_EQ(test_case.expect_frame_1_loaded,
              WasFrameWithScriptLoaded(child_1));
    EXPECT_EQ(test_case.expect_frame_2_loaded,
              WasFrameWithScriptLoaded(child_2));
  }
}

// Tests the "domainType" property of a declarative rule condition.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_DomainType) {
  struct {
    std::string url_filter;
    size_t id;
    base::Optional<std::string> domain_type;
  } rules_data[] = {
      {"child_frame.html?case=1", 1, std::string("firstParty")},
      {"child_frame.html?case=2", 2, std::string("thirdParty")},
      {"child_frame.html?case=3", 3, base::nullopt},
  };

  std::vector<TestRule> rules;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.id = rule_data.id;
    rule.condition->domain_type = rule_data.domain_type;
    rule.condition->resource_types = std::vector<std::string>({"sub_frame"});
    rules.push_back(rule);
  }
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  GURL url =
      embedded_test_server()->GetURL("example.com", "/domain_type_test.html");
  ui_test_utils::NavigateToURL(browser(), url);
  ASSERT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  ASSERT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());

  // The loaded page will consist of four pairs of iframes named
  // first_party_[1..4] and third_party_[1..4], with url path
  // child_frame.html?case=[1..4] respectively.
  struct {
    std::string child_frame_name;
    bool expect_frame_loaded;
  } cases[] = {
      // Url matched by rule 1. Only the first party frame is blocked.
      {"first_party_1", false},
      {"third_party_1", true},
      // Url matched by rule 2. Only the third party frame is blocked.
      {"first_party_2", true},
      {"third_party_2", false},
      // Url matched by rule 3. Both the first and third party frames are
      // blocked.
      {"first_party_3", false},
      {"third_party_3", false},
      // No matching rule.
      {"first_party_4", true},
      {"third_party_4", true},
  };

  for (const auto& test_case : cases) {
    SCOPED_TRACE(base::StringPrintf("Testing child frame named %s",
                                    test_case.child_frame_name.c_str()));

    content::RenderFrameHost* child =
        GetFrameByName(test_case.child_frame_name);
    EXPECT_TRUE(child);
    EXPECT_EQ(test_case.expect_frame_loaded, WasFrameWithScriptLoaded(child));
  }
}

// Tests allowing rules.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, Allow) {
  const int kNumRequests = 6;

  TestRule rule = CreateGenericRule();
  int id = kMinValidID;

  // Block all main-frame requests ending with numbers 1 to |kNumRequests|.
  std::vector<TestRule> rules;
  for (int i = 1; i <= kNumRequests; ++i) {
    rule.id = id++;
    rule.condition->url_filter = base::StringPrintf("num=%d|", i);
    rule.condition->resource_types = std::vector<std::string>({"main_frame"});
    rules.push_back(rule);
  }

  // Allow all main-frame requests ending with even numbers from 1 to
  // |kNumRequests|.
  for (int i = 2; i <= kNumRequests; i += 2) {
    rule.id = id++;
    rule.condition->url_filter = base::StringPrintf("num=%d|", i);
    rule.condition->resource_types = std::vector<std::string>({"main_frame"});
    rule.action->type = std::string("allow");
    rules.push_back(rule);
  }

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  for (int i = 1; i <= kNumRequests; ++i) {
    GURL url = embedded_test_server()->GetURL(
        "example.com",
        base::StringPrintf("/pages_with_script/page.html?num=%d", i));
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);

    // All requests ending with odd numbers should be blocked.
    const bool page_should_load = (i % 2 == 0);
    EXPECT_EQ(page_should_load, WasFrameWithScriptLoaded(GetMainFrame()));
    content::PageType expected_page_type =
        page_should_load ? content::PAGE_TYPE_NORMAL : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  }
}

// Tests that the extension ruleset is active only when the extension is
// enabled.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       Enable_Disable_Reload_Uninstall) {
  // Block all main frame requests to example.com
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("example.com");
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));
  const ExtensionId extension_id = last_loaded_extension_id();

  GURL url = embedded_test_server()->GetURL("example.com",
                                            "/pages_with_script/page.html");
  auto test_extension_enabled = [&](bool expected_enabled) {
    // Wait for any pending actions caused by extension state change.
    content::RunAllTasksUntilIdle();
    EXPECT_EQ(expected_enabled,
              ExtensionRegistry::Get(profile())->enabled_extensions().Contains(
                  extension_id));

    ui_test_utils::NavigateToURL(browser(), url);

    // If the extension is enabled, the |url| should be blocked.
    EXPECT_EQ(!expected_enabled, WasFrameWithScriptLoaded(GetMainFrame()));
    content::PageType expected_page_type =
        expected_enabled ? content::PAGE_TYPE_ERROR : content::PAGE_TYPE_NORMAL;
    EXPECT_EQ(expected_page_type, GetPageType());
  };

  {
    SCOPED_TRACE("Testing extension after load");
    test_extension_enabled(true);
  }

  {
    SCOPED_TRACE("Testing DisableExtension");
    DisableExtension(extension_id);
    test_extension_enabled(false);
  }

  {
    SCOPED_TRACE("Testing EnableExtension");
    EnableExtension(extension_id);
    test_extension_enabled(true);
  }

  {
    SCOPED_TRACE("Testing ReloadExtension");
    ReloadExtension(extension_id);
    test_extension_enabled(true);
  }

  {
    SCOPED_TRACE("Testing UninstallExtension");
    UninstallExtension(extension_id);
    test_extension_enabled(false);
  }
}

// Tests that multiple enabled extensions with declarative rulesets having
// blocking rules behave correctly.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_MultipleExtensions) {
  struct {
    std::string url_filter;
    int id;
    bool add_to_first_extension;
    bool add_to_second_extension;
  } rules_data[] = {
      {"block_both.com", 1, true, true},
      {"block_first.com", 2, true, false},
      {"block_second.com", 3, false, true},
  };

  std::vector<TestRule> rules_1, rules_2;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.condition->resource_types = std::vector<std::string>({"main_frame"});
    rule.id = rule_data.id;
    if (rule_data.add_to_first_extension)
      rules_1.push_back(rule);
    if (rule_data.add_to_second_extension)
      rules_2.push_back(rule);
  }

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
      rules_1, "extension_1", {URLPattern::kAllUrlsPattern}));
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
      rules_2, "extension_2", {URLPattern::kAllUrlsPattern}));

  struct {
    std::string host;
    bool expect_main_frame_loaded;
  } test_cases[] = {
      {"block_both.com", false},
      {"block_first.com", false},
      {"block_second.com", false},
      {"block_none.com", true},
  };

  for (const auto& test_case : test_cases) {
    GURL url = embedded_test_server()->GetURL(test_case.host,
                                              "/pages_with_script/page.html");
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(test_case.expect_main_frame_loaded,
              WasFrameWithScriptLoaded(GetMainFrame()));
    content::PageType expected_page_type = test_case.expect_main_frame_loaded
                                               ? content::PAGE_TYPE_NORMAL
                                               : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  }
}

// Tests that multiple enabled extensions with declarative rulesets having
// redirect rules behave correctly with preference given to more recently
// installed extensions.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       RedirectRequests_MultipleExtensions) {
  const int kNumExtensions = 4;

  auto redirect_url_for_extension_number = [this](size_t num) {
    return embedded_test_server()
        ->GetURL(std::to_string(num) + ".com", "/pages_with_script/index.html")
        .spec();
  };

  TestRule rule = CreateGenericRule();
  rule.id = kMinValidID;
  rule.priority = kMinValidPriority;
  rule.action->type = std::string("redirect");
  rule.condition->url_filter = std::string("example.com");
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});

  base::Time last_extension_install_time = base::Time::Min();

  // Add |kNumExtensions| each redirecting example.com to a different redirect
  // url.
  for (size_t i = 1; i <= kNumExtensions; ++i) {
    rule.action->redirect_url = redirect_url_for_extension_number(i);
    ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
        {rule}, std::to_string(i), {URLPattern::kAllUrlsPattern}));

    // Verify that the install time of this extension is greater than the last
    // extension.
    base::Time install_time = ExtensionPrefs::Get(profile())->GetInstallTime(
        last_loaded_extension_id());
    EXPECT_GT(install_time, last_extension_install_time);
    last_extension_install_time = install_time;
  }

  // Load a url on example.com. It should be redirected to the redirect url
  // corresponding to the most recently installed extension.
  GURL url = embedded_test_server()->GetURL("example.com",
                                            "/pages_with_script/page.html");
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  GURL final_url = web_contents()->GetLastCommittedURL();
  EXPECT_EQ(GURL(redirect_url_for_extension_number(kNumExtensions)), final_url);
}

// Tests a combination of blocking and redirect rules.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, BlockAndRedirect) {
  auto get_url_for_host = [this](std::string hostname) {
    return embedded_test_server()
        ->GetURL(hostname, "/pages_with_script/index.html")
        .spec();
  };

  struct {
    std::string url_filter;
    int id;
    std::string action_type;
    base::Optional<std::string> redirect_url;
  } rules_data[] = {
      {"example.com", 1, "redirect", get_url_for_host("yahoo.com")},
      {"yahoo.com", 2, "redirect", get_url_for_host("google.com")},
      {"abc.com", 3, "redirect", get_url_for_host("def.com")},
      {"def.com", 4, "block", base::nullopt},
      {"def.com", 5, "redirect", get_url_for_host("xyz.com")},
  };

  // Load the extension.
  std::vector<TestRule> rules;
  for (const auto& rule_data : rules_data) {
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = rule_data.url_filter;
    rule.id = rule_data.id;
    rule.priority = kMinValidPriority;
    rule.condition->resource_types = std::vector<std::string>({"main_frame"});
    rule.action->type = rule_data.action_type;
    rule.action->redirect_url = rule_data.redirect_url;
    rules.push_back(rule);
  }
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  struct {
    std::string hostname;
    bool expected_main_frame_loaded;
    base::Optional<std::string> expected_final_hostname;
    base::Optional<size_t> expected_redirect_chain_length;
  } test_cases[] = {
      // example.com -> yahoo.com -> google.com.
      {"example.com", true, std::string("google.com"), 3},
      // yahoo.com -> google.com.
      {"yahoo.com", true, std::string("google.com"), 2},
      // abc.com -> def.com (blocked).
      // Note def.com won't be redirected since blocking rules are given
      // priority over redirect rules.
      {"abc.com", false, base::nullopt, base::nullopt},
      // def.com (blocked).
      {"def.com", false, base::nullopt, base::nullopt},
  };

  for (const auto& test_case : test_cases) {
    std::string url = get_url_for_host(test_case.hostname);
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.c_str()));

    ui_test_utils::NavigateToURL(browser(), GURL(url));

    if (!test_case.expected_main_frame_loaded) {
      EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
      EXPECT_EQ(content::PAGE_TYPE_ERROR, GetPageType());
    } else {
      EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
      EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());

      GURL final_url = web_contents()->GetLastCommittedURL();
      EXPECT_EQ(GURL(get_url_for_host(*test_case.expected_final_hostname)),
                final_url);

      EXPECT_EQ(*test_case.expected_redirect_chain_length,
                web_contents()
                    ->GetController()
                    .GetLastCommittedEntry()
                    ->GetRedirectChain()
                    .size());
    }
  }
}

// Tests that the redirect url within an extension ruleset is chosen based on
// the highest priority matching rule.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, RedirectPriority) {
  const size_t kNumPatternTypes = 7;

  auto hostname_for_number = [](size_t num) {
    return std::to_string(num) + ".com";
  };

  auto redirect_url_for_priority = [this,
                                    hostname_for_number](size_t priority) {
    return embedded_test_server()
        ->GetURL(hostname_for_number(priority + 1),
                 "/pages_with_script/index.html")
        .spec();
  };

  std::vector<TestRule> rules;
  TestRule rule = CreateGenericRule();
  int id = kMinValidID;
  for (size_t i = 1; i <= kNumPatternTypes; i++) {
    // For pattern type |i|, add |i| rules with priority from 1 to |i|, each
    // with a different redirect url.
    std::string pattern = hostname_for_number(i) + "*page.html";

    for (size_t j = 1; j <= i; j++) {
      rule.id = id++;
      rule.priority = j;
      rule.action->type = std::string("redirect");
      rule.action->redirect_url = redirect_url_for_priority(j);
      rule.condition->url_filter = pattern;
      rule.condition->resource_types = std::vector<std::string>({"main_frame"});
      rules.push_back(rule);
    }
  }

  // Shuffle the rules to ensure that the order in which rules are added has no
  // effect on the test.
  base::RandomShuffle(rules.begin(), rules.end());
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  for (size_t i = 0; i <= kNumPatternTypes + 1; ++i) {
    GURL url = embedded_test_server()->GetURL(hostname_for_number(i),
                                              "/pages_with_script/page.html");
    SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
    EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
    GURL final_url = web_contents()->GetLastCommittedURL();

    bool expect_redirection = i >= 1 && i <= kNumPatternTypes;
    if (expect_redirection) {
      // For pattern type |i|, the highest prioirity for any rule was |i|.
      EXPECT_EQ(GURL(redirect_url_for_priority(i)), final_url);
    } else {
      EXPECT_EQ(url, final_url);
    }
  }
}

// Tests that only extensions enabled in incognito mode affect network requests
// from an incognito context.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       BlockRequests_Incognito) {
  // Block all main-frame requests to example.com.
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("example.com");
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  ExtensionId extension_id = last_loaded_extension_id();
  GURL url = embedded_test_server()->GetURL("example.com",
                                            "/pages_with_script/page.html");
  Browser* incognito_browser = CreateIncognitoBrowser();

  auto test_enabled_in_incognito = [&](bool expected_enabled_in_incognito) {
    // Wait for any pending actions caused by extension state change.
    content::RunAllTasksUntilIdle();

    EXPECT_EQ(expected_enabled_in_incognito,
              util::IsIncognitoEnabled(extension_id, profile()));

    // The url should be blocked in normal context.
    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
    EXPECT_EQ(content::PAGE_TYPE_ERROR, GetPageType());

    // In incognito context, the url should be blocked if the extension is
    // enabled in incognito mode.
    ui_test_utils::NavigateToURL(incognito_browser, url);
    EXPECT_EQ(!expected_enabled_in_incognito,
              WasFrameWithScriptLoaded(GetMainFrame(incognito_browser)));
    content::PageType expected_page_type = expected_enabled_in_incognito
                                               ? content::PAGE_TYPE_ERROR
                                               : content::PAGE_TYPE_NORMAL;
    EXPECT_EQ(expected_page_type, GetPageType(incognito_browser));
  };

  {
    // By default, the extension will be disabled in incognito.
    SCOPED_TRACE("Testing extension after load");
    test_enabled_in_incognito(false);
  }

  {
    // Enable the extension in incognito mode.
    SCOPED_TRACE("Testing extension after enabling it in incognito");
    util::SetIsIncognitoEnabled(extension_id, profile(), true /*enabled*/);
    test_enabled_in_incognito(true);
  }

  // Disable the extension in incognito mode.
  {
    SCOPED_TRACE("Testing extension after disabling it in incognito");
    util::SetIsIncognitoEnabled(extension_id, profile(), false /*enabled*/);
    test_enabled_in_incognito(false);
  }
}

// Ensure extensions can't intercept chrome:// urls, even after explicitly
// requesting access to <all_urls>.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, ChromeURLS) {
  // Have the extension block all chrome:// urls.
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("chrome://");
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  std::vector<const char*> test_urls = {
      chrome::kChromeUIFlagsURL, chrome::kChromeUIAboutURL,
      chrome::kChromeUIExtensionsURL, chrome::kChromeUIVersionURL};

  for (const char* url : test_urls) {
    ui_test_utils::NavigateToURL(browser(), GURL(url));
    EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  }
}

// Test that a packed extension with a DNR ruleset behaves correctly after
// browser restart.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest_Packed,
                       PRE_BrowserRestart) {
  // This is not tested for unpacked extensions since the unpacked extension
  // directory won't be persisted across browser restarts.
  ASSERT_EQ(ExtensionLoadType::PACKED, GetParam());

  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("example.com");
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  GURL url = embedded_test_server()->GetURL("example.com",
                                            "/pages_with_script/page.html");
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest_Packed,
                       BrowserRestart) {
  // This is not tested for unpacked extensions since the unpacked extension
  // directory won't be persisted across browser restarts.
  ASSERT_EQ(ExtensionLoadType::PACKED, GetParam());

  // Ensure that the DNR extension enabled in previous browser session still
  // correctly blocks network requests.
  GURL url = embedded_test_server()->GetURL("example.com",
                                            "/pages_with_script/page.html");
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
}

// Ensure that Blink's in-memory cache is cleared on adding/removing rulesets.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, RendererCacheCleared) {
  // Set-up an observer for RulesetMatcher to monitor requests to
  // script.js.
  URLRequestMonitor script_monitor(
      embedded_test_server()->GetURL("example.com", "/cached/script.js"));
  ScopedRulesetManagerTestObserver scoped_observer(
      &script_monitor,
      base::WrapRefCounted(ExtensionSystem::Get(profile())->info_map()));

  GURL url = embedded_test_server()->GetURL(
      "example.com", "/cached/page_with_cacheable_script.html");

  // With no extension loaded, the request to the script should succeed.
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));

  // NOTE: When the Network Service is enabled, the RulesetMatcher will not see
  // network requests if no rulesets are active.
  EXPECT_TRUE(
      base::FeatureList::IsEnabled(network::features::kNetworkService) ||
      script_monitor.GetAndResetRequestSeen(false));

  // Another request to |url| should not cause a network request for
  // script.js since it will be served by the renderer's in-memory
  // cache.
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_FALSE(script_monitor.GetAndResetRequestSeen(false));

  // Now block requests to script.js.
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("script.js");
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  // Adding an extension ruleset should have cleared the renderer's in-memory
  // cache. Hence the browser process will observe the request to
  // script.js and block it.
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  EXPECT_FALSE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_TRUE(script_monitor.GetAndResetRequestSeen(false));

  // Disable the extension and wait for the ruleset to be unloaded on the IO
  // thread.
  DisableExtension(last_loaded_extension_id());
  content::RunAllTasksUntilIdle();

  // Disabling the extension should cause the request to succeed again. The
  // request for the script will again be observed by the browser since it's not
  // in the renderer's in-memory cache.
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_TRUE(
      base::FeatureList::IsEnabled(network::features::kNetworkService) ||
      script_monitor.GetAndResetRequestSeen(false));
}

// Tests that proxy requests aren't intercepted. See https://crbug.com/794674.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       PacRequestsBypassRules) {
  // Load the extension.
  std::vector<TestRule> rules;
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*pac");
  rule.id = 1;
  rules.push_back(rule);
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Configure a PAC script. Need to do this after the extension is loaded, so
  // that the PAC isn't already loaded by the time the extension starts
  // affecting requests.
  PrefService* pref_service = browser()->profile()->GetPrefs();
  pref_service->Set(proxy_config::prefs::kProxy,
                    *ProxyConfigDictionary::CreatePacScript(
                        embedded_test_server()->GetURL("/self.pac").spec(),
                        true /* pac_mandatory */));
  // Flush the proxy configuration change over the Mojo pipe to avoid any races.
  ProfileNetworkContextServiceFactory::GetForContext(browser()->profile())
      ->FlushProxyConfigMonitorForTesting();

  // Verify that the extension can't intercept the network request.
  ui_test_utils::NavigateToURL(browser(), embedded_test_server()->GetURL(
                                              "/pages_with_script/page.html"));
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
}

// Ensure that an extension can't intercept requests on the chrome-extension
// scheme.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       InterceptExtensionScheme) {
  // Load two extensions. One blocks all urls, and the other blocks urls with
  // "google.com" as a substring.
  std::vector<TestRule> rules_1;
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*");
  rules_1.push_back(rule);

  std::vector<TestRule> rules_2;
  rule = CreateGenericRule();
  rule.condition->url_filter = std::string("google.com");
  rules_2.push_back(rule);

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
      rules_1, "extension_1", {URLPattern::kAllUrlsPattern}));
  const std::string extension_id_1 = last_loaded_extension_id();

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
      rules_2, "extension_2", {URLPattern::kAllUrlsPattern}));
  const std::string extension_id_2 = last_loaded_extension_id();

  auto get_manifest_url = [](const ExtensionId& extension_id) {
    return GURL(base::StringPrintf("%s://%s/manifest.json",
                                   extensions::kExtensionScheme,
                                   extension_id.c_str()));
  };

  // Extension 1 should not be able to block the request to its own
  // manifest.json or that of the Extension 2, even with "<all_urls>" host
  // permissions.
  ui_test_utils::NavigateToURL(browser(), get_manifest_url(extension_id_1));
  GURL final_url = web_contents()->GetLastCommittedURL();
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  ui_test_utils::NavigateToURL(browser(), get_manifest_url(extension_id_2));
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
}

// Tests the page allowing API.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest_Packed,
                       PRE_PageAllowingAPI_PersistedAcrossSessions) {
  // This is not tested for unpacked extensions since the unpacked extension
  // directory won't be persisted across browser sessions.
  ASSERT_EQ(ExtensionLoadType::PACKED, GetParam());

  set_has_background_script(true);
  LoadExtensionWithRules({});

  const Extension* dnr_extension = extension_service()->GetExtensionById(
      last_loaded_extension_id(), false /*include_disabled*/);
  ASSERT_TRUE(dnr_extension);
  EXPECT_EQ("Test extension", dnr_extension->name());

  constexpr char kGoogleDotCom[] = "https://www.google.com/";

  // Allow |kGoogleDotCom|.
  AddAllowedPages(dnr_extension->id(), {kGoogleDotCom});

  // Ensure that the page was allowed.
  VerifyGetAllowedPages(dnr_extension->id(), {kGoogleDotCom});
}

// Tests that the pages allowed using the page allowing API are
// persisted across browser sessions.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest_Packed,
                       PageAllowingAPI_PersistedAcrossSessions) {
  // This is not tested for unpacked extensions since the unpacked extension
  // directory won't be persisted across browser sessions.
  ASSERT_EQ(ExtensionLoadType::PACKED, GetParam());

  // Retrieve the extension installed in the previous browser session.
  ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
  const Extension* dnr_extension = nullptr;
  for (const scoped_refptr<const Extension>& extension :
       registry->enabled_extensions()) {
    if (extension->name() == "Test extension") {
      dnr_extension = extension.get();
      break;
    }
  }
  ASSERT_TRUE(dnr_extension);

  // Ensure the background page is ready before dispatching the script to it.
  WaitForBackgroundScriptToLoad(dnr_extension->id());

  constexpr char kGoogleDotCom[] = "https://www.google.com/";

  VerifyGetAllowedPages(dnr_extension->id(), {kGoogleDotCom});

  // Remove |kGoogleDotCom| from the set of allowed pages.
  RemoveAllowedPages(dnr_extension->id(), {kGoogleDotCom});

  VerifyGetAllowedPages(dnr_extension->id(), {});
}

// Tests that the page allowing API performs pattern matching correctly.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       PageAllowingAPI_Patterns) {
  // Load an extension which blocks requests to "script.js".
  set_has_background_script(true);
  std::vector<TestRule> rules;
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("script.js");
  rules.push_back(rule);
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // We'll be allowing these patterns subsequently.
  const std::vector<std::string> allowed_page_patterns = {
      "http://google.com:*/pages_with_script/page*.html",
      "http://*/*index.html",
      "http://example.com:*/pages_with_script/page2.html"};

  struct TestCase {
    std::string hostname;
    std::string path;
    bool expect_script_allowed_with_rules;
  } test_cases[] = {
      {"yahoo.com", "/pages_with_script/page.html", false},
      {"yahoo.com", "/pages_with_script/index.html", true},
      {"example.com", "/pages_with_script/page.html", false},
      {"example.com", "/pages_with_script/page2.html", true},
      {"google.com", "/pages_with_script/page.html", true},
      {"google.com", "/pages_with_script/page2.html", true},
  };

  const GURL script_url =
      embedded_test_server()->GetURL("/pages_with_script/script.js");
  auto verify_script_load = [this, script_url](bool expect_script_load) {
    // The page should have loaded correctly.
    EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
    EXPECT_EQ(expect_script_load, WasFrameWithScriptLoaded(GetMainFrame()));

    // The EmbeddedTestServer sees requests after the hostname has been
    // resolved.
    bool did_see_script_request =
        base::ContainsKey(GetAndResetRequestsToServer(), script_url);
    EXPECT_EQ(expect_script_load, did_see_script_request);
  };

  // "script.js" should have been blocked for all pages initially.
  for (const auto& test_case : test_cases) {
    GURL url =
        embedded_test_server()->GetURL(test_case.hostname, test_case.path);
    SCOPED_TRACE(base::StringPrintf("Testing %s with no page allowing rules",
                                    url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);

    verify_script_load(false);
  }

  // Now allow |allowed_page_patterns| and ensure the test expectations
  // are met.
  AddAllowedPages(last_loaded_extension_id(), allowed_page_patterns);
  for (const auto& test_case : test_cases) {
    GURL url =
        embedded_test_server()->GetURL(test_case.hostname, test_case.path);
    SCOPED_TRACE(base::StringPrintf("Testing %s with page allowing rules",
                                    url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);

    verify_script_load(test_case.expect_script_allowed_with_rules);
  }

  // Now remove the |allowed_page_patterns| and ensure that all requests to
  // "script.js" are blocked again.
  RemoveAllowedPages(last_loaded_extension_id(), allowed_page_patterns);
  for (const auto& test_case : test_cases) {
    GURL url =
        embedded_test_server()->GetURL(test_case.hostname, test_case.path);
    SCOPED_TRACE(base::StringPrintf(
        "Testing %s with all page allowing rules removed", url.spec().c_str()));

    ui_test_utils::NavigateToURL(browser(), url);

    verify_script_load(false);
  }
}

// Tests the page allowing API for subresource requests.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       PageAllowingAPI_Resources) {
  // Load an extension which blocks all requests.
  set_has_background_script(true);
  std::vector<TestRule> rules;

  // This blocks all subresource requests. By default the main-frame resource
  // type is excluded.
  TestRule rule = CreateGenericRule();
  rule.id = kMinValidID;
  rule.condition->url_filter = std::string("*");
  rules.push_back(rule);

  // Also block main frame requests.
  rule.id = kMinValidID + 1;
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});
  rules.push_back(rule);

  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  const GURL url = embedded_test_server()->GetURL("/allowing_api.html");

  auto verify_page_load = [this](bool expect_load) {
    EXPECT_EQ(expect_load, WasFrameWithScriptLoaded(GetMainFrame()));
    EXPECT_EQ(
        expect_load ? content::PAGE_TYPE_NORMAL : content::PAGE_TYPE_ERROR,
        GetPageType());

    // We will be loading "allowing_api.html" for the test. The following
    // lists the set of requests we expect to see if the page is loaded.
    static const std::vector<std::string> page_resources_path = {
        // Main frame request.
        "/allowing_api.html",

        // Main frame subrources.
        "/subresources/style.css", "/subresources/script.js",
        "/subresources/image.png", "/subresources/ping.mp3",
        "/child_frame_with_subresources.html",

        // Child frame subresources.
        "/iframe_subresources/style.css", "/iframe_subresources/script.js",
        "/iframe_subresources/image.png", "/iframe_subresources/ping.mp3",
        "/iframe_subresources/subframe.html",
    };

    const std::set<GURL> requests_seen = GetAndResetRequestsToServer();

    for (const auto& path : page_resources_path) {
      GURL expected_request_url = embedded_test_server()->GetURL(path);

      // Request to |expected_requested_url| should be seen by the server iff we
      // expect the page to load.
      if (expect_load) {
        EXPECT_TRUE(base::ContainsKey(requests_seen, expected_request_url))
            << expected_request_url.spec()
            << " was not requested from the server.";
      } else {
        EXPECT_FALSE(base::ContainsKey(requests_seen, expected_request_url))
            << expected_request_url.spec() << " request seen unexpectedly.";
      }
    }
  };

  // Initially the request for the page should be blocked. No request should
  // reach the server.
  ui_test_utils::NavigateToURL(browser(), url);
  verify_page_load(false /*expect_load*/);

  // Next we allow |url|. All requests with |url| as the top level frame
  // should be allowed.
  AddAllowedPages(last_loaded_extension_id(), {url.spec()});

  // Ensure that no requests made by the page load are blocked.
  ui_test_utils::NavigateToURL(browser(), url);
  verify_page_load(true /*expect_load*/);

  // Remove |url| from allowed pages.
  RemoveAllowedPages(last_loaded_extension_id(), {url.spec()});

  // Ensure that requests are blocked.
  ui_test_utils::NavigateToURL(browser(), url);
  verify_page_load(false /*expect_load*/);
}

// Tests that requests which can't be mapped to a render frame (e.g. non-
// navigation browser requests) are not affected by the page allowing API.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       PageAllowingAPI_BrowserRequests) {
  // Load an extension which blocks all requests.
  set_has_background_script(true);
  std::vector<TestRule> rules;
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*");
  rules.push_back(rule);
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Allow requests served from "google.com/*" pages.
  AddAllowedPages(last_loaded_extension_id(), {"http://google.com:*/*"});

  const GURL url = embedded_test_server()->GetURL(
      "google.com", "/pages_with_script/index.html");

  // A navigation to |url| should not be blocked as google.com/* pages have been
  // allowed. This will cause two network requests to index.html and
  // script.js.
  ui_test_utils::NavigateToURL(browser(), url);
  EXPECT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
  std::set<GURL> requests_seen = GetAndResetRequestsToServer();

  // The EmbeddedTestServer sees requests after the hostname has been resolved.
  EXPECT_TRUE(base::ContainsKey(
      requests_seen,
      embedded_test_server()->GetURL("/pages_with_script/index.html")));
  EXPECT_TRUE(base::ContainsKey(
      requests_seen,
      embedded_test_server()->GetURL("/pages_with_script/script.js")));

  // But a non-navigation browser initiated resource request should still be
  // blocked. This is because such a request can't be mapped to a top level
  // frame and hence won't be considered for allowing by the page
  // allowing API.
  auto request = std::make_unique<network::ResourceRequest>();
  request->url = embedded_test_server()->GetURL("google.com",
                                                "/pages_with_script/script.js");
  request->resource_type = content::ResourceType::RESOURCE_TYPE_SCRIPT;
  request->render_frame_id = MSG_ROUTING_NONE;

  // TODO(https://crbug.com/857577): remove this hack. When an unrelated
  // browser issued request (typically from GaiaAuthFetcher) has run, it causes
  // the StoragePartitionImpl to create and cache a URLLoaderFactory without the
  // web request proxying. This resets it so one with the web request proxying
  // is created the next time a request is made.
  base::RunLoop().RunUntilIdle();
  content::BrowserContext::GetDefaultStoragePartition(profile())
      ->ResetURLLoaderFactoryForBrowserProcessForTesting();

  auto loader = network::SimpleURLLoader::Create(std::move(request),
                                                 TRAFFIC_ANNOTATION_FOR_TESTS);
  content::SimpleURLLoaderTestHelper loader_helper;
  loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      content::BrowserContext::GetDefaultStoragePartition(profile())
          ->GetURLLoaderFactoryForBrowserProcess()
          .get(),
      loader_helper.GetCallback());
  loader_helper.WaitForCallback();

  EXPECT_FALSE(loader_helper.response_body());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, loader->NetError());
  EXPECT_FALSE(base::ContainsKey(
      GetAndResetRequestsToServer(),
      embedded_test_server()->GetURL("/pages_with_script/script.js")));
}

// Ensures that any <img> elements blocked by the API are collapsed.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, ImageCollapsed) {
  // Loads a page with an image and returns whether the image was collapsed.
  auto is_image_collapsed = [this]() {
    ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("google.com", "/image.html"));
    EXPECT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());
    bool is_image_collapsed = false;
    const std::string script =
        "domAutomationController.send(!!window.imageCollapsed);";
    EXPECT_TRUE(content::ExecuteScriptAndExtractBool(web_contents(), script,
                                                     &is_image_collapsed));
    return is_image_collapsed;
  };

  // Initially the image shouldn't be collapsed.
  EXPECT_FALSE(is_image_collapsed());

  // Load an extension which blocks all images.
  std::vector<TestRule> rules;
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*");
  rule.condition->resource_types = std::vector<std::string>({"image"});
  rules.push_back(rule);
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(rules));

  // Now the image request should be blocked and the corresponding DOM element
  // should be collapsed.
  EXPECT_TRUE(is_image_collapsed());
}

// Ensures that any <iframe> elements whose document load is blocked by the API,
// are collapsed.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, IFrameCollapsed) {
  // Verifies whether the frame with name |frame_name| is collapsed.
  auto test_frame_collapse = [this](const std::string& frame_name,
                                    bool expect_collapsed) {
    SCOPED_TRACE(base::StringPrintf("Testing frame %s", frame_name.c_str()));
    content::RenderFrameHost* frame = GetFrameByName(frame_name);
    ASSERT_TRUE(frame);

    EXPECT_EQ(!expect_collapsed, WasFrameWithScriptLoaded(frame));

    constexpr char kScript[] = R"(
        var iframe = document.getElementsByName('%s')[0];
        var collapsed = iframe.clientWidth === 0 && iframe.clientHeight === 0;
        domAutomationController.send(collapsed);
    )";
    bool collapsed = false;
    ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
        GetMainFrame(), base::StringPrintf(kScript, frame_name.c_str()),
        &collapsed));
    EXPECT_EQ(expect_collapsed, collapsed);
  };

  // Navigates frame with name |frame_name| to |url|.
  auto navigate_frame = [this](const std::string& frame_name, const GURL& url) {
    content::TestNavigationObserver navigation_observer(
        web_contents(), 1 /*number_of_navigations*/);
    ASSERT_TRUE(content::ExecuteScript(
        GetMainFrame(),
        base::StringPrintf("document.getElementsByName('%s')[0].src = '%s';",
                           frame_name.c_str(), url.spec().c_str())));
    navigation_observer.Wait();
  };

  const std::string kFrameName1 = "frame1";
  const std::string kFrameName2 = "frame2";
  const GURL page_url = embedded_test_server()->GetURL(
      "google.com", "/page_with_two_frames.html");

  // Load a page with two iframes (|kFrameName1| and |kFrameName2|). Initially
  // both the frames should be loaded successfully.
  ui_test_utils::NavigateToURL(browser(), page_url);
  ASSERT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  {
    SCOPED_TRACE("No extension loaded");
    test_frame_collapse(kFrameName1, false);
    test_frame_collapse(kFrameName2, false);
  }

  // Now load an extension which blocks all requests with "frame=1" in its url.
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("frame=1");
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  // Reloading the page should cause |kFrameName1| to be collapsed.
  ui_test_utils::NavigateToURL(browser(), page_url);
  ASSERT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));
  {
    SCOPED_TRACE("Extension loaded initial");
    test_frame_collapse(kFrameName1, true);
    test_frame_collapse(kFrameName2, false);
  }

  // Now interchange the "src" of the two frames. This should cause
  // |kFrameName2| to be collapsed and |kFrameName1| to be un-collapsed.
  GURL frame_url_1 = GetFrameByName(kFrameName1)->GetLastCommittedURL();
  GURL frame_url_2 = GetFrameByName(kFrameName2)->GetLastCommittedURL();
  navigate_frame(kFrameName1, frame_url_2);
  navigate_frame(kFrameName2, frame_url_1);
  {
    SCOPED_TRACE("Extension loaded src swapped");
    test_frame_collapse(kFrameName1, false);
    test_frame_collapse(kFrameName2, true);
  }
}

// Tests that we correctly reindex a corrupted ruleset. This is only tested for
// packed extensions, since the JSON ruleset is reindexed on each extension
// load for unpacked extensions, so corruption is not an issue.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest_Packed,
                       CorruptedIndexedRuleset) {
  // Set-up an observer for RulesetMatcher to monitor the number of extension
  // rulesets.
  RulesetCountWaiter ruleset_count_waiter;
  ScopedRulesetManagerTestObserver scoped_observer(
      &ruleset_count_waiter,
      base::WrapRefCounted(ExtensionSystem::Get(profile())->info_map()));

  const GURL url = embedded_test_server()->GetURL(
      "google.com", "/pages_with_script/index.html");

  // Verifies whether |url| was successfully loaded.
  auto verify_page_load = [this, &url](bool success) {
    ui_test_utils::NavigateToURL(browser(), url);
    EXPECT_EQ(success, WasFrameWithScriptLoaded(GetMainFrame()));

    content::PageType expected_page_type =
        success ? content::PAGE_TYPE_NORMAL : content::PAGE_TYPE_ERROR;
    EXPECT_EQ(expected_page_type, GetPageType());
  };

  // Initially no main frame requests should be blocked.
  {
    SCOPED_TRACE("Initial page load");
    verify_page_load(true);
  }

  // Load an extension which blocks all main frame requests.
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*");
  rule.condition->resource_types = std::vector<std::string>({"main_frame"});
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));
  ruleset_count_waiter.WaitForRulesetCount(1);

  const ExtensionId extension_id = last_loaded_extension_id();
  const base::FilePath extension_path =
      extension_service()
          ->GetExtensionById(extension_id, false /*include_disabled*/)
          ->path();

  // Loading the extension should cause main frame requests to be blocked.
  {
    SCOPED_TRACE("Page load after loading extension");
    verify_page_load(false);
  }

  // Overwrite the indexed ruleset file with arbitrary data to mimic corruption.
  {
    base::ScopedAllowBlockingForTesting scoped_allow_blocking;
    std::string corrupted_data = "data";
    ASSERT_EQ(static_cast<int>(corrupted_data.size()),
              base::WriteFile(file_util::GetIndexedRulesetPath(extension_path),
                              corrupted_data.c_str(), corrupted_data.size()));
  }

  // The extension should still continue to work since it doesn't need the
  // indexed ruleset while it is loaded.
  verify_page_load(false);

  // Now reload the extension and verify that we detect indexed ruleset
  // corruption and reindex the JSON ruleset.
  {
    DisableExtension(extension_id);
    ruleset_count_waiter.WaitForRulesetCount(0);

    base::HistogramTester tester;
    EnableExtension(extension_id);
    ruleset_count_waiter.WaitForRulesetCount(1);

    // Verify that loading the ruleset would have failed initially due to
    // checksum mismatch and later succeeded.
    EXPECT_EQ(1, tester.GetBucketCount(
                     "Extensions.DeclarativeNetRequest.LoadRulesetResult",
                     RulesetMatcher::LoadRulesetResult::
                         kLoadErrorRulesetVerification /*sample*/));
    EXPECT_EQ(1,
              tester.GetBucketCount(
                  "Extensions.DeclarativeNetRequest.LoadRulesetResult",
                  RulesetMatcher::LoadRulesetResult::kLoadSuccess /*sample*/));

    // Verify that reindexing succeeded.
    tester.ExpectUniqueSample(
        "Extensions.DeclarativeNetRequest.RulesetReindexSuccessful",
        true /*sample*/, 1 /*count*/);

    // The reindexing of the ruleset should cause the extension to work
    // correctly.
    SCOPED_TRACE("Page load after ruleset corruption");
    verify_page_load(false);
  }
}

// Tests that we surface a warning to the user if it's ruleset fails to load.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest,
                       WarningOnFailedRulesetLoad) {
  TestRule rule = CreateGenericRule();
  rule.condition->url_filter = std::string("*");
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules({rule}));

  const ExtensionId extension_id = last_loaded_extension_id();
  const auto* rules_monitor_service = BrowserContextKeyedAPIFactory<
      declarative_net_request::RulesMonitorService>::Get(profile());
  EXPECT_TRUE(rules_monitor_service->HasRegisteredRuleset(extension_id));

  // Mimic extension prefs corruption by overwriting the indexed ruleset
  // checksum.
  const int kInvalidRulesetChecksum = -1;
  ExtensionPrefs::Get(profile())->SetDNRRulesetChecksumForTesting(
      extension_id, kInvalidRulesetChecksum);

  TestExtensionRegistryObserver registry_observer(
      ExtensionRegistry::Get(profile()), extension_id);
  DisableExtension(extension_id);
  ASSERT_TRUE(registry_observer.WaitForExtensionUnloaded());
  EXPECT_FALSE(rules_monitor_service->HasRegisteredRuleset(extension_id));

  // Both loading the indexed ruleset and reindexing the ruleset should fail
  // now.
  base::HistogramTester tester;
  WarningService* warning_service = WarningService::Get(profile());
  WarningServiceObserver warning_observer(warning_service, extension_id);
  EnableExtension(extension_id);

  // Wait till we surface a warning.
  warning_observer.WaitForWarning();
  EXPECT_THAT(warning_service->GetWarningTypesAffectingExtension(extension_id),
              ::testing::ElementsAre(Warning::kRulesetFailedToLoad));

  EXPECT_FALSE(rules_monitor_service->HasRegisteredRuleset(extension_id));

  // Verify that loading the ruleset failed due to checksum mismatch.
  EXPECT_EQ(1, tester.GetBucketCount(
                   "Extensions.DeclarativeNetRequest.LoadRulesetResult",
                   RulesetMatcher::LoadRulesetResult::
                       kLoadErrorRulesetVerification /*sample*/));

  // Verify that re-indexing the ruleset failed.
  tester.ExpectUniqueSample(
      "Extensions.DeclarativeNetRequest.RulesetReindexSuccessful",
      false /*sample*/, 1 /*count*/);
}

// Test fixture to verify that host permissions for the request url and the
// request initiator are properly checked. Loads an example.com url with four
// sub-frames named frame_[1..4] from hosts frame_[1..4].com. The initiator for
// these frames will be example.com. Loads an extension set to block all sub-
// frames. Verifies that the correct frames are blocked depending on the host
// permissions for the extension.
class DeclarativeNetRequestHostPermissionsBrowserTest
    : public DeclarativeNetRequestBrowserTest {
 public:
  DeclarativeNetRequestHostPermissionsBrowserTest() {}

 protected:
  struct FrameLoadResult {
    std::string child_frame_name;
    bool expect_frame_loaded;
  };

  void LoadExtensionWithHostPermissions(const std::vector<std::string>& hosts) {
    std::vector<TestRule> rules;

    // Block all sub-frame requests.
    TestRule rule = CreateGenericRule();
    rule.condition->url_filter = std::string("*");
    rule.condition->resource_types = std::vector<std::string>({"sub_frame"});
    rules.push_back(rule);

    ASSERT_NO_FATAL_FAILURE(
        LoadExtensionWithRules(rules, "test_extension", hosts));
  }

  void RunTests(const std::vector<FrameLoadResult>& expected_results) {
    ASSERT_EQ(4u, expected_results.size());

    GURL url = embedded_test_server()->GetURL("example.com",
                                              "/page_with_four_frames.html");
    ui_test_utils::NavigateToURL(browser(), url);
    ASSERT_TRUE(WasFrameWithScriptLoaded(GetMainFrame()));

    for (const auto& frame_result : expected_results) {
      SCOPED_TRACE(base::StringPrintf("Testing child frame named %s",
                                      frame_result.child_frame_name.c_str()));

      content::RenderFrameHost* child =
          GetFrameByName(frame_result.child_frame_name);
      EXPECT_TRUE(child);
      EXPECT_EQ(frame_result.expect_frame_loaded,
                WasFrameWithScriptLoaded(child));
    }
  }

  std::string GetMatchPatternForDomain(const std::string& domain) const {
    return "*://*." + domain + ".com/*";
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(DeclarativeNetRequestHostPermissionsBrowserTest);
};

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestHostPermissionsBrowserTest,
                       AllURLs1) {
  // All frames should be blocked since the extension has access to all hosts.
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithHostPermissions({"<all_urls>"}));
  RunTests({{"frame_1", false},
            {"frame_2", false},
            {"frame_3", false},
            {"frame_4", false}});
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestHostPermissionsBrowserTest,
                       AllURLs2) {
  // All frames should be blocked since the extension has access to all hosts.
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithHostPermissions({"*://*/*"}));
  RunTests({{"frame_1", false},
            {"frame_2", false},
            {"frame_3", false},
            {"frame_4", false}});
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestHostPermissionsBrowserTest,
                       NoPermissions) {
  // The extension has no host permissions. No frames should be blocked.
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithHostPermissions({}));
  RunTests({{"frame_1", true},
            {"frame_2", true},
            {"frame_3", true},
            {"frame_4", true}});
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestHostPermissionsBrowserTest,
                       SubframesWithNoInitiatorPermissions) {
  // The extension has access to requests to "frame_1.com" and "frame_2.com",
  // but not the initiator of those requests (example.com). No frames should be
  // blocked.
  ASSERT_NO_FATAL_FAILURE(
      LoadExtensionWithHostPermissions({GetMatchPatternForDomain("frame_1"),
                                        GetMatchPatternForDomain("frame_2")}));
  RunTests({{"frame_1", true},
            {"frame_2", true},
            {"frame_3", true},
            {"frame_4", true}});
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestHostPermissionsBrowserTest,
                       SubframesWithInitiatorPermission) {
  // The extension has access to requests to "frame_1.com" and "frame_4.com",
  // and also the initiator of those requests (example.com). Hence |frame_1| and
  // |frame_4| should be blocked.
  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithHostPermissions(
      {GetMatchPatternForDomain("frame_1"), GetMatchPatternForDomain("frame_4"),
       GetMatchPatternForDomain("example")}));
  RunTests({{"frame_1", false},
            {"frame_2", true},
            {"frame_3", true},
            {"frame_4", false}});
}

// Fixture to test the "resourceTypes" and "excludedResourceTypes" fields of a
// declarative rule condition.
class DeclarativeNetRequestResourceTypeBrowserTest
    : public DeclarativeNetRequestBrowserTest {
 public:
  DeclarativeNetRequestResourceTypeBrowserTest() {}

 protected:
  // TODO(crbug.com/696822): Add tests for "object", "ping", "other", "font",
  // "csp_report".
  enum ResourceTypeMask {
    kNone = 0,
    kSubframe = 1 << 0,
    kStylesheet = 1 << 1,
    kScript = 1 << 2,
    kImage = 1 << 3,
    kXHR = 1 << 4,
    kMedia = 1 << 5,
    kWebSocket = 1 << 6,
    kAll = (1 << 7) - 1
  };

  struct TestCase {
    std::string hostname;
    int blocked_mask;
  };

  void RunTests(const std::vector<TestCase>& test_cases) {
    // Start a web socket test server to test the websocket resource type.
    net::SpawnedTestServer websocket_test_server(
        net::SpawnedTestServer::TYPE_WS, net::GetWebSocketTestDataDirectory());
    ASSERT_TRUE(websocket_test_server.Start());

    // The |websocket_url| will echo the message we send to it.
    GURL websocket_url = websocket_test_server.GetURL("echo-with-no-extension");

    auto execute_script = [](content::RenderFrameHost* frame,
                             const std::string& script) {
      bool subresource_loaded = false;
      EXPECT_TRUE(content::ExecuteScriptAndExtractBool(frame, script,
                                                       &subresource_loaded));
      return subresource_loaded;
    };

    for (const auto& test_case : test_cases) {
      GURL url = embedded_test_server()->GetURL(test_case.hostname,
                                                "/subresources.html");
      SCOPED_TRACE(base::StringPrintf("Testing %s", url.spec().c_str()));

      ui_test_utils::NavigateToURL(browser(), url);
      ASSERT_EQ(content::PAGE_TYPE_NORMAL, GetPageType());

      content::RenderFrameHost* frame = GetMainFrame();

      // sub-frame.
      EXPECT_EQ(
          !(test_case.blocked_mask & kSubframe),
          execute_script(
              frame, "domAutomationController.send(!!window.frameLoaded);"));

      // stylesheet
      EXPECT_EQ(!(test_case.blocked_mask & kStylesheet),
                execute_script(frame, "testStylesheet();"));

      // script
      EXPECT_EQ(!(test_case.blocked_mask & kScript),
                execute_script(frame, "testScript();"));

      // image
      EXPECT_EQ(!(test_case.blocked_mask & kImage),
                execute_script(frame, "testImage();"));

      // xhr
      EXPECT_EQ(!(test_case.blocked_mask & kXHR),
                execute_script(frame, "testXHR();"));

      // media
      EXPECT_EQ(!(test_case.blocked_mask & kMedia),
                execute_script(frame, "testMedia();"));

      // websocket
      EXPECT_EQ(!(test_case.blocked_mask & kWebSocket),
                execute_script(
                    frame, base::StringPrintf("testWebSocket('%s');",
                                              websocket_url.spec().c_str())));
    }
  }

  // Loads an extension to test blocking different resource types.
  void LoadExtension() {
    struct {
      std::string domain;
      size_t id;
      std::vector<std::string> resource_types;
      std::vector<std::string> excluded_resource_types;
    } rules_data[] = {
        {"block_subframe.com", 1, {"sub_frame"}, {}},
        {"block_stylesheet.com", 2, {"stylesheet"}, {}},
        {"block_script.com", 3, {"script"}, {}},
        {"block_image.com", 4, {"image"}, {}},
        {"block_xhr.com", 5, {"xmlhttprequest"}, {}},
        {"block_media.com", 6, {"media"}, {}},
        {"block_websocket.com", 7, {"websocket"}, {}},
        {"block_image_and_stylesheet.com", 8, {"image", "stylesheet"}, {}},
        {"block_subframe_and_xhr.com", 11, {"sub_frame", "xmlhttprequest"}, {}},
        {"block_all.com", 9, {}, {}},
        {"block_all_but_xhr_and_script.com",
         10,
         {},
         {"xmlhttprequest", "script"}},
    };

    std::vector<TestRule> rules;
    for (const auto& rule_data : rules_data) {
      TestRule rule = CreateGenericRule();

      // The "resourceTypes" property (i.e. |rule.condition->resource_types|)
      // should not be an empty list. It should either be omitted or be a non-
      // empty list.
      if (rule_data.resource_types.empty())
        rule.condition->resource_types = base::nullopt;
      else
        rule.condition->resource_types = rule_data.resource_types;

      rule.condition->excluded_resource_types =
          rule_data.excluded_resource_types;
      rule.id = rule_data.id;
      rule.condition->domains = std::vector<std::string>({rule_data.domain});
      // Don't specify the urlFilter, which should behaves the same as "*".
      rule.condition->url_filter = base::nullopt;
      rules.push_back(rule);
    }
    LoadExtensionWithRules(rules);
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(DeclarativeNetRequestResourceTypeBrowserTest);
};

// These are split into two tests to prevent a timeout. See crbug.com/787957.
IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestResourceTypeBrowserTest, Test1) {
  ASSERT_NO_FATAL_FAILURE(LoadExtension());
  RunTests({{"block_subframe.com", kSubframe},
            {"block_stylesheet.com", kStylesheet},
            {"block_script.com", kScript},
            {"block_image.com", kImage},
            {"block_xhr.com", kXHR},
            {"block_media.com", kMedia}});
}

IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestResourceTypeBrowserTest, Test2) {
  ASSERT_NO_FATAL_FAILURE(LoadExtension());
  RunTests({{"block_websocket.com", kWebSocket},
            {"block_image_and_stylesheet.com", kImage | kStylesheet},
            {"block_subframe_and_xhr.com", kSubframe | kXHR},
            {"block_all.com", kAll},
            {"block_all_but_xhr_and_script.com", kAll & ~kXHR & ~kScript},
            {"block_none.com", kNone}});
}

INSTANTIATE_TEST_CASE_P(,
                        DeclarativeNetRequestBrowserTest,
                        ::testing::Values(ExtensionLoadType::PACKED,
                                          ExtensionLoadType::UNPACKED));

INSTANTIATE_TEST_CASE_P(,
                        DeclarativeNetRequestHostPermissionsBrowserTest,
                        ::testing::Values(ExtensionLoadType::PACKED,
                                          ExtensionLoadType::UNPACKED));
INSTANTIATE_TEST_CASE_P(,
                        DeclarativeNetRequestResourceTypeBrowserTest,
                        ::testing::Values(ExtensionLoadType::PACKED,
                                          ExtensionLoadType::UNPACKED));

INSTANTIATE_TEST_CASE_P(,
                        DeclarativeNetRequestBrowserTest_Packed,
                        ::testing::Values(ExtensionLoadType::PACKED));

}  // namespace
}  // namespace declarative_net_request
}  // namespace extensions
