blob: 5bc89e369163c8895f323b0539ec8cbefd07b8ba [file] [log] [blame]
// 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 "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include <memory>
#include <string>
#include <vector>
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_test_util_internal.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_web_contents.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/no_vary_search.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/loader/referrer.mojom.h"
#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom.h"
namespace content {
namespace {
using testing::FieldsAre;
using testing::IsEmpty;
using testing::IsNull;
using testing::UnorderedElementsAreArray;
class PrefetchDocumentManagerTest : public RenderViewHostTestHarness {
public:
void SetUp() override {
RenderViewHostTestHarness::SetUp();
browser_context_ = std::make_unique<TestBrowserContext>();
web_contents_ = TestWebContents::Create(
browser_context_.get(),
SiteInstanceImpl::Create(browser_context_.get()));
web_contents_->NavigateAndCommit(GetSameOriginUrl("/"));
prefetch_service_ =
std::make_unique<TestPrefetchService>(browser_context_.get());
PrefetchDocumentManager::SetPrefetchServiceForTesting(
prefetch_service_.get());
}
void TearDown() override {
// The PrefetchService we created for the test contains a
// PrefetchOriginProber, which holds a raw pointer to the BrowserContext.
// When tearing down, it's important to free our PrefetchService
// before freeing the BrowserContext, to avoid any chance of a use after
// free.
PrefetchDocumentManager::SetPrefetchServiceForTesting(nullptr);
prefetch_service_.reset();
web_contents_.reset();
browser_context_.reset();
RenderViewHostTestHarness::TearDown();
}
RenderFrameHostImpl& GetPrimaryMainFrame() {
return web_contents_->GetPrimaryPage().GetMainDocument();
}
GURL GetSameOriginUrl(const std::string& path) {
return GURL("https://example.com" + path);
}
GURL GetSameSiteCrossOriginUrl(const std::string& path) {
return GURL("https://other.example.com" + path);
}
GURL GetCrossOriginUrl(const std::string& path) {
return GURL("https://other.com" + path);
}
void NavigateMainframeRendererTo(const GURL& url) {
std::unique_ptr<NavigationSimulator> simulator =
NavigationSimulator::CreateRendererInitiated(url,
&GetPrimaryMainFrame());
simulator->SetTransition(ui::PAGE_TRANSITION_LINK);
simulator->Start();
}
const std::vector<base::WeakPtr<PrefetchContainer>>& GetPrefetches() {
return prefetch_service_->prefetches_;
}
// Used to make sure that No-Vary-Search parsing error/warning message is sent
// to DevTools console.
std::string TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError parse_error) {
// Used to create responses.
const net::IsolationInfo info;
// Process the candidates with the |PrefetchDocumentManager| for the current
// document.
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(
&GetPrimaryMainFrame());
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
// Create candidate for private cross-origin prefetch. This candidate should
// be prefetched by |PrefetchDocumentManager|.
auto candidate1 = blink::mojom::SpeculationCandidate::New();
const auto test_url = GetCrossOriginUrl("/candidate1.html?a=2&b=3");
candidate1->action = blink::mojom::SpeculationAction::kPrefetch;
candidate1->requires_anonymous_client_ip_when_cross_origin = false;
candidate1->url = test_url;
candidate1->referrer = blink::mojom::Referrer::New();
candidates.push_back(std::move(candidate1));
prefetch_document_manager->ProcessCandidates(candidates);
// Now call TakePrefetchedResponse
network::mojom::URLResponseHeadPtr head =
network::mojom::URLResponseHead::New();
head->parsed_headers = network::mojom::ParsedHeaders::New();
head->parsed_headers->no_vary_search_with_parse_error =
network::mojom::NoVarySearchWithParseError::NewParseError(parse_error);
GetPrefetches()[0]->SimulatePrefetchEligibleForTest();
MakeServableStreamingURLLoaderForTest(GetPrefetches()[0].get(),
std::move(head), "empty");
auto& test_rfh = static_cast<TestRenderFrameHost&>(GetPrimaryMainFrame());
return test_rfh.GetConsoleMessages()[0];
}
private:
std::unique_ptr<TestBrowserContext> browser_context_;
std::unique_ptr<TestWebContents> web_contents_;
std::unique_ptr<TestPrefetchService> prefetch_service_;
};
TEST_F(PrefetchDocumentManagerTest, PopulateNoVarySearchHint) {
// Process the candidates with the |PrefetchDocumentManager| for the current
// document.
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(
&GetPrimaryMainFrame());
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
// Create candidate for private cross-origin prefetch. This candidate should
// be prefetched by |PrefetchDocumentManager|.
auto candidate1 = blink::mojom::SpeculationCandidate::New();
const auto test_url1 = GetCrossOriginUrl("/candidate1.html?a=2&b=3");
candidate1->action = blink::mojom::SpeculationAction::kPrefetch;
candidate1->requires_anonymous_client_ip_when_cross_origin = false;
candidate1->url = test_url1;
candidate1->referrer = blink::mojom::Referrer::New();
candidate1->no_vary_search_hint = network::mojom::NoVarySearch::New();
candidate1->no_vary_search_hint->vary_on_key_order = false;
candidate1->no_vary_search_hint->search_variance =
network::mojom::SearchParamsVariance::NewNoVaryParams({"a"});
auto candidate2 = blink::mojom::SpeculationCandidate::New();
const auto test_url2 = GetCrossOriginUrl("/candidate2.html?a=2&b=3");
candidate2->action = blink::mojom::SpeculationAction::kPrefetch;
candidate2->requires_anonymous_client_ip_when_cross_origin = false;
candidate2->url = test_url2;
candidate2->referrer = blink::mojom::Referrer::New();
candidate2->no_vary_search_hint = network::mojom::NoVarySearch::New();
candidate2->no_vary_search_hint->vary_on_key_order = true;
candidate2->no_vary_search_hint->search_variance =
network::mojom::SearchParamsVariance::NewVaryParams({"a"});
auto candidate3 = blink::mojom::SpeculationCandidate::New();
const auto test_url3 = GetCrossOriginUrl("/candidate3.html?a=2&b=3");
candidate3->action = blink::mojom::SpeculationAction::kPrefetch;
candidate3->requires_anonymous_client_ip_when_cross_origin = false;
candidate3->url = test_url3;
candidate3->referrer = blink::mojom::Referrer::New();
candidates.push_back(std::move(candidate1));
candidates.push_back(std::move(candidate2));
candidates.push_back(std::move(candidate3));
prefetch_document_manager->ProcessCandidates(candidates);
ASSERT_EQ(GetPrefetches().size(), 3u);
{
auto& prefetch = GetPrefetches()[0];
ASSERT_TRUE(prefetch);
ASSERT_TRUE(prefetch->GetNoVarySearchHint().has_value());
EXPECT_FALSE(prefetch->GetNoVarySearchHint()->vary_on_key_order());
EXPECT_THAT(prefetch->GetNoVarySearchHint()->no_vary_params(),
UnorderedElementsAreArray({"a"}));
}
{
auto& prefetch = GetPrefetches()[1];
ASSERT_TRUE(prefetch);
ASSERT_TRUE(prefetch->GetNoVarySearchHint().has_value());
EXPECT_TRUE(prefetch->GetNoVarySearchHint()->vary_on_key_order());
EXPECT_THAT(prefetch->GetNoVarySearchHint()->vary_params(),
UnorderedElementsAreArray({"a"}));
}
{
auto& prefetch = GetPrefetches()[2];
ASSERT_TRUE(prefetch);
EXPECT_FALSE(prefetch->GetNoVarySearchHint().has_value());
}
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithDefaultValue) {
EXPECT_THAT(TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kDefaultValue),
testing::HasSubstr("is equivalent to the default behavior"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithNotDictionary) {
EXPECT_THAT(TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kNotDictionary),
testing::HasSubstr("is not a dictionary"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithUnknownDictionaryKey) {
EXPECT_THAT(
TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kUnknownDictionaryKey),
testing::HasSubstr("contains unknown dictionary keys"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithNonBooleanKeyOrder) {
EXPECT_THAT(
TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kNonBooleanKeyOrder),
testing::HasSubstr(
"contains a \"key-order\" dictionary value that is not a boolean"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithParamsNotStringList) {
EXPECT_THAT(TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kParamsNotStringList),
testing::HasSubstr(
"contains a \"params\" dictionary value that is not a list"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithExceptNotStringList) {
EXPECT_THAT(
TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kExceptNotStringList),
testing::HasSubstr(
"contains an \"except\" dictionary value that is not a list"));
}
TEST_F(PrefetchDocumentManagerTest,
ProcessNoVarySearchResponseWithExceptWithoutTrueParams) {
EXPECT_THAT(
TriggerNoVarySearchParseErrorAndGetConsoleMessage(
network::mojom::NoVarySearchParseError::kExceptWithoutTrueParams),
testing::HasSubstr(
"contains an \"except\" dictionary key, without the \"params\""));
}
TEST_F(PrefetchDocumentManagerTest, ProcessSpeculationCandidates) {
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
auto referrer = blink::mojom::Referrer::New();
referrer->url = GetSameOriginUrl("/referrer");
// Create candidate for private cross-origin prefetch. This candidate should
// be prefetched by |PrefetchDocumentManager|.
auto candidate1 = blink::mojom::SpeculationCandidate::New();
candidate1->action = blink::mojom::SpeculationAction::kPrefetch;
candidate1->requires_anonymous_client_ip_when_cross_origin = true;
candidate1->url = GetCrossOriginUrl("/candidate1.html");
candidate1->referrer = referrer->Clone();
candidate1->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate1));
// Create candidate for non-private cross-origin prefetch. This candidate
// should be prefetched by |PrefetchDocumentManager|.
auto candidate2 = blink::mojom::SpeculationCandidate::New();
candidate2->action = blink::mojom::SpeculationAction::kPrefetch;
candidate2->requires_anonymous_client_ip_when_cross_origin = false;
candidate2->url = GetCrossOriginUrl("/candidate2.html");
candidate2->referrer = referrer->Clone();
candidate2->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate2));
// Create candidate for non-private cross-origin prefetch. This candidate
// should be prefetched by |PrefetchDocumentManager|.
auto candidate3 = blink::mojom::SpeculationCandidate::New();
candidate3->action = blink::mojom::SpeculationAction::kPrefetch;
candidate3->requires_anonymous_client_ip_when_cross_origin = false;
candidate3->url = GetSameOriginUrl("/candidate3.html");
candidate3->referrer = referrer->Clone();
candidate3->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate3));
// Create candidate for private cross-origin prefetch with subresources. This
// candidate should not be prefetched by |PrefetchDocumentManager|.
auto candidate4 = blink::mojom::SpeculationCandidate::New();
candidate4->action =
blink::mojom::SpeculationAction::kPrefetchWithSubresources;
candidate4->requires_anonymous_client_ip_when_cross_origin = true;
candidate4->url = GetCrossOriginUrl("/candidate4.html");
candidate4->referrer = referrer->Clone();
candidate4->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate4));
// Create candidate for prerender. This candidate should not be prefetched by
// |PrefetchDocumentManager|.
auto candidate5 = blink::mojom::SpeculationCandidate::New();
candidate5->action = blink::mojom::SpeculationAction::kPrerender;
candidate5->requires_anonymous_client_ip_when_cross_origin = false;
candidate5->url = GetCrossOriginUrl("/candidate5.html");
candidate5->referrer = referrer->Clone();
candidate5->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate5));
// Create candidate for private cross-origin prefetch with default eagerness.
// This candidate should be prefetched by |PrefetchDocumentManager|.
auto candidate6 = blink::mojom::SpeculationCandidate::New();
candidate6->action = blink::mojom::SpeculationAction::kPrefetch;
candidate6->requires_anonymous_client_ip_when_cross_origin = true;
candidate6->url = GetCrossOriginUrl("/candidate6.html");
candidate6->referrer = referrer->Clone();
candidate6->eagerness = blink::mojom::SpeculationEagerness::kConservative;
candidates.push_back(std::move(candidate6));
// Create candidate for same-site prefetch. This candidate should
// be prefetched by |PrefetchDocumentManager|.
auto candidate7 = blink::mojom::SpeculationCandidate::New();
candidate7->action = blink::mojom::SpeculationAction::kPrefetch;
candidate7->requires_anonymous_client_ip_when_cross_origin = false;
candidate7->url = GetSameSiteCrossOriginUrl("/candidate7.html");
candidate7->referrer = referrer->Clone();
candidate7->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate7));
// Create candidate for same-origin prefetch that requires a proxy if
// redirected to a cross-origin URL. This candidate should be prefetched by
// |PrefetchDocumentManager|.
auto candidate8 = blink::mojom::SpeculationCandidate::New();
candidate8->action = blink::mojom::SpeculationAction::kPrefetch;
candidate8->requires_anonymous_client_ip_when_cross_origin = true;
candidate8->url = GetSameOriginUrl("/candidate8.html");
candidate8->referrer = referrer->Clone();
candidate8->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate8));
// Process the candidates with the |PrefetchDocumentManager| for the current
// document.
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(
&GetPrimaryMainFrame());
prefetch_document_manager->ProcessCandidates(candidates);
// Check that the candidates that should be prefetched were sent to
// |PrefetchService|.
const auto& prefetch_urls = GetPrefetches();
ASSERT_EQ(prefetch_urls.size(), 6U);
EXPECT_EQ(prefetch_urls[0]->GetURL(), GetCrossOriginUrl("/candidate1.html"));
EXPECT_EQ(prefetch_urls[0]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/true,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_TRUE(
prefetch_urls[0]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
EXPECT_EQ(prefetch_urls[1]->GetURL(), GetCrossOriginUrl("/candidate2.html"));
EXPECT_EQ(prefetch_urls[1]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/false,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_TRUE(
prefetch_urls[1]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
EXPECT_EQ(prefetch_urls[2]->GetURL(), GetSameOriginUrl("/candidate3.html"));
EXPECT_EQ(prefetch_urls[2]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/false,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_FALSE(
prefetch_urls[2]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
EXPECT_EQ(prefetch_urls[3]->GetURL(), GetCrossOriginUrl("/candidate6.html"));
EXPECT_EQ(prefetch_urls[3]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/true,
blink::mojom::SpeculationEagerness::kConservative));
EXPECT_TRUE(
prefetch_urls[3]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
EXPECT_EQ(prefetch_urls[4]->GetURL(),
GetSameSiteCrossOriginUrl("/candidate7.html"));
EXPECT_EQ(prefetch_urls[4]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/false,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_FALSE(
prefetch_urls[4]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
EXPECT_EQ(prefetch_urls[5]->GetURL(), GetSameOriginUrl("/candidate8.html"));
EXPECT_EQ(prefetch_urls[5]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/true,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_FALSE(
prefetch_urls[5]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
// Check that the only remaining entries in candidates are those that
// shouldn't be prefetched by |PrefetchService|.
ASSERT_EQ(candidates.size(), 2U);
EXPECT_EQ(candidates[0]->url, GetCrossOriginUrl("/candidate4.html"));
EXPECT_EQ(candidates[1]->url, GetCrossOriginUrl("/candidate5.html"));
// Check IsPrefetchAttemptFailedOrDiscarded method
// Discarded candidate
EXPECT_TRUE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetCrossOriginUrl("/candidate4.html")));
// URLs that were not processed
EXPECT_TRUE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetSameOriginUrl("/random_page.html")));
// Prefetches with no status yet
EXPECT_FALSE(prefetch_urls[0]->HasPrefetchStatus());
EXPECT_FALSE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetCrossOriginUrl("/candidate1.html")));
// Prefetches with status
prefetch_urls[0]->SetPrefetchStatus(PrefetchStatus::kPrefetchSuccessful);
EXPECT_FALSE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetCrossOriginUrl("/candidate1.html")));
prefetch_urls[1]->SetPrefetchStatus(
PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps);
EXPECT_TRUE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetCrossOriginUrl("/candidate2.html")));
prefetch_urls[2]->SetPrefetchStatus(PrefetchStatus::kPrefetchFailedNetError);
EXPECT_TRUE(prefetch_document_manager->IsPrefetchAttemptFailedOrDiscarded(
GetCrossOriginUrl("/candidate3.html")));
}
// Link speculationrules prefetch is not started in fenced frame.
// `CanPrefetchNow()` check blocks speculationrules prefetch from fenced frames.
TEST_F(PrefetchDocumentManagerTest, FencedFrameDoesNotStartPrefetch) {
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
auto referrer = blink::mojom::Referrer::New();
referrer->url = GetSameOriginUrl("/referrer");
const GURL cross_origin_url = GetCrossOriginUrl("/candidate.html");
// Create candidate for private cross-origin prefetch. This candidate should
// be added to the queue of |PrefetchDocumentManager|. However, it will not be
// prefetched because it is from a fenced frame.
auto candidate = blink::mojom::SpeculationCandidate::New();
candidate->action = blink::mojom::SpeculationAction::kPrefetch;
candidate->requires_anonymous_client_ip_when_cross_origin = true;
candidate->url = cross_origin_url;
candidate->referrer = referrer->Clone();
candidate->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
candidates.push_back(std::move(candidate));
// Process the candidate with the |PrefetchDocumentManager| for the current
// document.
TestRenderFrameHost* fenced_frame_rfh =
static_cast<TestRenderFrameHost&>(GetPrimaryMainFrame())
.AppendFencedFrame();
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(fenced_frame_rfh);
prefetch_document_manager->ProcessCandidates(candidates);
// Check that the candidate was sent to |PrefetchService|.
const auto& prefetch_urls = GetPrefetches();
ASSERT_EQ(prefetch_urls.size(), 1U);
EXPECT_EQ(prefetch_urls[0]->GetURL(), cross_origin_url);
EXPECT_EQ(prefetch_urls[0]->GetPrefetchType(),
PrefetchType(PreloadingTriggerType::kSpeculationRule,
/*use_prefetch_proxy=*/true,
blink::mojom::SpeculationEagerness::kImmediate));
EXPECT_TRUE(
prefetch_urls[0]->IsIsolatedNetworkContextRequiredForCurrentPrefetch());
// `CanPrefetchNow()` blocks the speculationrules prefetch from fenced frame.
EXPECT_THAT(prefetch_document_manager->CanPrefetchNow(prefetch_urls[0].get()),
FieldsAre(false, IsNull()));
}
} // namespace
} // namespace content