// Copyright 2016 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/common/manifest_handlers/csp_info.h"

#include <string_view>

#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "components/version_info/channel.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/sandboxed_page_info.h"
#include "extensions/common/manifest_test.h"

namespace extensions {

namespace {

namespace errors = manifest_errors;
namespace keys = manifest_keys;

std::string GetInvalidManifestKeyError(std::string_view key) {
  return ErrorUtils::FormatErrorMessage(errors::kInvalidManifestKey, key);
}

const char kDefaultSandboxedPageCSP[] =
    "sandbox allow-scripts allow-forms allow-popups allow-modals; "
    "script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';";
const char kDefaultExtensionPagesCSP[] =
    "script-src 'self' blob: filesystem:; "
    "object-src 'self' blob: filesystem:;";
const char kDefaultSecureCSP[] = "script-src 'self';";

}  // namespace

using CSPInfoUnitTest = ManifestTest;

TEST_F(CSPInfoUnitTest, SandboxedPages) {
  // Sandboxed pages specified, no custom CSP value.
  scoped_refptr<Extension> extension1(
      LoadAndExpectSuccess("sandboxed_pages_valid_1.json"));

  // No sandboxed pages.
  scoped_refptr<Extension> extension2(
      LoadAndExpectSuccess("sandboxed_pages_valid_2.json"));

  // Sandboxed pages specified with a custom CSP value.
  scoped_refptr<Extension> extension3(
      LoadAndExpectSuccess("sandboxed_pages_valid_3.json"));

  // Sandboxed pages specified with wildcard, no custom CSP value.
  scoped_refptr<Extension> extension4(
      LoadAndExpectSuccess("sandboxed_pages_valid_4.json"));

  // Sandboxed pages specified with filename wildcard, no custom CSP value.
  scoped_refptr<Extension> extension5(
      LoadAndExpectSuccess("sandboxed_pages_valid_5.json"));

  // Sandboxed pages specified for a platform app with a custom CSP.
  scoped_refptr<Extension> extension6(
      LoadAndExpectSuccess("sandboxed_pages_valid_6.json"));

  // Sandboxed pages specified for a platform app with no custom CSP.
  scoped_refptr<Extension> extension7(
      LoadAndExpectSuccess("sandboxed_pages_valid_7.json"));

  const char kCustomSandboxedCSP[] =
      "sandbox; script-src 'self'; child-src 'self';";

  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension1.get(), "/test"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension1.get(), "/t%65st"));
  EXPECT_TRUE(SandboxedPageInfo::IsSandboxedPage(extension1.get(), "/t%65st"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension1.get(), "/Test"));
  EXPECT_TRUE(SandboxedPageInfo::IsSandboxedPage(extension1.get(), "/Test"));
  EXPECT_TRUE(SandboxedPageInfo::IsSandboxedPage(extension1.get(), "/tEsT"));
  EXPECT_EQ(
      kDefaultExtensionPagesCSP,
      CSPInfo::GetResourceContentSecurityPolicy(extension1.get(), "/none"));
  EXPECT_EQ(
      kDefaultExtensionPagesCSP,
      CSPInfo::GetResourceContentSecurityPolicy(extension2.get(), "/test"));
  EXPECT_EQ(kCustomSandboxedCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                     extension3.get(), "/test"));
  EXPECT_EQ(
      kDefaultExtensionPagesCSP,
      CSPInfo::GetResourceContentSecurityPolicy(extension3.get(), "/none"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension4.get(), "/test"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension5.get(), "/path/test.ext"));
  EXPECT_EQ(kDefaultSandboxedPageCSP,
            CSPInfo::GetResourceContentSecurityPolicy(extension5.get(),
                                                      "/path/test%2Eext"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension5.get(), "/path/test%2Eext"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension5.get(), "/PATH/TEST.EXT"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension5.get(), "/PATH/TEST.EXT"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension5.get(), "/path/TEST.ext"));
  EXPECT_EQ(
      kDefaultExtensionPagesCSP,
      CSPInfo::GetResourceContentSecurityPolicy(extension5.get(), "/test"));
  EXPECT_EQ(kCustomSandboxedCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                     extension6.get(), "/test"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension7.get(), "/test"));

  static constexpr char kManifestUtf8Sandboxed[] =
      R"({
           "name": "UTF-8 Sandboxed Page Test",
           "manifest_version": 3,
           "version": "0.1",
           "sandbox": {
             "pages": ["café.html"]
           }
         })";
  ManifestData manifest_data_utf8(
      base::test::ParseJsonDict(kManifestUtf8Sandboxed));
  scoped_refptr<const Extension> extension_utf8 =
      LoadAndExpectSuccess(manifest_data_utf8);
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension_utf8.get(), "/café.html"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension_utf8.get(), "/café.html"));
  EXPECT_EQ(kDefaultSandboxedPageCSP, CSPInfo::GetResourceContentSecurityPolicy(
                                          extension_utf8.get(), "/CAFÉ.html"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension_utf8.get(), "/CAFÉ.html"));
  EXPECT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension_utf8.get(), "/Café.html"));

  const Testcase testcases[] = {
      Testcase("sandboxed_pages_invalid_1.json",
               errors::kInvalidSandboxedPagesList),
      Testcase("sandboxed_pages_invalid_2.json", errors::kInvalidSandboxedPage),
      Testcase("sandboxed_pages_invalid_3.json",
               GetInvalidManifestKeyError(keys::kSandboxedPagesCSP)),
      Testcase("sandboxed_pages_invalid_4.json",
               GetInvalidManifestKeyError(keys::kSandboxedPagesCSP)),
      Testcase("sandboxed_pages_invalid_5.json",
               GetInvalidManifestKeyError(keys::kSandboxedPagesCSP))};
  RunTestcases(testcases, ExpectType::kError);
}

TEST_F(CSPInfoUnitTest, CSPStringKey) {
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess("csp_string_valid.json");
  ASSERT_TRUE(extension);
  EXPECT_EQ("script-src 'self'; default-src 'none';",
            CSPInfo::GetExtensionPagesCSP(extension.get()));

  // Manifest V2 extensions bypass the main world CSP in their isolated worlds.
  std::optional<std::string> isolated_world_csp =
      CSPInfo::GetIsolatedWorldCSP(*extension);
  ASSERT_TRUE(isolated_world_csp);
  EXPECT_TRUE(isolated_world_csp->empty());

  RunTestcase(Testcase("csp_invalid_1.json", GetInvalidManifestKeyError(
                                                 keys::kContentSecurityPolicy)),
              ExpectType::kError);
}

TEST_F(CSPInfoUnitTest, CSPDictionary_ExtensionPages) {
  static constexpr struct {
    const char* file_name;
    const char* csp;
  } cases[] = {{"csp_dictionary_valid_1.json", "default-src 'none'"},
               {"csp_dictionary_valid_2.json",
                "worker-src 'self'; script-src; default-src 'self'"},
               {"csp_empty_dictionary_valid.json", kDefaultSecureCSP}};

  for (const auto& test_case : cases) {
    SCOPED_TRACE(base::StringPrintf("Testing %s.", test_case.file_name));
    scoped_refptr<Extension> extension =
        LoadAndExpectSuccess(test_case.file_name);
    ASSERT_TRUE(extension.get());
    EXPECT_EQ(test_case.csp, CSPInfo::GetExtensionPagesCSP(extension.get()));
  }

  const Testcase testcases[] = {
      Testcase("csp_invalid_2.json",
               GetInvalidManifestKeyError(
                   keys::kContentSecurityPolicy_ExtensionPagesPath)),
      Testcase("csp_invalid_3.json",
               GetInvalidManifestKeyError(
                   keys::kContentSecurityPolicy_ExtensionPagesPath)),
      Testcase(
          "csp_missing_src.json",
          ErrorUtils::FormatErrorMessage(
              errors::kInvalidCSPMissingSecureSrc,
              keys::kContentSecurityPolicy_ExtensionPagesPath, "script-src")),
      Testcase("csp_insecure_src.json",
               ErrorUtils::FormatErrorMessage(
                   errors::kInvalidCSPInsecureValueError,
                   keys::kContentSecurityPolicy_ExtensionPagesPath,
                   "'unsafe-eval'", "worker-src")),
  };
  RunTestcases(testcases, ExpectType::kError);
}

// Tests the requirements for object-src specifications.
TEST_F(CSPInfoUnitTest, ObjectSrcRequirements) {
  enum class ManifestVersion {
    kMV2,
    kMV3,
  };

  static constexpr char kManifestV3Template[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "version": "0.1",
           "content_security_policy": {
             "extension_pages": "%s"
           }
         })";

  static constexpr char kManifestV2Template[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 2,
           "version": "0.1",
           "content_security_policy": "%s"
         })";

  auto get_manifest = [](ManifestVersion version, const char* input) {
    return base::test::ParseJsonDict(
        version == ManifestVersion::kMV2
            ? base::StringPrintf(kManifestV2Template, input)
            : base::StringPrintf(kManifestV3Template, input));
  };

  static constexpr struct {
    ManifestVersion version;
    const char* csp;
  } kPassingTestcases[] = {
      // object-src doesn't need to be explicitly specified in manifest V3.
      {ManifestVersion::kMV3, "script-src 'self'"},
      {ManifestVersion::kMV3, "default-src 'self'"},
      // Secure object-src specifications are allowed.
      {ManifestVersion::kMV3, "script-src 'self'; object-src 'self'"},
      {ManifestVersion::kMV3, "script-src 'self'; object-src 'none'"},
      {ManifestVersion::kMV3,
       ("script-src 'self'; object-src 'self'; frame-src 'self'; default-src "
        "https://google.com")},
      // Even though the object-src in the example below is effectively
      // https://google.com (because it falls back to the default-src), we
      // still allow it so that developers don't need to explicitly specify an
      // object-src just because they specified a default-src. The minimum CSP
      // (which includes `object-src 'self'`) still kicks in and prevents any
      // insecure use.
      {ManifestVersion::kMV3,
       "script-src 'self'; default-src https://google.com"},

      // In Manifest V2, object-src must be specified (if it's omitted, we add
      // it; see `kWarningTestcases` below).
      // Note: in MV2, our parsing will implicitly also add a trailing semicolon
      // if one isn't provided, so we always add one here so that the final CSP
      // matches.
      {ManifestVersion::kMV2, "script-src 'self'; object-src 'self';"},
      {ManifestVersion::kMV2, "script-src 'self'; object-src 'none';"},
      {ManifestVersion::kMV2,
       ("script-src 'self'; object-src 'self'; frame-src 'self'; default-src "
        "https://google.com;")},
      // Manifest V2 allows (secure) remote object-src specifications.
      {ManifestVersion::kMV2,
       "script-src 'self'; object-src https://google.com;"},
      {ManifestVersion::kMV2,
       "script-src 'self'; default-src https://google.com;"},
  };

  for (const auto& testcase : kPassingTestcases) {
    SCOPED_TRACE(testcase.csp);
    ManifestData manifest_data(get_manifest(testcase.version, testcase.csp));
    scoped_refptr<const Extension> extension =
        LoadAndExpectSuccess(manifest_data);
    ASSERT_TRUE(extension);
    EXPECT_EQ(testcase.csp, CSPInfo::GetExtensionPagesCSP(extension.get()));
  }

  static constexpr struct {
    ManifestVersion version;
    const char* csp;
    const char* expected_error;
  } kFailingTestcases[] = {
      // If an object-src *is* specified, it must be secure and must not allow
      // remotely-hosted code (in MV3).
      {ManifestVersion::kMV3,
       "script-src 'self'; object-src https://google.com",
       "*Insecure CSP value \"https://google.com\" in directive 'object-src'."},
  };

  for (const auto& testcase : kFailingTestcases) {
    SCOPED_TRACE(testcase.csp);
    ManifestData manifest_data(get_manifest(testcase.version, testcase.csp));
    LoadAndExpectError(manifest_data, testcase.expected_error);
  }

  static constexpr struct {
    ManifestVersion version;
    const char* csp;
    const char* expected_warning;
    const char* effective_csp;
  } kWarningTestcases[] = {
      // In MV2, if an object-src is not provided, we will warn and synthesize
      // one.
      {ManifestVersion::kMV2, "script-src 'self'",
       ("'content_security_policy': CSP directive 'object-src' must be "
        "specified (either explicitly, or implicitly via 'default-src') "
        "and must allowlist only secure resources."),
       "script-src 'self'; object-src 'self';"},
      // Similarly, if an insecure (e.g. http) object-src is provided, we simply
      // ignore it.
      {ManifestVersion::kMV2, "script-src 'self'; object-src http://google.com",
       ("'content_security_policy': Ignored insecure CSP value "
        "\"http://google.com\" in directive 'object-src'."),
       "script-src 'self'; object-src;"}};

  for (const auto& testcase : kWarningTestcases) {
    // Special case: In MV2, if the developer doesn't provide an object-src, we
    // insert one ('self') and emit a warning.
    ManifestData manifest_data(get_manifest(testcase.version, testcase.csp));
    scoped_refptr<const Extension> extension =
        LoadAndExpectWarning(manifest_data, testcase.expected_warning);
    ASSERT_TRUE(extension);
    EXPECT_EQ(testcase.effective_csp,
              CSPInfo::GetExtensionPagesCSP(extension.get()));
  }
}

TEST_F(CSPInfoUnitTest, AllowWasmInMV3) {
  struct {
    const char* file_name;
    const char* csp;
  } cases[] = {{"csp_dictionary_with_wasm.json",
                "worker-src 'self' 'wasm-unsafe-eval'; default-src 'self'"},
               {"csp_dictionary_with_unsafe_wasm.json",
                "worker-src 'self' 'wasm-unsafe-eval'; default-src 'self'"},
               {"csp_dictionary_empty_v3.json", "script-src 'self';"},
               {"csp_dictionary_valid_1.json", "default-src 'none'"},
               {"csp_omitted_mv2.json", kDefaultExtensionPagesCSP}};

  for (const auto& test_case : cases) {
    SCOPED_TRACE(base::StringPrintf("Testing %s.", test_case.file_name));
    scoped_refptr<Extension> extension =
        LoadAndExpectSuccess(test_case.file_name);
    ASSERT_TRUE(extension.get());
    EXPECT_EQ(test_case.csp, CSPInfo::GetExtensionPagesCSP(extension.get()));
  }
}

TEST_F(CSPInfoUnitTest, CSPDictionary_Sandbox) {
  ScopedCurrentChannel channel(version_info::Channel::UNKNOWN);

  const char kCustomSandboxedCSP[] =
      "sandbox; script-src https://www.google.com";
  const char kCustomExtensionPagesCSP[] = "script-src; object-src;";

  struct {
    const char* file_name;
    const char* resource_path;
    const char* expected_csp;
  } success_cases[] = {
      {"sandbox_dictionary_1.json", "/test", kCustomSandboxedCSP},
      {"sandbox_dictionary_1.json", "/index", kDefaultSecureCSP},
      {"sandbox_dictionary_2.json", "/test", kDefaultSandboxedPageCSP},
      {"sandbox_dictionary_2.json", "/index", kCustomExtensionPagesCSP},
  };

  for (const auto& test_case : success_cases) {
    SCOPED_TRACE(base::StringPrintf("%s with path %s", test_case.file_name,
                                    test_case.resource_path));
    scoped_refptr<Extension> extension =
        LoadAndExpectSuccess(test_case.file_name);
    ASSERT_TRUE(extension);
    EXPECT_EQ(test_case.expected_csp,
              CSPInfo::GetResourceContentSecurityPolicy(
                  extension.get(), test_case.resource_path));
  }

  const Testcase testcases[] = {
      {"sandbox_both_keys.json", errors::kSandboxPagesCSPKeyNotAllowed},
      {"sandbox_csp_with_dictionary.json",
       errors::kSandboxPagesCSPKeyNotAllowed},
      {"sandbox_invalid_type.json",
       GetInvalidManifestKeyError(
           keys::kContentSecurityPolicy_SandboxedPagesPath)},
      {"unsandboxed_csp.json",
       GetInvalidManifestKeyError(
           keys::kContentSecurityPolicy_SandboxedPagesPath)}};
  RunTestcases(testcases, ExpectType::kError);
}

// Ensures that using a dictionary for the keys::kContentSecurityPolicy manifest
// key is mandatory for manifest v3 extensions and that defaults are applied
// correctly.
TEST_F(CSPInfoUnitTest, CSPDictionaryMandatoryForV3) {
  LoadAndExpectError("csp_invalid_type_v3.json",
                     GetInvalidManifestKeyError(keys::kContentSecurityPolicy));

  const char* default_case_filenames[] = {"csp_dictionary_empty_v3.json",
                                          "csp_dictionary_missing_v3.json"};

  // First, run through with loading the extensions as packed extensions.
  for (const char* filename : default_case_filenames) {
    SCOPED_TRACE(filename);
    scoped_refptr<Extension> extension =
        LoadAndExpectSuccess(filename, mojom::ManifestLocation::kInternal);
    ASSERT_TRUE(extension);

    std::optional<std::string> isolated_world_csp =
        CSPInfo::GetIsolatedWorldCSP(*extension);
    ASSERT_TRUE(isolated_world_csp);
    std::string expected_csp = base::StringPrintf(
        "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' "
        "%s; object-src 'self';",
        extension->dynamic_url().spec().c_str());
    EXPECT_EQ(expected_csp, *isolated_world_csp);

    EXPECT_EQ(kDefaultSandboxedPageCSP,
              CSPInfo::GetSandboxContentSecurityPolicy(extension.get()));
    EXPECT_EQ(kDefaultSecureCSP,
              CSPInfo::GetExtensionPagesCSP(extension.get()));

    EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
              *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html",
                                              /*is_service_worker=*/false));
  }

  // Repeat the test, loading the extensions as unpacked extensions.
  // The minimum CSP we append (and thus the isolated world CSP) should be
  // different, while the other CSPs should remain the same.
  for (const char* filename : default_case_filenames) {
    SCOPED_TRACE(filename);
    scoped_refptr<Extension> extension =
        LoadAndExpectSuccess(filename, mojom::ManifestLocation::kUnpacked);
    ASSERT_TRUE(extension);

    std::optional<std::string> isolated_world_csp =
        CSPInfo::GetIsolatedWorldCSP(*extension);
    ASSERT_TRUE(isolated_world_csp);
    std::string expected_csp = base::StringPrintf(
        "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' "
        "http://localhost:* http://127.0.0.1:* %s; object-src 'self';",
        extension->dynamic_url().spec().c_str());
    EXPECT_EQ(expected_csp, *isolated_world_csp);

    EXPECT_EQ(kDefaultSandboxedPageCSP,
              CSPInfo::GetSandboxContentSecurityPolicy(extension.get()));
    EXPECT_EQ(kDefaultSecureCSP,
              CSPInfo::GetExtensionPagesCSP(extension.get()));

    EXPECT_EQ(CSPHandler::GetMinimumUnpackedMV3CSPForTesting(),
              *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html",
                                              /*is_service_worker=*/false));
  }
}

// Ensure the CSP dictionary is disallowed for mv2 extensions.
TEST_F(CSPInfoUnitTest, CSPDictionaryDisallowedForV2) {
  LoadAndExpectError("csp_dictionary_mv2.json",
                     GetInvalidManifestKeyError(keys::kContentSecurityPolicy));
}

// Ensure that service workers ignore the sandbox.pages CSP and instead use the
// stricter extension CSP.
TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnored) {
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess("sandboxed_pages_valid_1.json");
  ASSERT_TRUE(extension);

  static constexpr char kSandboxedPath[] = "/test";
  ASSERT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));

  // If not a service worker, the sandboxed page CSP should be returned.
  EXPECT_EQ(kDefaultSandboxedPageCSP,
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/false));

  // If a service worker, the extension pages CSP should be returned (even if
  // the path is sandboxed). For MV2, this is the default extension pages CSP.
  EXPECT_EQ(kDefaultExtensionPagesCSP,
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/true));
}

// Ensure that MV3 service workers ignore the sandbox.pages CSP even if they are
// explicitly listed there.
TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnoredMV3) {
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess("sandboxed_sw_mv3.json");
  ASSERT_TRUE(extension);

  static constexpr char kSandboxedPath[] = "/sw.js";
  ASSERT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));

  // If not a service worker, the sandboxed page CSP should be returned.
  EXPECT_EQ(kDefaultSandboxedPageCSP,
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/false));

  // If a service worker, the extension pages CSP should be returned (even if
  // the path is sandboxed).
  EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/true));
}

// Ensure that even with a custom sandbox CSP (which may be insecure), the
// service worker still gets the strict MV3 CSP.
TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnoredWithCustomCSP) {
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess("sandboxed_sw_mv3_with_csp.json");
  ASSERT_TRUE(extension);

  static constexpr char kSandboxedPath[] = "/sw.js";
  ASSERT_TRUE(
      SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));

  // If not a service worker, the sandboxed page CSP should be returned.
  // Note: kDefaultSandboxedPageCSP is not used here because we provided a
  // custom one.
  const std::string& custom_sandbox_csp =
      CSPInfo::GetSandboxContentSecurityPolicy(extension.get());
  EXPECT_EQ(custom_sandbox_csp,
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/false));

  // If a service worker, the extension pages CSP should be returned (even if
  // the path is sandboxed).
  EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
            *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
                                            /*is_service_worker=*/true));
}

// Tests that extensions of the "user script" type also have CSP enforced for
// Manifest V3+.
TEST_F(CSPInfoUnitTest, UserScriptStrictCSP) {
  ManifestData manifest_data(base::test::ParseJsonDict(R"({
    "name": "User Script Strict",
    "manifest_version": 3,
    "version": "1.0",
    "converted_from_user_script": true
  })"));
  scoped_refptr<const Extension> extension =
      LoadAndExpectSuccess(manifest_data);
  ASSERT_TRUE(extension);
  ASSERT_EQ(Manifest::Type::kUserScript, extension->GetType());

  // Should receive default secure manifest CSP.
  EXPECT_EQ("script-src 'self';",
            CSPInfo::GetExtensionPagesCSP(extension.get()));
  // Like items with Manifest::Type::kExtension, TYPE_USER_SCRIPT items should
  // also get a "minimum CSP" to append to the list of CSPs. This ensures every
  // item has at least a minimally-strict CSP that rejects remotely-hosted
  // code.
  EXPECT_NE(nullptr,
            CSPInfo::GetMinimumCSPToAppend(*extension, "page.html", false));
}

TEST_F(CSPInfoUnitTest, ValidateDefaultMV3ExtensionPagesCSP) {
  static const char kDefaultMV3CSP[] = "script-src 'self';";

  static constexpr char kManifestV3[] =
      R"({
           "name": "Test MV3 Extension",
           "manifest_version": 3,
           "version": "0.1"
         })";

  ManifestData manifest_data_mv3(base::test::ParseJsonDict(kManifestV3));
  scoped_refptr<const Extension> extension_mv3 =
      LoadAndExpectSuccess(manifest_data_mv3);
  EXPECT_EQ(3, extension_mv3.get()->manifest_version());
  EXPECT_EQ(Manifest::Type::kExtension, extension_mv3.get()->GetType());
  EXPECT_EQ(kDefaultMV3CSP, CSPInfo::GetResourceContentSecurityPolicy(
                                extension_mv3.get(), "/test"));
}

TEST_F(CSPInfoUnitTest, ValidateDefaultMV2ExtensionPagesCSP) {
  const char kDefaultMV2CSP[] =
      "script-src 'self' blob: filesystem:; "
      "object-src 'self' blob: filesystem:;";

  static constexpr char kManifestV2[] =
      R"({
           "name": "Test MV2 Extension",
           "manifest_version": 2,
           "version": "0.1"
         })";

  ManifestData manifest_data_mv2(base::test::ParseJsonDict(kManifestV2));
  scoped_refptr<const Extension> extension_mv2 =
      LoadAndExpectSuccess(manifest_data_mv2);
  EXPECT_EQ(2, extension_mv2.get()->manifest_version());
  EXPECT_EQ(Manifest::Type::kExtension, extension_mv2.get()->GetType());
  EXPECT_EQ(kDefaultMV2CSP, CSPInfo::GetResourceContentSecurityPolicy(
                                extension_mv2.get(), "/test"));
}

TEST_F(CSPInfoUnitTest, ValidateDefaultMV2PlatformAppPagesCSP) {
#define PLATFORM_APP_LOCAL_CSP_SOURCES "'self' blob: filesystem: data:"

  // clang-format off
const char kDefaultPlatformAppContentSecurityPolicy[] =
    "default-src 'self' blob: filesystem:;"
    " connect-src * data: blob: filesystem:;"
    " style-src " PLATFORM_APP_LOCAL_CSP_SOURCES " 'unsafe-inline';"
    " img-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    " frame-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    " font-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    " media-src * data: blob: filesystem:;"
    " script-src 'self' blob: filesystem: 'wasm-unsafe-eval';";
  // clang-format on

#undef PLATFORM_APP_LOCAL_CSP_SOURCES

  static constexpr char kManifestV2PlatformApp[] =
      R"({
           "name": "Test MV2 Platform App",
           "manifest_version": 2,
           "version": "0.1",
           "app": {
             "background": {
               "scripts": [
                 "script.js"
               ]
             }
           }
         })";

  ManifestData manifest_data_mv2_platform_app(
      base::test::ParseJsonDict(kManifestV2PlatformApp));
  scoped_refptr<const Extension> extension_mv2_platform_app =
      LoadAndExpectSuccess(manifest_data_mv2_platform_app);
  EXPECT_EQ(2, extension_mv2_platform_app.get()->manifest_version());
  EXPECT_EQ(Manifest::Type::kPlatformApp,
            extension_mv2_platform_app.get()->GetType());
  EXPECT_EQ(kDefaultPlatformAppContentSecurityPolicy,
            CSPInfo::GetResourceContentSecurityPolicy(
                extension_mv2_platform_app.get(), "/test"));
}

}  // namespace extensions
