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

#include <memory>
#include <string>

#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/security_principal.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_exposed_isolation_level.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/commit_message_delayer.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/render_document_feature.h"
#include "net/dns/mock_host_resolver.h"
#include "url/gurl.h"

// Unassigned SiteInstances occur when ShouldAssignSiteForURL returns false,
// allowing a given SiteInstance to be reused for a future navigation. This test
// suite covers how Chrome should behave in these cases, and when SiteInstances
// should be reused or replaced. Note that there are some differences between
// about:blank and custom embedder-defined cases.
//
// TODO(crbug.com/40214665): We would like to enforce the fact that unassigned
// SiteInstances only ever exist in their own BrowsingInstance. The exact way to
// achieve that is still unclear. We might only allow leaving SiteInstances
// unassigned for empty schemes, or make the siteless behavior kick in only for
// the first navigation in a BrowsingInstance, for example.
//
// The test suite covers expectations for the following cases:
// - Navigation to about:blank, renderer/browser initiated.
// - Navigation from about:blank, renderer/browser initiated.
// - Navigation to embedder defined url, renderer/browser initiated.
// - Navigation from embedder defined url, renderer/browser initiated.
// - Initial empty document in popups.
// - Navigation in a popup, to about:blank, renderer/browser initiated.
// - Navigation in a popup, to embedder defined url, renderer/browser initiated.
// - Initial empty document in iframes.
// - Navigation in an iframe, to about:blank, renderer initiated.
// - Navigation in an iframe, to embedder defined url, renderer initiated.
// - Interactions with crossOriginIsolated pages.
// - Some bug reproducers, testing things like races and history navigations.

namespace content {

namespace {
const std::string kEmptySchemeForTesting = "empty-scheme";
}  // namespace

// Note that this test suite is parametrized for RenderDocument and
// BackForwardCache, like many tests involving navigations, and SiteInstance
// picking. This is due to the fact that both features have an important impact
// on navigations and are likely to interact.
class UnassignedSiteInstanceBrowserTest
    : public ContentBrowserTest,
      public ::testing::WithParamInterface<std::tuple<std::string, bool>> {
 public:
  UnassignedSiteInstanceBrowserTest()
      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
    InitBackForwardCacheFeature(&feature_list_for_back_forward_cache_,
                                std::get<1>(GetParam()));
    url::AddEmptyDocumentScheme(kEmptySchemeForTesting.c_str());
  }

  // Provides meaningful param names instead of /0, /1, ...
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [render_document_level, enable_back_forward_cache] = info.param;
    return base::StringPrintf(
        "%s_%s",
        GetRenderDocumentLevelNameForTestParams(render_document_level).c_str(),
        enable_back_forward_cache ? "BFCacheEnabled" : "BFCacheDisabled");
  }

  void SetUpOnMainThread() override {
    // Allow all hosts on HTTPS.
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    // Support multiple sites on the test server.
    host_resolver()->AddRule("*", "127.0.0.1");
    https_server_.AddDefaultHandlers(GetTestDataFilePath());
    ASSERT_TRUE(embedded_test_server()->Start());
    ASSERT_TRUE(https_server_.Start());

    regular_url_ = https_server_.GetURL("a.test", "/title1.html");
    cross_origin_isolated_url_ =
        https_server_.GetURL("a.test",
                             "/set-header?"
                             "Cross-Origin-Opener-Policy: same-origin&"
                             "Cross-Origin-Embedder-Policy: require-corp");
    unassigned_url_ = GURL("about:blank");
    embedder_defined_unassigned_url_ = GURL(kEmptySchemeForTesting + "://test");

    regular_http_url_ =
        embedded_test_server()->GetURL("a.test", "/title1.html");
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    mock_cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  WebContentsImpl* web_contents() {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  net::EmbeddedTestServer* https_server() { return &https_server_; }

  void DisableBackForwardCache(
      BackForwardCacheImpl::DisableForTestingReason reason) const {
    return static_cast<WebContentsImpl*>(shell()->web_contents())
        ->GetController()
        .GetBackForwardCache()
        .DisableForTesting(reason);
  }

  // Returns an url that assigns a site to the SiteInstance it lives in.
  const GURL& regular_url() const { return regular_url_; }

  // Returns regular_url()'s origin, including port if Origin Isolation is
  // enabled.
  GURL RegularUrlOriginMaybeWithPort() const {
    GURL result = regular_url();
    GURL::Replacements replacements;
    replacements.ClearPath();
    if (!SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(
            shell()->web_contents()->GetBrowserContext())) {
      // Only include the port for origin-isolated urls.
      replacements.ClearPort();
    }
    return result.ReplaceComponents(replacements);
  }

  // Returns an url that assigns a site to the SiteInstance it lives in and is
  // crossOriginIsolated.
  const GURL& cross_origin_isolated_url() const {
    return cross_origin_isolated_url_;
  }

  // Returns an url that does not assign a site to the SiteInstance it lives in.
  const GURL& unassigned_url() const { return unassigned_url_; }

  // Returns an url from an empty document scheme that does not assign a site to
  // the SiteInstance it lives in, as decided by the content embedder.
  const GURL& embedder_defined_unassigned_url() const {
    return embedder_defined_unassigned_url_;
  }

  // Returns an url that assigns a site to the SiteInstance it lives in, and
  // that is served using HTTP.
  const GURL& regular_http_url() const { return regular_http_url_; }

 private:
  net::EmbeddedTestServer https_server_;
  content::ContentMockCertVerifier mock_cert_verifier_;

  GURL regular_url_;
  GURL cross_origin_isolated_url_;
  GURL unassigned_url_;
  GURL embedder_defined_unassigned_url_;

  // This is the only URL that uses HTTP instead of HTTPS, because
  // IsolateOriginsForTesting() has a hardcoded HTTP filter in it. It should
  // only be used for that specific purpose.
  GURL regular_http_url_;

  base::test::ScopedFeatureList feature_list_for_render_document_;
  base::test::ScopedFeatureList feature_list_for_back_forward_cache_;
  url::ScopedSchemeRegistryForTests scheme_registry_;
};

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       RendererInitiatedNavigationTo) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), regular_url()));
  RenderFrameHostImpl* initial_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> initial_si = initial_rfh->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());

  // Do a renderer-initiated navigation to an unassigned url.
  EXPECT_TRUE(NavigateToURLFromRenderer(initial_rfh, unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();

  if (!BackForwardCache::IsBackForwardCacheFeatureEnabled()) {
    // Normally we reuse the SiteInstance.
    EXPECT_TRUE(unassigned_si->HasSite());
    EXPECT_EQ(unassigned_si, initial_si);
  } else {
    // With back/forward cache, we will swap browsing instance for same-site
    // navigations, not reusing the previous SiteInstance.
    EXPECT_FALSE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));

    // Because this was a renderer-initiated navigation, and the unassigned URL
    // is about:blank, the committed document inherits its origin from the
    // initiator which is `regular_url()` (https://crbug.com/585649).  Hence,
    // the new SiteInstance should not stay unassigned, but rather it will have
    // a site that reflects the initiator.  See https://crbug.com/1426928.
    EXPECT_TRUE(unassigned_si->HasSite());
  }
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       BrowserInitiatedNavigationTo) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());

  // Do a browser initiated navigation to an unassigned url.
  EXPECT_TRUE(NavigateToURL(shell(), unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();

  if (!BackForwardCache::IsBackForwardCacheFeatureEnabled()) {
    // Normally we reuse the SiteInstance.
    EXPECT_TRUE(unassigned_si->HasSite());
    EXPECT_EQ(unassigned_si, initial_si);
  } else {
    // With back/forward cache, we will swap browsing instance for same-site
    // navigations, not reusing the previous SiteInstance.
    EXPECT_FALSE(unassigned_si->HasSite());
    EXPECT_FALSE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));
  }
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       RendererInitiatedNavigationFrom) {
  // Get a base page without a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), unassigned_url()));
  RenderFrameHostImpl* unassigned_url_rfh =
      web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      unassigned_url_rfh->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());

  // Do a renderer-initiated navigation to an assigned url. We reuse the
  // unassigned SiteInstance and set its site.
  EXPECT_TRUE(NavigateToURLFromRenderer(unassigned_url_rfh, regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_EQ(unassigned_si, initial_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       BrowserInitiatedNavigationFrom) {
  // Get a base page without a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());

  // Do a browser-initiated navigation to an assigned url. We reuse the
  // unassigned SiteInstance and set its site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_EQ(unassigned_si, initial_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       RendererInitiatedNavigationTo_CustomUrl) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* initial_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> initial_si = initial_rfh->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());

  // Do a renderer-initiated navigation to an embedder-defined unassigned url.
  // We use a new related or unrelated SiteInstance depending on the
  // ProactivelySwapBrowsingInstance feature. This differs from the base
  // unassigned behavior, because about:blank is explicitly considered as same
  // site with all other site, but custom embedder-defined unassigned urls are
  // not.
  EXPECT_TRUE(NavigateToURLFromRenderer(initial_rfh,
                                        embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());
  if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
    EXPECT_FALSE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));
  } else {
    EXPECT_TRUE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));
  }
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       BrowserInitiatedNavigationTo_CustomUrl) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());

  // Do a browser initiated navigation to an embedder-defined unassigned url.
  // We do not reuse the assigned SiteInstance. We use a new SiteInstance in its
  // own BrowsingInstance.
  // Note: This differs from the renderer initiated behavior. This comes from
  // the fact that IsBrowsingInstanceSwapAllowedForPageTransition() explicitly
  // excludes renderer initiated navigations from generic cross-site
  // BrowsingInstance swap.
  EXPECT_TRUE(NavigateToURL(web_contents(), embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());
  EXPECT_FALSE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       RendererInitiatedNavigationFrom_CustomUrl) {
  // Get a base page without a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), embedder_defined_unassigned_url()));
  RenderFrameHostImpl* unassigned_url_rfh =
      web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      unassigned_url_rfh->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());

  // Do a renderer-initiated navigation to an assigned url. We reuse the
  // unassigned SiteInstance and set its site, because the process is unused.
  EXPECT_TRUE(NavigateToURLFromRenderer(unassigned_url_rfh, regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_EQ(unassigned_si, initial_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       BrowserInitiatedNavigationFrom_CustomUrl) {
  // Get a base page without a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());

  // Do a browser-initiated navigation to an assigned url. We reuse the
  // unassigned SiteInstance and set its site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_EQ(unassigned_si, initial_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InPopup_InitialAboutBlank) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a popup. It should reuse the opener SiteInstance.
  ShellAddedObserver shell_observer;
  EXPECT_TRUE(ExecJs(original_rfh, "window.open()"));
  scoped_refptr<SiteInstanceImpl> popup_si =
      static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents())
          ->GetPrimaryMainFrame()
          ->GetSiteInstance();
  EXPECT_TRUE(popup_si->HasSite());
  EXPECT_EQ(popup_si, original_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InPopup_RendererInitiatedNavigateTo) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin popup.
  ShellAddedObserver shell_observer;
  EXPECT_TRUE(
      ExecJs(original_rfh, JsReplace("window.open($1)", regular_url())));
  WebContentsImpl* popup_web_contents =
      static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents());
  EXPECT_TRUE(WaitForLoadStop(popup_web_contents));
  scoped_refptr<SiteInstanceImpl> popup_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(popup_si->HasSite());
  EXPECT_EQ(popup_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(popup_si->IsDefaultSiteInstance());
  }

  // In the popup, do a renderer-initiated navigation to an unassigned url. We
  // should reuse the SiteInstance.
  EXPECT_TRUE(NavigateToURLFromRenderer(
      popup_web_contents->GetPrimaryMainFrame(), unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(post_navigation_si->HasSite());
  EXPECT_EQ(post_navigation_si, original_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InPopup_BrowserInitiatedNavigateTo) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin popup.
  ShellAddedObserver shell_observer;
  EXPECT_TRUE(
      ExecJs(original_rfh, JsReplace("window.open($1)", regular_url())));
  WebContentsImpl* popup_web_contents =
      static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents());
  EXPECT_TRUE(WaitForLoadStop(popup_web_contents));
  scoped_refptr<SiteInstanceImpl> popup_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(popup_si->HasSite());
  EXPECT_EQ(popup_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(popup_si->IsDefaultSiteInstance());
  }

  // In the popup, do a browser-initiated navigation to an unassigned url. We
  // should reuse the SiteInstance.
  EXPECT_TRUE(NavigateToURL(popup_web_contents, unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(post_navigation_si->HasSite());
  EXPECT_EQ(post_navigation_si, original_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InPopup_RendererInitiatedNavigateTo_CustomUrl) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin popup.
  ShellAddedObserver shell_observer;
  EXPECT_TRUE(
      ExecJs(original_rfh, JsReplace("window.open($1)", regular_url())));
  WebContentsImpl* popup_web_contents =
      static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents());
  EXPECT_TRUE(WaitForLoadStop(popup_web_contents));
  scoped_refptr<SiteInstanceImpl> popup_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(popup_si->HasSite());
  EXPECT_EQ(popup_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(popup_si->IsDefaultSiteInstance());
  }

  // In the popup, do a renderer-initiated navigation to an embedder-defined
  // unassigned url. We use another related SiteInstance. Note that contrary to
  // its main window counterpart, here we never swap BrowsingInstance, because
  // ProactivelySwapBrowsingInstance never applies to popups.
  EXPECT_TRUE(
      NavigateToURLFromRenderer(popup_web_contents->GetPrimaryMainFrame(),
                                embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(post_navigation_si->HasSite());
  EXPECT_TRUE(post_navigation_si->IsRelatedSiteInstance(original_si.get()));
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InPopup_BrowserInitiatedNavigateTo_CustomUrl) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin popup.
  ShellAddedObserver shell_observer;
  EXPECT_TRUE(
      ExecJs(original_rfh, JsReplace("window.open($1)", regular_url())));
  WebContentsImpl* popup_web_contents =
      static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents());
  EXPECT_TRUE(WaitForLoadStop(popup_web_contents));
  scoped_refptr<SiteInstanceImpl> popup_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(popup_si->HasSite());
  EXPECT_EQ(popup_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(popup_si->IsDefaultSiteInstance());
  }

  // In the popup, do a browser-initiated navigation to an embedder-defined
  // unassigned url. We swap browsing instance because the navigation is
  // considered cross-site.
  EXPECT_TRUE(
      NavigateToURL(popup_web_contents, embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      popup_web_contents->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(post_navigation_si->HasSite());
  EXPECT_FALSE(post_navigation_si->IsRelatedSiteInstance(original_si.get()));
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       CrossOriginIsolated_BrowserInitiatedNavigationTo) {
  // Get a base crossOriginIsolated page with a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), cross_origin_isolated_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_TRUE(initial_si->IsCrossOriginIsolated());

  // Do a browser initiated navigation to an unassigned url.
  EXPECT_TRUE(NavigateToURL(shell(), unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();

  // We explicitly disable SiteInstance reuse for now. Restricting which custom
  // sites are allowed to be described by the embedder as unassigned might
  // change that in the future.
  EXPECT_FALSE(unassigned_si->HasSite());
  EXPECT_FALSE(unassigned_si->IsRelatedSiteInstance(initial_si.get()));
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       CrossOriginIsolated_BrowserInitiatedNavigationFrom) {
  // Get a base page without a site.
  EXPECT_TRUE(NavigateToURL(web_contents(), unassigned_url()));
  scoped_refptr<SiteInstanceImpl> unassigned_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_FALSE(unassigned_si->HasSite());

  // Do a browser-initiated navigation to a crossOriginIsolated assigned url. We
  // should not reuse the unassigned SiteInstance currently, because we have no
  // guarantee that the unassigned page won't load content in the process.
  // Restricting which custom sites are allowed to be described by the embedder
  // as unassigned might change that in the future.
  EXPECT_TRUE(NavigateToURL(shell(), cross_origin_isolated_url()));
  scoped_refptr<SiteInstanceImpl> initial_si =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_TRUE(initial_si->HasSite());
  EXPECT_TRUE(initial_si->IsCrossOriginIsolated());
  EXPECT_FALSE(initial_si->IsRelatedSiteInstance(unassigned_si.get()));
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InIframe_InitialAboutBlank) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create an iframe. The initial empty document should reuse the parent
  // SiteInstance.
  ASSERT_TRUE(ExecJs(original_rfh, R"(
    const frame = document.createElement('iframe');
    document.body.appendChild(frame);
  )"));
  scoped_refptr<SiteInstanceImpl> iframe_si =
      original_rfh->child_at(0)->current_frame_host()->GetSiteInstance();
  EXPECT_TRUE(iframe_si->HasSite());
  EXPECT_EQ(iframe_si, original_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InIframe_RendererInitiatedNavigateTo) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin iframe. It should reuse the parent SiteInstance.
  ASSERT_TRUE(ExecJs(original_rfh, JsReplace(R"(
    const frame = document.createElement('iframe');
    frame.src = $1;
    document.body.appendChild(frame);
  )",
                                             regular_url())));
  EXPECT_TRUE(WaitForLoadStop(web_contents()));
  RenderFrameHostImpl* iframe_rfh =
      original_rfh->child_at(0)->current_frame_host();
  scoped_refptr<SiteInstanceImpl> iframe_si = iframe_rfh->GetSiteInstance();
  EXPECT_TRUE(iframe_si->HasSite());
  EXPECT_EQ(iframe_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(iframe_si->IsDefaultSiteInstance());
  }

  // In the iframe, navigate to an unassigned url. We reuse the SiteInstance.
  EXPECT_TRUE(NavigateToURLFromRenderer(iframe_rfh, unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      original_rfh->child_at(0)->current_frame_host()->GetSiteInstance();
  EXPECT_TRUE(post_navigation_si->HasSite());
  EXPECT_EQ(post_navigation_si, original_si);
}

IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       InIframe_RendererInitiatedNavigateTo_CustomUrl) {
  // Get a base page with a site.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  RenderFrameHostImpl* original_rfh = web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstanceImpl> original_si = original_rfh->GetSiteInstance();
  EXPECT_TRUE(original_si->HasSite());

  // Create a same-origin iframe. It should reuse the parent SiteInstance.
  ASSERT_TRUE(ExecJs(original_rfh, JsReplace(R"(
    const frame = document.createElement('iframe');
    frame.src = $1;
    document.body.appendChild(frame);
  )",
                                             regular_url())));
  EXPECT_TRUE(WaitForLoadStop(web_contents()));
  RenderFrameHostImpl* iframe_rfh =
      original_rfh->child_at(0)->current_frame_host();
  scoped_refptr<SiteInstanceImpl> iframe_si = iframe_rfh->GetSiteInstance();
  EXPECT_TRUE(iframe_si->HasSite());
  EXPECT_EQ(iframe_si, original_si);

  if (!AreStrictSiteInstancesEnabled()) {
    EXPECT_TRUE(iframe_si->IsDefaultSiteInstance());
  }

  // In the iframe, navigate to an embedder defined unassigned url. We use a new
  // related SiteInstance because the navigation is considered cross-site.
  EXPECT_TRUE(
      NavigateToURLFromRenderer(iframe_rfh, embedder_defined_unassigned_url()));
  scoped_refptr<SiteInstanceImpl> post_navigation_si =
      original_rfh->child_at(0)->current_frame_host()->GetSiteInstance();

  if (AreStrictSiteInstancesEnabled()) {
    EXPECT_FALSE(post_navigation_si->HasSite());
    EXPECT_TRUE(post_navigation_si->IsRelatedSiteInstance(original_si.get()));
  } else {
    // On Android, we sometimes do not get a related SiteInstance, but the same
    // default SiteInstance to reduce memory footprint.
    EXPECT_TRUE(post_navigation_si->IsDefaultSiteInstance());
    EXPECT_TRUE(post_navigation_si->HasSite());
    EXPECT_EQ(post_navigation_si, original_si);
  }
}

// Ensure that coming back to a NavigationEntry with a previously unassigned
// SiteInstance (which is now used for another site) properly switches processes
// and SiteInstances.  See https://crbug.com/945399.
IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       BackToNowAssignedSiteInstance) {
  // Navigate to a URL that does not assign site URLs.
  EXPECT_TRUE(NavigateToURL(shell(), embedder_defined_unassigned_url()));
  EXPECT_EQ(embedder_defined_unassigned_url(),
            web_contents()->GetLastCommittedURL());
  scoped_refptr<SiteInstanceImpl> instance1(
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
  RenderProcessHost* process1 = instance1->GetProcess();
  EXPECT_EQ(GURL(), instance1->GetSecurityPrincipal().GetDeprecatedSiteURL());

  // Navigate to page that uses up the site. It should reuse the previous
  // SiteInstance and set its site URL.
  EXPECT_TRUE(NavigateToURL(shell(), regular_url()));
  EXPECT_EQ(instance1,
            web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
  EXPECT_TRUE(instance1->HasSite());
  if (AreStrictSiteInstancesEnabled()) {
    EXPECT_EQ(RegularUrlOriginMaybeWithPort(),
              instance1->GetSecurityPrincipal().GetDeprecatedSiteURL());
  } else {
    EXPECT_TRUE(instance1->IsDefaultSiteInstance());
  }

  // The previously committed entry should get a new, related instance to avoid
  // a SiteInstance mismatch when returning to it. See http://crbug.com/992198
  // for further context.
  scoped_refptr<SiteInstanceImpl> prev_entry_instance =
      web_contents()
          ->GetController()
          .GetEntryAtIndex(0)
          ->root_node()
          ->frame_entry->site_instance();
  EXPECT_NE(prev_entry_instance, instance1);
  EXPECT_NE(prev_entry_instance, nullptr);
  EXPECT_TRUE(prev_entry_instance->IsRelatedSiteInstance(instance1.get()));
  EXPECT_EQ(GURL(),
            prev_entry_instance->GetSecurityPrincipal().GetDeprecatedSiteURL());

  // Navigate to bar.com, which destroys the previous RenderProcessHost.
  GURL other_regular_url(
      https_server()->GetURL("another.test", "/title1.html"));
  RenderProcessHostWatcher exit_observer(
      process1, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // With BackForwardCache, old process won't be deleted on navigation as it is
  // still in use by the bfcached document, disable back-forward cache to ensure
  // that the process gets deleted.
  DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);

  EXPECT_TRUE(NavigateToURL(shell(), other_regular_url));
  exit_observer.Wait();

  if (AreStrictSiteInstancesEnabled()) {
    EXPECT_NE(instance1,
              web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
  } else {
    // Verify that the new navigation also results in a default SiteInstance,
    // and verify that it is not related to |instance1| because the navigation
    // swapped to a new BrowsingInstance.
    EXPECT_TRUE(web_contents()
                    ->GetPrimaryMainFrame()
                    ->GetSiteInstance()
                    ->IsDefaultSiteInstance());
    EXPECT_FALSE(instance1->IsRelatedSiteInstance(
        web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
  }

  // At this point, process1 is deleted, and the first entry is unfortunately
  // pointing to instance1, which has been locked to url2 and has no process.
  EXPECT_FALSE(instance1->HasProcess());
  if (AreAllSitesIsolatedForTesting()) {
    // In site-per-process, we cannot use foo.com's SiteInstance for a.com.
    EXPECT_FALSE(instance1->IsSuitableForUrlInfo(
        UrlInfo::CreateForTesting(embedder_defined_unassigned_url())));
  } else if (AreStrictSiteInstancesEnabled()) {
    // If neither foo.com nor a.com require dedicated processes, and we're using
    // default SiteInstanceGroup instead of default SiteInstance, then we can
    // use the same process.
    EXPECT_TRUE(instance1->IsSuitableForUrlInfo(
        UrlInfo::CreateForTesting(embedder_defined_unassigned_url())));
  } else {
    // Since |instance1| is a default SiteInstance AND this test explicitly
    // ensures that ShouldAssignSiteForURL(url1) will return false, |url1|
    // cannot be placed in the default SiteInstance. This also means that |url1|
    // cannot be placed in the same process as the default SiteInstance.
    EXPECT_FALSE(instance1->IsSuitableForUrlInfo(
        UrlInfo::CreateForTesting(embedder_defined_unassigned_url())));
  }

  // Go back to url1's entry, which should swap to a new SiteInstance with an
  // unused site URL.
  TestNavigationObserver observer(web_contents());
  web_contents()->GetController().GoToOffset(-2);
  observer.Wait();
  scoped_refptr<SiteInstanceImpl> new_instance =
      web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
  EXPECT_EQ(embedder_defined_unassigned_url(),
            web_contents()->GetLastCommittedURL());
  EXPECT_NE(instance1, new_instance);
  EXPECT_EQ(GURL(),
            new_instance->GetSecurityPrincipal().GetDeprecatedSiteURL());
  EXPECT_TRUE(new_instance->HasProcess());

  // Because embedder_defined_unassigned_url does not set a site URL, it should
  // not lock the new process either, so that it can be used for subsequent
  // navigations.
  RenderProcessHost* new_process = new_instance->GetProcess();
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  EXPECT_TRUE(policy->CanAccessOrigin(
      new_process->GetDeprecatedID(),
      url::Origin::Create(embedder_defined_unassigned_url()),
      ChildProcessSecurityPolicyImpl::AccessType::kCanCommitNewOrigin));
  EXPECT_TRUE(policy->CanAccessOrigin(
      new_process->GetDeprecatedID(), url::Origin::Create(regular_url()),
      ChildProcessSecurityPolicyImpl::AccessType::kCanCommitNewOrigin));
}

// Check that when a navigation to a URL that doesn't require assigning a site
// URL is in progress, another navigation can't reuse the same process in the
// meantime.  Such reuse previously led to a renderer kill when the unassigned
// URL later committed; a real-world example of the unassigned URL was
// chrome-native://newtab.  See https://crbug.com/970046.
IN_PROC_BROWSER_TEST_P(UnassignedSiteInstanceBrowserTest,
                       NavigationRacesWithCommitInunassignedSiteInstance) {
  if (ShouldCreateNewHostForAllFrames()) {
    // This test involves starting a navigation while another navigation is
    // committing, which might lead to deletion of a pending commit RFH, which
    // will crash when RenderDocument is enabled. Skip the test if so.
    // TODO(crbug.com/40186427): Update this test to work under
    // navigation queueing, which will prevent the deletion of the pending
    // commit RFH but still fails because this test waits for the new navigation
    // to get to the WillProcessResponse stage before finishing the commit of
    // the pending commit RFH.
    return;
  }
  // Prepare for a second navigation to a normal URL.  Ensure it's isolated so
  // that it requires a process lock on all platforms.
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  policy->AddFutureIsolatedOrigins(
      {url::Origin::Create(regular_url())},
      ChildProcessSecurityPolicy::IsolatedOriginSource::TEST);

  // Create a new shell where the normal url origin isolation will take effect.
  Shell* shell = CreateBrowser();
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderProcessHost* regular_process = nullptr;
  TestNavigationManager regular_manager(web_contents, regular_url());
  auto& current_isolation_context =
      root->current_frame_host()->GetSiteInstance()->GetIsolationContext();
  auto site_info = SiteInfo::CreateForTesting(current_isolation_context,
                                              GURL("https://a.test"));
  EXPECT_TRUE(site_info.RequiresDedicatedProcess(current_isolation_context));

  // Set up the work to be done after the renderer is asked to commit
  // |embedder_defined_unassigned_url|, but before the corresponding
  // DidCommitProvisionalLoad IPC is processed.  This will start a navigation to
  // |regular_url| and wait for its response.
  auto did_commit_callback =
      base::BindLambdaForTesting([&](RenderFrameHost* rfh) {
        // The navigation should stay in the initial empty SiteInstance, with
        // the site still unassigned.
        EXPECT_FALSE(
            static_cast<SiteInstanceImpl*>(rfh->GetSiteInstance())->HasSite());
        EXPECT_EQ(ShouldCreateNewHostForAllFrames(),
                  !!root->render_manager()->speculative_frame_host());

        shell->LoadURL(regular_url());
        // This will cause |regular_manager| to spin up a nested message loop
        // while we're blocked in the current message loop
        // (within DidCommitNavigationInterceptor).  Thus, it's important to
        // allow nestable tasks in |regular_manager|'s message loop, so that it
        // can process the response before we unblock the
        // DidCommitNavigationInterceptor's message loop and finish processing
        // the commit.
        regular_manager.AllowNestableTasks();
        EXPECT_TRUE(regular_manager.WaitForResponse());

        // The foo.com navigation should swap to a new process, since it is not
        // safe to reuse |embedder_defined_unassigned_url|'s process before
        // |embedder_defined_unassigned_url| commits.
        EXPECT_TRUE(root->render_manager()->speculative_frame_host());
        regular_process =
            root->render_manager()->speculative_frame_host()->GetProcess();

        regular_manager.ResumeNavigation();
        // After returning here, the commit for
        // |embedder_defined_unassigned_url| will be processed.
      });

  CommitMessageDelayer commit_delayer(
      web_contents, embedder_defined_unassigned_url() /* deferred_url */,
      std::move(did_commit_callback));

  // Start the first navigation, which does not assign a site URL.
  shell->LoadURL(embedder_defined_unassigned_url());

  // The navigation should stay in the initial empty SiteInstance, so there
  // shouldn't be a speculative RFH at this point, unless RenderDocument is
  // enabled for all frames.
  EXPECT_EQ(ShouldCreateNewHostForAllFrames(),
            !!root->render_manager()->speculative_frame_host());

  // Wait for the DidCommit IPC for |embedder_defined_unassigned_url|, and
  // before processing it, trigger a navigation to |regular_url| and wait for
  // its response.
  commit_delayer.Wait();

  // Check that the renderer hasn't been killed.  At this point, it should've
  // successfully committed the navigation to |embedder_defined_unassigned_url|,
  // and it shouldn't be locked.
  EXPECT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
  EXPECT_EQ(embedder_defined_unassigned_url(),
            web_contents->GetPrimaryMainFrame()->GetLastCommittedURL());
  RenderProcessHost* process1 =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  EXPECT_FALSE(
      web_contents->GetPrimaryMainFrame()->GetSiteInstance()->HasSite());
  auto process1_lock = process1->GetProcessLock();
  EXPECT_FALSE(process1_lock.is_invalid());
  EXPECT_TRUE(process1_lock.AllowsAnySite());

  // Now wait for second navigation to finish and ensure it also succeeds.
  ASSERT_TRUE(regular_manager.WaitForNavigationFinished());
  EXPECT_TRUE(regular_manager.was_successful());
  EXPECT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
  EXPECT_EQ(regular_url(),
            web_contents->GetPrimaryMainFrame()->GetLastCommittedURL());

  // The regular url navigation should've used a different process, locked to
  // a.test.
  BrowserContext* browser_context = web_contents->GetBrowserContext();
  RenderProcessHost* process2 =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  EXPECT_NE(process1, process2);
  EXPECT_EQ(RegularUrlOriginMaybeWithPort(), web_contents->GetPrimaryMainFrame()
                                                 ->GetSiteInstance()
                                                 ->GetSecurityPrincipal()
                                                 .GetDeprecatedSiteURL());
  EXPECT_EQ(ProcessLock::FromSiteInfo(SiteInfo::CreateForTesting(
                IsolationContext(browser_context), regular_url())),
            policy->GetProcessLock(process2->GetID()));

  // Ensure also that the regular url process didn't change midway through the
  // navigation.
  EXPECT_EQ(regular_process, process2);
}

static auto kTestParams =
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool());
INSTANTIATE_TEST_SUITE_P(All,
                         UnassignedSiteInstanceBrowserTest,
                         kTestParams,
                         UnassignedSiteInstanceBrowserTest::DescribeParams);

}  // namespace content
