blob: c4a1fafb0a841d20631f7d72546d19d27cc8d98f [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/loader/navigation_early_hints_manager.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/cert_test_util.h"
#include "net/test/quic_simple_test_server.h"
#include "net/test/test_data_directory.h"
namespace content {
using PreloadedResources = NavigationEarlyHintsManager::PreloadedResources;
namespace {
struct HeaderField {
HeaderField(const std::string& name, const std::string& value)
: name(name), value(value) {}
std::string name;
std::string value;
};
struct ResponseEntry {
ResponseEntry(const std::string& path, net::HttpStatusCode status_code)
: path(path) {
headers[":path"] = path;
headers[":status"] = base::StringPrintf("%d", status_code);
}
void AddEarlyHints(const std::vector<HeaderField>& headers) {
spdy::Http2HeaderBlock hints_headers;
for (const auto& header : headers)
hints_headers.AppendValueOrAddHeader(header.name, header.value);
early_hints.push_back(std::move(hints_headers));
}
std::string path;
spdy::Http2HeaderBlock headers;
std::string body;
std::vector<spdy::Http2HeaderBlock> early_hints;
};
const char kPageWithHintedScriptPath[] = "/page_with_hinted_js.html";
const char kPageWithHintedScriptBody[] = "<script src=\"/hinted.js\"></script>";
const char kHintedScriptPath[] = "/hinted.js";
const char kHintedScriptBody[] = "document.title = 'Done';";
const char kHintedStylesheetPath[] = "/hinted.css";
const char kHintedStylesheetBody[] = "/*empty*/";
} // namespace
// Most tests use EmbeddedTestServer but this uses QuicSimpleTestServer because
// Early Hints are only plumbed over HTTP/2 or HTTP/3 (QUIC).
class NavigationEarlyHintsTest : public ContentBrowserTest {
public:
NavigationEarlyHintsTest() = default;
~NavigationEarlyHintsTest() override = default;
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ConfigureMockCertVerifier();
ASSERT_TRUE(net::QuicSimpleTestServer::Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kOriginToForceQuicOn, "*");
mock_cert_verifier_.SetUpCommandLine(command_line);
feature_list_.InitAndEnableFeature(
features::kEarlyHintsPreloadForNavigation);
ContentBrowserTest::SetUpCommandLine(command_line);
}
void TearDown() override {
base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait;
net::QuicSimpleTestServer::Shutdown();
ContentBrowserTest::TearDown();
}
protected:
void SetUpInProcessBrowserTestFixture() override {
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void ConfigureMockCertVerifier() {
auto test_cert =
net::ImportCertFromFile(net::GetTestCertsDirectory(), "quic-chain.pem");
net::CertVerifyResult verify_result;
verify_result.verified_cert = test_cert;
mock_cert_verifier_.mock_cert_verifier()->AddResultForCert(
test_cert, verify_result, net::OK);
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
}
HeaderField CreatePreloadLinkForScript() {
return HeaderField(
"link",
base::StringPrintf("<%s>; rel=preload; as=script", kHintedScriptPath));
}
HeaderField CreatePreloadLinkForStylesheet() {
return HeaderField("link",
base::StringPrintf("<%s>; rel=preload; as=stylesheet",
kHintedStylesheetPath));
}
void RegisterResponse(const ResponseEntry& entry) {
net::QuicSimpleTestServer::AddResponseWithEarlyHints(
entry.path, entry.headers, entry.body, entry.early_hints);
}
void RegisterHintedScriptResource() {
ResponseEntry hinted_script_entry(kHintedScriptPath, net::HTTP_OK);
hinted_script_entry.headers["content-type"] = "application/javascript";
hinted_script_entry.body = kHintedScriptBody;
RegisterResponse(hinted_script_entry);
}
void RegisterHintedStylesheetResource() {
ResponseEntry hinted_script_entry(kHintedStylesheetPath, net::HTTP_OK);
hinted_script_entry.headers["content-type"] = "text/css";
hinted_script_entry.body = kHintedStylesheetBody;
RegisterResponse(hinted_script_entry);
}
ResponseEntry CreatePageEntryWithHintedScript(
net::HttpStatusCode status_code) {
RegisterHintedScriptResource();
ResponseEntry entry(kPageWithHintedScriptPath, status_code);
entry.body = kPageWithHintedScriptBody;
HeaderField link_header = CreatePreloadLinkForScript();
entry.AddEarlyHints({std::move(link_header)});
return entry;
}
bool NavigateToURLAndWaitTitle(const GURL& url, const std::string& title) {
std::u16string title16 = base::ASCIIToUTF16(title);
TitleWatcher title_watcher(shell()->web_contents(), title16);
if (!NavigateToURL(shell(), url))
return false;
return title16 == title_watcher.WaitAndGetTitle();
}
NavigationEarlyHintsManager* GetEarlyHintsManager() {
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetMainFrame());
return rfh->early_hints_manager();
}
PreloadedResources WaitForPreloadedResources() {
base::RunLoop loop;
PreloadedResources result;
if (!GetEarlyHintsManager())
return result;
GetEarlyHintsManager()->WaitForPreloadsFinishedForTesting(
base::BindLambdaForTesting([&](PreloadedResources preloaded_resources) {
result = preloaded_resources;
loop.Quit();
}));
loop.Run();
return result;
}
private:
base::test::ScopedFeatureList feature_list_;
ContentMockCertVerifier mock_cert_verifier_;
};
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, Basic) {
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NavigationServerError) {
ResponseEntry entry =
CreatePageEntryWithHintedScript(net::HTTP_INTERNAL_SERVER_ERROR);
entry.body = "Internal Server Error";
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithHintedScriptPath)));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_TRUE(preloads.empty());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, Redirect) {
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND);
entry.headers["location"] = "/";
entry.body = "";
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
net::QuicSimpleTestServer::GetFileURL("/")));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_TRUE(preloads.empty());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidPreloadLink) {
const std::string kPath = "/hinted.html";
RegisterHintedScriptResource();
ResponseEntry entry(kPath, net::HTTP_OK);
entry.body = "body";
entry.AddEarlyHints(
{HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=invalid",
kHintedScriptPath))});
RegisterResponse(entry);
EXPECT_TRUE(
NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath)));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_TRUE(preloads.empty());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DuplicatePreloads) {
RegisterHintedScriptResource();
RegisterHintedStylesheetResource();
ResponseEntry entry(kPageWithHintedScriptPath, net::HTTP_OK);
entry.body = kPageWithHintedScriptBody;
// Set two Early Hints responses which contain duplicate preload link headers.
HeaderField script_link_header = CreatePreloadLinkForScript();
HeaderField stylesheet_link_header = CreatePreloadLinkForStylesheet();
entry.AddEarlyHints({script_link_header});
entry.AddEarlyHints({script_link_header, stylesheet_link_header});
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 2UL);
GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
GURL stylesheet_url =
net::QuicSimpleTestServer::GetFileURL(kHintedStylesheetPath);
EXPECT_TRUE(preloads.contains(script_url));
EXPECT_TRUE(preloads.contains(stylesheet_url));
}
} // namespace content