blob: 6799f68432f7d7046e15d2d55aab0c485f996539 [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/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/web_package/web_bundle_builder.h"
#include "components/web_package/web_bundle_utils.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/web_package/web_bundle_utils.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.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/content_mock_cert_verifier.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
const char kUrnUuidURL[] = "urn:uuid:429fcc4e-0696-4bad-b099-ee9175f023ae";
const char kUrnUuidURL2[] = "urn:uuid:e219d992-b7f7-4da7-9722-481bc40cfda1";
const char kUuidInPackageURL[] =
"uuid-in-package:429fcc4e-0696-4bad-b099-ee9175f023ae";
const char kUuidInPackageURL2[] =
"uuid-in-package:e219d992-b7f7-4da7-9722-481bc40cfda1";
class TestBrowserClient : public ContentBrowserClient {
public:
TestBrowserClient() = default;
~TestBrowserClient() override = default;
bool HandleExternalProtocol(
const GURL& url,
base::RepeatingCallback<WebContents*()> web_contents_getter,
int child_id,
int frame_tree_node_id,
NavigationUIData* navigation_data,
bool is_main_frame,
network::mojom::WebSandboxFlags sandbox_flags,
ui::PageTransition page_transition,
bool has_user_gesture,
const absl::optional<url::Origin>& initiating_origin,
mojo::PendingRemote<network::mojom::URLLoaderFactory>* out_factory)
override {
EXPECT_FALSE(observed_url_.has_value());
observed_url_ = url;
return true;
}
GURL observed_url() const { return observed_url_ ? *observed_url_ : GURL(); }
private:
absl::optional<GURL> observed_url_;
};
class FinishNavigationObserver : public WebContentsObserver {
public:
FinishNavigationObserver() = default;
~FinishNavigationObserver() override = default;
explicit FinishNavigationObserver(WebContents* contents,
const GURL& expected_url,
base::OnceClosure done_closure)
: WebContentsObserver(contents),
expected_url_(expected_url),
done_closure_(std::move(done_closure)) {}
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
if (navigation_handle->GetURL() == expected_url_) {
error_code_ = navigation_handle->GetNetErrorCode();
std::move(done_closure_).Run();
}
}
const absl::optional<net::Error>& error_code() const { return error_code_; }
private:
GURL expected_url_;
base::OnceClosure done_closure_;
absl::optional<net::Error> error_code_;
};
int64_t GetTestDataFileSize(const base::FilePath::CharType* file_path) {
int64_t file_size;
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath test_data_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir));
CHECK(base::GetFileSize(test_data_dir.Append(base::FilePath(file_path)),
&file_size));
return file_size;
}
FrameTreeNode* GetFirstChild(WebContents* web_contents) {
return static_cast<WebContentsImpl*>(web_contents)
->GetPrimaryFrameTree()
.root()
->child_at(0);
}
enum class UuidScheme { UrnUuid, UuidInPackage };
enum class ElementType { Link, Script };
} // namespace
// Tests for both <script type=webbundle> and <link rel=webbundle>.
class WebBundleElementBrowserTest : public ContentBrowserTest,
public ::testing::WithParamInterface<
std::tuple<ElementType, UuidScheme>> {
public:
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
ElementType element_type;
UuidScheme uuid_scheme;
std::tie(element_type, uuid_scheme) = info.param;
return base::StringPrintf(
"%sElementWith%sScheme",
element_type == ElementType::Link ? "Link" : "Script",
uuid_scheme == UuidScheme::UrnUuid ? "UrnUuid" : "UuidInPackage");
}
protected:
WebBundleElementBrowserTest() {
feature_list_.InitAndEnableFeature(features::kSubresourceWebBundles);
}
~WebBundleElementBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
original_client_ = SetBrowserClientForTesting(&browser_client_);
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.RegisterRequestHandler(base::BindRepeating(
&WebBundleElementBrowserTest::HandleHugeWebBundleRequest,
base::Unretained(this)));
https_server_.RegisterRequestHandler(base::BindRepeating(
&WebBundleElementBrowserTest::HandleTestWebBundleRequest,
base::Unretained(this)));
https_server_.RegisterRequestMonitor(base::BindRepeating(
&WebBundleElementBrowserTest::MonitorResourceRequest,
base::Unretained(this)));
https_server_.RegisterRequestHandler(base::BindRepeating(
&WebBundleElementBrowserTest::InvalidResponseHandler,
base::Unretained(this)));
https_server_.AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server_.Start());
}
void TearDownOnMainThread() override {
ContentBrowserTest::TearDownOnMainThread();
SetBrowserClientForTesting(original_client_);
}
void SetUpInProcessBrowserTestFixture() override {
ContentBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
ContentBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
ElementType GetElementType() { return std::get<0>(GetParam()); }
UuidScheme GetUuidScheme() { return std::get<1>(GetParam()); }
std::string GetScriptForWebBundle(const char* web_bundle_url) {
if (GetElementType() == ElementType::Link) {
return base::StringPrintf(R"HTML(
{
const link = document.createElement('link');
link.rel = 'webbundle';
link.href = '%s';
link.onload = () => window.domAutomationController.send('loaded');
link.onerror = () => window.domAutomationController.send('failed');
document.body.appendChild(link);
}
)HTML",
web_bundle_url);
}
return base::StringPrintf(R"HTML(
{
const script = document.createElement('script');
script.type = 'webbundle';
script.textContent = JSON.stringify({"source": '%s'});
script.onload = () => window.domAutomationController.send('loaded');
script.onerror = () => window.domAutomationController.send('failed');
document.body.appendChild(script);
}
)HTML",
web_bundle_url);
}
const char* GetUuidURLPrefix() {
return GetUuidScheme() == UuidScheme::UrnUuid ? "urn:uuid:"
: "uuid-in-package:";
}
GURL GetUuidURL() {
return GetUuidScheme() == UuidScheme::UrnUuid ? GURL(kUrnUuidURL)
: GURL(kUuidInPackageURL);
}
GURL GetUuidURL2() {
return GetUuidScheme() == UuidScheme::UrnUuid ? GURL(kUrnUuidURL2)
: GURL(kUuidInPackageURL2);
}
const char* GetUuidTestBundlePath() {
return GetUuidScheme() == UuidScheme::UrnUuid
? "/web_bundle/urn-uuid.wbn"
: "/web_bundle/uuid-in-package.wbn";
}
const char* GetUuidTestPagePath() {
if (GetElementType() == ElementType::Script) {
return GetUuidScheme() == UuidScheme::UrnUuid
? "/web_bundle/script_web_bundle_urn_uuid.html"
: "/web_bundle/script_web_bundle_uuid_in_package.html";
}
return GetUuidScheme() == UuidScheme::UrnUuid
? "/web_bundle/link_web_bundle_urn_uuid.html"
: "/web_bundle/link_web_bundle_uuid_in_package.html";
}
void CreateIframeAndWaitForOnload(const std::string& url) {
DOMMessageQueue dom_message_queue(shell()->web_contents());
std::string message;
ExecuteScriptAsync(
shell(),
"let iframe = document.createElement('iframe');"
"iframe.src = '" +
url +
"';"
"iframe.onload = function() {"
" window.domAutomationController.send('iframe.onload');"
"};"
"document.body.appendChild(iframe);");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe.onload\"", message);
}
std::unique_ptr<net::test_server::HttpResponse> HandleHugeWebBundleRequest(
const net::test_server::HttpRequest& request) {
// Handler should return huge bundles only for "/web_bundle/huge.wbn" and
// "/web_bundle/huge2.wbn".
if (!(request.relative_url == "/web_bundle/huge.wbn" ||
request.relative_url == "/web_bundle/huge2.wbn"))
return nullptr;
GURL primary_url(https_server_.GetURL("/web_bundle/huge.txt"));
web_package::WebBundleBuilder builder(primary_url.spec(),
"" /* manifest_url */);
builder.AddExchange(
primary_url.spec(),
{{":status", "200"}, {"content-type", "text/plain"}},
// The body size should be greater than kDefaultMaxMemoryPerProcess / 2.
std::string(web_package::kDefaultMaxMemoryPerProcess / 2 + 1000, 'X'));
auto bundle = builder.CreateBundle();
std::string body(reinterpret_cast<const char*>(bundle.data()),
bundle.size());
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content(body);
http_response->set_content_type("application/webbundle");
http_response->AddCustomHeader("X-Content-Type-Options", "nosniff");
return http_response;
}
std::unique_ptr<net::test_server::HttpResponse> HandleTestWebBundleRequest(
const net::test_server::HttpRequest& request) {
if (request.relative_url != "/web_bundle/test.wbn")
return nullptr;
GURL test1_url(https_server_.GetURL("/web_bundle/test1.txt"));
GURL test2_url(https_server_.GetURL("/web_bundle/test2.txt"));
web_package::WebBundleBuilder builder("" /* fallback_url */,
"" /* manifest_url */);
builder.AddExchange(test1_url.spec(),
{{":status", "200"}, {"content-type", "text/plain"}},
"test1");
builder.AddExchange(test2_url.spec(),
{{":status", "200"}, {"content-type", "text/plain"}},
"test2");
auto bundle = builder.CreateBundle();
std::string body(reinterpret_cast<const char*>(bundle.data()),
bundle.size());
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content(body);
http_response->set_content_type("application/webbundle");
http_response->AddCustomHeader("X-Content-Type-Options", "nosniff");
return http_response;
}
std::unique_ptr<net::test_server::HttpResponse> InvalidResponseHandler(
const net::test_server::HttpRequest& request) {
if (request.relative_url != "/web_bundle/invalid-response")
return nullptr;
return std::make_unique<net::test_server::RawHttpResponse>(
"", "Not a valid HTTP response.");
}
GURL GetObservedUnknownSchemeUrl() { return browser_client_.observed_url(); }
net::EmbeddedTestServer* https_server() { return &https_server_; }
void MonitorResourceRequest(const net::test_server::HttpRequest& request) {
// This should be called on `EmbeddedTestServer::io_thread_`.
EXPECT_FALSE(
content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::AutoLock auto_lock(lock_);
request_count_by_path_[request.GetURL().PathForRequest()]++;
}
int GetRequestCount(const GURL& url) {
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
base::AutoLock auto_lock(lock_);
return request_count_by_path_[url.PathForRequest()];
}
private:
content::ContentMockCertVerifier mock_cert_verifier_;
raw_ptr<ContentBrowserClient> original_client_ = nullptr;
TestBrowserClient browser_client_;
base::test::ScopedFeatureList feature_list_;
net::EmbeddedTestServer https_server_{
net::EmbeddedTestServer::Type::TYPE_HTTPS};
// Counts of requests sent to the server. Keyed by path (not by full URL)
std::map<std::string, int> request_count_by_path_ GUARDED_BY(lock_);
base::Lock lock_;
};
#if defined(OS_MAC)
// TODO(https://crbug.com/1263334): Flakes on macOS.
#define MAYBE_ChangeLinkElementHref DISABLED_ChangeLinkElementHref
#else
#define MAYBE_ChangeLinkElementHref ChangeLinkElementHref
#endif // defined(OS_MAC)
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest,
MAYBE_ChangeLinkElementHref) {
// This test is only for the <link> element.
if (GetElementType() == ElementType::Script)
return;
GURL url(https_server()->GetURL("/web_bundle/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// We load a huge.wbn and change the reference to another bundle url. If we
// don't release webbundle resources immediately when a link element's
// reference is changed, the load should fail because of a memory quota in
// the network process. See https://crbug.com/1249338.
DOMMessageQueue dom_message_queue(shell()->web_contents());
ExecuteScriptAsync(shell(),
R"HTML(
(async () => {
const link = await addLinkAndWaitForLoad("/web_bundle/huge.wbn", [
// resources are dummy. The test shouldn't depends on this value.
"http://example.com/web-bundle/huge.txt",
]);
await changeLinkHrefAndWaitForLoad(link, "/web_bundle/huge2.wbn")
window.domAutomationController.send('webbundle loaded after change');
})();
function addLinkAndWaitForLoad(url, resources) {
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.rel = "webbundle";
link.href = url;
for (const resource of resources) {
link.resources.add(resource);
}
link.onload = () => resolve(link);
link.onerror = () => reject(link);
document.body.appendChild(link);
});
}
function changeLinkHrefAndWaitForLoad(link, url) {
return new Promise((resolve, reject) => {
link.href = url;
link.onload = () => resolve(link);
link.onerror = () => reject(link);
});
}
)HTML");
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"webbundle loaded after change\"", message);
}
#if defined(OS_MAC) || defined(OS_WIN)
// TODO(https://crbug.com/1263334): Flakes on macOS and Windows.
#define MAYBE_RemoveLinkElement DISABLED_RemoveLinkElement
#else
#define MAYBE_RemoveLinkElement RemoveLinkElement
#endif // defined(OS_MAC)
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, MAYBE_RemoveLinkElement) {
// This test is only for the <link> element.
if (GetElementType() == ElementType::Script)
return;
GURL url(https_server()->GetURL("/web_bundle/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// We load a huge.wbn twice, by adding and removing a link element. If we
// don't release webbundle resources immediately when a link element is
// removed, the 2nd load should fail because of a memory quota in the network
// process. See https://crbug.com/1211659.
DOMMessageQueue dom_message_queue(shell()->web_contents());
ExecuteScriptAsync(shell(),
R"HTML(
(async () => {
for (let i = 0; i < 2; ++i) {
const link = await addLinkAndWaitForLoad("/web_bundle/huge.wbn", [
// resources are dummy. The test shouldn't depends on this value.
"http://example.com/web-bundle/huge.txt",
]);
link.remove();
}
window.domAutomationController.send('webbundle loaded');
})();
function addLinkAndWaitForLoad(url, resources) {
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.rel = "webbundle";
link.href = url;
for (const resource of resources) {
link.resources.add(resource);
}
link.onload = () => resolve(link);
link.onerror = () => reject(link);
document.body.appendChild(link);
});
}
)HTML");
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"webbundle loaded\"", message);
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest,
WebBundleResourceShouldBeReused) {
// The tentative spec:
// https://docs.google.com/document/d/1GEJ3wTERGEeTG_4J0QtAwaNXhPTza0tedd00A7vPVsw/edit
// Tests that webbundle resources are surely re-used when we remove a <script
// type=webbunble> and add a new <script type=webbundle> with the same bundle
// URL to the removed one, in the same microtask scope.
// Skip if the test's ElementType parameter is <link>.
if (GetElementType() == ElementType::Link)
return;
GURL url(https_server()->GetURL("/web_bundle/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
{
// Add a <script type=webbundle>.
DOMMessageQueue dom_message_queue(shell()->web_contents());
ExecuteScriptAsync(shell(),
R"HTML(
const script = document.createElement("script");
script.type = "webbundle";
script.textContent =
JSON.stringify({"source": "/web_bundle/test.wbn",
"resources": ["/web_bundle/test1.txt"]});
document.body.appendChild(script);
(async () => {
const response = await fetch("/web_bundle/test1.txt");
const text = await response.text();
window.domAutomationController.send(`fetch: ${text}`);
})();
)HTML");
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ(message, "\"fetch: test1\"");
EXPECT_EQ(GetRequestCount(https_server()->GetURL("/web_bundle/test.wbn")),
1);
}
{
// Remove the <script type=webbundle> from the document, and then add a new
// <script type=webbundle> whose bundle URL is same to the removed one in
// the same microtask scope, The added element should re-use the webbundle
// resource which the old <script type=webbundle> has been using. Thus, the
// bundle shouldn't be fetched twice.
DOMMessageQueue dom_message_queue(shell()->web_contents());
ExecuteScriptAsync(shell(),
R"HTML(
script.remove();
const script2 = document.createElement("script");
script2.type = "webbundle";
script2.textContent =
JSON.stringify({"source": "/web_bundle/test.wbn",
"resources": ["/web_bundle/test2.txt"]});
document.body.appendChild(script2);
(async () => {
const response = await fetch("/web_bundle/test2.txt");
const text = await response.text();
window.domAutomationController.send(`fetch: ${text}`);
})();
)HTML");
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ(message, "\"fetch: test2\"")
<< "A new script element's rule should be effective.";
EXPECT_EQ(GetRequestCount(https_server()->GetURL("/web_bundle/test.wbn")),
1)
<< "A bundle should not be fetched twice.";
}
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, SubframeLoad) {
base::HistogramTester histogram_tester;
GURL url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Create an iframe with a uuid-in-package resource in a bundle.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("let iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
// Check the metrics recorded in the network process.
FetchHistogramsFromChildProcesses();
int64_t web_bundle_size = GetTestDataFileSize(
GetUuidScheme() == UuidScheme::UrnUuid
? FILE_PATH_LITERAL("content/test/data/web_bundle/urn-uuid.wbn")
: FILE_PATH_LITERAL(
"content/test/data/web_bundle/uuid-in-package.wbn"));
histogram_tester.ExpectUniqueSample("SubresourceWebBundles.ReceivedSize",
web_bundle_size, 1);
histogram_tester.ExpectUniqueSample("SubresourceWebBundles.ContentLength",
web_bundle_size, 1);
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, SubframeLoadError) {
GURL url(https_server()->GetURL("/web_bundle/invalid_web_bundle.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Attempt to create an iframe with a resource in a broken WebBundle.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("let iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
*finish_navigation_observer.error_code());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, BundleFetchError) {
base::HistogramTester histogram_tester;
GURL url(https_server()->GetURL("/web_bundle/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// "/web_bundle/invalid-response" returns an invalid HTTP response which
// causes ERR_INVALID_HTTP_RESPONSE network error.
DOMMessageQueue dom_message_queue(shell()->web_contents());
ExecuteScriptAsync(shell(),
GetScriptForWebBundle("/web_bundle/invalid-response"));
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"failed\"", message);
// Check the metrics recorded in the network process.
FetchHistogramsFromChildProcesses();
histogram_tester.ExpectUniqueSample(
"SubresourceWebBundles.BundleFetchErrorCode",
-net::ERR_INVALID_HTTP_RESPONSE, 1);
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest,
BundleRedirectionIsForbidden) {
GURL url(https_server()->GetURL("/web_bundle/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"* URL redirection of Subresource Web Bundles is currently not "
"supported.");
DOMMessageQueue dom_message_queue(shell()->web_contents());
const std::pair<const char*, const char*> test_cases[] = {
{"/web_bundle/urn-uuid.wbn", "loaded"},
{"/server-redirect?/web_bundle/urn-uuid.wbn", "failed"}};
for (const auto& pair : test_cases) {
const char* url = pair.first;
std::string expected_message = pair.second;
ExecuteScriptAsync(shell(), GetScriptForWebBundle(url));
std::string message;
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"" + expected_message + "\"", message);
if (expected_message == "failed")
console_observer.Wait();
}
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, FollowLink) {
GURL url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Clicking a link to a uuid-in-package resource in a bundle should not be
// loaded from the bundle.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"document.getElementById('link').click();"));
run_loop.Run();
EXPECT_EQ(net::ERR_ABORTED, *finish_navigation_observer.error_code());
EXPECT_EQ(GetUuidURL(), GetObservedUnknownSchemeUrl());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, IframeChangeSource) {
GURL main_url(https_server()->GetURL("/simple_page.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Create an iframe whose document has <link rel="webbundle">.
// Or <script type="webbundle">, depending on the test param.
CreateIframeAndWaitForOnload(GetUuidTestPagePath());
// Attempt to navigate the iframe to a bundled resource.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(shell(), base::StringPrintf("iframe.src = '%s';",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::ERR_ABORTED, *finish_navigation_observer.error_code());
EXPECT_EQ(GetUuidURL(), GetObservedUnknownSchemeUrl());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, IframeFollowLink) {
GURL main_url(https_server()->GetURL("/simple_page.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Create an iframe whose document has <link rel="webbundle">.
// Or <script type="webbundle">, depending on the test param.
CreateIframeAndWaitForOnload(GetUuidTestPagePath());
// Click a link inside the iframe. The resource should not be loaded from
// the bundle.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(shell(),
"iframe.contentDocument.getElementById('link').click();");
run_loop.Run();
EXPECT_EQ(net::ERR_ABORTED, *finish_navigation_observer.error_code());
EXPECT_EQ(GetUuidURL(), GetObservedUnknownSchemeUrl());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest,
NavigationFromSiblingFrame) {
GURL main_url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Create an iframe and wait for the initial load.
DOMMessageQueue dom_message_queue(shell()->web_contents());
std::string message;
ExecuteScriptAsync(shell(),
"let iframe1 = document.createElement('iframe');"
"iframe1.name = 'iframe1';"
"iframe1.src = '/simple_page.html';"
"iframe1.onload = function() {"
" window.domAutomationController.send('iframe1.onload');"
"};"
"document.body.appendChild(iframe1);");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe1.onload\"", message);
// Create another iframe and wait for the initial load.
ExecuteScriptAsync(shell(),
"let iframe2 = document.createElement('iframe');"
"iframe2.name = 'iframe2';"
"iframe2.src = '/simple_page.html';"
"iframe2.onload = function() {"
" window.domAutomationController.send('iframe2.onload');"
"};"
"document.body.appendChild(iframe2);");
EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
EXPECT_EQ("\"iframe2.onload\"", message);
// Navigate iframe2 to a uuid-in-package URL by clicking a link in iframe1,
// which should not be loaded from the WebBundle associated with the parent
// document.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("let a = iframe1.contentDocument.createElement('a');"
"a.href = '%s';"
"a.target = 'iframe2';"
"iframe1.contentDocument.body.appendChild(a);"
"a.click();",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::ERR_ABORTED, *finish_navigation_observer.error_code());
EXPECT_EQ(GetUuidURL(), GetObservedUnknownSchemeUrl());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest,
GrandChildShouldNotBeLoadedFromBundle) {
GURL main_url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Create an iframe with a uuid-in-package resource, which has a nested iframe
// with another uuid-in-package resource in the bundle. The resource for the
// nested should iframe not be loaded from the bundle.
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(), base::StringPrintf(
"let iframe = document.createElement('iframe');"
"iframe.src = '%s1084e1fc-2122-4155-a4dd-28efb2e8ccb1';"
"document.body.appendChild(iframe);",
GetUuidURLPrefix()));
run_loop.Run();
EXPECT_EQ(net::ERR_ABORTED, *finish_navigation_observer.error_code());
EXPECT_EQ(GetUuidURL(), GetObservedUnknownSchemeUrl());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, NetworkIsolationKey) {
GURL bundle_url(
https_server()->GetURL("bundle.test", GetUuidTestBundlePath()));
GURL page_url(https_server()->GetURL(
"page.test", "/web_bundle/frame_parent.html?wbn=" + bundle_url.spec() +
"&frame=" + GetUuidURL().spec().c_str()));
EXPECT_TRUE(NavigateToURL(shell(), page_url));
std::u16string expected_title(u"OK");
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
RenderFrameHost* main_frame = shell()->web_contents()->GetMainFrame();
RenderFrameHost* urn_frame = ChildFrameAt(main_frame, 0);
EXPECT_EQ("https://page.test https://bundle.test",
urn_frame->GetNetworkIsolationKey().ToString());
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, ReloadSubframe) {
GURL url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Create an iframe with a uuid-in-package resource in a bundle.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("let iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
}
FrameTreeNode* iframe_node = GetFirstChild(shell()->web_contents());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL());
// Reload the iframe.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
iframe_node->current_frame_host()->Reload();
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
}
}
IN_PROC_BROWSER_TEST_P(WebBundleElementBrowserTest, SubframeHistoryNavigation) {
GURL url(https_server()->GetURL(GetUuidTestPagePath()));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Create an iframe with a uuid-in-package resource in a bundle.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("let iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
GetUuidURL().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
}
FrameTreeNode* iframe_node = GetFirstChild(shell()->web_contents());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL());
// Navigate the iframe to a page outside the bundle.
{
GURL another_page_url(https_server()->GetURL("/simple_page.html"));
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), another_page_url, run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node,
base::StringPrintf("location.href = '%s';",
another_page_url.spec().c_str())));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), another_page_url);
}
// Back navigate the iframe to the uuid-in-package resource in the bundle.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "history.back()"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL());
}
// Navigate the iframe to another uuid-in-package resource in the bundle, by
// changing iframe.src attribute.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL2(), run_loop.QuitClosure());
ExecuteScriptAsync(
shell(),
base::StringPrintf("iframe.src = '%s';", GetUuidURL2().spec().c_str()));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL2());
}
// Back navigate the iframe to the first uuid-in-package resource.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL(), run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "history.back()"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL());
}
// Forward navigate the iframe to the second uuid-in-package resource.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL2(), run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "history.forward()"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL2());
}
GURL url_with_hash(GetUuidURL2().spec() + "#hash");
// Same document navigation.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), url_with_hash, run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "location.href = '#hash';"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), url_with_hash);
}
// Back from same document navigation.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), GetUuidURL2(), run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "history.back()"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), GetUuidURL2());
}
// Forward navigate to #hash.
{
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(
shell()->web_contents(), url_with_hash, run_loop.QuitClosure());
EXPECT_TRUE(ExecJs(iframe_node, "history.forward()"));
run_loop.Run();
EXPECT_EQ(net::OK, *finish_navigation_observer.error_code());
EXPECT_EQ(iframe_node->current_url(), url_with_hash);
}
}
INSTANTIATE_TEST_SUITE_P(
WebBundleElementBrowserTest,
WebBundleElementBrowserTest,
testing::Combine(testing::Values(ElementType::Link, ElementType::Script),
testing::Values(UuidScheme::UrnUuid,
UuidScheme::UuidInPackage)),
WebBundleElementBrowserTest::DescribeParams);
} // namespace content