// 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 "third_party/blink/public/common/manifest/manifest.h"

#include <stdint.h>

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "content/browser/manifest/manifest_manager_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/page.h"
#include "content/public/browser/page_manifest_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/blink/public/mojom/manifest/manifest_manager.mojom.h"

namespace content {
namespace {

using ::testing::Contains;
using ::testing::HasSubstr;

class ManifestBrowserTest;

// Mock of a WebContentsDelegate that catches messages sent to the console.
class MockWebContentsDelegate : public WebContentsDelegate {
 public:
  explicit MockWebContentsDelegate(ManifestBrowserTest* test) : test_(test) {}

  bool DidAddMessageToConsole(WebContents* source,
                              blink::mojom::ConsoleMessageLevel log_level,
                              const std::u16string& message,
                              int32_t line_no,
                              const std::u16string& source_id) override;
  PreloadingEligibility IsPrerender2Supported(
      WebContents& web_contents,
      PreloadingTriggerType trigger_type) override {
    return PreloadingEligibility::kEligible;
  }
  WebContents* AddNewContents(
      WebContents* source,
      std::unique_ptr<WebContents> new_contents,
      const GURL& target_url,
      WindowOpenDisposition disposition,
      const blink::mojom::WindowFeatures& window_features,
      bool user_gesture,
      bool* was_blocked) override;

 private:
  raw_ptr<ManifestBrowserTest> test_ = nullptr;
};

class ManifestBrowserTest : public ContentBrowserTest,
                            public WebContentsObserver {
 protected:
  friend MockWebContentsDelegate;

  ManifestBrowserTest()
      : cors_embedded_test_server_(
            std::make_unique<net::EmbeddedTestServer>()) {
    cors_embedded_test_server_->ServeFilesFromSourceDirectory(
        GetTestDataFilePath());
  }

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

  ~ManifestBrowserTest() override {}

  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();
    DCHECK(shell()->web_contents());

    mock_web_contents_delegate_ =
        std::make_unique<MockWebContentsDelegate>(this);
    shell()->web_contents()->SetDelegate(mock_web_contents_delegate_.get());
    Observe(shell()->web_contents());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  void GetManifestAndWait() {
    GetManifestAndWait(shell()->web_contents()->GetPrimaryPage());
  }

  void GetManifestAndWait(Page& page) {
    page.GetManifest(base::BindOnce(&ManifestBrowserTest::OnGetManifest,
                                    base::Unretained(this)));

    message_loop_runner_ = new MessageLoopRunner();
    message_loop_runner_->Run();
  }

  void OnGetManifest(blink::mojom::ManifestRequestResult result,
                     const GURL& manifest_url,
                     blink::mojom::ManifestPtr manifest) {
    manifest_url_ = manifest_url;
    manifest_ = std::move(manifest);
    message_loop_runner_->Quit();
  }

  const blink::mojom::Manifest& manifest() const {
    DCHECK(manifest_);
    return *manifest_;
  }

  const GURL& manifest_url() const { return manifest_url_; }

  int GetConsoleErrorCount() const {
    // The IPCs reporting console errors are not FIFO with the manifest IPCs.
    // Waiting for a round-trip channel-associated message will wait until any
    // already enqueued channel-associated IPCs arrive at the browser process.
    mojo::AssociatedRemote<blink::mojom::ManifestManager> remote;
    shell()
        ->web_contents()
        ->GetPrimaryMainFrame()
        ->GetRemoteAssociatedInterfaces()
        ->GetInterface(&remote);
    remote.FlushForTesting();
    return console_errors_.size();
  }

  const std::vector<std::string>& console_errors() const {
    return console_errors_;
  }

  void OnReceivedConsoleError(std::u16string_view message) {
    console_errors_.push_back(base::UTF16ToUTF8(message));
  }

  net::EmbeddedTestServer* cors_embedded_test_server() const {
    return cors_embedded_test_server_.get();
  }

  const std::vector<GURL>& reported_manifest_urls() {
    return reported_manifest_urls_;
  }

  // Contains the number of manifests that had been received when each favicon
  // URL change is received.
  const std::vector<size_t>& manifests_reported_when_favicon_url_updated() {
    return manifests_reported_when_favicon_url_updated_;
  }

  // WebContentsObserver:
  void DidUpdateFaviconURL(
      RenderFrameHost* rfh,
      const std::vector<blink::mojom::FaviconURLPtr>& candidates,
      blink::mojom::FaviconUpdateReason reason) override {
    manifests_reported_when_favicon_url_updated_.push_back(
        reported_manifest_urls_.size());
  }

  void DidUpdateWebManifestURL(RenderFrameHost* rfh,
                               const GURL& manifest_url) override {
    if (manifest_url.is_empty()) {
      reported_manifest_urls_.emplace_back();
      return;
    }
    EXPECT_TRUE(manifest_url.is_valid());
    reported_manifest_urls_.push_back(manifest_url);
  }

 private:
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
  std::unique_ptr<MockWebContentsDelegate> mock_web_contents_delegate_;
  std::unique_ptr<net::EmbeddedTestServer> cors_embedded_test_server_;
  GURL manifest_url_;
  blink::mojom::ManifestPtr manifest_ = blink::mojom::Manifest::New();
  std::vector<std::string> console_errors_;
  std::vector<GURL> reported_manifest_urls_;
  std::vector<size_t> manifests_reported_when_favicon_url_updated_;
};

// The implementation of DidAddMessageToConsole isn't inlined because it needs
// to know about |test_|.
bool MockWebContentsDelegate::DidAddMessageToConsole(
    WebContents* source,
    blink::mojom::ConsoleMessageLevel log_level,
    const std::u16string& message,
    int32_t line_no,
    const std::u16string& source_id) {
  DCHECK_EQ(source->GetDelegate(), this);

  if (log_level == blink::mojom::ConsoleMessageLevel::kError ||
      log_level == blink::mojom::ConsoleMessageLevel::kWarning) {
    test_->OnReceivedConsoleError(message);
  }
  return false;
}

WebContents* MockWebContentsDelegate::AddNewContents(
    WebContents* source,
    std::unique_ptr<WebContents> new_contents,
    const GURL& target_url,
    WindowOpenDisposition disposition,
    const blink::mojom::WindowFeatures& window_features,
    bool user_gesture,
    bool* was_blocked) {
  return test_->shell()->AddNewContents(
      source, std::move(new_contents), target_url, disposition, window_features,
      user_gesture, was_blocked);
}

// If a page has no manifest, requesting a manifest should return the empty
// manifest. The URL should be empty.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, NoManifest) {
  GURL test_url = embedded_test_server()->GetURL("/manifest/no-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
  EXPECT_TRUE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  EXPECT_TRUE(reported_manifest_urls().empty());
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(0u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page manifest points to a 404 URL, requesting the manifest should return
// the empty manifest. However, the manifest URL will be non-empty.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, 404Manifest) {
  GURL test_url = embedded_test_server()->GetURL("/manifest/404-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
  EXPECT_FALSE(manifest_url().is_empty());
  // 1 error for syntax errors in manifest/thereisnomanifestthere.json.
  EXPECT_EQ(1, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
}

// If a page has an empty manifest, requesting the manifest should return the
// manifest with default values. The manifest URL should be non-empty.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, EmptyManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  ASSERT_EQ(test_url.GetWithoutFilename(), manifest().scope);
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page's manifest can't be parsed correctly, requesting the manifest
// should return an empty manifest. The manifest URL should be non-empty.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, ParseErrorManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/parse-error-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(1, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page has a manifest that can be fetched and parsed, requesting the
// manifest should return a properly filled manifest. The manifest URL should be
// non-empty.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, SampleManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/sample-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());

  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page changes manifest during its life-time, requesting the manifest
// should return the current manifest.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, DynamicManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/dynamic-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));
  std::vector<GURL> expected_manifest_urls;

  {
    GetManifestAndWait();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
    EXPECT_TRUE(manifest_url().is_empty());
    EXPECT_TRUE(reported_manifest_urls().empty());
  }

  {
    std::string manifest_link =
        embedded_test_server()->GetURL("/manifest/sample-manifest.json").spec();
    ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

    GetManifestAndWait();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_FALSE(blink::IsDefaultManifest(manifest(), test_url));
    EXPECT_FALSE(manifest_url().is_empty());
    expected_manifest_urls.push_back(manifest_url());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
  }
  {
    std::string manifest_link =
        embedded_test_server()->GetURL("/manifest/empty-manifest.json").spec();
    ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

    GetManifestAndWait();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_FALSE(manifest_url().is_empty());
    expected_manifest_urls.push_back(manifest_url());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
  }

  {
    ASSERT_TRUE(ExecJs(shell(), "clearManifest()"));

    GetManifestAndWait();
    // There is always a default manifest.
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
    EXPECT_TRUE(manifest_url().is_empty());
    expected_manifest_urls.push_back(manifest_url());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
    ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
    EXPECT_EQ(0u, manifests_reported_when_favicon_url_updated()[0]);
  }

  EXPECT_EQ(0, GetConsoleErrorCount());
}

// This page has a manifest with only file handlers specified. Asking
// for just the manifest should succeed with a non empty manifest.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, FileHandlerManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/file-handler-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_FALSE(manifest().file_handlers.empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
}

// If a page's manifest lives in a different origin, it should follow the CORS
// rules and requesting the manifest should return an empty manifest (unless the
// response contains CORS headers).
// Flaky: crbug.com/1122546
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, DISABLED_CorsManifest) {
  ASSERT_TRUE(cors_embedded_test_server()->Start());
  ASSERT_NE(embedded_test_server()->port(),
            cors_embedded_test_server()->port());

  GURL test_url =
      embedded_test_server()->GetURL("/manifest/dynamic-manifest.html");
  std::vector<GURL> expected_manifest_urls;

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  std::string manifest_link = cors_embedded_test_server()
                                  ->GetURL("/manifest/sample-manifest.json")
                                  .spec();
  ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_THAT(console_errors(), Contains(HasSubstr("CORS")));
  EXPECT_EQ(1, GetConsoleErrorCount());
  expected_manifest_urls.push_back(manifest_url());
  EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());

  // The purpose of this second load is to make sure the first load is fully
  // finished. The first load will fail because of Access Control error but the
  // underlying Blink loader will continue fetching the file. There is no
  // reliable way to know when the fetch is finished from the browser test
  // except by fetching the same file from same origin, making it succeed when
  // it is actually fully loaded.
  manifest_link =
      embedded_test_server()->GetURL("/manifest/sample-manifest.json").spec();
  ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));
  GetManifestAndWait();
  expected_manifest_urls.push_back(manifest_url());
  EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(0u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page's manifest lives in a different origin, it should be accessible if
// it has valid access controls headers.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, CorsManifestWithAcessControls) {
  ASSERT_TRUE(cors_embedded_test_server()->Start());
  ASSERT_NE(embedded_test_server()->port(),
            cors_embedded_test_server()->port());

  GURL test_url =
      embedded_test_server()->GetURL("/manifest/dynamic-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  std::string manifest_link = cors_embedded_test_server()
                                  ->GetURL("/manifest/manifest-cors.json")
                                  .spec();
  ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(0u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page's manifest is in an insecure origin while the page is in a secure
// origin, requesting the manifest should return the empty manifest.
// TODO(crbug.com/40742592): Flaky test.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, DISABLED_MixedContentManifest) {
  ASSERT_TRUE(cors_embedded_test_server()->Start());
  std::unique_ptr<net::EmbeddedTestServer> https_server(
      new net::EmbeddedTestServer(net::EmbeddedTestServer::TYPE_HTTPS));
  https_server->ServeFilesFromSourceDirectory(GetTestDataFilePath());

  ASSERT_TRUE(https_server->Start());

  GURL test_url = https_server->GetURL("/manifest/dynamic-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GURL manifest_link = cors_embedded_test_server()->GetURL(
      "insecure.example", "/manifest/manifest-cors.json");
  // Ensure the manifest really is mixed content:
  ASSERT_FALSE(network::IsUrlPotentiallyTrustworthy(manifest_link));
  ASSERT_TRUE(ExecJs(shell(), JsReplace("setManifestTo($1)", manifest_link)));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_THAT(console_errors(), Contains(HasSubstr("Mixed Content")));
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(0u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page's manifest has some parsing errors, they should show up in the
// developer console.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, ParsingErrorsManifest) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/parsing-errors.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  ASSERT_EQ(test_url.GetWithoutFilename(), manifest().scope);
  EXPECT_EQ(7, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
}

// If a page has a manifest and the page is navigated to a page without a
// manifest, the page's manifest should be updated.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, Navigation) {
  std::vector<GURL> expected_manifest_urls;
  {
    GURL test_url =
        embedded_test_server()->GetURL("/manifest/sample-manifest.html");

    ASSERT_TRUE(NavigateToURL(shell(), test_url));

    GetManifestAndWait();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_FALSE(manifest_url().is_empty());
    EXPECT_EQ(0, GetConsoleErrorCount());
    expected_manifest_urls.push_back(manifest_url());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
    ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
    EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
  }

  {
    GURL test_url =
        embedded_test_server()->GetURL("/manifest/no-manifest.html");

    ASSERT_TRUE(NavigateToURL(shell(), test_url));

    GetManifestAndWait();
    EXPECT_TRUE(blink::IsDefaultManifest(manifest(), test_url));
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_EQ(0, GetConsoleErrorCount());
    EXPECT_TRUE(manifest_url().is_empty());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
    ASSERT_EQ(2u, manifests_reported_when_favicon_url_updated().size());
    EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[1]);
  }

  {
    GURL test_url =
        embedded_test_server()->GetURL("/manifest/sample-manifest.html");

    ASSERT_TRUE(NavigateToURL(shell(), test_url));

    GetManifestAndWait();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
    EXPECT_FALSE(manifest_url().is_empty());
    EXPECT_EQ(0, GetConsoleErrorCount());
    expected_manifest_urls.push_back(manifest_url());
    EXPECT_EQ(expected_manifest_urls, reported_manifest_urls());
    ASSERT_EQ(3u, manifests_reported_when_favicon_url_updated().size());
    EXPECT_EQ(2u, manifests_reported_when_favicon_url_updated()[2]);
  }
}

// This is required as the CallbackList uses const ref, and TestFuture requires
// copying or move support. This manually clones from the const ref so the
// result can be moved into the TestFuture.
base::expected<blink::mojom::ManifestPtr, blink::mojom::RequestManifestErrorPtr>
CopyMojoExpectedConstRef(
    const base::expected<blink::mojom::ManifestPtr,
                         blink::mojom::RequestManifestErrorPtr>&
        const_ref_result) {
  if (const_ref_result.has_value()) {
    return base::ok(const_ref_result->Clone());
  } else {
    return base::unexpected(const_ref_result.error()->Clone());
  }
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, ManifestSubscription) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/dynamic-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  // The subscription should resolve after the manifest is added.
  {
    base::test::TestFuture<base::expected<
        blink::mojom::ManifestPtr, blink::mojom::RequestManifestErrorPtr>>
        manifest_future;
    PageManifestManager* manifest_manager = PageManifestManager::GetOrCreate(
        shell()->web_contents()->GetPrimaryPage());
    auto subscription = manifest_manager->GetSpecifiedManifest(
        base::BindOnce(&CopyMojoExpectedConstRef)
            .Then(manifest_future.GetCallback()));

    std::string manifest_link =
        embedded_test_server()->GetURL("/manifest/sample-manifest.json").spec();
    ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

    ASSERT_TRUE(manifest_future.Wait());
    ASSERT_TRUE(manifest_future.Get().has_value());
    blink::mojom::Manifest& manifest = *manifest_future.Get().value();
    EXPECT_FALSE(blink::IsEmptyManifest(manifest));
    EXPECT_FALSE(blink::IsDefaultManifest(manifest, test_url));
    EXPECT_FALSE(manifest.manifest_url.is_empty());
  }

  // Navigate away and back to have a different 'page', so we aren't getting the
  // old cached manifest.
  GURL no_manifest_url =
      embedded_test_server()->GetURL("/manifest/no-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), no_manifest_url));
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  {
    base::test::TestFuture<base::expected<
        blink::mojom::ManifestPtr, blink::mojom::RequestManifestErrorPtr>>
        manifest_future;
    PageManifestManager* manifest_manager = PageManifestManager::GetOrCreate(
        shell()->web_contents()->GetPrimaryPage());
    auto subscription = manifest_manager->GetSpecifiedManifest(
        base::BindOnce(&CopyMojoExpectedConstRef)
            .Then(manifest_future.GetCallback()));

    std::string manifest_link =
        embedded_test_server()->GetURL("/manifest/nomanifesthere.json").spec();
    ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

    ASSERT_TRUE(manifest_future.Wait());
    ASSERT_FALSE(manifest_future.Get().has_value())
        << manifest_future.Get().value()->start_url.spec();
    blink::mojom::RequestManifestError& error = *manifest_future.Get().error();
    EXPECT_EQ(blink::mojom::ManifestRequestResult::kManifestFailedToFetch,
              error.error);
  }
}

// If a page has a manifest and the page is navigated using pushState (ie. same
// page), it should keep its manifest state.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, PushStateNavigation) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/sample-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  {
    TestNavigationObserver navigation_observer(shell()->web_contents(), 1);
    ASSERT_TRUE(ExecJs(
        shell(), "history.pushState({foo: \"bar\"}, 'page', 'page.html');"));
    navigation_observer.Wait();
  }

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(2u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[1]);
}

// If a page has a manifest and is navigated using an anchor (ie. same page), it
// should keep its manifest state.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, AnchorNavigation) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/sample-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));
  {
    TestNavigationObserver navigation_observer(shell()->web_contents(), 1);
    ASSERT_TRUE(ExecJs(shell(),
                       "var a = document.createElement('a'); a.href='#foo';"
                       "document.body.appendChild(a); a.click();"));
    navigation_observer.Wait();
  }

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(2u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[1]);
}

using ManifestIconDownloaderBrowserTest = ManifestBrowserTest;

// Bad icon measures the `kNoImageFound` histogram, and the chrome-url histogram
// isn't measured.
IN_PROC_BROWSER_TEST_F(ManifestIconDownloaderBrowserTest, BadIcon) {
  base::HistogramTester tester;
  GURL icon_url = embedded_test_server()->GetURL("/manifest/bad_icon.png");

  base::test::TestFuture<const SkBitmap&> test_future;
  content::ManifestIconDownloader::Download(
      shell()->web_contents(), icon_url, /*ideal_icon_size_in_px=*/192,
      /*minimum_icon_size_in_px=*/192, /*maximum_icon_size_in_px=*/192,
      test_future.GetCallback(), /*square_only=*/false);
  EXPECT_TRUE(test_future.Wait());

  EXPECT_TRUE(test_future.Get().drawsNothing());

  tester.ExpectBucketCount("WebApp.ManifestIconDownloader.Result",
                           ManifestIconDownloader::Result::kNoImageFound, 1);
  tester.ExpectTotalCount("WebApp.ManifestIconDownloader.ChromeUrl.Result", 0);
}

IN_PROC_BROWSER_TEST_F(ManifestIconDownloaderBrowserTest, HungIcon) {
  base::HistogramTester tester;
  GURL icon_url = embedded_test_server()->GetURL("/manifest/hung");

  base::test::TestFuture<const SkBitmap&> test_future;
  content::ManifestIconDownloader::Download(
      shell()->web_contents(), icon_url, /*ideal_icon_size_in_px=*/64,
      /*minimum_icon_size_in_px=*/64, /*maximum_icon_size_in_px=*/64,
      test_future.GetCallback(), /*square_only=*/false);
  EXPECT_TRUE(test_future.Wait());

  EXPECT_TRUE(test_future.Get().drawsNothing());

  tester.ExpectBucketCount("WebApp.ManifestIconDownloader.Result",
                           ManifestIconDownloader::Result::kNoImageFound, 1);
  tester.ExpectTotalCount("WebApp.ManifestIconDownloader.ChromeUrl.Result", 0);
}

// Valid icon measures the `kSuccess` histogram.
IN_PROC_BROWSER_TEST_F(ManifestIconDownloaderBrowserTest, CorrectIcon) {
  base::HistogramTester tester;
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/icon-manifest.html");
  GURL icon_url = embedded_test_server()->GetURL("/manifest/128x128-red.png");

  base::test::TestFuture<const SkBitmap&> test_future;
  ASSERT_TRUE(NavigateToURL(shell(), test_url));
  content::ManifestIconDownloader::Download(
      shell()->web_contents(), icon_url, /*ideal_icon_size_in_px=*/128,
      /*minimum_icon_size_in_px=*/128, /*maximum_icon_size_in_px=*/128,
      test_future.GetCallback(), /*square_only=*/false);
  EXPECT_TRUE(test_future.Wait());

  // A valid image is drawn.
  EXPECT_FALSE(test_future.Get().drawsNothing());

  tester.ExpectBucketCount("WebApp.ManifestIconDownloader.Result",
                           ManifestIconDownloader::Result::kSuccess, 1);
}

std::unique_ptr<net::test_server::HttpResponse> CustomHandleRequestForCookies(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url == "/index.html") {
    std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
        new net::test_server::BasicHttpResponse());
    http_response->set_code(net::HTTP_OK);
    http_response->set_content_type("text/html");
    http_response->set_content(
        "<html><head>"
        "<link rel=manifest crossorigin='use-credentials' href=/manifest.json>"
        "</head></html>");
    return std::move(http_response);
  }

  const auto& iter = request.headers.find("Cookie");
  if (iter == request.headers.end() ||
      request.relative_url != "/manifest.json") {
    return nullptr;
  }

  std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
      new net::test_server::BasicHttpResponse());
  http_response->set_code(net::HTTP_OK);
  http_response->set_content_type("application/json");
  http_response->set_content(
      base::StringPrintf("{\"name\": \"%s\"}", iter->second.c_str()));

  return std::move(http_response);
}

// This tests that when fetching a Manifest with 'use-credentials' set, the
// cookies associated with it are passed along the request.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, UseCredentialsSendCookies) {
  std::unique_ptr<net::EmbeddedTestServer> custom_embedded_test_server(
      new net::EmbeddedTestServer());
  custom_embedded_test_server->RegisterRequestHandler(
      base::BindRepeating(&CustomHandleRequestForCookies));

  ASSERT_TRUE(custom_embedded_test_server->Start());

  ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(),
                        custom_embedded_test_server->base_url(), "foobar"));

  ASSERT_TRUE(NavigateToURL(
      shell(), custom_embedded_test_server->GetURL("/index.html")));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);

  // The custom embedded test server will fill the name field with the cookie
  // content.
  EXPECT_EQ(u"foobar", manifest().name);
}

std::unique_ptr<net::test_server::HttpResponse> CustomHandleRequestForNoCookies(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url == "/index.html") {
    std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
        new net::test_server::BasicHttpResponse());
    http_response->set_code(net::HTTP_OK);
    http_response->set_content_type("text/html");
    http_response->set_content(
        "<html><head><link rel=manifest href=/manifest.json></head></html>");
    return std::move(http_response);
  }

  const auto& iter = request.headers.find("Cookie");
  if (iter != request.headers.end() ||
      request.relative_url != "/manifest.json") {
    return nullptr;
  }

  std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
      new net::test_server::BasicHttpResponse());
  http_response->set_code(net::HTTP_OK);
  http_response->set_content_type("application/json");
  http_response->set_content("{\"name\": \"no cookies\"}");

  return std::move(http_response);
}

// This tests that when fetching a Manifest without 'use-credentials' set, the
// cookies associated with it are not passed along the request.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, NoUseCredentialsNoCookies) {
  std::unique_ptr<net::EmbeddedTestServer> custom_embedded_test_server(
      new net::EmbeddedTestServer());
  custom_embedded_test_server->RegisterRequestHandler(
      base::BindRepeating(&CustomHandleRequestForNoCookies));

  ASSERT_TRUE(custom_embedded_test_server->Start());

  ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(),
                        custom_embedded_test_server->base_url(), "foobar"));

  ASSERT_TRUE(NavigateToURL(
      shell(), custom_embedded_test_server->GetURL("/index.html")));

  GetManifestAndWait();
  EXPECT_FALSE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  ASSERT_EQ(1u, reported_manifest_urls().size());
  EXPECT_EQ(manifest_url(), reported_manifest_urls()[0]);
  ASSERT_EQ(1u, manifests_reported_when_favicon_url_updated().size());
  EXPECT_EQ(1u, manifests_reported_when_favicon_url_updated()[0]);

  // The custom embedded test server will fill set the name to 'no cookies' if
  // it did not find cookies.
  EXPECT_EQ(u"no cookies", manifest().name);
}

// This tests that fetching a Manifest from a unique origin always fails,
// regardless of the CORS headers on the manifest. It also tests that no
// manifest change notifications are reported when the origin is unique.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, UniqueOrigin) {
  GURL test_url = embedded_test_server()->GetURL("/manifest/sandboxed.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));
  std::string manifest_link =
      embedded_test_server()->GetURL("/manifest/sample-manifest.json").spec();
  ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

  // Same-origin manifest will not be fetched from a unique origin, regardless
  // of CORS headers. Manifest URL is still returned though.
  GetManifestAndWait();
  EXPECT_TRUE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  EXPECT_EQ(0u, reported_manifest_urls().size());

  manifest_link =
      embedded_test_server()->GetURL("/manifest/manifest-cors.json").spec();
  ASSERT_TRUE(ExecJs(shell(), "setManifestTo('" + manifest_link + "')"));

  GetManifestAndWait();
  EXPECT_TRUE(blink::IsEmptyManifest(manifest()));
  EXPECT_FALSE(manifest_url().is_empty());
  EXPECT_EQ(0, GetConsoleErrorCount());
  EXPECT_EQ(0u, reported_manifest_urls().size());
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, AboutBlank) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/sample-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ShellAddedObserver new_shell_observer;
  ASSERT_TRUE(ExecJs(shell(),
                     "var link = document.createElement('a');"
                     "link.href = 'about:blank';"
                     "link.target = '_blank';"
                     "link.id = 'click_me';"
                     "document.body.appendChild(link);"
                     "link.click();"));

  Shell* new_shell = new_shell_observer.GetShell();
  WebContents* new_contents = new_shell->web_contents();
  EXPECT_TRUE(WaitForLoadStop(new_contents));

  // The new window should have about:blank URL but origin of the opener.
  EXPECT_TRUE(new_contents->GetLastCommittedURL().IsAboutBlank());
  EXPECT_EQ(
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
      new_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
  EXPECT_FALSE(
      new_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());

  GetManifestAndWait(new_contents->GetPrimaryPage());
  EXPECT_TRUE(blink::IsEmptyManifest(manifest()));
  EXPECT_TRUE(manifest_url().is_empty());
}

// This is testing the crash scenario encountered by https://crbug.com/1369363.
// In it a GetManifest() request by WebAppInstallTask was interrupted by a page
// navigation which destructed the internal ManifestManagerHost and forced the
// GetManifest() callback to be invoked during the destruction stack frame. The
// callback, which considered empty manifests valid for proceeding, proceeded to
// read other data on the (not destroyed) WebContents that were also in the
// middle of destruction and triggered a UAF crash.
//
// This test checks that the callback does not get invoked during the
// ManifestManagerHost destruction stack frame and other fields of the
// WebContents are still valid to synchronously access by the callback.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       GetManifestInterruptedByDestruction) {
  // Attempting to fetch the manifest on this page will hang forever, giving
  // this test time to interrupt the manifest request with the destruction of
  // ManifestManagerHost.
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("/manifest/hung-manifest.html")));

  base::RunLoop run_loop;
  WebContents* web_contents = shell()->web_contents();
  web_contents->GetPrimaryPage().GetManifest(base::BindLambdaForTesting(
      [&](blink::mojom::ManifestRequestResult, const GURL& url,
          blink::mojom::ManifestPtr manifest) {
        EXPECT_TRUE(url.is_empty());
        EXPECT_TRUE(blink::IsEmptyManifest(manifest));

        // Accessing fields on the web_contents at this point in time should be
        // safe.
        std::ignore = web_contents->GetFaviconURLs().empty();

        run_loop.Quit();
      }));

  // Unchecked downcast to get access to the
  // ReinitializeDocumentAssociatedDataForTesting() method. This method was not
  // put on the base class to avoid polluting the public interface with
  // implementation details only used by this test.
  auto* render_frame_host_impl =
      static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame());

  // Resetting the DocumentAssociatedData, which owns ManifestManagerHost and
  // the pending GetManifest() callback, forces the callback to be invoked.
  // This is intended to reproduce the effects of a page navigation seen in
  // https://crbug.com/1369363, this shortcut is used as it was too difficult to
  // determine the exact sequence of JavaScript commands needed to cause the
  // page navigation to trigger
  // RenderFrameHostImpl::DidCommitNavigationInternal() (which reassigns the
  // DocumentAssociatedData that owns ManifestManagerHost) without first
  // triggering the callback safely as the in flight network request was
  // cancelled.
  render_frame_host_impl->ReinitializeDocumentAssociatedDataForTesting();

  run_loop.Run();
}

class ManifestBrowserPrerenderingTest : public ManifestBrowserTest {
 public:
  ManifestBrowserPrerenderingTest()
      : prerender_helper_(
            base::BindRepeating(&ManifestBrowserPrerenderingTest::web_contents,
                                base::Unretained(this))) {}

  ~ManifestBrowserPrerenderingTest() override = default;

 protected:
  test::PrerenderTestHelper& prerender_helper() { return prerender_helper_; }

 private:
  test::PrerenderTestHelper prerender_helper_;
};

// Manifest fetching & parsing should work during prerendering.
IN_PROC_BROWSER_TEST_F(ManifestBrowserPrerenderingTest,
                       GetManifestInPrerendering) {
  GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");

  ASSERT_TRUE(NavigateToURL(shell(), test_url));
  {
    base::test::TestFuture<blink::mojom::ManifestRequestResult, const GURL&,
                           blink::mojom::ManifestPtr>
        manifest_future;
    web_contents()->GetPrimaryPage().GetManifest(manifest_future.GetCallback());
    ASSERT_TRUE(manifest_future.Wait());
    EXPECT_FALSE(manifest_future.Get<GURL>().is_empty());
    EXPECT_FALSE(blink::IsEmptyManifest(
        *manifest_future.Get<blink::mojom::ManifestPtr>()));
  }

  GURL prerender_url =
      embedded_test_server()->GetURL("/manifest/sample-manifest.html");
  // Loads a page in the prerender.
  PrerenderHostId host_id = prerender_helper().AddPrerender(prerender_url);
  content::RenderFrameHost* prerender_rfh =
      prerender_helper().GetPrerenderedMainFrameHost(host_id);
  {
    // The manifest should be loadable during prerender.
    base::test::TestFuture<blink::mojom::ManifestRequestResult, const GURL&,
                           blink::mojom::ManifestPtr>
        manifest_future;
    prerender_rfh->GetPage().GetManifest(manifest_future.GetCallback());
    ASSERT_TRUE(manifest_future.Wait());
    EXPECT_FALSE(manifest_future.Get<GURL>().is_empty());
    EXPECT_FALSE(blink::IsEmptyManifest(
        *manifest_future.Get<blink::mojom::ManifestPtr>()));
  }

  prerender_helper().NavigatePrimaryPage(prerender_url);
  {
    // It should still be valid after prerender.
    base::test::TestFuture<blink::mojom::ManifestRequestResult, const GURL&,
                           blink::mojom::ManifestPtr>
        manifest_future;
    prerender_rfh->GetPage().GetManifest(manifest_future.GetCallback());
    ASSERT_TRUE(manifest_future.Wait());
    EXPECT_FALSE(manifest_future.Get<GURL>().is_empty());
    EXPECT_FALSE(blink::IsEmptyManifest(
        *manifest_future.Get<blink::mojom::ManifestPtr>()));
  }
}

class ManifestFencedFrameBrowserTest : public ManifestBrowserTest {
 public:
  ManifestFencedFrameBrowserTest() = default;
  ~ManifestFencedFrameBrowserTest() override = default;

 protected:
  test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

 private:
  test::FencedFrameTestHelper fenced_frame_test_helper_;
};

// Manifest fetching & parsing should work in a fenced frame.
IN_PROC_BROWSER_TEST_F(ManifestFencedFrameBrowserTest,
                       GetManifestInFencedFrame) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  const GURL fenced_frame_url =
      embedded_test_server()->GetURL("/fenced_frames/title1.html");

  content::RenderFrameHost* fenced_frame_rfh =
      fenced_frame_test_helper().CreateFencedFrame(
          web_contents()->GetPrimaryMainFrame(), fenced_frame_url);

  // Add a manifest to `fenced_frame_rfh`.
  ASSERT_TRUE(ExecJs(fenced_frame_rfh,
                     R"( var link = document.createElement('link');
                         link.rel = 'manifest';
                         link.href = '../manifest/sample-manifest.json';
                         document.head.appendChild(link);)"));

  // Manifest fetches should still work in a fenced frame. It's the caller's
  // responsibility to be discerning about which frames it gets the manifest
  // for.
  base::test::TestFuture<blink::mojom::ManifestRequestResult, const GURL&,
                         blink::mojom::ManifestPtr>
      manifest_future;
  fenced_frame_rfh->GetPage().GetManifest(manifest_future.GetCallback());
  ASSERT_TRUE(manifest_future.Wait());
  EXPECT_FALSE(manifest_future.Get<GURL>().is_empty());
  EXPECT_FALSE(blink::IsEmptyManifest(
      *manifest_future.Get<blink::mojom::ManifestPtr>()));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, BadMessage_StartUrlCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site start_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = GURL("https://evil.com/");
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith("Manifest start_url (https://evil.com/) "
                                    "must be same-origin with the document"));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, BadMessage_IdCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site start_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = GURL("https://evil.com/");
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest id must be same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, BadMessage_ScopeCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site start_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = GURL("https://evil.com");

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest scope must be same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       BadMessage_ShareTargetActionCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site share_target action triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::Manifest::ShareTarget share_target;
  share_target.action = GURL("https://evil.com");
  bad_manifest->share_target = std::move(share_target);

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith(
          "Manifest share_target must be same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       BadMessage_FileHandlersActionCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site share_target action triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestFileHandlerPtr file_handler =
      blink::mojom::ManifestFileHandler::New();
  file_handler->action = GURL("https://evil.com");
  bad_manifest->file_handlers.push_back(std::move(file_handler));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith(
          "Manifest file_handlers must be same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(
    ManifestBrowserTest,
    BadMessage_FileHandlersAcceptExtensionInvalidFormatCharacter) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestFileHandlerPtr file_handler =
      blink::mojom::ManifestFileHandler::New();
  file_handler->action = test_url;

  std::vector<std::u16string> extensions;
  extensions.push_back(u".png\u202E");
  file_handler->accept[u"image/png"] = std::move(extensions);

  bad_manifest->file_handlers.push_back(std::move(file_handler));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest file_handlers accept extension contains invalid "
                  "control or format characters."));
}

IN_PROC_BROWSER_TEST_F(
    ManifestBrowserTest,
    BadMessage_FileHandlersAcceptExtensionInvalidControlCharacter) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestFileHandlerPtr file_handler =
      blink::mojom::ManifestFileHandler::New();
  file_handler->action = test_url;

  std::vector<std::u16string> extensions;
  extensions.push_back(u".png\u0001");
  file_handler->accept[u"image/png"] = std::move(extensions);

  bad_manifest->file_handlers.push_back(std::move(file_handler));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest file_handlers accept extension contains invalid "
                  "control or format characters."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       BadMessage_ProtocolHandlersActionCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site share_target action triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestProtocolHandlerPtr protocol_handler =
      blink::mojom::ManifestProtocolHandler::New();
  protocol_handler->url = GURL("https://evil.com");
  bad_manifest->protocol_handlers.push_back(std::move(protocol_handler));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith(
          "Manifest protocol_handlers must be same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       BadMessage_NoteTakingNewNoteUrlCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site note_taking new_note_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestNoteTakingPtr note_taking =
      blink::mojom::ManifestNoteTaking::New();
  note_taking->new_note_url = GURL("https://evil.com");
  bad_manifest->note_taking = std::move(note_taking);

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith("Manifest note_taking new_note_url must be "
                                    "same-origin with the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       BadMessage_LockScreenStartUrlCrossOrigin) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site lock_screen start_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::mojom::ManifestLockScreenPtr lock_screen =
      blink::mojom::ManifestLockScreen::New();
  lock_screen->start_url = GURL("https://evil.com");
  bad_manifest->lock_screen = std::move(lock_screen);

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith("Manifest lock_screen start_url must be "
                                    "same-origin with the document."));
}

// Tests that if a compromised renderer bypasses the manifest parser and sends
// cross-site migration data directly, the browser correctly rejects it and
// kills the renderer with a bad message.
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       MigrateCrossSiteBadMessage_MigrateToId) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site migrate_to.id triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  // Inject cross-origin ID to trigger bad message.
  auto migrate_to = blink::mojom::ManifestMigrateTo::New();
  migrate_to->id = GURL("https://www.other_example.com/manifest");
  migrate_to->install_url = GURL("https://www.other_example.com/install");
  bad_manifest->migrate_to = std::move(migrate_to);

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest migrate_to id must be the same site as the "
                  "document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       MigrateCrossSiteBadMessage_MigrateToInstallUrl) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site migrate_to.install_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  // Inject cross-origin install_url to trigger bad message.
  auto migrate_to = blink::mojom::ManifestMigrateTo::New();
  migrate_to->id = embedded_test_server()->GetURL("/manifest/new");
  migrate_to->install_url = GURL("https://www.other_example.com/install");
  bad_manifest->migrate_to = std::move(migrate_to);

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest migrate_to install_url must be the same site as "
                  "the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       MigrateCrossSiteBadMessage_MigrateFromId) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site migrate_from.id triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  // Inject cross-origin ID to trigger bad message.
  auto migrate_from = blink::mojom::ManifestMigrateFrom::New();
  migrate_from->id = GURL("https://www.other_example.com/manifest");
  bad_manifest->migrate_from.push_back(std::move(migrate_from));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest migrate_from id must be the same site as the "
                  "document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       MigrateCrossSiteBadMessage_MigrateFromInstallUrl) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a cross-site migrate_from.install_url triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  // Inject cross-origin install_url to trigger bad message.
  auto migrate_from = blink::mojom::ManifestMigrateFrom::New();
  migrate_from->id = embedded_test_server()->GetURL("/manifest/old");
  migrate_from->install_url = GURL("https://www.other_example.com/install");
  bad_manifest->migrate_from.push_back(std::move(migrate_from));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith(
                  "Manifest migrate_from install_url must be the same site "
                  "as the document."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       ManifestShortcutUrlOutsideScopeBadMessage) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that a shortcut url outside the scope triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  // Inject out-of-scope shortcut to trigger bad message.
  blink::Manifest::ShortcutItem shortcut;
  shortcut.url = embedded_test_server()->GetURL("/out-of-scope/");
  bad_manifest->shortcuts.push_back(std::move(shortcut));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith("Manifest shortcut urls must be within scope."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
                       ManifestIconInvalidSchemeBadMessage) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  // Test that an icon with invalid scheme triggers a bad message.
  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  blink::Manifest::ImageResource icon;
  icon.src = GURL("ftp://evil.com/icon.png");
  bad_manifest->icons.push_back(std::move(icon));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith("Manifest icon urls must be http, https, data, or "
                            "match the document scheme."));
}

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, ProtocolInvalidSchemeBadMessage) {
  const GURL test_url =
      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
      shell()->web_contents()->GetPrimaryPage());

  mojo::FakeMessageDispatchContext fake_dispatch_context;
  auto bad_manifest = blink::mojom::Manifest::New();
  bad_manifest->start_url = test_url;
  bad_manifest->id = test_url;
  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");

  auto protocol_handler = blink::mojom::ManifestProtocolHandler::New();
  protocol_handler->protocol = u"https";
  protocol_handler->url =
      embedded_test_server()->GetURL("/manifest/handler?q=%s");
  bad_manifest->protocol_handlers.push_back(std::move(protocol_handler));

  mojo::test::BadMessageObserver bad_message_observer;
  host->ValidateAndMaybeOverrideManifestForTesting(
      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
  EXPECT_THAT(
      bad_message_observer.WaitForBadMessage(),
      ::testing::StartsWith(
          "Manifest protocol_handlers protocol is invalid or restricted"));
}

// Local waiter to wait for WebContentsObserver::DidUpdateWebManifestURL.
class ManifestUrlUpdateWaiter : public content::WebContentsObserver {
 public:
  ManifestUrlUpdateWaiter(content::WebContents* web_contents,
                          const GURL& manifest_url)
      : content::WebContentsObserver(web_contents),
        expected_manifest_url_(manifest_url) {}

  void DidUpdateWebManifestURL(content::RenderFrameHost* rfh,
                               const GURL& manifest_url) override {
    if (manifest_url == expected_manifest_url_) {
      run_loop_.Quit();
    }
  }

  void Wait() { run_loop_.Run(); }

 private:
  GURL expected_manifest_url_;
  // A nestable RunLoop is needed for async mojo communications for the
  // ManifestManager to complete running.
  base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
};

IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, ManifestUrlChangedDuringFetch) {
  std::unique_ptr<net::EmbeddedTestServer> custom_server =
      std::make_unique<net::EmbeddedTestServer>();

  custom_server->ServeFilesFromSourceDirectory(GetTestDataFilePath());

  // Perform a request handler that changes the manifest to point a new one, and
  // prevents the older one from returning a response.
  custom_server->RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const net::test_server::HttpRequest& request)
          -> std::unique_ptr<net::test_server::HttpResponse> {
        if (request.relative_url == "/manifest/manifest_old.json") {
          GURL new_manifest_url =
              custom_server->GetURL("/manifest/manifest_new.json");
          content::GetUIThreadTaskRunner({})->PostTask(
              FROM_HERE,
              base::BindOnce(
                  [](content::WebContents* web_contents,
                     const GURL& new_manifest_url) {
                    ManifestUrlUpdateWaiter waiter(web_contents,
                                                   new_manifest_url);
                    EXPECT_TRUE(
                        ExecJs(web_contents,
                               "setManifestTo('/manifest/manifest_new.json')"));
                    waiter.Wait();
                  },
                  shell()->web_contents(), new_manifest_url));
          // Return a delayed response set to the timeout duration of
          // browser tests, so that we give ample time for the new manifest to
          // be loaded.
          auto delayed_manifest_response =
              std::make_unique<net::test_server::DelayedHttpResponse>(
                  base::Seconds(30));
          delayed_manifest_response->set_code(net::HTTP_OK);
          delayed_manifest_response->set_content(
              "{'name': 'Manifest_Old', 'start_url': '/', 'scope': '/', 'id': "
              "'/'}");
          return delayed_manifest_response;
        }
        return nullptr;
      }));

  ASSERT_TRUE(custom_server->Start());
  GURL test_url = custom_server->GetURL("/manifest/manifest_index.html");
  ASSERT_TRUE(NavigateToURL(shell(), test_url));

  base::test::TestFuture<base::expected<blink::mojom::ManifestPtr,
                                        blink::mojom::RequestManifestErrorPtr>>
      manifest_future;
  PageManifestManager* manifest_manager = PageManifestManager::GetOrCreate(
      shell()->web_contents()->GetPrimaryPage());
  auto subscription = manifest_manager->GetSpecifiedManifest(
      base::BindOnce(&CopyMojoExpectedConstRef)
          .Then(manifest_future.GetCallback()));

  ASSERT_TRUE(manifest_future.Wait());
  ASSERT_TRUE(manifest_future.Get().has_value());
  blink::mojom::Manifest& manifest = *manifest_future.Get().value();
  EXPECT_EQ(u"Manifest_New", manifest.name);
}

}  // namespace
}  // namespace content
