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

#include "extensions/browser/extension_protocols.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/power_monitor_test.h"
#include "base/test/test_file_util.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_content_verifier_delegate.h"
#include "chrome/browser/extensions/chrome_extensions_browser_client.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_profile.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/content_verifier/content_verifier.h"
#include "extensions/browser/content_verifier/test_utils.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/file_util.h"
#include "extensions/test/test_extension_dir.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/loader/referrer_utils.h"

using extensions::ExtensionRegistry;
using network::mojom::URLLoader;
using testing::_;
using testing::StrictMock;

namespace extensions {
namespace {

constexpr char kValidTrialToken1[] = "valid_token_1";
constexpr char kValidTrialToken2[] = "valid_token_2";
constexpr char kTrialTokensHeaderValue[] = "valid_token_1, valid_token_2";

base::FilePath GetTestPath(const std::string& name) {
  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
  return path.AppendASCII("extensions").AppendASCII(name);
}

base::FilePath GetContentVerifierTestPath() {
  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &path));
  return path.AppendASCII("content_hash_fetcher")
      .AppendASCII("different_sized_files");
}

scoped_refptr<const Extension> CreateTestExtension(const std::string& name,
                                                   bool incognito_split_mode,
                                                   int manifest_version) {
  return ExtensionBuilder(name)
      .SetManifestVersion(manifest_version)
      .SetManifestKey("incognito", incognito_split_mode ? "split" : "spanning")
      .SetPath(GetTestPath("response_headers"))
      .SetLocation(mojom::ManifestLocation::kInternal)
      .Build();
}

scoped_refptr<const Extension> CreateWebStoreExtension(int manifest_version) {
  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(chrome::DIR_RESOURCES, &path));
  path = path.AppendASCII("web_store");

  return ExtensionBuilder("WebStore")
      .SetManifestVersion(manifest_version)
      .SetManifestKey("icons",
                      base::Value::Dict().Set("16", "webstore_icon_16.png"))
      .SetManifestKey(
          "web_accessible_resources",
          manifest_version == 3
              ? base::Value::List().Append(
                    base::Value::Dict()
                        .Set("resources",
                             base::Value::List().Append("webstore_icon_16.png"))
                        .Set("matches", base::Value::List().Append("*://*/*")))
              : base::Value::List().Append("webstore_icon_16.png"))
      .SetPath(path)
      .SetLocation(mojom::ManifestLocation::kComponent)
      .Build();
}

scoped_refptr<const Extension> CreateTestResponseHeaderExtension(
    int manifest_version) {
  if (manifest_version == 3) {
    return ExtensionBuilder("An extension with web-accessible resources")
        .SetManifestVersion(3)
        .SetManifestKey(
            "web_accessible_resources",
            base::Value::List().Append(
                base::Value::Dict()
                    .Set("resources", base::Value::List().Append("test.dat"))
                    .Set("matches", base::Value::List().Append("*://*/*"))))
        .SetManifestKey("background", base::Value::Dict().Set("service_worker",
                                                              "background.js"))
        .SetManifestKey("trial_tokens", base::Value::List()
                                            .Append(kValidTrialToken1)
                                            .Append(kValidTrialToken2))
        .SetPath(GetTestPath("response_headers"))
        .Build();
  }
  return ExtensionBuilder("An extension with web-accessible resources")
      .SetManifestVersion(manifest_version)
      .SetManifestKey("web_accessible_resources",
                      base::Value::List().Append("test.dat"))
      .SetManifestKey(
          "background",
          base::Value::Dict().Set("scripts",
                                  base::Value::List().Append("background.js")))
      .SetPath(GetTestPath("response_headers"))
      .Build();
}

scoped_refptr<const Extension> CreateTestModuleResponseHeaderExtension(
    int manifest_version) {
  return ExtensionBuilder("A module extension")
      .SetManifestVersion(manifest_version)
      .SetManifestKey("export", base::Value::Dict())
      .SetPath(GetTestPath("response_headers"))
      .Build();
}

scoped_refptr<const Extension> CreateTestModuleImporterResponseHeaderExtension(
    int manifest_version,
    const std::string& module_extension_id) {
  if (manifest_version == 3) {
    return ExtensionBuilder("A module importer extension")
        .SetManifestVersion(3)
        .SetManifestKey("import",
                        base::Value::List().Append(
                            base::Value::Dict().Set("id", module_extension_id)))
        .SetManifestKey("trial_tokens", base::Value::List()
                                            .Append(kValidTrialToken1)
                                            .Append(kValidTrialToken2))
        .SetPath(GetTestPath("response_headers"))
        .Build();
  }
  return ExtensionBuilder("A module importer extension")
      .SetManifestVersion(manifest_version)
      .SetManifestKey("import",
                      base::Value::List().Append(
                          base::Value::Dict().Set("id", module_extension_id)))
      .SetPath(GetTestPath("response_headers"))
      .Build();
}

// Helper function to create a |ResourceRequest| for testing purposes.
network::ResourceRequest CreateResourceRequest(
    const std::string& method,
    network::mojom::RequestDestination destination,
    const GURL& url) {
  network::ResourceRequest request;
  request.method = method;
  request.url = url;
  request.site_for_cookies =
      net::SiteForCookies::FromUrl(url);  // bypass third-party cookie blocking.
  request.request_initiator =
      url::Origin::Create(url);  // ensure initiator set.
  request.referrer_policy = blink::ReferrerUtils::GetDefaultNetReferrerPolicy();
  request.destination = destination;
  request.is_outermost_main_frame =
      destination == network::mojom::RequestDestination::kDocument;
  return request;
}

// The result of either a URLRequest of a URLLoader response (but not both)
// depending on the on test type.
class GetResult {
 public:
  GetResult(network::mojom::URLResponseHeadPtr response, int result)
      : response_(std::move(response)), result_(result) {}
  GetResult(GetResult&& other) : result_(other.result_) {}

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

  ~GetResult() = default;

  std::string GetResponseHeaderByName(const std::string& name) const {
    if (!response_ || !response_->headers) {
      return std::string();
    }
    return response_->headers->GetNormalizedHeader(name).value_or(
        std::string());
  }

  bool HasContentLengthHeader() {
    std::string content_length =
        GetResponseHeaderByName(net::HttpRequestHeaders::kContentLength);

    int length_value = 0;
    return !content_length.empty() &&
           base::StringToInt(content_length, &length_value) && length_value > 0;
  }

  bool HeaderIsPresent(const std::string& name) {
    return !GetResponseHeaderByName(name).empty();
  }

  int result() const { return result_; }

 private:
  network::mojom::URLResponseHeadPtr response_;
  int result_;
};

}  // namespace

// This test lives in src/chrome instead of src/extensions because it tests
// functionality delegated back to Chrome via ChromeExtensionsBrowserClient.
// See chrome/browser/extensions/chrome_url_request_util.cc.
class ExtensionProtocolsTestBase : public testing::Test,
                                   public testing::WithParamInterface<int> {
 public:
  explicit ExtensionProtocolsTestBase(bool force_incognito)
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
        rvh_test_enabler_(new content::RenderViewHostTestEnabler()),
        force_incognito_(force_incognito) {}

  void SetUp() override {
    testing::Test::SetUp();
    testing_profile_ = TestingProfile::Builder().Build();
    contents_ = CreateTestWebContents();

    // Set up content verification.
    base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
    command_line->AppendSwitchASCII(
        switches::kExtensionContentVerification,
        switches::kExtensionContentVerificationEnforce);
    content_verifier_ = new ContentVerifier(
        browser_context(),
        std::make_unique<ChromeContentVerifierDelegate>(browser_context()));
    content_verifier_->Start();
    static_cast<TestExtensionSystem*>(ExtensionSystem::Get(browser_context()))
        ->set_content_verifier(content_verifier_.get());
    loader_factory_.Bind(
        CreateExtensionNavigationURLLoaderFactory(browser_context(), false));
  }

  void TearDown() override {
    loader_factory_.reset();
    content_verifier_->Shutdown();
    // Shut down the PowerMonitor if initialized.
    base::PowerMonitor::GetInstance()->ShutdownForTesting();
  }

  GetResult RequestOrLoad(const GURL& url,
                          network::mojom::RequestDestination destination) {
    return LoadURL(url, destination);
  }

  void AddExtension(const scoped_refptr<const Extension>& extension,
                    bool incognito_enabled,
                    bool notifications_disabled) {
    EXPECT_TRUE(extension_registry()->AddEnabled(extension));
    extension_registry()->TriggerOnLoaded(extension.get());
    ExtensionPrefs::Get(browser_context())
        ->SetIsIncognitoEnabled(extension->id(), incognito_enabled);
  }

  void RemoveExtension(const scoped_refptr<const Extension>& extension,
                       const UnloadedExtensionReason reason) {
    EXPECT_TRUE(extension_registry()->RemoveEnabled(extension->id()));
    extension_registry()->TriggerOnUnloaded(extension.get(), reason);
    if (reason == UnloadedExtensionReason::DISABLE)
      EXPECT_TRUE(extension_registry()->AddDisabled(extension));
  }

  // Helper method to create a URL request/loader, call RequestOrLoad on it, and
  // return the result. If |extension| hasn't already been added to
  // extension_registry(), this will add it.
  GetResult DoRequestOrLoad(const scoped_refptr<Extension> extension,
                            const std::string& relative_path) {
    if (!extension_registry()->enabled_extensions().Contains(extension->id())) {
      AddExtension(extension.get(),
                   /*incognito_enabled=*/false,
                   /*notifications_disabled=*/false);
    }
    return RequestOrLoad(extension->GetResourceURL(relative_path),
                         network::mojom::RequestDestination::kDocument);
  }

  ExtensionRegistry* extension_registry() {
    return ExtensionRegistry::Get(browser_context());
  }

  content::BrowserContext* browser_context() {
    return force_incognito_ ? testing_profile_->GetPrimaryOTRProfile(
                                  /*create_if_needed=*/true)
                            : testing_profile_.get();
  }

  void EnableSimulationOfSystemSuspendForRequests() {
    power_monitor_source_.emplace();
  }

 protected:
  scoped_refptr<ContentVerifier> content_verifier_;

 private:
  GetResult LoadURL(const GURL& url,
                    network::mojom::RequestDestination destination) {
    constexpr int32_t kRequestId = 28;

    mojo::PendingRemote<network::mojom::URLLoader> loader;
    network::TestURLLoaderClient client;
    loader_factory_->CreateLoaderAndStart(
        loader.InitWithNewPipeAndPassReceiver(), kRequestId,
        network::mojom::kURLLoadOptionNone,
        CreateResourceRequest("GET", destination, url), client.CreateRemote(),
        net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));

    // If `power_monitor_source_` is set, simulates power suspend and resume
    // notifications. These notifications are posted tasks that will be executed
    // by `client.RunUntilComplete()`.
    if (power_monitor_source_) {
      power_monitor_source_->Suspend();
      power_monitor_source_->Resume();
    }

    client.RunUntilComplete();
    return GetResult(client.response_head().Clone(),
                     client.completion_status().error_code);
  }

  std::unique_ptr<content::WebContents> CreateTestWebContents() {
    auto site_instance = content::SiteInstance::Create(browser_context());
    return content::WebContentsTester::CreateTestWebContents(
        browser_context(), std::move(site_instance));
  }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
  mojo::Remote<network::mojom::URLLoaderFactory> loader_factory_;
  std::unique_ptr<TestingProfile> testing_profile_;
  std::unique_ptr<content::WebContents> contents_;
  const bool force_incognito_;

  std::optional<base::test::ScopedPowerMonitorTestSource> power_monitor_source_;
};

class ExtensionProtocolsTest : public ExtensionProtocolsTestBase {
 public:
  ExtensionProtocolsTest()
      : ExtensionProtocolsTestBase(false /*force_incognito*/) {}
};

class ExtensionProtocolsIncognitoTest : public ExtensionProtocolsTestBase {
 public:
  ExtensionProtocolsIncognitoTest()
      : ExtensionProtocolsTestBase(true /*force_incognito*/) {}
};

// A specialization that will only run on MV3 extensions.
using ExtensionProtocolsMV3Test = ExtensionProtocolsTest;

INSTANTIATE_TEST_SUITE_P(MV2, ExtensionProtocolsTest, ::testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3, ExtensionProtocolsTest, ::testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV2,
                         ExtensionProtocolsIncognitoTest,
                         ::testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3,
                         ExtensionProtocolsIncognitoTest,
                         ::testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV3, ExtensionProtocolsMV3Test, ::testing::Values(3));

// Tests that making a chrome-extension request in an incognito context is
// only allowed under the right circumstances (if the extension is allowed
// in incognito, and it's either a non-main-frame request or a split-mode
// extension).
TEST_P(ExtensionProtocolsIncognitoTest, IncognitoRequest) {
  struct TestCase {
    // Inputs.
    std::string name;
    bool incognito_split_mode;
    bool incognito_enabled;

    // Expected result.
    bool should_allow_main_frame_load;
  } test_cases[] = {
      {"spanning disabled", false, false, false},
      {"split disabled", true, false, false},
      {"spanning enabled", false, true, false},
      {"split enabled", true, true, true},
  };

  for (const auto& test_case : test_cases) {
    scoped_refptr<const Extension> extension = CreateTestExtension(
        test_case.name, test_case.incognito_split_mode, GetParam());
    AddExtension(extension, test_case.incognito_enabled, false);

    // First test a main frame request.
    // It doesn't matter that the resource doesn't exist. If the resource
    // is blocked, we should see BLOCKED_BY_CLIENT. Otherwise, the request
    // should just fail because the file doesn't exist.
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("404.html"),
                      network::mojom::RequestDestination::kDocument);

    if (test_case.should_allow_main_frame_load) {
      EXPECT_EQ(net::ERR_FILE_NOT_FOUND, get_result.result()) << test_case.name;
    } else {
      EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result())
          << test_case.name;
    }
    // Subframe navigation requests are blocked in ExtensionNavigationThrottle
    // which isn't added in this unit test. This is tested in an integration
    // test in ExtensionResourceRequestPolicyTest.IframeNavigateToInaccessible.
    RemoveExtension(extension, UnloadedExtensionReason::UNINSTALL);
  }
}

// Tests getting a resource for a component extension works correctly, both when
// the extension is enabled and when it is disabled.
TEST_P(ExtensionProtocolsTest, ComponentResourceRequest) {
  scoped_refptr<const Extension> extension =
      CreateWebStoreExtension(GetParam());
  AddExtension(extension, false, false);

  // First test it with the extension enabled.
  {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("webstore_icon_16.png"),
                      network::mojom::RequestDestination::kVideo);
    EXPECT_EQ(net::OK, get_result.result());
    EXPECT_TRUE(get_result.HasContentLengthHeader());
    EXPECT_EQ("image/png", get_result.GetResponseHeaderByName(
                               net::HttpRequestHeaders::kContentType));
    // TODO(crbug.com/333078381): remove "Content-Security-Policy" header from
    // images.
    EXPECT_TRUE(get_result.HeaderIsPresent("Content-Security-Policy"));
  }

  // And then test it with the extension disabled.
  RemoveExtension(extension, UnloadedExtensionReason::DISABLE);
  {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("webstore_icon_16.png"),
                      network::mojom::RequestDestination::kVideo);
    EXPECT_EQ(net::OK, get_result.result());
    EXPECT_TRUE(get_result.HasContentLengthHeader());
    EXPECT_EQ("image/png", get_result.GetResponseHeaderByName(
                               net::HttpRequestHeaders::kContentType));
  }
}

// Tests that a URL request for resource from an extension returns a few
// expected response headers.
TEST_P(ExtensionProtocolsTest, ResourceRequestResponseHeaders) {
  scoped_refptr<const Extension> extension =
      CreateTestResponseHeaderExtension(GetParam());
  AddExtension(extension, false, false);

  {
    auto get_result = RequestOrLoad(extension->GetResourceURL("test.dat"),
                                    network::mojom::RequestDestination::kVideo);
    EXPECT_EQ(net::OK, get_result.result());

    // Check that cache-related headers are set.
    std::string etag = get_result.GetResponseHeaderByName("ETag");
    EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
    EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));

    EXPECT_EQ("no-cache", get_result.GetResponseHeaderByName("Cache-Control"));

    // We set test.dat as web-accessible, so it should have CORS headers.
    EXPECT_EQ(
        "*", get_result.GetResponseHeaderByName("Access-Control-Allow-Origin"));
    EXPECT_EQ("cross-origin", get_result.GetResponseHeaderByName(
                                  "Cross-Origin-Resource-Policy"));

    // Only background service worker script should be allowed to load as a
    // service worker.
    EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));

    // COEP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));

    // CSP header does not make sense in non-document responses
    // TODO(crbug.com/333078381): remove "Content-Security-Policy" header from
    // non-document responses and update this check.
    EXPECT_TRUE(get_result.HeaderIsPresent("Content-Security-Policy"));

    // COOP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));

    // Origin Trials header does not make sense in video resource responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Origin-Trial"));
  }
}

// Tests that request for background script returns a few expected response
// headers.
TEST_P(ExtensionProtocolsTest, BackgroundScriptRequestResponseHeaders) {
  const int manifest_version = GetParam();
  scoped_refptr<const Extension> extension =
      CreateTestResponseHeaderExtension(manifest_version);
  AddExtension(extension, false, false);

  {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("background.js"),
                      network::mojom::RequestDestination::kServiceWorker);
    EXPECT_EQ(net::OK, get_result.result());

    // Check that cache-related headers are set.
    std::string etag = get_result.GetResponseHeaderByName("ETag");
    EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
    EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));

    EXPECT_EQ("no-cache", get_result.GetResponseHeaderByName("Cache-Control"));

    // Background scripts are not web-accessible, so do not need CORS headers.
    EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));

    // Only background service worker script should be allowed to load as a
    // service worker.
    if (manifest_version == 3) {
      EXPECT_EQ("/",
                get_result.GetResponseHeaderByName("Service-Worker-Allowed"));
    } else {
      EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));
    }

    // COEP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));

    // Even though CSP is currently not respected for service workers, it
    // probably should be. We continue to send a CSP header for service worker
    // scripts for when this changes.
    // See also
    // https://github.com/w3c/webappsec-csp/issues/336#issuecomment-1274730655
    if (manifest_version == 3) {
      EXPECT_EQ("script-src 'self';",
                get_result.GetResponseHeaderByName("Content-Security-Policy"));
    } else {
      EXPECT_EQ(
          "script-src 'self' blob: filesystem:; object-src 'self' blob: "
          "filesystem:;",
          get_result.GetResponseHeaderByName("Content-Security-Policy"));
    }

    // COOP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
  }
}

// Tests that request for background service worker returns Origin-Trial
// response header.
TEST_P(ExtensionProtocolsMV3Test, BackgroundScriptRequestResponseHeaders) {
  EXPECT_EQ(3, GetParam());
  scoped_refptr<const Extension> extension =
      CreateTestResponseHeaderExtension(GetParam());
  AddExtension(extension, false, false);

  {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("background.js"),
                      network::mojom::RequestDestination::kServiceWorker);
    EXPECT_EQ(net::OK, get_result.result());

    // In MV3-style service workers origin trail tokens are served via service
    // worker Origin-Trial header.
    EXPECT_EQ(kTrialTokensHeaderValue,
              get_result.GetResponseHeaderByName("Origin-Trial"));
  }
}

// TODO(crbug.com/333078381): Add a test checking that:
// - when background.page or background.service_worker is specified requesting
//   generated background page fails
// - when no background is specified, requesting generated background page fails

TEST_P(ExtensionProtocolsTest, BackgroundPageRequestResponseHeaders) {
  const int manifest_version = GetParam();
  scoped_refptr<const Extension> extension =
      CreateTestResponseHeaderExtension(manifest_version);
  AddExtension(extension, false, false);

  {
    auto get_result = RequestOrLoad(
        extension->GetResourceURL(kGeneratedBackgroundPageFilename),
        network::mojom::RequestDestination::kDocument);
    EXPECT_EQ(net::OK, get_result.result());

    // Check that cache-related headers are omitted
    // TODO(crbug.com/333078381): consider adding these headers to generated
    // pages.
    EXPECT_FALSE(get_result.HeaderIsPresent("ETag"));
    EXPECT_FALSE(get_result.HeaderIsPresent("Cache-Control"));

    // Background pages are not web-accessible, so do not need CORS headers.
    EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));

    // Background page does not need to be loaded as a service worker.
    EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));

    // Background page does not load cross-origin content so does not need COEP
    // header.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));

    if (manifest_version == 3) {
      EXPECT_EQ("script-src 'self';",
                get_result.GetResponseHeaderByName("Content-Security-Policy"));
    } else {
      EXPECT_EQ(
          "script-src 'self' blob: filesystem:; object-src 'self' blob: "
          "filesystem:;",
          get_result.GetResponseHeaderByName("Content-Security-Policy"));
    }

    // COOP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
  }
}

// Tests that resources from imported module extensions get appropriately
// loaded with proper headers or rejected
TEST_P(ExtensionProtocolsTest, ModuleRequestResponseHeaders) {
  const int manifest_version = GetParam();
  scoped_refptr<const Extension> module_extension =
      CreateTestModuleResponseHeaderExtension(manifest_version);
  scoped_refptr<const Extension> importer_extension =
      CreateTestModuleImporterResponseHeaderExtension(manifest_version,
                                                      module_extension->id());
  AddExtension(module_extension, false, false);
  AddExtension(importer_extension, false, false);

  // Not imported id will fail.
  {
    auto get_result =
        RequestOrLoad(importer_extension->GetResourceURL(
                          "_modules/modaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test.dat"),
                      network::mojom::RequestDestination::kDocument);
    EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
  }
  {
    auto get_result =
        RequestOrLoad(importer_extension->GetResourceURL(
                          "_modules/modaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test.dat"),
                      network::mojom::RequestDestination::kServiceWorker);
    EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
  }

  // Imported resources get loaded with proper headers (inherited from
  // importer).
  {
    auto get_result =
        RequestOrLoad(importer_extension->GetResourceURL(
                          "_modules/" + module_extension->id() + "/test.dat"),
                      network::mojom::RequestDestination::kDocument);
    EXPECT_EQ(net::OK, get_result.result());

    // Check that cache-related headers are set.
    std::string etag = get_result.GetResponseHeaderByName("ETag");
    EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
    EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));

    // Background pages are not web-accessible, so do not need CORS headers.
    EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));

    // Background page does not need to be loaded as a service worker.
    EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));

    // Background page does not load cross-origin content so does not need COEP
    // header.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));

    if (manifest_version == 3) {
      EXPECT_EQ("script-src 'self';",
                get_result.GetResponseHeaderByName("Content-Security-Policy"));
    } else {
      EXPECT_EQ(
          "script-src 'self' blob: filesystem:; object-src 'self' blob: "
          "filesystem:;",
          get_result.GetResponseHeaderByName("Content-Security-Policy"));
    }

    // COOP header does not make sense in non-document responses.
    EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
  }
}

// Tests that request for background service worker returns Origin-Trial
// response header.
TEST_P(ExtensionProtocolsMV3Test, ModuleRequestResponseHeaders) {
  EXPECT_EQ(3, GetParam());
  const int manifest_version = GetParam();
  scoped_refptr<const Extension> module_extension =
      CreateTestModuleResponseHeaderExtension(manifest_version);
  scoped_refptr<const Extension> importer_extension =
      CreateTestModuleImporterResponseHeaderExtension(manifest_version,
                                                      module_extension->id());
  AddExtension(module_extension, false, false);
  AddExtension(importer_extension, false, false);

  // Imported resources get loaded with proper headers (inherited from
  // importer).
  {
    auto get_result =
        RequestOrLoad(importer_extension->GetResourceURL(
                          "_modules/" + module_extension->id() + "/test.dat"),
                      network::mojom::RequestDestination::kDocument);
    EXPECT_EQ(net::OK, get_result.result());

    // Origin-Trial header should contain trials inherited from importer.
    EXPECT_EQ(kTrialTokensHeaderValue,
              get_result.GetResponseHeaderByName("Origin-Trial"));
  }
}

TEST_P(ExtensionProtocolsTest, InvalidBackgroundScriptRequest) {
  const int manifest_version = GetParam();
  scoped_refptr<const Extension> extension =
      CreateTestResponseHeaderExtension(manifest_version);
  AddExtension(extension, false, false);

  // Requesting script from background key with invalid destination is
  // forbidden.
  std::vector<network::mojom::RequestDestination> destinations = {
      // TODO(crbug.com/333078381): carefully consider which other
      // request destinations should be allowed or blocked and update
      // this test
      network::mojom::RequestDestination::kJson,
      network::mojom::RequestDestination::kStyle,
      network::mojom::RequestDestination::kVideo,
  };
  if (!base::FeatureList::IsEnabled(blink::features::kPlzDedicatedWorker)) {
    destinations.push_back(network::mojom::RequestDestination::kWorker);
  }
  for (network::mojom::RequestDestination destination : destinations) {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("background.js"), destination);
    EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result()) << destination;
  }
}

// Tests that a URL request for main frame or subframe from an extension
// succeeds, but subresources fail. See http://crbug.com/312269.
TEST_P(ExtensionProtocolsTest, AllowFrameRequests) {
  scoped_refptr<const Extension> extension =
      CreateTestExtension("foo", false, GetParam());
  AddExtension(extension, false, false);

  // All MAIN_FRAME requests should succeed. SUB_FRAME requests that are not
  // explicitly listed in web_accessible_resources or same-origin to the parent
  // should not succeed.
  {
    auto get_result =
        RequestOrLoad(extension->GetResourceURL("test.dat"),
                      network::mojom::RequestDestination::kDocument);
    EXPECT_EQ(net::OK, get_result.result());
  }

  // Subframe navigation requests are blocked in ExtensionNavigationThrottle
  // which isn't added in this unit test. This is tested in an integration test
  // in ExtensionResourceRequestPolicyTest.IframeNavigateToInaccessible.

  // And subresource types, such as media, should fail.
  {
    auto get_result = RequestOrLoad(extension->GetResourceURL("test.dat"),
                                    network::mojom::RequestDestination::kVideo);
    EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
  }
}

// Make sure requests for paths ending with a separator aren't allowed. See
// https://crbug.com/356878412.
TEST_P(ExtensionProtocolsTest, PathsWithTrailingSeparatorsAreNotAllowed) {
  base::FilePath extension_dir = GetTestPath("simple_with_file");
  std::string error;
  scoped_refptr<Extension> extension = file_util::LoadExtension(
      extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
      &error);
  ASSERT_NE(extension.get(), nullptr) << "error: " << error;

  // Loading "/file.html" should succeed.
  EXPECT_EQ(net::OK, DoRequestOrLoad(extension, "file.html").result());

  // Loading "/file.html/" should fail.
  base::FilePath relative_path =
      base::FilePath(FILE_PATH_LITERAL("file.html")).AsEndingWithSeparator();
  EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}

// Make sure directories with an index.html file aren't serving the file, i.e.
// index.html doesn't get any special treatment.
TEST_P(ExtensionProtocolsTest, DirectoryWithIndexHtml) {
  base::FilePath extension_dir = GetTestPath("simple_with_index_html");
  std::string error;
  scoped_refptr<Extension> extension = file_util::LoadExtension(
      extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
      &error);
  ASSERT_NE(extension.get(), nullptr) << "error: " << error;

  // Loading "/test_dir" should fail.
  base::FilePath relative_path(FILE_PATH_LITERAL("test_dir"));
  EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());

  // Loading "/test_dir/" should fail.
  relative_path = relative_path.AsEndingWithSeparator();
  EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());

  // Loading "/test_dir/index.html" explicitly should succeed.
  relative_path = relative_path.AppendASCII("index.html");
  EXPECT_EQ(net::OK,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}

TEST_P(ExtensionProtocolsTest, MetadataFolder) {
  base::FilePath extension_dir = GetTestPath("metadata_folder");
  std::string error;
  scoped_refptr<Extension> extension = file_util::LoadExtension(
      extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
      &error);
  ASSERT_NE(extension.get(), nullptr) << "error: " << error;

  // Loading "/test.html" should succeed.
  EXPECT_EQ(net::OK, DoRequestOrLoad(extension, "test.html").result());

  // Loading "/_metadata/verified_contents.json" should fail.
  base::FilePath relative_path =
      base::FilePath(kMetadataFolder).Append(kVerifiedContentsFilename);
  EXPECT_TRUE(base::PathExists(extension_dir.Append(relative_path)));
  EXPECT_NE(net::OK,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());

  // Loading "/_metadata/a.txt" should also fail.
  relative_path = base::FilePath(kMetadataFolder).AppendASCII("a.txt");
  EXPECT_TRUE(base::PathExists(extension_dir.Append(relative_path)));
  EXPECT_NE(net::OK,
            DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}

// Tests that unreadable files and deleted files correctly go through
// ContentVerifyJob.
TEST_P(ExtensionProtocolsTest, VerificationSeenForFileAccessErrors) {
  // Unzip extension containing verification hashes to a temporary directory.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  base::FilePath unzipped_path = temp_dir.GetPath();
  scoped_refptr<Extension> extension =
      content_verifier_test_utils::UnzipToDirAndLoadExtension(
          GetContentVerifierTestPath().AppendASCII("source.zip"),
          unzipped_path);
  ASSERT_TRUE(extension.get());
  ExtensionId extension_id = extension->id();

  const std::string kJs("1024.js");
  base::FilePath kRelativePath(FILE_PATH_LITERAL("1024.js"));

  // Valid and readable 1024.js.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kJs).result());
    EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
  }

  // chmod -r 1024.js.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    base::FilePath file_path = unzipped_path.AppendASCII(kJs);
    ASSERT_TRUE(base::MakeFileUnreadable(file_path));
    EXPECT_EQ(net::ERR_ACCESS_DENIED, DoRequestOrLoad(extension, kJs).result());
    EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
    // NOTE: In production, hash mismatch would have disabled |extension|, but
    // since UnzipToDirAndLoadExtension() doesn't add the extension to
    // ExtensionRegistry, ChromeContentVerifierDelegate won't disable it.
    // TODO(lazyboy): We may want to update this to more closely reflect the
    // real flow.
  }

  // Delete 1024.js.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    base::FilePath file_path = unzipped_path.AppendASCII(kJs);
    ASSERT_TRUE(base::DieFileDie(file_path, false));
    EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
              DoRequestOrLoad(extension, kJs).result());
    EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
  }
}

// Tests that zero byte files correctly go through ContentVerifyJob.
TEST_P(ExtensionProtocolsTest, VerificationSeenForZeroByteFile) {
  const std::string kEmptyJs("empty.js");
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  base::FilePath unzipped_path = temp_dir.GetPath();

  scoped_refptr<Extension> extension =
      content_verifier_test_utils::UnzipToDirAndLoadExtension(
          GetContentVerifierTestPath().AppendASCII("source.zip"),
          unzipped_path);
  ASSERT_TRUE(extension.get());

  base::FilePath kRelativePath(FILE_PATH_LITERAL("empty.js"));
  ExtensionId extension_id = extension->id();

  // Sanity check empty.js.
  base::FilePath file_path = unzipped_path.AppendASCII(kEmptyJs);
  std::optional<int64_t> foo_file_size = base::GetFileSize(file_path);
  ASSERT_TRUE(foo_file_size.has_value());
  ASSERT_EQ(0, foo_file_size.value());

  // Request empty.js.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kEmptyJs).result());
    EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
  }

  // chmod -r empty.js.
  // Unreadable empty file results in hash mismatch.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    ASSERT_TRUE(base::MakeFileUnreadable(file_path));
    EXPECT_EQ(net::ERR_ACCESS_DENIED,
              DoRequestOrLoad(extension, kEmptyJs).result());
    EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
  }

  // rm empty.js.
  // Deleted empty file results in hash mismatch.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    ASSERT_TRUE(base::DieFileDie(file_path, false));
    EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
              DoRequestOrLoad(extension, kEmptyJs).result());
    EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
  }
}

TEST_P(ExtensionProtocolsTest, VerifyScriptListedAsIcon) {
  const std::string kBackgroundJs("background.js");
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  base::FilePath unzipped_path = temp_dir.GetPath();

  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &path));

  scoped_refptr<Extension> extension =
      content_verifier_test_utils::UnzipToDirAndLoadExtension(
          path.AppendASCII("content_hash_fetcher")
              .AppendASCII("manifest_mislabeled_script")
              .AppendASCII("source.zip"),
          unzipped_path);
  ASSERT_TRUE(extension.get());

  base::FilePath kRelativePath(FILE_PATH_LITERAL("background.js"));
  ExtensionId extension_id = extension->id();

  // Request background.js.
  {
    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kBackgroundJs).result());
    EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
  }

  // Modify background.js and request it.
  {
    base::FilePath file_path = unzipped_path.AppendASCII("background.js");
    const std::string content = "new content";
    EXPECT_TRUE(base::WriteFile(file_path, content));

    TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
    EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kBackgroundJs).result());
    EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
  }
}

// Tests that mime types are properly set for returned extension resources.
TEST_P(ExtensionProtocolsTest, MimeTypesForKnownFiles) {
  TestExtensionDir test_dir;
  const int manifest_version = GetParam();
  constexpr char kManifestV2[] = R"(
      {
        "name": "Test Ext",
        "manifest_version": 2,
        "version": "1",
        "web_accessible_resources": ["*"]
      })";
  constexpr char kManifestV3[] = R"(
      {
        "name": "Test Ext",
        "manifest_version": 3,
        "version": "1",
        "web_accessible_resources": [{
          "resources": [ "*" ],
          "matches": [ "*://*/*" ]
        }]
      })";
  const char* kManifest = manifest_version == 3 ? kManifestV3 : kManifestV2;
  test_dir.WriteManifest(kManifest);
  base::Value::Dict manifest = base::test::ParseJsonDict(kManifest);
  ASSERT_FALSE(manifest.empty());

  test_dir.WriteFile(FILE_PATH_LITERAL("json_file.json"), "{}");
  test_dir.WriteFile(FILE_PATH_LITERAL("js_file.js"), "function() {}");

  base::FilePath unpacked_path = test_dir.UnpackedPath();
  ASSERT_TRUE(base::PathExists(unpacked_path.AppendASCII("json_file.json")));
  std::string error;
  scoped_refptr<const Extension> extension =
      ExtensionBuilder()
          .SetManifest(std::move(manifest))
          .SetPath(unpacked_path)
          .SetLocation(mojom::ManifestLocation::kInternal)
          .Build();
  ASSERT_TRUE(extension);

  AddExtension(extension.get(), false, false);

  struct {
    const char* file_name;
    const char* expected_mime_type;
  } test_cases[] = {
      {"json_file.json", "application/json"},
      {"js_file.js", "text/javascript"},
      {"mem_file.mem", ""},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(test_case.file_name);
    EXPECT_EQ(
        test_case.expected_mime_type,
        RequestOrLoad(extension->GetResourceURL(test_case.file_name),
                      network::mojom::RequestDestination::kEmpty)
            .GetResponseHeaderByName(net::HttpRequestHeaders::kContentType));
  }
}

// Tests that requests for extension resources (including the generated
// background page) are not aborted on system suspend.
TEST_P(ExtensionProtocolsTest, ExtensionRequestsNotAborted) {
  base::FilePath extension_dir =
      GetTestPath("common").AppendASCII("background_script");
  std::string error;
  scoped_refptr<Extension> extension = file_util::LoadExtension(
      extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
      &error);
  ASSERT_TRUE(extension.get()) << error;

  EnableSimulationOfSystemSuspendForRequests();

  // Request the generated background page. Ensure the request completes
  // successfully.
  EXPECT_EQ(net::OK,
            DoRequestOrLoad(extension.get(), kGeneratedBackgroundPageFilename)
                .result());

  // Request the background.js file. Ensure the request completes successfully.
  EXPECT_EQ(net::OK,
            DoRequestOrLoad(extension.get(), "background.js").result());
}

}  // namespace extensions