blob: 15bc94385a16b036cfef5fe98535d73f4a25beba [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cmath>
#include <map>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <tuple>
#include <vector>
#include "base/barrier_closure.h"
#include "base/functional/overloaded.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/statistics_recorder.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_future.h"
#include "base/test/with_feature_override.h"
#include "base/time/time.h"
#include "content/browser/private_aggregation/private_aggregation_manager_impl.h"
#include "content/browser/private_aggregation/private_aggregation_test_utils.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/shared_storage/shared_storage_document_service_impl.h"
#include "content/browser/shared_storage/shared_storage_event_params.h"
#include "content/browser/shared_storage/shared_storage_header_observer.h"
#include "content/browser/shared_storage/shared_storage_worklet_driver.h"
#include "content/browser/shared_storage/shared_storage_worklet_host.h"
#include "content/browser/shared_storage/shared_storage_worklet_host_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.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/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/fenced_frame_test_util.h"
#include "content/public/test/shared_storage_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_select_url_fenced_frame_config_observer.h"
#include "content/public/test/test_shared_storage_header_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/fenced_frame_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
#include "third_party/blink/public/common/shared_storage/shared_storage_utils.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
namespace content {
using testing::Pair;
using testing::UnorderedElementsAre;
using SharedStorageReportingMap = base::flat_map<std::string, ::GURL>;
using SharedStorageUrlSpecWithMetadata =
SharedStorageEventParams::SharedStorageUrlSpecWithMetadata;
namespace {
using WorkletHosts = SharedStorageWorkletHostManager::WorkletHosts;
constexpr char kSharedStorageWorkletExpiredMessage[] =
"The sharedStorage worklet cannot execute further operations because the "
"previous operation did not include the option \'keepAlive: true\'.";
constexpr auto& SetOperation =
SharedStorageWriteOperationAndResult::SetOperation;
constexpr auto& AppendOperation =
SharedStorageWriteOperationAndResult::AppendOperation;
constexpr auto& DeleteOperation =
SharedStorageWriteOperationAndResult::DeleteOperation;
constexpr auto& ClearOperation =
SharedStorageWriteOperationAndResult::ClearOperation;
constexpr char kSimplePagePath[] = "/simple_page.html";
constexpr char kTitle1Path[] = "/title1.html";
constexpr char kTitle2Path[] = "/title2.html";
constexpr char kTitle3Path[] = "/title3.html";
constexpr char kTitle4Path[] = "/title4.html";
constexpr char kFencedFramePath[] = "/fenced_frames/title0.html";
constexpr char kPageWithBlankIframePath[] = "/page_with_blank_iframe.html";
constexpr char kPngPath[] = "/shared_storage/pixel.png";
constexpr char kDestroyedStatusHistogram[] =
"Storage.SharedStorage.Worklet.DestroyedStatus";
constexpr char kTimingKeepAliveDurationHistogram[] =
"Storage.SharedStorage.Worklet.Timing."
"KeepAliveEndedDueToOperationsFinished.KeepAliveDuration";
constexpr char kErrorTypeHistogram[] =
"Storage.SharedStorage.Worklet.Error.Type";
constexpr char kTimingUsefulResourceHistogram[] =
"Storage.SharedStorage.Worklet.Timing.UsefulResourceDuration";
constexpr char kTimingRunExecutedInWorkletHistogram[] =
"Storage.SharedStorage.Document.Timing.Run.ExecutedInWorklet";
constexpr char kTimingSelectUrlExecutedInWorkletHistogram[] =
"Storage.SharedStorage.Document.Timing.SelectURL.ExecutedInWorklet";
constexpr char kSelectUrlBudgetStatusHistogram[] =
"Storage.SharedStorage.Worklet.SelectURL.BudgetStatus";
constexpr double kBudgetAllowed = 5.0;
constexpr int kStalenessThresholdDays = 1;
constexpr int kSelectURLOverallBitBudget = 12;
constexpr int kSelectURLSiteBitBudget = 6;
constexpr char kGenerateURLsListScript[] = R"(
function generateUrls(size) {
return new Array(size).fill(0).map((e, i) => {
return {
url: '/fenced_frames/title' + i.toString() + '.html',
reportingMetadata: {
'click': '/fenced_frames/report' + i.toString() + '.html',
'mouse interaction':
'/fenced_frames/report' + (i + 1).toString() + '.html'
}
}
});
}
)";
constexpr char kRemainingBudgetPrefix[] = "remaining budget: ";
constexpr char kEmptyAccessControlAllowOriginReplacement[] = "";
constexpr char kEmptySharedStorageCrossOriginAllowedReplacement[] = "";
std::string TimeDeltaToString(base::TimeDelta delta) {
return base::StrCat({base::NumberToString(delta.InMilliseconds()), "ms"});
}
using MockPrivateAggregationShellContentBrowserClient =
MockPrivateAggregationContentBrowserClientBase<
ContentBrowserTestContentBrowserClient>;
void WaitForHistogram(const std::string& histogram_name) {
// Continue if histogram was already recorded.
if (base::StatisticsRecorder::FindHistogram(histogram_name))
return;
// Else, wait until the histogram is recorded.
base::RunLoop run_loop;
auto histogram_observer =
std::make_unique<base::StatisticsRecorder::ScopedHistogramSampleObserver>(
histogram_name,
base::BindLambdaForTesting(
[&](const char* histogram_name, uint64_t name_hash,
base::HistogramBase::Sample sample) { run_loop.Quit(); }));
run_loop.Run();
}
void WaitForHistograms(const std::vector<std::string>& histogram_names) {
for (const auto& name : histogram_names)
WaitForHistogram(name);
}
std::string SerializeOptionalString(std::optional<std::string> str) {
if (str)
return *str;
return "std::nullopt";
}
std::string SerializeOptionalBool(std::optional<bool> b) {
if (b)
return (*b) ? "true" : "false";
return "std::nullopt";
}
std::string SerializeOptionalUrlsWithMetadata(
std::optional<std::vector<SharedStorageUrlSpecWithMetadata>>
urls_with_metadata) {
if (!urls_with_metadata)
return "std::nullopt";
std::vector<std::string> urls_str_vector = {"{ "};
for (const auto& url_with_metadata : *urls_with_metadata) {
urls_str_vector.push_back("{url: ");
urls_str_vector.push_back(url_with_metadata.url);
urls_str_vector.push_back(", reporting_metadata: { ");
for (const auto& metadata_pair : url_with_metadata.reporting_metadata) {
urls_str_vector.push_back("{");
urls_str_vector.push_back(metadata_pair.first);
urls_str_vector.push_back(" : ");
urls_str_vector.push_back(metadata_pair.second);
urls_str_vector.push_back("} ");
}
urls_str_vector.push_back("}} ");
}
urls_str_vector.push_back("}");
return base::StrCat(urls_str_vector);
}
bool IsErrorMessage(const content::WebContentsConsoleObserver::Message& msg) {
return msg.log_level == blink::mojom::ConsoleMessageLevel::kError;
}
auto describe_param = [](const auto& info) {
return base::StrCat({"ResolveSelectURLTo", info.param ? "Config" : "URN"});
};
} // namespace
class TestSharedStorageWorkletHost : public SharedStorageWorkletHost {
public:
TestSharedStorageWorkletHost(
SharedStorageDocumentServiceImpl& document_service,
const url::Origin& frame_origin,
const GURL& script_source_url,
network::mojom::CredentialsMode credentials_mode,
const std::vector<blink::mojom::OriginTrialFeature>&
origin_trial_features,
mojo::PendingAssociatedReceiver<blink::mojom::SharedStorageWorkletHost>
worklet_host,
blink::mojom::SharedStorageDocumentService::CreateWorkletCallback
callback,
bool should_defer_worklet_messages)
: SharedStorageWorkletHost(document_service,
frame_origin,
script_source_url,
credentials_mode,
origin_trial_features,
std::move(worklet_host),
std::move(callback)),
should_defer_worklet_messages_(should_defer_worklet_messages) {}
~TestSharedStorageWorkletHost() override = default;
// Separate from `WaitForWorkletResponses()` so that we can wait for it
// without having to set an expected response count beforehand. The worklet
// host won't exist before the first call to either `addModule(), `run()`, or
// `selectURL()`. In the correct flow, `addModule()` will be called first.
void WaitForAddModule() {
if (add_module_called_) {
ResetAddModuleCalledAndMaybeCloseWorklet();
return;
}
add_module_waiter_ = std::make_unique<base::RunLoop>();
add_module_waiter_->Run();
add_module_waiter_.reset();
ResetAddModuleCalledAndMaybeCloseWorklet();
}
// Only applies to `run()` and `selectURL()`. Must be set before calling the
// operation. Precondition: Either `addModule()`, `run()`, or `selectURL()`
// has previously been called so that this worklet host exists.
void SetExpectedWorkletResponsesCount(size_t count) {
expected_worklet_responses_count_ = count;
response_expectation_set_ = true;
}
// Only applies to `run()` and `selectURL()`.
// Precondition: `SetExpectedWorkletResponsesCount()` has been called with the
// desired expected `count`, followed by the operation(s) itself/themselves.
void WaitForWorkletResponses() {
if (worklet_responses_count_ >= expected_worklet_responses_count_) {
ResetResponseCountsAndMaybeCloseWorklet();
return;
}
worklet_responses_count_waiter_ = std::make_unique<base::RunLoop>();
worklet_responses_count_waiter_->Run();
worklet_responses_count_waiter_.reset();
ResetResponseCountsAndMaybeCloseWorklet();
}
void set_should_defer_worklet_messages(bool should_defer_worklet_messages) {
should_defer_worklet_messages_ = should_defer_worklet_messages;
}
const std::vector<base::OnceClosure>& pending_worklet_messages() {
return pending_worklet_messages_;
}
void DidAddMessageToConsole(blink::mojom::ConsoleMessageLevel level,
const std::string& message) override {
DidAddMessageToConsoleHelper(level, message, /*initial_message=*/true);
}
void DidAddMessageToConsoleHelper(blink::mojom::ConsoleMessageLevel level,
const std::string& message,
bool initial_message) {
if (should_defer_worklet_messages_ && initial_message) {
pending_worklet_messages_.push_back(base::BindOnce(
&TestSharedStorageWorkletHost::DidAddMessageToConsoleHelper,
weak_ptr_factory_.GetWeakPtr(), level, message,
/*initial_message=*/false));
return;
}
SharedStorageWorkletHost::DidAddMessageToConsole(level, message);
}
void FireKeepAliveTimerNow() {
ASSERT_TRUE(GetKeepAliveTimerForTesting().IsRunning());
GetKeepAliveTimerForTesting().FireNow();
}
void ExecutePendingWorkletMessages() {
for (auto& callback : pending_worklet_messages_) {
std::move(callback).Run();
}
}
private:
void OnAddModuleOnWorkletFinished(
blink::mojom::SharedStorageDocumentService::CreateWorkletCallback
callback,
bool success,
const std::string& error_message) override {
OnAddModuleOnWorkletFinishedHelper(std::move(callback), success,
error_message,
/*initial_message=*/true);
}
void OnAddModuleOnWorkletFinishedHelper(
blink::mojom::SharedStorageDocumentService::CreateWorkletCallback
callback,
bool success,
const std::string& error_message,
bool initial_message) {
bool in_keep_alive = IsInKeepAlivePhase();
if (should_defer_worklet_messages_ && initial_message) {
pending_worklet_messages_.push_back(base::BindOnce(
&TestSharedStorageWorkletHost::OnAddModuleOnWorkletFinishedHelper,
weak_ptr_factory_.GetWeakPtr(), std::move(callback), success,
error_message, /*initial_message=*/false));
} else {
SharedStorageWorkletHost::OnAddModuleOnWorkletFinished(
std::move(callback), success, error_message);
}
if (initial_message)
OnAddModuleResponseReceived();
if (!in_keep_alive) {
ProcessAddModuleExpirationIfWorkletExpired();
}
}
void OnRunOperationOnWorkletFinished(
base::TimeTicks start_time,
bool success,
const std::string& error_message) override {
OnRunOperationOnWorkletFinishedHelper(start_time, success, error_message,
/*initial_message=*/true);
}
void OnRunOperationOnWorkletFinishedHelper(base::TimeTicks start_time,
bool success,
const std::string& error_message,
bool initial_message) {
bool in_keep_alive = IsInKeepAlivePhase();
if (should_defer_worklet_messages_ && initial_message) {
pending_worklet_messages_.push_back(base::BindOnce(
&TestSharedStorageWorkletHost::OnRunOperationOnWorkletFinishedHelper,
weak_ptr_factory_.GetWeakPtr(), start_time, success, error_message,
/*initial_message=*/false));
} else {
SharedStorageWorkletHost::OnRunOperationOnWorkletFinished(
start_time, success, error_message);
}
if (initial_message)
OnWorkletResponseReceived();
if (!in_keep_alive) {
ProcessRunOrSelectURLExpirationIfWorkletExpired();
}
}
void OnRunURLSelectionOperationOnWorkletFinished(
const GURL& urn_uuid,
base::TimeTicks start_time,
bool script_execution_success,
const std::string& script_execution_error_message,
uint32_t index,
BudgetResult budget_result) override {
OnRunURLSelectionOperationOnWorkletFinishedHelper(
urn_uuid, start_time, script_execution_success,
script_execution_error_message, index, std::move(budget_result),
/*initial_message=*/true);
}
void OnRunURLSelectionOperationOnWorkletFinishedHelper(
const GURL& urn_uuid,
base::TimeTicks start_time,
bool script_execution_success,
const std::string& script_execution_error_message,
uint32_t index,
BudgetResult budget_result,
bool initial_message) {
bool in_keep_alive = IsInKeepAlivePhase();
if (should_defer_worklet_messages_ && initial_message) {
pending_worklet_messages_.push_back(base::BindOnce(
&TestSharedStorageWorkletHost::
OnRunURLSelectionOperationOnWorkletFinishedHelper,
weak_ptr_factory_.GetWeakPtr(), urn_uuid, start_time,
script_execution_success, script_execution_error_message, index,
std::move(budget_result),
/*initial_message=*/false));
} else {
SharedStorageWorkletHost::OnRunURLSelectionOperationOnWorkletFinished(
urn_uuid, start_time, script_execution_success,
script_execution_error_message, index, std::move(budget_result));
}
if (initial_message)
OnWorkletResponseReceived();
if (!in_keep_alive) {
ProcessRunOrSelectURLExpirationIfWorkletExpired();
}
}
void ExpireWorklet() override {
// We must defer the destruction of the expired worklet until the rest of
// the test worklet code has run, in order to avoid segmentation faults. In
// particular, if either the `add_module_waiter_` or the
// `worklet_responses_count_waiter_` is running, we must quit it first
// before we actually destroy the worklet (regardless of how many worklet
// responses have been received). Hence we save a callback to destroy the
// worklet after we quit the waiter. The `Quit()` will occur in
// `Process*ExpirationIfWorkletExpired()` after returning to
// `On*OnWorkletFinishedHelper()`. If no waiter is running, we still have to
// finish running whichever `On*OnWorkletFinishedHelper()` triggered the
// call to `ExpireWorklet()`.
DCHECK(!pending_expire_worklet_callback_);
pending_expire_worklet_callback_ =
base::BindOnce(&TestSharedStorageWorkletHost::ExpireWorkletOnBaseClass,
weak_ptr_factory_.GetWeakPtr());
}
void ExpireWorkletOnBaseClass() { SharedStorageWorkletHost::ExpireWorklet(); }
void ProcessAddModuleExpirationIfWorkletExpired() {
if (!pending_expire_worklet_callback_) {
return;
}
// We can't have both waiters running at the same time.
DCHECK(!worklet_responses_count_waiter_);
if (add_module_waiter_ && add_module_waiter_->running()) {
// The worklet is expired and needs to be destroyed. Since
// `add_module_waiter_` is running, quitting it will return us to
// `WaitForAddModule()`, where the waiter will be reset, and then, in
// `ResetAddModuleCalledAndMaybeCloseWorklet()`, the
// `pending_expire_worklet_callback_` callback will be run.
add_module_waiter_->Quit();
}
std::move(pending_expire_worklet_callback_).Run();
// Do not add code after this. The worklet has been destroyed.
}
void ProcessRunOrSelectURLExpirationIfWorkletExpired() {
if (!pending_expire_worklet_callback_) {
return;
}
// We can't have both waiters running at the same time.
DCHECK(!add_module_waiter_);
if (worklet_responses_count_waiter_ &&
worklet_responses_count_waiter_->running()) {
// The worklet is expired and needs to be destroyed. Since
// `worklet_responses_count_waiter_` is running, quitting it will return
// us to `WaitForWorkletResponses()`, where the waiter will be reset, and
// then, in `ResetResponseCountsAndMaybeCloseWorklet()`, the
// `pending_expire_worklet_callback_` callback will be run.
worklet_responses_count_waiter_->Quit();
return;
}
if (response_expectation_set_) {
// We expect a call to `WaitForWorkletResponses()`, which will run the
// callback.
return;
}
// No response expectation has been set, so we do expect a call to
// `WaitForWorkletResponses()`.
std::move(pending_expire_worklet_callback_).Run();
// Do not add code after this. The worklet has been destroyed.
}
void OnAddModuleResponseReceived() {
add_module_called_ = true;
if (add_module_waiter_ && add_module_waiter_->running()) {
add_module_waiter_->Quit();
}
}
void OnWorkletResponseReceived() {
++worklet_responses_count_;
if (worklet_responses_count_waiter_ &&
worklet_responses_count_waiter_->running() &&
worklet_responses_count_ >= expected_worklet_responses_count_) {
worklet_responses_count_waiter_->Quit();
}
}
void ResetAddModuleCalledAndMaybeCloseWorklet() {
add_module_called_ = false;
if (pending_expire_worklet_callback_) {
std::move(pending_expire_worklet_callback_).Run();
// Do not add code after this. The worklet has been destroyed.
}
}
void ResetResponseCountsAndMaybeCloseWorklet() {
expected_worklet_responses_count_ = 0u;
worklet_responses_count_ = 0u;
response_expectation_set_ = false;
if (pending_expire_worklet_callback_) {
std::move(pending_expire_worklet_callback_).Run();
// Do not add code after this. The worklet has been destroyed.
}
}
base::TimeDelta GetKeepAliveTimeout() const override {
// Configure a timeout large enough so that the scheduled task won't run
// automatically. Instead, we will manually call OneShotTimer::FireNow().
return base::Seconds(30);
}
// Whether or not `addModule()` has been called since the last time (if any)
// that `add_module_waiter_` was reset.
bool add_module_called_ = false;
std::unique_ptr<base::RunLoop> add_module_waiter_;
// How many worklet operations have finished. This only includes `selectURL()`
// and `run()`.
size_t worklet_responses_count_ = 0;
size_t expected_worklet_responses_count_ = 0;
bool response_expectation_set_ = false;
std::unique_ptr<base::RunLoop> worklet_responses_count_waiter_;
// Whether we should defer messages received from the worklet environment to
// handle them later. This includes request callbacks (e.g. for `addModule()`,
// `selectURL()` and `run()`), as well as commands initiated from the worklet
// (e.g. `console.log()`).
bool should_defer_worklet_messages_;
std::vector<base::OnceClosure> pending_worklet_messages_;
// This callback will be non-null if the worklet is pending expiration due to
// the option `keepAlive: false` (which is the default value) being received
// in the most recent call to `run()` or `selectURL()`.
base::OnceClosure pending_expire_worklet_callback_;
base::WeakPtrFactory<TestSharedStorageWorkletHost> weak_ptr_factory_{this};
};
class TestSharedStorageObserver
: public SharedStorageWorkletHostManager::SharedStorageObserverInterface {
public:
using Access =
std::tuple<AccessType, int, std::string, SharedStorageEventParams>;
void OnSharedStorageAccessed(
const base::Time& access_time,
AccessType type,
int main_frame_id,
const std::string& owner_origin,
const SharedStorageEventParams& params) override {
accesses_.emplace_back(type, main_frame_id, owner_origin, params);
}
void OnUrnUuidGenerated(const GURL& urn_uuid) override {}
void OnConfigPopulated(
const std::optional<FencedFrameConfig>& config) override {}
bool EventParamsMatch(const SharedStorageEventParams& expected_params,
const SharedStorageEventParams& actual_params) {
if (expected_params.script_source_url != actual_params.script_source_url) {
LOG(ERROR) << "expected `script_source_url`: '"
<< SerializeOptionalString(expected_params.script_source_url)
<< "'";
LOG(ERROR) << "actual `sript_source_url`: '"
<< SerializeOptionalString(actual_params.script_source_url)
<< "'";
return false;
}
if (expected_params.operation_name != actual_params.operation_name) {
LOG(ERROR) << "expected `operation_name`: '"
<< SerializeOptionalString(expected_params.operation_name)
<< "'";
LOG(ERROR) << "actual `operation_name`: '"
<< SerializeOptionalString(actual_params.operation_name)
<< "'";
return false;
}
if (expected_params.urls_with_metadata !=
actual_params.urls_with_metadata) {
LOG(ERROR) << "expected `urls_with_metadata`: "
<< SerializeOptionalUrlsWithMetadata(
expected_params.urls_with_metadata);
LOG(ERROR) << "actual `urls_with_metadata`: "
<< SerializeOptionalUrlsWithMetadata(
actual_params.urls_with_metadata);
return false;
}
if (expected_params.key != actual_params.key) {
LOG(ERROR) << "expected `key`: '"
<< SerializeOptionalString(expected_params.key) << "'";
LOG(ERROR) << "actual key: '"
<< SerializeOptionalString(actual_params.key) << "'";
return false;
}
if (expected_params.value != actual_params.value) {
LOG(ERROR) << "expected `value`: '"
<< SerializeOptionalString(expected_params.value) << "'";
LOG(ERROR) << "actual `value`: '"
<< SerializeOptionalString(actual_params.value) << "'";
return false;
}
if (expected_params.ignore_if_present != actual_params.ignore_if_present) {
LOG(ERROR) << "expected `ignore_if_present`: "
<< SerializeOptionalBool(expected_params.ignore_if_present);
LOG(ERROR) << "actual `ignore_if_present`: "
<< SerializeOptionalBool(actual_params.ignore_if_present);
return false;
}
if (expected_params.serialized_data && !actual_params.serialized_data) {
LOG(ERROR) << "`serialized_data` unexpectedly null";
LOG(ERROR) << "expected `serialized_data`: '"
<< SerializeOptionalString(expected_params.serialized_data)
<< "'";
LOG(ERROR) << "actual `serialized_data`: '"
<< SerializeOptionalString(actual_params.serialized_data)
<< "'";
return false;
}
if (!expected_params.serialized_data && actual_params.serialized_data) {
LOG(ERROR) << "`serialized_data` unexpectedly non-null";
LOG(ERROR) << "expected `serialized_data`: '"
<< SerializeOptionalString(expected_params.serialized_data)
<< "'";
LOG(ERROR) << "actual `serialized_data`: '"
<< SerializeOptionalString(actual_params.serialized_data)
<< "'";
return false;
}
return true;
}
bool AccessesMatch(const Access& expected_access,
const Access& actual_access) {
if (std::get<0>(expected_access) != std::get<0>(actual_access)) {
LOG(ERROR) << "expected `type`: " << std::get<0>(expected_access);
LOG(ERROR) << "actual `type`: " << std::get<0>(actual_access);
return false;
}
if (std::get<1>(expected_access) != std::get<1>(actual_access)) {
LOG(ERROR) << "expected `main_frame_id`: '"
<< std::get<1>(expected_access) << "'";
LOG(ERROR) << "actual `main_frame_id`: '" << std::get<1>(actual_access)
<< "'";
return false;
}
if (std::get<2>(expected_access) != std::get<2>(actual_access)) {
LOG(ERROR) << "expected `origin`: '" << std::get<2>(expected_access)
<< "'";
LOG(ERROR) << "actual `origin`: '" << std::get<2>(actual_access) << "'";
return false;
}
return EventParamsMatch(std::get<3>(expected_access),
std::get<3>(actual_access));
}
void ExpectAccessObserved(const std::vector<Access>& expected_accesses) {
ASSERT_EQ(expected_accesses.size(), accesses_.size());
for (size_t i = 0; i < accesses_.size(); ++i) {
EXPECT_TRUE(AccessesMatch(expected_accesses[i], accesses_[i]));
if (!AccessesMatch(expected_accesses[i], accesses_[i]))
LOG(ERROR) << "Event access at index " << i << " differs";
}
}
private:
std::vector<Access> accesses_;
};
class TestSharedStorageWorkletHostManager
: public SharedStorageWorkletHostManager {
public:
using SharedStorageWorkletHostManager::SharedStorageWorkletHostManager;
~TestSharedStorageWorkletHostManager() override = default;
std::unique_ptr<SharedStorageWorkletHost> CreateWorkletHostHelper(
SharedStorageDocumentServiceImpl& document_service,
const url::Origin& frame_origin,
const GURL& script_source_url,
network::mojom::CredentialsMode credentials_mode,
const std::vector<blink::mojom::OriginTrialFeature>&
origin_trial_features,
mojo::PendingAssociatedReceiver<blink::mojom::SharedStorageWorkletHost>
worklet_host,
blink::mojom::SharedStorageDocumentService::CreateWorkletCallback
callback) override {
return std::make_unique<TestSharedStorageWorkletHost>(
document_service, frame_origin, script_source_url, credentials_mode,
origin_trial_features, std::move(worklet_host), std::move(callback),
should_defer_worklet_messages_);
}
// Precondition: there's only one eligible worklet host.
TestSharedStorageWorkletHost* GetAttachedWorkletHost() {
std::vector<TestSharedStorageWorkletHost*> worklet_hosts =
GetAttachedWorkletHosts();
DCHECK_EQ(worklet_hosts.size(), 1u);
return worklet_hosts[0];
}
std::vector<TestSharedStorageWorkletHost*> GetAttachedWorkletHosts() {
std::vector<TestSharedStorageWorkletHost*> results;
for (auto& [document_service, worklet_hosts] :
GetAttachedWorkletHostsForTesting()) {
for (auto& [raw_worklet_host, worklet_host] : worklet_hosts) {
results.push_back(
static_cast<TestSharedStorageWorkletHost*>(raw_worklet_host));
}
}
return results;
}
// Precondition: there's only one eligible worklet host.
TestSharedStorageWorkletHost* GetKeepAliveWorkletHost() {
DCHECK_EQ(1u, GetKeepAliveWorkletHostsCount());
return static_cast<TestSharedStorageWorkletHost*>(
GetKeepAliveWorkletHostsForTesting().begin()->second.get());
}
// Precondition: `frame` is associated with a
// `SharedStorageDocumentServiceImpl`, and there's a single attached
// `SharedStorageWorkletHost` for that document service.
TestSharedStorageWorkletHost* GetAttachedWorkletHostForFrame(
RenderFrameHost* frame) {
std::vector<TestSharedStorageWorkletHost*> worklet_hosts =
GetAttachedWorkletHostsForFrame(frame);
DCHECK_EQ(worklet_hosts.size(), 1u);
return worklet_hosts[0];
}
// Precondition: `frame` is associated with a
// `SharedStorageDocumentServiceImpl`.
std::vector<TestSharedStorageWorkletHost*> GetAttachedWorkletHostsForFrame(
RenderFrameHost* frame) {
SharedStorageDocumentServiceImpl* document_service = DocumentUserData<
SharedStorageDocumentServiceImpl>::GetForCurrentDocument(frame);
DCHECK(document_service);
WorkletHosts& worklet_hosts =
GetAttachedWorkletHostsForTesting().at(document_service);
std::vector<TestSharedStorageWorkletHost*> results;
for (auto& [raw_worklet_host, worklet_host] : worklet_hosts) {
results.push_back(
static_cast<TestSharedStorageWorkletHost*>(raw_worklet_host));
}
return results;
}
void ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(
bool should_defer_worklet_messages) {
should_defer_worklet_messages_ = should_defer_worklet_messages;
}
size_t GetAttachedWorkletHostsCount() {
size_t count = 0;
for (const auto& [document_service, worklet_hosts] :
GetAttachedWorkletHostsForTesting()) {
count += worklet_hosts.size();
}
return count;
}
size_t GetKeepAliveWorkletHostsCount() {
return GetKeepAliveWorkletHostsForTesting().size();
}
private:
bool should_defer_worklet_messages_ = false;
};
class SharedStorageBrowserTestBase : public ContentBrowserTest {
public:
using AccessType = TestSharedStorageObserver::AccessType;
SharedStorageBrowserTestBase() {
privacy_sandbox_ads_apis_override_feature_.InitAndEnableFeature(
features::kPrivacySandboxAdsAPIsOverride);
shared_storage_feature_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{blink::features::kSharedStorageAPI,
{
{"SharedStorageBitBudget", base::NumberToString(kBudgetAllowed)},
{"SharedStorageStalenessThreshold",
TimeDeltaToString(base::Days(kStalenessThresholdDays))},
}},
{blink::features::kSharedStorageAPIM125, {}}},
/*disabled_features=*/{});
fenced_frame_feature_.InitAndEnableFeature(blink::features::kFencedFrames);
}
void SetUpOnMainThread() override {
auto test_worklet_host_manager =
std::make_unique<TestSharedStorageWorkletHostManager>();
observer_ = std::make_unique<TestSharedStorageObserver>();
test_worklet_host_manager->AddSharedStorageObserver(observer_.get());
test_worklet_host_manager_ = test_worklet_host_manager.get();
static_cast<StoragePartitionImpl*>(GetStoragePartition())
->OverrideSharedStorageWorkletHostManagerForTesting(
std::move(test_worklet_host_manager));
host_resolver()->AddRule("*", "127.0.0.1");
MakeMockPrivateAggregationShellContentBrowserClient();
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsPrivacySandboxReportingDestinationAttested)
.WillByDefault(testing::Return(true));
FinishSetup();
}
MockPrivateAggregationShellContentBrowserClient& browser_client() {
return *browser_client_;
}
virtual bool ResolveSelectURLToConfig() { return false; }
StoragePartition* GetStoragePartition() {
return shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition();
}
void TearDownOnMainThread() override { test_worklet_host_manager_ = nullptr; }
// Virtual so that derived classes can use a different flavor of mock instead
// of `testing::NiceMock`.
virtual void MakeMockPrivateAggregationShellContentBrowserClient() {
browser_client_ = std::make_unique<
testing::NiceMock<MockPrivateAggregationShellContentBrowserClient>>();
}
// Virtual so that derived classes can delay starting the server, and/or add
// other set up steps.
virtual void FinishSetup() {
https_server()->AddDefaultHandlers(GetTestDataFilePath());
https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
SetupCrossSiteRedirector(https_server());
ASSERT_TRUE(https_server()->Start());
}
void ExpectAccessObserved(
const std::vector<TestSharedStorageObserver::Access>& expected_accesses) {
observer_->ExpectAccessObserved(expected_accesses);
}
double GetRemainingBudget(const url::Origin& origin) {
base::test::TestFuture<SharedStorageWorkletHost::BudgetResult> future;
static_cast<StoragePartitionImpl*>(GetStoragePartition())
->GetSharedStorageManager()
->GetRemainingBudget(net::SchemefulSite(origin), future.GetCallback());
return future.Take().bits;
}
FrameTreeNode* PrimaryFrameTreeNodeRoot() {
return static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
}
int MainFrameId() { return PrimaryFrameTreeNodeRoot()->frame_tree_node_id(); }
SharedStorageBudgetMetadata* GetSharedStorageBudgetMetadata(
const GURL& urn_uuid) {
FencedFrameURLMapping& fenced_frame_url_mapping =
PrimaryFrameTreeNodeRoot()
->current_frame_host()
->GetPage()
.fenced_frame_urls_map();
SharedStorageBudgetMetadata* metadata =
fenced_frame_url_mapping.GetSharedStorageBudgetMetadataForTesting(
GURL(urn_uuid));
return metadata;
}
SharedStorageReportingMap GetSharedStorageReportingMap(const GURL& urn_uuid) {
FencedFrameURLMapping& fenced_frame_url_mapping =
PrimaryFrameTreeNodeRoot()
->current_frame_host()
->GetPage()
.fenced_frame_urls_map();
FencedFrameURLMappingTestPeer fenced_frame_url_mapping_test_peer(
&fenced_frame_url_mapping);
SharedStorageReportingMap reporting_map;
fenced_frame_url_mapping_test_peer.GetSharedStorageReportingMap(
GURL(urn_uuid), &reporting_map);
return reporting_map;
}
void ExecuteScriptInWorklet(
const ToRenderFrameHost& execution_target,
const std::string& script,
GURL* out_module_script_url,
size_t expected_total_host_count = 1u,
bool keep_alive_after_operation = true,
std::optional<std::string> context_id = std::nullopt,
std::string* out_error = nullptr,
bool wait_for_operation_finish = true) {
DCHECK(out_module_script_url);
base::StringPairs run_function_body_replacement;
run_function_body_replacement.emplace_back("{{RUN_FUNCTION_BODY}}", script);
std::string host =
execution_target.render_frame_host()->GetLastCommittedOrigin().host();
*out_module_script_url = https_server()->GetURL(
host, net::test_server::GetFilePathWithReplacements(
"/shared_storage/customizable_module.js",
run_function_body_replacement));
EXPECT_TRUE(ExecJs(execution_target,
JsReplace("sharedStorage.worklet.addModule($1)",
*out_module_script_url)));
// There may be more than one host in the worklet host manager if we are
// executing inside a nested fenced frame that was created using
// `selectURL()`.
EXPECT_EQ(expected_total_host_count,
test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(execution_target.render_frame_host())
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(
execution_target,
JsReplace("window.keepWorklet = $1;", keep_alive_after_operation)));
testing::AssertionResult result = ExecJs(
execution_target,
base::StrCat(
{"sharedStorage.run('test-operation', {keepAlive: keepWorklet",
context_id.has_value()
? JsReplace(", privateAggregationConfig: {contextId: $1}});",
context_id.value())
: "});"}));
EXPECT_EQ(!!result, out_error == nullptr);
if (out_error) {
*out_error = std::string(result.message());
return;
}
if (wait_for_operation_finish) {
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(execution_target.render_frame_host())
->WaitForWorkletResponses();
}
}
FrameTreeNode* CreateIFrame(FrameTreeNode* root, const GURL& url) {
size_t initial_child_count = root->child_count();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(initial_child_count + 1, root->child_count());
FrameTreeNode* child_node = root->child_at(initial_child_count);
TestFrameNavigationObserver observer(child_node);
EXPECT_EQ(url.spec(), EvalJs(root, JsReplace("f.src = $1;", url)));
observer.Wait();
return child_node;
}
// Create an iframe of origin `origin` inside `parent_node`, and run
// sharedStorage.selectURL() on 8 urls. If `parent_node` is not specified,
// the primary frame tree's root node will be chosen. This generates an URN
// associated with `origin` and 3 bits of shared storage budget.
std::optional<GURL> SelectFrom8URLsInContext(
const url::Origin& origin,
FrameTreeNode* parent_node = nullptr,
bool keep_alive_after_operation = true) {
if (!parent_node)
parent_node = PrimaryFrameTreeNodeRoot();
// If this is called inside a fenced frame, creating an iframe will need
// "Supports-Loading-Mode: fenced-frame" response header. Thus, we simply
// always set the path to `kFencedFramePath`.
GURL iframe_url = origin.GetURL().Resolve(kFencedFramePath);
FrameTreeNode* iframe = CreateIFrame(parent_node, iframe_url);
EXPECT_TRUE(ExecJs(iframe, JsReplace("window.keepWorklet = $1;",
keep_alive_after_operation)));
EXPECT_TRUE(ExecJs(iframe, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)"));
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(iframe->current_frame_host())
->SetExpectedWorkletResponsesCount(1);
// Generate 8 candidates urls in to a list variable `urls`.
EXPECT_TRUE(ExecJs(iframe, kGenerateURLsListScript));
EXPECT_TRUE(
ExecJs(iframe, JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(
GetStoragePartition());
EvalJsResult result = EvalJs(iframe, R"(
(async function() {
const urls = generateUrls(8);
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: keepWorklet
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
if (observed_urn_uuid.has_value()) {
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(iframe->current_frame_host())
->WaitForWorkletResponses();
}
return observed_urn_uuid;
}
// Prerequisite: The worklet for `frame` has registered a
// "remaining-budget-operation" that logs the remaining budget to the console
// after `kRemainingBudgetPrefix`. Also, if any previous operations are
// called, they use the option `keepAlive: true`.
double RemainingBudgetViaJSForFrame(FrameTreeNode* frame,
bool keep_alive_after_operation = true) {
DCHECK(frame);
WebContentsConsoleObserver console_observer(shell()->web_contents());
const std::string kRemainingBudgetPrefixStr(kRemainingBudgetPrefix);
console_observer.SetPattern(base::StrCat({kRemainingBudgetPrefixStr, "*"}));
EXPECT_TRUE(ExecJs(frame, JsReplace("window.keepWorklet = $1;",
keep_alive_after_operation)));
// There is 1 "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(frame->current_frame_host())
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(frame, R"(
sharedStorage.run('remaining-budget-operation',
{
data: {},
keepAlive: keepWorklet
}
);
)"));
bool observed = console_observer.Wait();
EXPECT_TRUE(observed);
if (!observed) {
return nan("");
}
EXPECT_LE(1u, console_observer.messages().size());
std::string console_message =
base::UTF16ToUTF8(console_observer.messages().back().message);
EXPECT_TRUE(base::StartsWith(console_message, kRemainingBudgetPrefixStr));
std::string result_string = console_message.substr(
kRemainingBudgetPrefixStr.size(),
console_message.size() - kRemainingBudgetPrefixStr.size());
double result = 0.0;
EXPECT_TRUE(base::StringToDouble(result_string, &result));
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(frame->current_frame_host())
->WaitForWorkletResponses();
return result;
}
double RemainingBudgetViaJSForOrigin(const url::Origin& origin) {
FrameTreeNode* iframe =
CreateIFrame(PrimaryFrameTreeNodeRoot(), origin.GetURL());
EXPECT_TRUE(ExecJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
return RemainingBudgetViaJSForFrame(iframe);
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
TestSharedStorageWorkletHostManager& test_worklet_host_manager() {
DCHECK(test_worklet_host_manager_);
return *test_worklet_host_manager_;
}
~SharedStorageBrowserTestBase() override = default;
protected:
test::FencedFrameTestHelper fenced_frame_test_helper_;
base::test::ScopedFeatureList privacy_sandbox_ads_apis_override_feature_;
base::test::ScopedFeatureList shared_storage_feature_;
base::test::ScopedFeatureList fenced_frame_feature_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::HistogramTester histogram_tester_;
raw_ptr<TestSharedStorageWorkletHostManager> test_worklet_host_manager_ =
nullptr;
std::unique_ptr<TestSharedStorageObserver> observer_;
std::unique_ptr<MockPrivateAggregationShellContentBrowserClient>
browser_client_;
};
class SharedStorageBrowserTest : public SharedStorageBrowserTestBase,
public testing::WithParamInterface<bool> {
public:
SharedStorageBrowserTest() {
fenced_frame_api_change_feature_.InitWithFeatureState(
blink::features::kFencedFramesAPIChanges, ResolveSelectURLToConfig());
}
bool ResolveSelectURLToConfig() override { return GetParam(); }
~SharedStorageBrowserTest() override = default;
private:
base::test::ScopedFeatureList fenced_frame_api_change_feature_;
};
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, AddModule_Success) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// Navigate again to record histograms.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
WaitForHistograms(
{kDestroyedStatusHistogram, kTimingUsefulResourceHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, AddModule_ScriptNotFound) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
std::string expected_error = base::StrCat(
{"a JavaScript error: \"Error: Failed to load ",
https_server()
->GetURL("a.test", "/shared_storage/nonexistent_module.js")
.spec(),
" HTTP status = 404 Not Found.\"\n"});
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/nonexistent_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(0u, console_observer.messages().size());
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/nonexistent_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, AddModule_RedirectNotAllowed) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
std::string expected_error = base::StrCat(
{"a JavaScript error: \"Error: Unexpected redirect on ",
https_server()
->GetURL("a.test",
"/server-redirect?shared_storage/simple_module.js")
.spec(),
".\"\n"});
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule(
'/server-redirect?shared_storage/simple_module.js');
)");
EXPECT_EQ(expected_error, result.error);
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(0u, console_observer.messages().size());
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/server-redirect?shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
AddModule_ScriptExecutionFailure) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/erroneous_module.js');
)");
EXPECT_THAT(
result.error,
testing::HasSubstr("ReferenceError: undefinedVariable is not defined"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Start executing erroneous_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/erroneous_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
AddModule_MultipleAddModuleFailure) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)");
EXPECT_THAT(
result.error,
testing::HasSubstr("addModule() can only be invoked once per worklet"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// Navigate again to record histograms.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
WaitForHistograms(
{kDestroyedStatusHistogram, kTimingUsefulResourceHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, RunOperation_Success) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_Failure_RunOperationBeforeAddModule) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'},
keepAlive: true});
)");
EXPECT_THAT(
result.error,
testing::HasSubstr(
"sharedStorage.worklet.addModule() has to be called before run()"));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
content::FetchHistogramsFromChildProcesses();
// Navigate to terminate the worklet.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 1);
histogram_tester_.ExpectBucketCount(
kErrorTypeHistogram, blink::SharedStorageWorkletErrorType::kSuccess, 1);
histogram_tester_.ExpectBucketCount(
kErrorTypeHistogram, blink::SharedStorageWorkletErrorType::kRunWebVisible,
1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_Failure_InvalidOptionsArgument) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EvalJsResult result = EvalJs(shell(), R"(
function testFunction() {}
sharedStorage.run(
'test-operation', {data: {'customKey': testFunction}});
)");
EXPECT_THAT(
result.error,
testing::HasSubstr("function testFunction() {} could not be cloned"));
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 0);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_VerifyUndefinedData) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('test-operation', /*options=*/{});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_Failure_BlobDataTypeNotSupportedInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
const blob = new Blob(["abc"], {type: 'text/plain'});
sharedStorage.run('test-operation', /*options=*/{data: blob});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("Cannot deserialize data.",
base::UTF16ToUTF8(console_observer.messages()[2].message));
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_VerifyCryptoKeyData) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
const myPromise = new Promise((resolve, reject) => {
crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
).then((key) => {
sharedStorage.run('test-operation', /*options=*/{data: key})
.then(() => { resolve(); });
});
});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ(
"CryptoKey, algorithm: {\"length\":256,\"name\":\"AES-GCM\"} usages: "
"[\"encrypt\",\"decrypt\"] extractable: true",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_Failure_ErrorInRunOperation) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule(
'shared_storage/erroneous_function_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing erroneous_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ("Finish executing erroneous_function_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[2].log_level);
EXPECT_THAT(
base::UTF16ToUTF8(console_observer.messages()[3].message),
testing::HasSubstr("ReferenceError: undefinedVariable is not defined"));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[3].log_level);
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/erroneous_function_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
RunOperation_SecondRunOperationAfterKeepAliveTrueRun_Success) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'},
keepAlive: true});
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(8u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[5].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[6].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[7].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 2);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
RunOperation_SecondRunOperationAfterKeepAliveFalseRun_Failure) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'},
keepAlive: false});
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)");
EXPECT_THAT(result.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
RunOperation_SecondRunOperationAfterKeepAliveDefaultRun_Failure) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)");
EXPECT_THAT(result.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, WorkletDestroyed) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms(
{kDestroyedStatusHistogram, kTimingUsefulResourceHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, TwoWorklets) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kPageWithBlankIframePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
RenderFrameHost* iframe =
PrimaryFrameTreeNodeRoot()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module2.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
NavigateIframeToURL(shell()->web_contents(), "test_iframe",
GURL(url::kAboutBlankURL));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("Executing simple_module2.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[2].message));
// Navigate again to record histograms.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
WaitForHistograms(
{kDestroyedStatusHistogram, kTimingUsefulResourceHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 2);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 2);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module2.js"))},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
KeepAlive_StartBeforeAddModuleComplete_EndAfterAddModuleComplete) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(true);
WebContentsConsoleObserver console_observer(shell()->web_contents());
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
// Navigate to trigger keep-alive
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager().GetKeepAliveWorkletHost()->WaitForAddModule();
// Three pending messages are expected: two for console.log and one for
// `addModule()` response.
EXPECT_EQ(3u, test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->pending_worklet_messages()
.size());
// Execute all the deferred messages. This will terminate the keep-alive.
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->ExecutePendingWorkletMessages();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Expect no console logging, as messages logged during keep-alive are
// dropped.
EXPECT_EQ(0u, console_observer.messages().size());
WaitForHistograms({kDestroyedStatusHistogram, kTimingUsefulResourceHistogram,
kTimingKeepAliveDurationHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::
kKeepAliveEndedDueToOperationsFinished,
1);
histogram_tester_.ExpectTotalCount(kTimingKeepAliveDurationHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
KeepAlive_StartBeforeAddModuleComplete_EndAfterTimeout) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(true);
WebContentsConsoleObserver console_observer(shell()->web_contents());
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
// Navigate to trigger keep-alive
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager().GetKeepAliveWorkletHost()->WaitForAddModule();
// Three pending messages are expected: two for console.log and one for
// `addModule()` response.
EXPECT_EQ(3u, test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->pending_worklet_messages()
.size());
// Fire the keep-alive timer. This will terminate the keep-alive.
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->FireKeepAliveTimerNow();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms(
{kDestroyedStatusHistogram, kTimingUsefulResourceHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kKeepAliveEndedDueToTimeout,
1);
histogram_tester_.ExpectTotalCount(kTimingKeepAliveDurationHistogram, 0);
histogram_tester_.ExpectUniqueSample(kTimingUsefulResourceHistogram, 100, 1);
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(),
url::Origin::Create(url).Serialize(),
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
KeepAlive_StartBeforeRunOperationComplete_EndAfterRunOperationComplete) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, console_observer.messages().size());
// Configure the worklet host to defer processing the subsequent `run()`
// response.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->set_should_defer_worklet_messages(true);
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}})
)"));
// Navigate to trigger keep-alive
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->WaitForWorkletResponses();
// Four pending messages are expected: three for console.log and one for
// `run()` response.
EXPECT_EQ(4u, test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->pending_worklet_messages()
.size());
// Execute all the deferred messages. This will terminate the keep-alive.
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->ExecutePendingWorkletMessages();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Expect no more console logging, as messages logged during keep-alive was
// dropped.
EXPECT_EQ(2u, console_observer.messages().size());
WaitForHistograms({kDestroyedStatusHistogram, kTimingUsefulResourceHistogram,
kTimingKeepAliveDurationHistogram,
kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectUniqueSample(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::
kKeepAliveEndedDueToOperationsFinished,
1);
histogram_tester_.ExpectTotalCount(kTimingKeepAliveDurationHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, KeepAlive_SubframeWorklet) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kPageWithBlankIframePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Configure the worklet host for the subframe to defer worklet responses.
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(true);
RenderFrameHost* iframe =
PrimaryFrameTreeNodeRoot()->child_at(0)->current_frame_host();
EvalJsResult result = EvalJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
// Navigate away to let the subframe's worklet enter keep-alive.
NavigateIframeToURL(shell()->web_contents(), "test_iframe",
GURL(url::kAboutBlankURL));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Ensure that the response is deferred.
test_worklet_host_manager().GetKeepAliveWorkletHost()->WaitForAddModule();
// Three pending messages are expected: two for console.log and one for
// `addModule()` response.
EXPECT_EQ(3u, test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->pending_worklet_messages()
.size());
// Configure the worklet host for the main frame to handle worklet responses
// directly.
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(false);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module2.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Execute all the deferred messages. This will terminate the keep-alive.
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->ExecutePendingWorkletMessages();
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Expect loggings only from executing top document's worklet.
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Executing simple_module2.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Navigate again to record histograms.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
WaitForHistograms({kDestroyedStatusHistogram, kTimingUsefulResourceHistogram,
kTimingKeepAliveDurationHistogram});
histogram_tester_.ExpectBucketCount(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::
kKeepAliveEndedDueToOperationsFinished,
1);
histogram_tester_.ExpectBucketCount(
kDestroyedStatusHistogram,
blink::SharedStorageWorkletDestroyedStatus::kDidNotEnterKeepAlive, 1);
histogram_tester_.ExpectTotalCount(kTimingKeepAliveDurationHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingUsefulResourceHistogram, 2);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module2.js"))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
RenderProcessHostDestroyedDuringWorkletKeepAlive_SameOrigin) {
// The test assumes pages gets deleted after navigation, letting the worklet
// enter keep-alive phase. To ensure this, disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(true);
EvalJsResult result = EvalJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)",
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
// Navigate to trigger keep-alive
RenderFrameDeletedObserver rfh_deleted_observer(
shell()->web_contents()->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(),
https_server()->GetURL("c.test", kSimplePagePath)));
rfh_deleted_observer.WaitUntilDeleted();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// The BrowserContext will be destroyed right after this test body, which will
// cause the RenderProcessHost to be destroyed before the keep-alive
// SharedStorageWorkletHost. Expect no fatal error.
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
RenderProcessHostDestroyedDuringWorkletKeepAlive_CrossOrigin) {
// The test assumes pages gets deleted after navigation, letting the worklet
// enter keep-alive phase. To ensure this, disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
test_worklet_host_manager()
.ConfigureShouldDeferWorkletMessagesOnWorkletHostCreation(true);
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EvalJsResult result = EvalJs(
shell(),
JsReplace("sharedStorage.createWorklet($1)", module_script_url.spec()),
EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
// Navigate to trigger keep-alive
RenderFrameDeletedObserver rfh_deleted_observer(
shell()->web_contents()->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(),
https_server()->GetURL("c.test", kSimplePagePath)));
rfh_deleted_observer.WaitUntilDeleted();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// The BrowserContext will be destroyed right after this test body, which will
// cause the RenderProcessHost to be destroyed before the keep-alive
// SharedStorageWorkletHost. Expect no fatal error.
}
// Test that there's no need to charge budget if the input urls' size is 1.
// This specifically tests the operation success scenario.
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
SelectURL_BudgetMetadata_OperationSuccess_SingleInputURL) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html",
reportingMetadata: {
"click": "fenced_frames/report1.html",
"mouse interaction": "fenced_frames/report2.html"
}
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, 0.0);
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html")),
Pair("mouse interaction",
https_server()->GetURL("a.test",
"/fenced_frames/report2.html"))));
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget, 1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{{"click",
https_server()
->GetURL("a.test", "/fenced_frames/report1.html")
.spec()},
{"mouse interaction",
https_server()
->GetURL("a.test", "/fenced_frames/report2.html")
.spec()}}}}))}});
}
// Test that there's no need to charge budget if the input urls' size is 1.
// This specifically tests the operation failure scenario.
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
SelectURL_BudgetMetadata_OperationFailure_SingleInputURL) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html",
reportingMetadata: {
"click": "fenced_frames/report1.html"
}
}
],
{
data: {'mockResult': -1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, 0.0);
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html"))));
EXPECT_EQ(
"Promise resolved to a number outside the length of the input urls.",
base::UTF16ToUTF8(console_observer.messages().back().message));
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{{"click",
https_server()
->GetURL("a.test", "/fenced_frames/report1.html")
.spec()}}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
SelectURL_BudgetMetadata_Origin) {
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL("a.test", kPageWithBlankIframePath)));
GURL iframe_url = https_server()->GetURL("b.test", kSimplePagePath);
NavigateIframeToURL(shell()->web_contents(), "test_iframe", iframe_url);
RenderFrameHost* iframe =
PrimaryFrameTreeNodeRoot()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(iframe, JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(iframe, R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata: {
"click": "fenced_frames/report1.html"
}
},
{
url: "fenced_frames/title2.html"
}
],
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("b.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, std::log2(3));
SharedStorageReportingMap reporting_map =
GetSharedStorageReportingMap(observed_urn_uuid.value());
EXPECT_FALSE(reporting_map.empty());
EXPECT_EQ(1U, reporting_map.size());
EXPECT_EQ("click", reporting_map.begin()->first);
EXPECT_EQ(https_server()->GetURL("b.test", "/fenced_frames/report1.html"),
reporting_map.begin()->second);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(iframe_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"b.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("b.test",
"/fenced_frames/title0.html"),
{}},
{https_server()->GetURL("b.test",
"/fenced_frames/title1.html"),
{{"click",
https_server()
->GetURL("b.test", "/fenced_frames/report1.html")
.spec()}}},
{https_server()->GetURL("b.test",
"/fenced_frames/title2.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
SelectURL_SecondSelectURLAfterKeepAliveTrueSelectURL_Success) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer1(
GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: true
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result1.error.empty());
const std::optional<GURL>& observed_urn_uuid1 = config_observer1.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid1.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid1.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result1.ExtractString(), observed_urn_uuid1->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer1.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config1 =
config_observer1.GetConfig();
EXPECT_TRUE(fenced_frame_config1.has_value());
EXPECT_EQ(fenced_frame_config1->urn_uuid(), observed_urn_uuid1.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
TestSelectURLFencedFrameConfigObserver config_observer2(
GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result2 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result2.error.empty());
const std::optional<GURL>& observed_urn_uuid2 = config_observer2.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid2.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid2.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result2.ExtractString(), observed_urn_uuid2->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer2.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config2 =
config_observer2.GetConfig();
EXPECT_TRUE(fenced_frame_config2.has_value());
EXPECT_EQ(fenced_frame_config2->urn_uuid(), observed_urn_uuid2.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
2);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
SelectURL_SecondSelectURLAfterKeepAliveFalseSelectURL_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer1(
GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: false
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result1.error.empty());
const std::optional<GURL>& observed_urn_uuid1 = config_observer1.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid1.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid1.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result1.ExtractString(), observed_urn_uuid1->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer1.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config1 =
config_observer1.GetConfig();
EXPECT_TRUE(fenced_frame_config1.has_value());
EXPECT_EQ(fenced_frame_config1->urn_uuid(), observed_urn_uuid1.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
TestSelectURLFencedFrameConfigObserver config_observer2(
GetStoragePartition());
EvalJsResult result2 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_THAT(result2.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_FALSE(config_observer2.ConfigObserved());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
SelectURL_SecondSelectURLAfterKeepAliveDefaultSelectURL_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer1(
GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
const char select_url_script[] = R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)";
EvalJsResult result1 = EvalJs(shell(), select_url_script);
EXPECT_TRUE(result1.error.empty());
const std::optional<GURL>& observed_urn_uuid1 = config_observer1.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid1.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid1.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result1.ExtractString(), observed_urn_uuid1->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer1.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config1 =
config_observer1.GetConfig();
EXPECT_TRUE(fenced_frame_config1.has_value());
EXPECT_EQ(fenced_frame_config1->urn_uuid(), observed_urn_uuid1.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
TestSelectURLFencedFrameConfigObserver config_observer2(
GetStoragePartition());
EvalJsResult result2 = EvalJs(shell(), select_url_script);
EXPECT_THAT(result2.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_FALSE(config_observer2.ConfigObserved());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
SelectURL_SelectURLAfterKeepAliveFalseRun_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'},
keepAlive: false});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_THAT(result.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_FALSE(config_observer.ConfigObserved());
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
SelectURL_SelectURLAfterKeepAliveTrueRun_Success) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms({kTimingRunExecutedInWorkletHistogram,
kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
SelectURL_SelectURLAfterKeepAliveDefaultRun_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_THAT(result.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_FALSE(config_observer.ConfigObserved());
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("Start executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("{\"customKey\":\"customValue\"}",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages()[4].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_RunAfterKeepAliveTrueSelectURL_Success) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: true
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ("Finish executing 'test-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms({kTimingRunExecutedInWorkletHistogram,
kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_RunAfterKeepAliveFalseSelectURL_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: false
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result1.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result1.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EvalJsResult result2 = EvalJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)");
EXPECT_THAT(result2.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
RunOperation_RunAfterKeepAliveDefaultSelectURL_Failure) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result1.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result1.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EvalJsResult result2 = EvalJs(shell(), R"(
sharedStorage.run(
'test-operation', {data: {'customKey': 'customValue'}});
)");
EXPECT_THAT(result2.error,
testing::HasSubstr(kSharedStorageWorkletExpiredMessage));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
SelectURL_ReportingMetadata_EmptyReportEvent) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html",
reportingMetadata: {
"": "fenced_frames/report1.html"
}
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, 0.0);
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html"))));
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
std::string origin_str = url::Origin::Create(main_url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(https_server()->GetURL(
"a.test", "/shared_storage/simple_module.js"))},
{AccessType::kDocumentSelectURL, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSelectURL(
"test-url-selection-operation", blink::CloneableMessage(),
std::vector<SharedStorageUrlSpecWithMetadata>(
{{https_server()->GetURL("a.test",
"/fenced_frames/title0.html"),
{{"", https_server()
->GetURL("a.test", "/fenced_frames/report1.html")
.spec()}}}}))}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, SetAppendOperationInDocument) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
sharedStorage.set('key1', 'value1');
sharedStorage.set('key1', 'value111');
sharedStorage.set('key2', 'value2');
sharedStorage.set('key2', 'value222', {ignoreIfPresent: true});
sharedStorage.set('key3', 'value3');
sharedStorage.append('key3', 'value333');
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('key0'));
console.log(await sharedStorage.get('key1'));
console.log(await sharedStorage.get('key2'));
console.log(await sharedStorage.get('key3'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("value0",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("value111",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("value2",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("value3value333",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("4", base::UTF16ToUTF8(console_observer.messages()[4].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key1", "value1", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key1", "value111", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key2", "value2", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key2", "value222", true)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key3", "value3", false)},
{AccessType::kDocumentAppend, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAppend("key3", "value333")},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key1")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key2")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key3")},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, DeleteOperationInDocument) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
sharedStorage.delete('key0');
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
console.log(await sharedStorage.get('key0'));
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[1].log_level);
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentDelete, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, ClearOperationInDocument) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
sharedStorage.clear();
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[0].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentClear, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, SetAppendOperationInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
sharedStorage.set('key0', 'value0');
sharedStorage.set('key1', 'value1');
sharedStorage.set('key1', 'value111');
sharedStorage.set('key2', 'value2');
sharedStorage.set('key2', 'value222', {ignoreIfPresent: true});
sharedStorage.set('key3', 'value3');
sharedStorage.append('key3', 'value333');
console.log(await sharedStorage.get('key0'));
console.log(await sharedStorage.get('key1'));
console.log(await sharedStorage.get('key2'));
console.log(await sharedStorage.get('key3'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("value0",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("value111",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("value2",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("value3value333",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("4", base::UTF16ToUTF8(console_observer.messages()[4].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key1", "value1", false)},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key1", "value111", false)},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key2", "value2", false)},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key2", "value222", true)},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key3", "value3", false)},
{AccessType::kWorkletAppend, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAppend("key3", "value333")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key1")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key2")},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key3")},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
AppendOperationFailedInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
await sharedStorage.set('k', 'a'.repeat(2621439));
// This will fail due to the storage quota being reached.
await sharedStorage.append('k', 'a');
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages()[0].message),
testing::HasSubstr("sharedStorage.append() failed"));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("k", std::string(2621439, 'a'),
false)},
{AccessType::kWorkletAppend, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAppend("k", "a")}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, DeleteOperationInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
sharedStorage.set('key0', 'value0');
console.log(await sharedStorage.length());
console.log(await sharedStorage.get('key0'));
sharedStorage.delete('key0');
console.log(await sharedStorage.length());
console.log(await sharedStorage.get('key0'));
)",
&out_script_url);
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("value0",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[1].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[2].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[3].log_level);
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kWorkletDelete, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, ClearOperationInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
sharedStorage.set('key0', 'value0');
console.log(await sharedStorage.length());
console.log(await sharedStorage.get('key0'));
sharedStorage.clear();
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("value0",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[2].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kWorkletClear, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, ConsoleErrorInWorklet) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.error('error0');
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
EXPECT_EQ("error0",
base::UTF16ToUTF8(console_observer.messages()[0].message));
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, GetOperationInWorklet) {
base::SimpleTestClock clock;
base::RunLoop loop;
static_cast<StoragePartitionImpl*>(GetStoragePartition())
->GetSharedStorageManager()
->OverrideClockForTesting(&clock, loop.QuitClosure());
loop.Run();
clock.SetNow(base::Time::Now());
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/getter_module.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'key0'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
// Advance clock so that key will expire.
clock.Advance(base::Days(kStalenessThresholdDays) + base::Seconds(1));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'key0'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("sharedStorage.length(): 1",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("sharedStorage.get('key0'): value0",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("sharedStorage.length(): 0",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("sharedStorage.get('key0'): undefined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[0].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[1].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[2].log_level);
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kInfo,
console_observer.messages()[3].log_level);
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 2);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("get-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("get-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletGet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForGetOrDelete("key0")}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
AccessStorageInSameOriginDocument) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
)"));
EXPECT_TRUE(
NavigateToURL(shell(), https_server()->GetURL("a.test", "/title1.html")));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[0].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
AccessStorageInDifferentOriginDocument) {
GURL url1 = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url1));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
)"));
GURL url2 = https_server()->GetURL("b.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), url2));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[0].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin2_str = url::Origin::Create(url2).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(),
url::Origin::Create(url1).Serialize(),
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentAddModule, MainFrameId(), origin2_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin2_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletLength, MainFrameId(), origin2_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest, KeysAndEntriesOperation) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('key0', 'value0');
sharedStorage.set('key1', 'value1');
sharedStorage.set('key2', 'value2');
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
for await (const key of sharedStorage.keys()) {
console.log(key);
}
for await (const [key, value] of sharedStorage.entries()) {
console.log(key + ';' + value);
}
)",
&out_script_url);
EXPECT_EQ(6u, console_observer.messages().size());
EXPECT_EQ("key0", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("key1", base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("key2", base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("key0;value0",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("key1;value1",
base::UTF16ToUTF8(console_observer.messages()[4].message));
EXPECT_EQ("key2;value2",
base::UTF16ToUTF8(console_observer.messages()[5].message));
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
std::string origin_str = url::Origin::Create(url).Serialize();
ExpectAccessObserved(
{{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key0", "value0", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key1", "value1", false)},
{AccessType::kDocumentSet, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForSet("key2", "value2", false)},
{AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url)},
{AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage())},
{AccessType::kWorkletKeys, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()},
{AccessType::kWorkletEntries, MainFrameId(), origin_str,
SharedStorageEventParams::CreateDefault()}});
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
KeysAndEntriesOperation_MultipleBatches) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_TRUE(ExecJs(shell(), R"(
for (let i = 0; i < 150; ++i) {
sharedStorage.set('key' + i.toString().padStart(3, '0'),
'value' + i.toString().padStart(3, '0'));
}
)"));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
for await (const key of sharedStorage.keys()) {
console.log(key);
}
for await (const [key, value] of sharedStorage.entries()) {
console.log(key + ';' + value);
}
)",
&out_script_url);
EXPECT_EQ(300u, console_observer.messages().size());
std::string origin_str = url::Origin::Create(url).Serialize();
std::vector<TestSharedStorageObserver::Access> expected_accesses;
for (int i = 0; i < 150; ++i) {
std::string zero_padded_i = base::NumberToString(i);
zero_padded_i.insert(zero_padded_i.begin(), 3 - zero_padded_i.size(), '0');
std::string padded_key = base::StrCat({"key", zero_padded_i});
std::string padded_value = base::StrCat({"value", zero_padded_i});
EXPECT_EQ(padded_key,
base::UTF16ToUTF8(console_observer.messages()[i].message));
EXPECT_EQ(base::JoinString({padded_key, padded_value}, ";"),
base::UTF16ToUTF8(console_observer.messages()[i + 150].message));
expected_accesses.emplace_back(AccessType::kDocumentSet, MainFrameId(),
origin_str,
SharedStorageEventParams::CreateForSet(
padded_key, padded_value, false));
}
WaitForHistograms({kTimingRunExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingRunExecutedInWorkletHistogram, 1);
expected_accesses.emplace_back(
AccessType::kDocumentAddModule, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForAddModule(out_script_url));
expected_accesses.emplace_back(
AccessType::kDocumentRun, MainFrameId(), origin_str,
SharedStorageEventParams::CreateForRun("test-operation",
blink::CloneableMessage()));
expected_accesses.emplace_back(AccessType::kWorkletKeys, MainFrameId(),
origin_str,
SharedStorageEventParams::CreateDefault());
expected_accesses.emplace_back(AccessType::kWorkletEntries, MainFrameId(),
origin_str,
SharedStorageEventParams::CreateDefault());
ExpectAccessObserved(expected_accesses);
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateWorklet_SameOrigin_Success) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"a.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
kEmptyAccessControlAllowOriginReplacement,
kEmptySharedStorageCrossOriginAllowedReplacement)));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
TestSharedStorageWorkletHost* worklet_host =
test_worklet_host_manager().GetAttachedWorkletHost();
EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
worklet_host->GetProcessHost());
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateWorklet_CrossOrigin_FailedCors) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
kEmptyAccessControlAllowOriginReplacement,
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
// The network error for `createWorklet()` won't be revealed to the
// cross-origin caller. But we can verify the error indirectly, by running a
// subsequent operation and checking the console error.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
new Promise((resolve, reject) => {
sharedStorage.createWorklet($1).then((worklet) => {
window.testWorklet = worklet;
resolve();
});
})
)",
module_script_url.spec())));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Expect the run() operation.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
window.testWorklet.run('test-operation', {
data: {
'set-key': 'key0',
'set-value': 'value0'
},
keepAlive: true
});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Cannot find operation name.",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
CreateWorklet_CrossOrigin_FailedSharedStorageWorkletAllowedResponseHeaderCheck) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
kEmptySharedStorageCrossOriginAllowedReplacement)));
// The network error for `createWorklet()` won't be revealed to the
// cross-origin caller. But we can verify the error indirectly, by running a
// subsequent operation and checking the console error.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
new Promise((resolve, reject) => {
sharedStorage.createWorklet($1).then((worklet) => {
window.testWorklet = worklet;
resolve();
});
})
)",
module_script_url.spec())));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Expect the run() operation.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
window.testWorklet.run('test-operation', {
data: {
'set-key': 'key0',
'set-value': 'value0'
},
keepAlive: true
});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Cannot find operation name.",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
}
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateWorklet_CrossOrigin_Success) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
TestSharedStorageWorkletHost* worklet_host =
test_worklet_host_manager().GetAttachedWorkletHost();
// The worklet host should reuse the main frame's process on Android without
// strict site isolation; otherwise, it should use a new process.
bool expected_use_new_process = AreAllSitesIsolatedForTesting();
bool actual_use_new_process =
(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess() !=
worklet_host->GetProcessHost());
EXPECT_EQ(expected_use_new_process, actual_use_new_process);
}
// Start a worklet under b.test (cross-origin to the main frame's origin), and
// then append a subframe under b.test. Assert that they share the same process.
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateWorkletAndSubframe_CrossOrigin) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
TestSharedStorageWorkletHost* worklet_host =
test_worklet_host_manager().GetAttachedWorkletHost();
GURL iframe_url = https_server()->GetURL("b.test", "/empty.thml");
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
EXPECT_EQ(worklet_host->GetProcessHost(),
iframe_node->current_frame_host()->GetProcess());
}
// Append a subframe under b.test (cross-origin to the main frame's origin), and
// then start a worklet under b.test. Assert that they share the same process.
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateSubframeAndWorklet_CrossOrigin) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL iframe_url = https_server()->GetURL("b.test", "/empty.thml");
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
TestSharedStorageWorkletHost* worklet_host =
test_worklet_host_manager().GetAttachedWorkletHost();
EXPECT_EQ(worklet_host->GetProcessHost(),
iframe_node->current_frame_host()->GetProcess());
}
// Start one worklet under b.test (cross-origin to the main frame's origin),
// and then start another worklet under b.test. Assert that they share the same
// process.
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CreateTwoWorklets_CrossOrigin) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
EXPECT_TRUE(ExecJs(shell(), JsReplace("sharedStorage.createWorklet($1)",
module_script_url.spec())));
std::vector<TestSharedStorageWorkletHost*> worklet_hosts =
test_worklet_host_manager().GetAttachedWorkletHosts();
EXPECT_EQ(worklet_hosts.size(), 2u);
EXPECT_EQ(worklet_hosts[0]->GetProcessHost(),
worklet_hosts[1]->GetProcessHost());
}
// Start a worklet under b.test via createWorklet(), and then start a worklet
// under b.test's iframe. Assert that the data stored in the first worklet can
// be retrieved in the second worklet.
IN_PROC_BROWSER_TEST_P(SharedStorageBrowserTest,
CrossOriginWorklet_VerifyDataOrigin) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL module_script_url = https_server()->GetURL(
"b.test", net::test_server::GetFilePathWithReplacements(
"/shared_storage/module_with_custom_header.js",
SharedStorageCrossOriginWorkletResponseHeaderReplacement(
"Access-Control-Allow-Origin: *",
"Shared-Storage-Cross-Origin-Worklet-Allowed: ?1")));
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
new Promise((resolve, reject) => {
sharedStorage.createWorklet($1).then((worklet) => {
window.testWorklet = worklet;
resolve();
});
})
)",
module_script_url.spec())));
// Expect the run() operation.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
window.testWorklet.run('test-operation', {
data: {
'set-key': 'key0',
'set-value': 'value0'
},
keepAlive: true
});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
GURL iframe_url = https_server()->GetURL("b.test", "/empty.thml");
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node->current_frame_host(), R"(
console.log(await sharedStorage.get('key0'));
)",
&out_script_url, /*expected_total_host_count=*/2);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("value0",
base::UTF16ToUTF8(console_observer.messages()[0].message));
}
INSTANTIATE_TEST_SUITE_P(All,
SharedStorageBrowserTest,
testing::Bool(),
describe_param);
class SharedStorageAllowURNsInIframesBrowserTest
: public SharedStorageBrowserTestBase,
public testing::WithParamInterface<bool> {
public:
SharedStorageAllowURNsInIframesBrowserTest() {
allow_urns_in_frames_feature_.InitAndEnableFeature(
blink::features::kAllowURNsInIframes);
fenced_frame_api_change_feature_.InitWithFeatureState(
blink::features::kFencedFramesAPIChanges, ResolveSelectURLToConfig());
}
bool ResolveSelectURLToConfig() override { return GetParam(); }
private:
base::test::ScopedFeatureList allow_urns_in_frames_feature_;
base::test::ScopedFeatureList fenced_frame_api_change_feature_;
};
IN_PROC_BROWSER_TEST_P(SharedStorageAllowURNsInIframesBrowserTest,
RenderSelectURLResultInIframe) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), *urn_uuid);
EXPECT_EQ(iframe_node->current_url(),
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
GURL new_page_url = https_server()->GetURL("c.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(iframe_node, JsReplace("top.location = $1", new_page_url)));
top_navigation_observer.Wait();
// After the top navigation, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
}
INSTANTIATE_TEST_SUITE_P(All,
SharedStorageAllowURNsInIframesBrowserTest,
testing::Bool(),
describe_param);
class SharedStorageFencedFrameInteractionBrowserTestBase
: public SharedStorageBrowserTestBase {
public:
using FencedFrameNavigationTarget = absl::variant<GURL, std::string>;
// TODO(crbug.com/40256120): This function should be removed. Use
// `CreateFencedFrame` in fenced_frame_test_util.h instead.
FrameTreeNode* CreateFencedFrame(FrameTreeNode* root,
const FencedFrameNavigationTarget& target) {
size_t initial_child_count = root->child_count();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(initial_child_count + 1, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(initial_child_count));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult result = NavigateFencedFrame(root, target);
observer.Wait();
return fenced_frame_root_node;
}
FrameTreeNode* CreateFencedFrame(const FencedFrameNavigationTarget& target) {
return CreateFencedFrame(PrimaryFrameTreeNodeRoot(), target);
}
EvalJsResult NavigateFencedFrame(FrameTreeNode* root,
const FencedFrameNavigationTarget& target) {
return EvalJs(
root,
absl::visit(base::Overloaded{
[](const GURL& url) {
return JsReplace(
"f.config = new FencedFrameConfig($1);", url);
},
[](const std::string& config) {
return JsReplace("f.config = window[$1]", config);
},
},
target));
}
// Precondition: There is exactly one existing fenced frame.
void NavigateExistingFencedFrame(FrameTreeNode* existing_fenced_frame,
const FencedFrameNavigationTarget& target) {
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
TestFrameNavigationObserver observer(existing_fenced_frame);
EXPECT_TRUE(ExecJs(root, "var f = document.querySelector('fencedframe');"));
EvalJsResult result = NavigateFencedFrame(root, target);
observer.Wait();
}
};
class SharedStorageFencedFrameInteractionBrowserTest
: public SharedStorageFencedFrameInteractionBrowserTestBase {
public:
bool ResolveSelectURLToConfig() override { return true; }
};
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_FinishBeforeStartingFencedFrameNavigation) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Start executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("Finish executing simple_module.js",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata: {
"click": "fenced_frames/report1.html"
}
},
{
url: "fenced_frames/title2.html"
}
],
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, std::log2(3));
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html"))));
GURL url0 = https_server()->GetURL("a.test", "/fenced_frames/title0.html");
GURL url1 = https_server()->GetURL("a.test", "/fenced_frames/title1.html");
GURL url2 = https_server()->GetURL("a.test", "/fenced_frames/title2.html");
EXPECT_EQ(6u, console_observer.messages().size());
EXPECT_EQ("Start executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ(base::StrCat({"[\"", url0.spec(), "\",\"", url1.spec(), "\",\"",
url2.spec(), "\"]"}),
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("{\"mockResult\":1}",
base::UTF16ToUTF8(console_observer.messages()[4].message));
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages()[5].message));
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult navigation_result = NavigateFencedFrame(
root, ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
observer.Wait();
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_FinishAfterStartingFencedFrameNavigation) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Configure the worklet host to defer processing the subsequent
// `selectURL()` response.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->set_should_defer_worklet_messages(true);
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata: {
"click": "fenced_frames/report1.html"
}
},
{
url: "fenced_frames/title2.html"
}
],
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult navigation_result = NavigateFencedFrame(
root, ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
// After the previous EvalJs, the NavigationRequest should have been created,
// but may not have begun. Wait for BeginNavigation() and expect it to be
// deferred on fenced frame url mapping.
NavigationRequest* request = fenced_frame_root_node->navigation_request();
if (!request->is_deferred_on_fenced_frame_url_mapping_for_testing()) {
base::RunLoop run_loop;
request->set_begin_navigation_callback_for_testing(
run_loop.QuitWhenIdleClosure());
run_loop.Run();
EXPECT_TRUE(request->is_deferred_on_fenced_frame_url_mapping_for_testing());
}
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
FencedFrameURLMappingTestPeer url_mapping_test_peer(&url_mapping);
EXPECT_TRUE(
url_mapping_test_peer.HasObserver(observed_urn_uuid.value(), request));
// Execute the deferred messages. This should finish the url mapping and
// resume the deferred navigation.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->ExecutePendingWorkletMessages();
observer.Wait();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, std::log2(3));
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html"))));
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
histogram_tester_.ExpectTotalCount(
"Storage.SharedStorage.Timing.UrlMappingDuringNavigation", 1);
}
// Tests that the URN from SelectURL() is valid in different
// context in the page, but it's not valid in a new page.
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_URNLifetime) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(url::Origin::Create(main_url));
ASSERT_TRUE(urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(*urn_uuid));
FrameTreeNode* iframe_node = PrimaryFrameTreeNodeRoot()->child_at(0);
// Navigate the iframe to about:blank.
TestFrameNavigationObserver observer(iframe_node);
EXPECT_TRUE(ExecJs(iframe_node, JsReplace("window.location.href=$1",
GURL(url::kAboutBlankURL))));
observer.Wait();
// Verify that the `urn_uuid` is still valid in the main page.
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
// Navigate to a new page. Verify that the `urn_uuid` is not valid in this
// new page.
GURL new_page_main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), new_page_main_url));
fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_NE(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
// Tests that if the URN mapping is not finished before the keep-alive timeout,
// the mapping will be considered to be failed when the timeout is reached.
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_NotFinishBeforeKeepAliveTimeout) {
// The test assumes pages get deleted after navigation. To ensure this,
// disable back/forward cache.
content::DisableBackForwardCacheForTesting(
shell()->web_contents(),
content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL url = https_server()->GetURL("a.test", kPageWithBlankIframePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHost* iframe =
PrimaryFrameTreeNodeRoot()->child_at(0)->current_frame_host();
EXPECT_TRUE(ExecJs(iframe, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
// Configure the worklet host to defer processing the subsequent
// `selectURL()` response.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->set_should_defer_worklet_messages(true);
EXPECT_TRUE(ExecJs(iframe, kGenerateURLsListScript));
EXPECT_TRUE(ExecJs(iframe, JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(iframe, R"(
(async function() {
const urls = generateUrls(8);
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
if (ResolveSelectURLToConfig()) {
// Preserve the config in a variable. It is then installed to the new fenced
// frame. Without this step, the config will be gone after navigating the
// iframe to about::blank.
EXPECT_TRUE(ExecJs(root, R"(var fenced_frame_config = document
.getElementById('test_iframe')
.contentWindow
.select_url_result;)"));
}
// Navigate away to let the subframe's worklet enter keep-alive.
NavigateIframeToURL(shell()->web_contents(), "test_iframe",
GURL(url::kAboutBlankURL));
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(1u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->WaitForWorkletResponses();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(2U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(1));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult navigation_result = NavigateFencedFrame(
root, ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("fenced_frame_config")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
// After the previous EvalJs, the NavigationRequest should have been created,
// but may not have begun. Wait for BeginNavigation() and expect it to be
// deferred on fenced frame url mapping.
NavigationRequest* request = fenced_frame_root_node->navigation_request();
if (!request->is_deferred_on_fenced_frame_url_mapping_for_testing()) {
base::RunLoop run_loop;
request->set_begin_navigation_callback_for_testing(
run_loop.QuitWhenIdleClosure());
run_loop.Run();
EXPECT_TRUE(request->is_deferred_on_fenced_frame_url_mapping_for_testing());
}
ASSERT_FALSE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_FALSE(fenced_frame_config.has_value());
// Fire the keep-alive timer. This will terminate the keep-alive, and the
// deferred navigation will resume to navigate to the default url (at index
// 0).
test_worklet_host_manager()
.GetKeepAliveWorkletHost()
->FireKeepAliveTimerNow();
EXPECT_EQ(0u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
observer.Wait();
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, 0.0);
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report0.html")),
Pair("mouse interaction",
https_server()->GetURL("a.test",
"/fenced_frames/report1.html"))));
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title0.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
// The worklet execution sequence for `selectURL()` doesn't complete, so the
// `kTimingSelectUrlExecutedInWorkletHistogram` histogram isn't recorded.
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
0);
// The worklet is destructed. The config corresponds to the unresolved URN is
// populated in the destructor of `SharedStorageWorkletHost`.
ASSERT_TRUE(config_observer.ConfigObserved());
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_WorkletReturnInvalidIndex) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata:
{
"click": "fenced_frames/report1.html"
}
},
{
url: "fenced_frames/title2.html"
}
],
{
data: {'mockResult': 3},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid->spec());
EXPECT_EQ(
"Promise resolved to a number outside the length of the input urls.",
base::UTF16ToUTF8(console_observer.messages().back().message));
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, std::log2(3));
EXPECT_TRUE(GetSharedStorageReportingMap(observed_urn_uuid.value()).empty());
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult navigation_result = NavigateFencedFrame(
root, ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
observer.Wait();
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title0.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_DuplicateUrl) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata:
{
"click": "fenced_frames/report1.html"
}
},
{
url: "fenced_frames/title2.html"
}
],
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
EXPECT_TRUE(metadata);
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin("a.test")));
EXPECT_DOUBLE_EQ(metadata->budget_to_charge, std::log2(3));
EXPECT_THAT(GetSharedStorageReportingMap(observed_urn_uuid.value()),
UnorderedElementsAre(
Pair("click", https_server()->GetURL(
"a.test", "/fenced_frames/report1.html"))));
FrameTreeNode* root = PrimaryFrameTreeNodeRoot();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EvalJsResult navigation_result = NavigateFencedFrame(
root, ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
observer.Wait();
EXPECT_EQ(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"),
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
FencedFrameNavigateSelf_NoBudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
TestFrameNavigationObserver observer(fenced_frame_root_node);
EXPECT_TRUE(ExecJs(fenced_frame_root_node, "location.reload()"));
observer.Wait();
// No budget withdrawal as the fenced frame did not initiate a top navigation.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
FencedFrameNavigateTop_BudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
GURL new_page_url = https_server()->GetURL("c.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(fenced_frame_root_node,
JsReplace("window.open($1, '_unfencedTop')", new_page_url)));
top_navigation_observer.Wait();
// After the top navigation, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
FencedFrameNavigateFromParentToRegularURLAndThenOpenPopup_NoBudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
GURL new_frame_url = https_server()->GetURL("c.test", kFencedFramePath);
TestFrameNavigationObserver observer(fenced_frame_root_node);
std::string navigate_fenced_frame_script = JsReplace(
"var f = document.getElementsByTagName('fencedframe')[0]; f.config = new "
"FencedFrameConfig($1);",
new_frame_url);
EXPECT_TRUE(ExecJs(shell(), navigate_fenced_frame_script));
observer.Wait();
GURL new_page_url = https_server()->GetURL("d.test", kSimplePagePath);
OpenPopup(fenced_frame_root_node, new_page_url, /*name=*/"");
// No budget withdrawal as the initial fenced frame was navigated away by its
// parent before it triggers a top navigation.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
url::Origin new_frame_origin = url::Origin::Create(new_frame_url);
EXPECT_DOUBLE_EQ(GetRemainingBudget(new_frame_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(new_frame_origin),
kBudgetAllowed);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
FencedFrameNavigateSelfAndThenNavigateTop_BudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
{
GURL new_frame_url = https_server()->GetURL("c.test", kFencedFramePath);
TestFrameNavigationObserver observer(fenced_frame_root_node);
EXPECT_TRUE(ExecJs(fenced_frame_root_node,
JsReplace("window.location.href=$1", new_frame_url)));
observer.Wait();
}
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
{
GURL new_page_url = https_server()->GetURL("d.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(fenced_frame_root_node,
JsReplace("window.open($1, '_unfencedTop')", new_page_url)));
top_navigation_observer.Wait();
}
// After the top navigation, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
// TODO(crbug.com/40233168): Reenable this test when it is possible to create a
// nested fenced frame with no reporting metadata, that can call _unfencedTop.
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
DISABLED_NestedFencedFrameNavigateTop_BudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
GURL nested_fenced_frame_url =
https_server()->GetURL("c.test", kFencedFramePath);
FrameTreeNode* nested_fenced_frame_root_node =
CreateFencedFrame(fenced_frame_root_node, nested_fenced_frame_url);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
GURL new_page_url = https_server()->GetURL("d.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(nested_fenced_frame_root_node,
JsReplace("window.open($1, '_unfencedTop')", new_page_url)));
top_navigation_observer.Wait();
// After the top navigation, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
NestedFencedFrameNavigateTop_BudgetWithdrawalFromTwoMetadata) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin_1 =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid_1 =
SelectFrom8URLsInContext(shared_storage_origin_1);
ASSERT_TRUE(urn_uuid_1.has_value());
FrameTreeNode* fenced_frame_root_node_1 = CreateFencedFrame(*urn_uuid_1);
url::Origin shared_storage_origin_2 =
url::Origin::Create(https_server()->GetURL("c.test", kSimplePagePath));
std::optional<GURL> urn_uuid_2 = SelectFrom8URLsInContext(
shared_storage_origin_2, fenced_frame_root_node_1);
ASSERT_TRUE(urn_uuid_2.has_value());
FrameTreeNode* fenced_frame_root_node_2 =
CreateFencedFrame(fenced_frame_root_node_1, *urn_uuid_2);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin_1), kBudgetAllowed);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin_2), kBudgetAllowed);
GURL new_page_url = https_server()->GetURL("d.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(fenced_frame_root_node_2,
JsReplace("window.open($1, '_unfencedTop')", new_page_url)));
top_navigation_observer.Wait();
// After the top navigation, log(8)=3 bits should have been withdrawn from
// both `shared_storage_origin_1` and `shared_storage_origin_2`.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin_1),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin_2),
kBudgetAllowed - 3);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
SelectURLNotAllowedInFencedFrameNotOriginatedFromSharedStorage) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
GURL fenced_frame_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
FrameTreeNode* fenced_frame_root_node_1 =
static_cast<RenderFrameHostImpl*>(
fenced_frame_test_helper_.CreateFencedFrame(
shell()->web_contents()->GetPrimaryMainFrame(), fenced_frame_url,
net::OK, blink::FencedFrame::DeprecatedFencedFrameMode::kDefault))
->frame_tree_node();
EXPECT_TRUE(ExecJs(fenced_frame_root_node_1,
JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
EvalJsResult result = EvalJs(fenced_frame_root_node_1, R"(
sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
)");
EXPECT_THAT(
result.error,
testing::HasSubstr(
"The \"shared-storage\" Permissions Policy denied the method on "
"window.sharedStorage."));
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURLNotAllowedInNestedFencedFrame) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin_1 =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid_1 =
SelectFrom8URLsInContext(shared_storage_origin_1);
ASSERT_TRUE(urn_uuid_1.has_value());
FrameTreeNode* fenced_frame_root_node_1 = CreateFencedFrame(*urn_uuid_1);
url::Origin shared_storage_origin_2 =
url::Origin::Create(https_server()->GetURL("c.test", kSimplePagePath));
std::optional<GURL> urn_uuid_2 = SelectFrom8URLsInContext(
shared_storage_origin_2, fenced_frame_root_node_1);
ASSERT_TRUE(urn_uuid_2.has_value());
FrameTreeNode* fenced_frame_root_node_2 =
CreateFencedFrame(fenced_frame_root_node_1, *urn_uuid_2);
EXPECT_TRUE(ExecJs(fenced_frame_root_node_2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(fenced_frame_root_node_2,
JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
EvalJsResult result = EvalJs(fenced_frame_root_node_2, R"(
sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
)");
EXPECT_THAT(result.error,
testing::HasSubstr(
"selectURL() is called in a context with a fenced frame "
"depth (2) exceeding the maximum allowed number (1)."));
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
IframeInFencedFrameNavigateTop_BudgetWithdrawal) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
GURL nested_fenced_frame_url =
https_server()->GetURL("c.test", kFencedFramePath);
FrameTreeNode* nested_fenced_frame_root_node =
CreateIFrame(fenced_frame_root_node, nested_fenced_frame_url);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
GURL new_page_url = https_server()->GetURL("d.test", kSimplePagePath);
TestNavigationObserver top_navigation_observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(nested_fenced_frame_root_node,
JsReplace("window.open($1, '_unfencedTop')", new_page_url)));
top_navigation_observer.Wait();
// After the top navigation, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
FencedFrame_PopupTwice_BudgetWithdrawalOnce) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(*urn_uuid);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
OpenPopup(fenced_frame_root_node,
https_server()->GetURL("c.test", kSimplePagePath), /*name=*/"");
// After the popup, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
OpenPopup(fenced_frame_root_node,
https_server()->GetURL("c.test", kSimplePagePath), /*name=*/"");
// The budget can only be withdrawn once for each urn_uuid.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
TwoFencedFrames_DifferentURNs_EachPopupOnce_BudgetWithdrawalTwice) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin = url::Origin::Create(main_url);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), kGenerateURLsListScript));
EXPECT_TRUE(ExecJs(shell(), "window.urls = generateUrls(8);"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
// There are 2 more "worklet operations": both `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(2);
TestSelectURLFencedFrameConfigObserver config_observer_1(
GetStoragePartition());
EvalJsResult result_1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result_1 = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: true
}
);
if (resolveSelectURLToConfig &&
!(select_url_result_1 instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result_1;
})()
)");
EXPECT_TRUE(result_1.error.empty());
const std::optional<GURL>& observed_urn_uuid_1 =
config_observer_1.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid_1.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid_1.value()));
TestSelectURLFencedFrameConfigObserver config_observer_2(
GetStoragePartition());
EvalJsResult result_2 = EvalJs(shell(), R"(
(async function() {
window.select_url_result_2 = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: true
}
);
if (resolveSelectURLToConfig &&
!(select_url_result_2 instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result_2;
})()
)");
EXPECT_TRUE(result_2.error.empty());
const std::optional<GURL>& observed_urn_uuid_2 =
config_observer_2.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid_2.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid_2.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer_1.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config_1 =
config_observer_1.GetConfig();
EXPECT_TRUE(fenced_frame_config_1.has_value());
EXPECT_EQ(fenced_frame_config_1->urn_uuid(), observed_urn_uuid_1.value());
ASSERT_TRUE(config_observer_2.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config_2 =
config_observer_2.GetConfig();
EXPECT_TRUE(fenced_frame_config_2.has_value());
EXPECT_EQ(fenced_frame_config_2->urn_uuid(), observed_urn_uuid_2.value());
FrameTreeNode* fenced_frame_root_node_1 = CreateFencedFrame(
ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result_1")
: FencedFrameNavigationTarget(observed_urn_uuid_1.value()));
FrameTreeNode* fenced_frame_root_node_2 = CreateFencedFrame(
ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result_2")
: FencedFrameNavigationTarget(observed_urn_uuid_2.value()));
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()),
kBudgetAllowed);
OpenPopup(fenced_frame_root_node_1,
https_server()->GetURL("b.test", kSimplePagePath), /*name=*/"");
// After the popup, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
OpenPopup(fenced_frame_root_node_2,
https_server()->GetURL("b.test", kSimplePagePath), /*name=*/"");
// After the popup, log(8)=3 bits should have been withdrawn from the
// original shared storage origin. The budget for `shared_storage_origin` can
// be charged once for each distinct URN, and therefore here it gets charged
// twice.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3 - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3 - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
2);
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameInteractionBrowserTest,
TwoFencedFrames_SameURNs_EachPopupOnce_BudgetWithdrawalOnce) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin = url::Origin::Create(main_url);
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_root_node_1 = CreateFencedFrame(*urn_uuid);
FrameTreeNode* fenced_frame_root_node_2 = CreateFencedFrame(*urn_uuid);
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin), kBudgetAllowed);
EXPECT_DOUBLE_EQ(
RemainingBudgetViaJSForFrame(PrimaryFrameTreeNodeRoot()->child_at(0)),
kBudgetAllowed);
OpenPopup(fenced_frame_root_node_1,
https_server()->GetURL("b.test", kSimplePagePath), /*name=*/"");
// After the popup, log(8)=3 bits should have been withdrawn from the
// original shared storage origin.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
OpenPopup(fenced_frame_root_node_2,
https_server()->GetURL("b.test", kSimplePagePath), /*name=*/"");
// The budget can only be withdrawn once for each urn_uuid.
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_InsufficientBudget) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin = url::Origin::Create(main_url);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), kGenerateURLsListScript));
EXPECT_TRUE(ExecJs(shell(), "window.urls = generateUrls(8);"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer_1(
GetStoragePartition());
// There are 2 more "worklet operations": both `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(2);
EvalJsResult result_1 = EvalJs(shell(), R"(
(async function() {
window.select_url_result_1 = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: true
}
);
if (resolveSelectURLToConfig &&
!(select_url_result_1 instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result_1;
})()
)");
EXPECT_TRUE(result_1.error.empty());
const std::optional<GURL>& observed_urn_uuid_1 =
config_observer_1.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid_1.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid_1.value()));
FrameTreeNode* fenced_frame_root_node_1 = CreateFencedFrame(
ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result_1")
: FencedFrameNavigationTarget(observed_urn_uuid_1.value()));
OpenPopup(fenced_frame_root_node_1,
https_server()->GetURL("b.test", kSimplePagePath), /*name=*/"");
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
TestSelectURLFencedFrameConfigObserver config_observer_2(
GetStoragePartition());
EvalJsResult result_2 = EvalJs(shell(), R"(
(async function() {
window.select_url_result_2 = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result_2 instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result_2;
})()
)");
EXPECT_TRUE(result_2.error.empty());
const std::optional<GURL>& observed_urn_uuid_2 =
config_observer_2.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid_2.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid_2.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer_1.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config_1 =
config_observer_1.GetConfig();
EXPECT_TRUE(fenced_frame_config_1.has_value());
EXPECT_EQ(fenced_frame_config_1->urn_uuid(), observed_urn_uuid_1.value());
ASSERT_TRUE(config_observer_2.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config_2 =
config_observer_2.GetConfig();
EXPECT_TRUE(fenced_frame_config_2.has_value());
EXPECT_EQ(fenced_frame_config_2->urn_uuid(), observed_urn_uuid_2.value());
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
// The failed mapping due to insufficient budget (i.e. `urn_uuid_2`) should
// not incur any budget withdrawal on subsequent top navigation from inside
// the fenced frame.
FrameTreeNode* fenced_frame_root_node_2 = CreateFencedFrame(
ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result_2")
: FencedFrameNavigationTarget(observed_urn_uuid_2.value()));
OpenPopup(fenced_frame_root_node_2,
https_server()->GetURL("c.test", kSimplePagePath), /*name=*/"");
EXPECT_DOUBLE_EQ(GetRemainingBudget(shared_storage_origin),
kBudgetAllowed - 3);
EXPECT_DOUBLE_EQ(RemainingBudgetViaJSForOrigin(shared_storage_origin),
kBudgetAllowed - 3);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
2);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget, 1);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSiteNavigationBudget,
1);
}
// When number of urn mappings limit has been reached, subsequent `selectURL()`
// calls will fail.
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
SelectURL_Fails_ExceedNumOfUrnMappingsLimit) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// `selectURL()` succeeds when map is not full.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
EXPECT_TRUE(ExecJs(shell(), "window.keepWorklet = true;"));
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
std::string select_url_script = R"(
sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: keepWorklet
}
);
)";
EXPECT_TRUE(ExecJs(shell(), select_url_script));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
FencedFrameURLMapping& fenced_frame_url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
FencedFrameURLMappingTestPeer fenced_frame_url_mapping_test_peer(
&fenced_frame_url_mapping);
// Fill the map until its size reaches the limit.
GURL url("https://a.test");
fenced_frame_url_mapping_test_peer.FillMap(url);
// No need to keep the worklet after the next select operation.
EXPECT_TRUE(ExecJs(shell(), "window.keepWorklet = false;"));
EvalJsResult extra_result = EvalJs(shell(), select_url_script);
// `selectURL()` fails when map is full.
std::string expected_error = base::StrCat(
{"a JavaScript error: \"Error: ",
"sharedStorage.selectURL() failed because number of urn::uuid to url ",
"mappings has reached the limit.\"\n"});
EXPECT_EQ(expected_error, extra_result.error);
}
class SharedStorageFencedFrameDocumentGetBrowserTest
: public SharedStorageFencedFrameInteractionBrowserTest {
public:
SharedStorageFencedFrameDocumentGetBrowserTest() {
fenced_frame_feature_.InitAndEnableFeature(
/*feature=*/
blink::features::kFencedFramesLocalUnpartitionedDataAccess);
}
private:
base::test::ScopedFeatureList fenced_frame_feature_;
};
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
GetAllowedInNetworkRestrictedFencedFrame) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
(async () => {
await window.fence.disableUntrustedNetwork();
return sharedStorage.get('test');
})();
)");
EXPECT_EQ(get_result, "apple");
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
GetRejectsInFencedFrameWithoutRestrictedNetwork) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
sharedStorage.get('test');
)");
EXPECT_THAT(
get_result.error,
testing::HasSubstr(
"sharedStorage.get() is not allowed in a fenced frame until network "
"access for it and all descendent frames has been revoked with "
"window.fence.disableUntrustedNetwork()"));
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
GetInFencedFrameOnlyFetchesValuesFromCurrentOrigin) {
// sharedStorage.set() for a.test
GURL main_frame_url1 = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url1));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
// sharedStorage.set() for b.test
GURL main_frame_url2 = https_server()->GetURL("b.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url2));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'banana');
)"));
// An a.test fenced frame embedded in b.test should only read a.test's set
// values.
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
(async () => {
await window.fence.disableUntrustedNetwork();
return sharedStorage.get('test');
})();
)");
EXPECT_EQ(get_result, "apple");
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
GetRejectsInMainFrame) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
EvalJsResult get_result_main_frame = EvalJs(shell(), R"(
sharedStorage.get('test');
)");
EXPECT_THAT(
get_result_main_frame.error,
testing::HasSubstr("Cannot call get() outside of a fenced frame."));
}
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
GetRejectsInIFrame) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
FrameTreeNode* iframe_root =
CreateIFrame(PrimaryFrameTreeNodeRoot(), main_frame_url);
EvalJsResult get_result_iframe = EvalJs(iframe_root, R"(
sharedStorage.get('test');
)");
EXPECT_THAT(
get_result_iframe.error,
testing::HasSubstr("Cannot call get() outside of a fenced frame."));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameDocumentGetBrowserTest,
GetAllowedInNetworkRestrictedNestedFencedFrameIfParentStillHasNetwork) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
GURL fenced_frame_url = https_server()->GetURL("a.test", kFencedFramePath);
// The parent fenced frame never calls window.fence.disableUntrustedNetwork().
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(fenced_frame_url);
FrameTreeNode* nested_fenced_frame_root_node =
CreateFencedFrame(fenced_frame_root_node, fenced_frame_url);
EvalJsResult get_result = EvalJs(nested_fenced_frame_root_node, R"(
(async () => {
await window.fence.disableUntrustedNetwork();
return sharedStorage.get('test');
})();
)");
EXPECT_EQ(get_result, "apple");
}
IN_PROC_BROWSER_TEST_F(
SharedStorageFencedFrameDocumentGetBrowserTest,
GetNotAllowedInNetworkRestrictedParentFencedFrameIfChildStillHasNetwork) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.set('test', 'apple');
)"));
GURL fenced_frame_url = https_server()->GetURL("a.test", kFencedFramePath);
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(fenced_frame_url);
CreateFencedFrame(fenced_frame_root_node, fenced_frame_url);
// Note that we do *not* await the call to disableUntrustedNetwork, because we
// need to operate in the top frame while the nested frame still hasn't
// disabled network access.
EXPECT_TRUE(ExecJs(fenced_frame_root_node, R"(
(async () => {
window.fence.disableUntrustedNetwork();
})();
)"));
// Wait before calling sharedStorage.get() in case the fenced frame was given
// access to Shared Storage without disableUntrustedNetwork() actually
// resolving.
base::RunLoop disable_network_wait;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, disable_network_wait.QuitClosure(), base::Milliseconds(500));
disable_network_wait.Run();
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
sharedStorage.get('test');
)");
EXPECT_THAT(
get_result.error,
testing::HasSubstr(
"sharedStorage.get() is not allowed in a fenced frame until network "
"access for it and all descendent frames has been revoked with "
"window.fence.disableUntrustedNetwork()"));
}
class SharedStorageSelectURLNotAllowedInFencedFrameBrowserTest
: public SharedStorageFencedFrameInteractionBrowserTest {
public:
SharedStorageSelectURLNotAllowedInFencedFrameBrowserTest() {
shared_storage_feature_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{blink::features::kSharedStorageAPI,
{{"SharedStorageBitBudget", base::NumberToString(kBudgetAllowed)},
{"SharedStorageMaxAllowedFencedFrameDepthForSelectURL", "0"}}}},
/*disabled_features=*/{});
}
private:
base::test::ScopedFeatureList shared_storage_feature_;
};
IN_PROC_BROWSER_TEST_F(SharedStorageSelectURLNotAllowedInFencedFrameBrowserTest,
SelectURLNotAllowedInFencedFrame) {
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
url::Origin shared_storage_origin =
url::Origin::Create(https_server()->GetURL("b.test", kSimplePagePath));
std::optional<GURL> urn_uuid =
SelectFrom8URLsInContext(shared_storage_origin);
ASSERT_TRUE(urn_uuid.has_value());
FrameTreeNode* fenced_frame_node = CreateFencedFrame(*urn_uuid);
EXPECT_TRUE(ExecJs(fenced_frame_node, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)"));
EXPECT_EQ(2u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
EXPECT_EQ(0u, test_worklet_host_manager().GetKeepAliveWorkletHostsCount());
EXPECT_TRUE(ExecJs(fenced_frame_node,
JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
EvalJsResult result = EvalJs(fenced_frame_node, R"(
sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
}
],
{
data: {'mockResult': 0},
resolveToConfig: resolveSelectURLToConfig
}
);
)");
EXPECT_THAT(result.error,
testing::HasSubstr(
"selectURL() is called in a context with a fenced frame "
"depth (1) exceeding the maximum allowed number (0)."));
}
class SharedStorageReportEventBrowserTest
: public SharedStorageFencedFrameInteractionBrowserTest {
void FinishSetup() override {
https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
}
};
IN_PROC_BROWSER_TEST_F(SharedStorageReportEventBrowserTest,
SelectURL_ReportEvent) {
net::test_server::ControllableHttpResponse response1(
https_server(), "/fenced_frames/report1.html");
net::test_server::ControllableHttpResponse response2(
https_server(), "/fenced_frames/report2.html");
ASSERT_TRUE(https_server()->Start());
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
url::Origin shared_storage_origin = url::Origin::Create(main_url);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
TestSelectURLFencedFrameConfigObserver config_observer(GetStoragePartition());
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), R"(
(async function() {
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: "fenced_frames/title0.html"
},
{
url: "fenced_frames/title1.html",
reportingMetadata: {
"click": "fenced_frames/report1.html",
"mouse interaction": "fenced_frames/report2.html"
}
}
],
{
data: {'mockResult': 1},
resolveToConfig: resolveSelectURLToConfig
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
EXPECT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(
ResolveSelectURLToConfig()
? FencedFrameNavigationTarget("select_url_result")
: FencedFrameNavigationTarget(observed_urn_uuid.value()));
std::string event_data1 = "this is a click";
EXPECT_TRUE(
ExecJs(fenced_frame_root_node,
JsReplace("window.fence.reportEvent({"
" eventType: 'click',"
" eventData: $1,"
" destination: ['shared-storage-select-url']});",
event_data1)));
response1.WaitForRequest();
EXPECT_EQ(response1.http_request()->content, event_data1);
std::string event_data2 = "this is a mouse interaction";
EXPECT_TRUE(
ExecJs(fenced_frame_root_node,
JsReplace("window.fence.reportEvent({"
" eventType: 'mouse interaction',"
" eventData: $1,"
" destination: ['shared-storage-select-url']});",
event_data2)));
response2.WaitForRequest();
EXPECT_EQ(response2.http_request()->content, event_data2);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
1);
}
class SharedStoragePrivateAggregationDisabledBrowserTest
: public SharedStorageBrowserTestBase {
public:
SharedStoragePrivateAggregationDisabledBrowserTest() {
private_aggregation_feature_.InitAndDisableFeature(
blink::features::kPrivateAggregationApi);
}
private:
base::test::ScopedFeatureList private_aggregation_feature_;
};
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationDisabledBrowserTest,
PrivateAggregationNotDefined) {
EXPECT_TRUE(NavigateToURL(shell(),
https_server()->GetURL("a.test", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url);
ASSERT_EQ(1u, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages()[0].message),
testing::HasSubstr("privateAggregation is not defined"));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
}
class SharedStoragePrivateAggregationDisabledForSharedStorageOnlyBrowserTest
: public SharedStorageBrowserTestBase {
public:
SharedStoragePrivateAggregationDisabledForSharedStorageOnlyBrowserTest() {
private_aggregation_feature_.InitAndEnableFeatureWithParameters(
blink::features::kPrivateAggregationApi,
{{"enabled_in_shared_storage", "false"}});
}
private:
base::test::ScopedFeatureList private_aggregation_feature_;
};
IN_PROC_BROWSER_TEST_F(
SharedStoragePrivateAggregationDisabledForSharedStorageOnlyBrowserTest,
PrivateAggregationNotDefined) {
EXPECT_TRUE(NavigateToURL(shell(),
https_server()->GetURL("a.test", kSimplePagePath)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url);
ASSERT_EQ(1u, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages()[0].message),
testing::HasSubstr("privateAggregation is not defined"));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
}
class SharedStoragePrivateAggregationEnabledBrowserTest
: public SharedStorageBrowserTestBase {
public:
// TODO(alexmt): Consider factoring out along with FLEDGE definition.
class TestPrivateAggregationManagerImpl
: public PrivateAggregationManagerImpl {
public:
TestPrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host)
: PrivateAggregationManagerImpl(std::move(budgeter),
std::move(host),
/*storage_partition=*/nullptr) {}
};
SharedStoragePrivateAggregationEnabledBrowserTest() {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kPrivateAggregationApi,
blink::features::kSharedStorageAPIM118},
/*disabled_features=*/{});
}
void SetUpOnMainThread() override {
SharedStorageBrowserTestBase::SetUpOnMainThread();
a_test_origin_ = https_server()->GetOrigin("a.test");
auto* storage_partition_impl =
static_cast<StoragePartitionImpl*>(GetStoragePartition());
private_aggregation_host_ = new PrivateAggregationHost(
/*on_report_request_details_received=*/mock_callback_.Get(),
storage_partition_impl->browser_context());
storage_partition_impl->OverridePrivateAggregationManagerForTesting(
std::make_unique<TestPrivateAggregationManagerImpl>(
std::make_unique<MockPrivateAggregationBudgeter>(),
base::WrapUnique<PrivateAggregationHost>(
private_aggregation_host_.get())));
EXPECT_TRUE(NavigateToURL(
shell(), https_server()->GetURL("a.test", kSimplePagePath)));
}
void TearDownOnMainThread() override {
private_aggregation_host_ = nullptr;
SharedStorageBrowserTestBase::TearDownOnMainThread();
}
void MakeMockPrivateAggregationShellContentBrowserClient() override {
browser_client_ =
std::make_unique<MockPrivateAggregationShellContentBrowserClient>();
}
const base::MockRepeatingCallback<
void(PrivateAggregationHost::ReportRequestGenerator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>,
PrivateAggregationBudgetKey,
PrivateAggregationBudgeter::BudgetDeniedBehavior)>&
mock_callback() {
return mock_callback_;
}
protected:
url::Origin a_test_origin_;
private:
raw_ptr<PrivateAggregationHost> private_aggregation_host_;
base::test::ScopedFeatureList scoped_feature_list_;
base::MockRepeatingCallback<void(
PrivateAggregationHost::ReportRequestGenerator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>,
PrivateAggregationBudgetKey,
PrivateAggregationBudgeter::BudgetDeniedBehavior)>
mock_callback_;
};
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
BasicTest) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_TRUE(request.additional_fields().empty());
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kDontSendReport);
run_loop.Quit();
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url);
EXPECT_TRUE(console_observer.messages().empty());
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
RejectedTest) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: -1n, value: 2});
)",
&out_script_url);
ASSERT_EQ(1u, console_observer.messages().size());
EXPECT_THAT(
base::UTF16ToUTF8(console_observer.messages()[0].message),
testing::HasSubstr(
"contribution['bucket'] is negative or does not fit in 128 bits"));
EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kError,
console_observer.messages()[0].log_level);
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
MultipleRequests) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 2u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.payload_contents().contributions[1].bucket, 3);
EXPECT_EQ(request.payload_contents().contributions[1].value, 4);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kDontSendReport);
run_loop.Quit();
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
privateAggregation.contributeToHistogram({bucket: 3n, value: 4});
)",
&out_script_url);
EXPECT_TRUE(console_observer.messages().empty());
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
TimeoutBeforeOperationFinish) {
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kSendNullReport);
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
// Run an operation that returns a promise that never resolves.
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return new Promise(() => {});
)",
&out_script_url, /*expected_total_host_count=*/1u,
/*keep_alive_after_operation=*/true,
/*context_id=*/"example_context_id",
/*out_error=*/nullptr,
/*wait_for_operation_finish=*/false);
// Wait for 5 seconds for the timeout to be reached.
{
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Seconds(5));
run_loop.Run();
}
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
ContextId) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kSendNullReport);
run_loop.Quit();
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url, /*expected_total_host_count=*/1u,
/*keep_alive_after_operation=*/true,
/*context_id=*/"example_context_id");
EXPECT_TRUE(console_observer.messages().empty());
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
ContextIdEmptyString) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "")));
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kSendNullReport);
run_loop.Quit();
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url, /*expected_total_host_count=*/1u,
/*keep_alive_after_operation=*/true,
/*context_id=*/
"");
EXPECT_TRUE(console_observer.messages().empty());
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
ContextIdMaxAllowedLength) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(mock_callback(), Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id",
"an_example_of_a_context_id_with_the_"
"exact_maximum_allowed_length")));
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kSendNullReport);
run_loop.Quit();
}));
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(
shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url, /*expected_total_host_count=*/1u,
/*keep_alive_after_operation=*/true,
/*context_id=*/
"an_example_of_a_context_id_with_the_exact_maximum_allowed_length");
EXPECT_TRUE(console_observer.messages().empty());
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
ContextIdTooLong) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_CALL(mock_callback(), Run).Times(0);
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll))
.Times(0);
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage))
.Times(0);
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
std::string out_error;
ExecuteScriptInWorklet(
shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url, /*expected_total_host_count=*/1u,
/*keep_alive_after_operation=*/true,
/*context_id=*/
"this_is_an_example_of_a_context_id_that_is_too_long_to_be_allowed",
&out_error);
EXPECT_THAT(
out_error,
testing::HasSubstr("Error: contextId length cannot be larger than 64"));
EXPECT_TRUE(console_observer.messages().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
PrivateAggregationPermissionsPolicyNone) {
GURL url = https_server()->GetURL(
"a.test",
"/shared_storage/private_aggregation_permissions_policy_none.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_THAT(
base::UTF16ToUTF8(console_observer.messages()[0].message),
testing::HasSubstr("The \"private-aggregation\" Permissions Policy "
"denied the method on privateAggregation"));
}
// This is a regression test for crbug.com/1428110.
IN_PROC_BROWSER_TEST_F(SharedStoragePrivateAggregationEnabledBrowserTest,
SimultaneousOperationsReportsArentBatchedTogether) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_CALL(browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client(),
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kPrivateAggregationApiSharedStorage));
ON_CALL(browser_client(), IsPrivateAggregationAllowed)
.WillByDefault(testing::Return(true));
ON_CALL(browser_client(), IsSharedStorageAllowed)
.WillByDefault(testing::Return(true));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/slow_and_fast_module.js');
)"));
EXPECT_EQ(1u, test_worklet_host_manager().GetAttachedWorkletHostsCount());
base::RunLoop run_loop;
base::RepeatingClosure barrier =
base::BarrierClosure(/*num_closures=*/3, run_loop.QuitClosure());
int num_one_contribution_reports = 0;
EXPECT_CALL(mock_callback(), Run)
.Times(3)
.WillRepeatedly(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
if (request.payload_contents().contributions.size() == 1u) {
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 3);
EXPECT_EQ(request.payload_contents().contributions[0].value, 1);
++num_one_contribution_reports;
} else {
ASSERT_EQ(request.payload_contents().contributions.size(), 2u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 1);
EXPECT_EQ(request.payload_contents().contributions[1].bucket, 2);
EXPECT_EQ(request.payload_contents().contributions[1].value, 1);
}
EXPECT_EQ(request.shared_info().reporting_origin, a_test_origin_);
EXPECT_EQ(budget_key.origin(), a_test_origin_);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kDontSendReport);
barrier.Run();
}));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('slow-operation', {keepAlive: true});
sharedStorage.run('slow-operation', {keepAlive: true});
sharedStorage.run('fast-operation');
)"));
run_loop.Run();
// Ensures we saw exactly one report for the fast operation (and therefore two
// for the slow operations).
EXPECT_EQ(num_one_contribution_reports, 1);
}
class SharedStorageSelectURLLimitBrowserTest
: public SharedStorageBrowserTestBase,
public testing::WithParamInterface<std::tuple<bool, bool>> {
public:
SharedStorageSelectURLLimitBrowserTest() {
if (LimitSelectURLCalls()) {
select_url_limit_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{blink::features::kSharedStorageSelectURLLimit,
{{"SharedStorageSelectURLBitBudgetPerPageLoad",
base::NumberToString(kSelectURLOverallBitBudget)},
{"SharedStorageSelectURLBitBudgetPerSitePerPageLoad",
base::NumberToString(kSelectURLSiteBitBudget)}}}},
/*disabled_features=*/{});
} else {
select_url_limit_feature_list_.InitAndDisableFeature(
blink::features::kSharedStorageSelectURLLimit);
}
fenced_frame_api_change_feature_.InitWithFeatureState(
blink::features::kFencedFramesAPIChanges, ResolveSelectURLToConfig());
}
bool LimitSelectURLCalls() const { return std::get<0>(GetParam()); }
bool ResolveSelectURLToConfig() override { return std::get<1>(GetParam()); }
// Precondition: `addModule('shared_storage/simple_module.js')` has been
// called in the main frame.
void RunSuccessfulSelectURLInMainFrame(
std::string host_str,
int num_urls,
WebContentsConsoleObserver* console_observer) {
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(shell(), host_str,
num_urls);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url = https_server()->GetURL(
host_str, base::StrCat({"/fenced_frames/title",
base::NumberToString(num_urls - 1), ".html"}));
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, std::log2(num_urls));
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer->messages().back().message));
}
// Precondition: `addModule('shared_storage/simple_module.js')` has NOT been
// called in `iframe_node`.
void RunSuccessfulSelectURLInIframe(
FrameTreeNode* iframe_node,
int num_urls,
WebContentsConsoleObserver* console_observer) {
std::string host_str =
iframe_node->current_frame_host()->GetLastCommittedURL().host();
EXPECT_TRUE(ExecJs(iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(iframe_node, host_str,
num_urls);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url = https_server()->GetURL(
host_str, base::StrCat({"/fenced_frames/title",
base::NumberToString(num_urls - 1), ".html"}));
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, std::log2(num_urls));
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer->messages().back().message));
}
// Precondition: `addModule('shared_storage/simple_module.js')` has been
// called in the `execution_target`.
std::optional<std::pair<GURL, double>>
RunSelectURLExtractingMappedURLAndBudgetToCharge(
const ToRenderFrameHost& execution_target,
std::string host_str,
int num_urls) {
TestSelectURLFencedFrameConfigObserver config_observer(
GetStoragePartition());
// There is 1 "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(execution_target.render_frame_host())
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = RunSelectURLScript(execution_target, num_urls);
EXPECT_TRUE(result.error.empty()) << result.error;
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
if (!observed_urn_uuid.has_value()) {
return std::nullopt;
}
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
if (!ResolveSelectURLToConfig()) {
EXPECT_EQ(result.ExtractString(), observed_urn_uuid->spec());
}
test_worklet_host_manager()
.GetAttachedWorkletHostForFrame(execution_target.render_frame_host())
->WaitForWorkletResponses();
const std::optional<FencedFrameConfig>& config =
config_observer.GetConfig();
if (!config.has_value()) {
return std::nullopt;
}
EXPECT_TRUE(config->mapped_url().has_value());
SharedStorageBudgetMetadata* metadata =
GetSharedStorageBudgetMetadata(observed_urn_uuid.value());
if (!metadata) {
return std::nullopt;
}
EXPECT_EQ(metadata->site,
net::SchemefulSite(https_server()->GetOrigin(host_str)));
return std::make_pair(config->mapped_url()->GetValueIgnoringVisibility(),
metadata->budget_to_charge);
}
private:
EvalJsResult RunSelectURLScript(const ToRenderFrameHost& execution_target,
int num_urls,
bool keep_alive_after_operation = true) {
EXPECT_TRUE(ExecJs(execution_target, kGenerateURLsListScript));
EXPECT_TRUE(
ExecJs(execution_target, JsReplace("window.numUrls = $1;", num_urls)));
EXPECT_TRUE(ExecJs(execution_target,
JsReplace("window.resolveSelectURLToConfig = $1;",
ResolveSelectURLToConfig())));
EXPECT_TRUE(ExecJs(
execution_target,
JsReplace("window.keepWorklet = $1;", keep_alive_after_operation)));
EvalJsResult result = EvalJs(execution_target, R"(
(async function() {
const urls = generateUrls(numUrls);
window.select_url_result = await sharedStorage.selectURL(
'test-url-selection-operation',
urls,
{
data: {'mockResult': numUrls - 1},
resolveToConfig: resolveSelectURLToConfig,
keepAlive: keepWorklet
}
);
if (resolveSelectURLToConfig &&
!(select_url_result instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.select_url_result;
})()
)");
return result;
}
base::test::ScopedFeatureList select_url_limit_feature_list_;
base::test::ScopedFeatureList fenced_frame_api_change_feature_;
};
INSTANTIATE_TEST_SUITE_P(All,
SharedStorageSelectURLLimitBrowserTest,
testing::Combine(testing::Bool(), testing::Bool()),
[](const auto& info) {
return base::StrCat(
{"LimitSelectURLCalls",
std::get<0>(info.param) ? "Enabled"
: "Disabled",
"_ResolveSelectURLTo",
std::get<1>(info.param) ? "Config" : "URN"});
});
IN_PROC_BROWSER_TEST_P(SharedStorageSelectURLLimitBrowserTest,
SelectURL_MainFrame_SameEntropy_SiteLimitReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// This test relies on the assumption that `kSelectURLOverallBitBudget` is set
// to be greater than or equal to `kSelectURLSiteBitBudget`.
EXPECT_GE(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget);
// Here each call to `selectURL()` will have 8 input URLs, and hence
// 3 = log2(8) bits of entropy.
int call_limit = kSelectURLSiteBitBudget / 3;
for (int i = 0; i < call_limit; i++) {
RunSuccessfulSelectURLInMainFrame("a.test", /*num_urls=*/8,
&console_observer);
}
if (LimitSelectURLCalls()) {
// The limit for `selectURL()` has now been reached for "a.test". Make one
// more call, which will return the default URL due to insufficient site
// pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(shell(), "a.test",
/*num_urls=*/8);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInMainFrame("a.test", /*num_urls=*/8,
&console_observer);
}
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
call_limit + 1);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
call_limit);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
1);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
call_limit + 1);
}
}
IN_PROC_BROWSER_TEST_P(SharedStorageSelectURLLimitBrowserTest,
SelectURL_MainFrame_DifferentEntropy_SiteLimitReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// This test relies on the assumptions that `kSelectURLOverallBitBudget` is
// set to be greater than or equal to `kSelectURLSiteBitBudget` and that the
// latter is at least 3.
EXPECT_GE(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget);
EXPECT_GE(kSelectURLSiteBitBudget, 3);
// Here the first call to `selectURL()` will have 8 input URLs, and hence
// 3 = log2(8) bits of entropy, and the subsequent calls will each have 4
// input URLs, and hence 2 = log2(4) bits of entropy.
int input4_call_limit = (kSelectURLSiteBitBudget - 3) / 2;
RunSuccessfulSelectURLInMainFrame("a.test", /*num_urls=*/8,
&console_observer);
for (int i = 0; i < input4_call_limit; i++) {
RunSuccessfulSelectURLInMainFrame("a.test", /*num_urls=*/4,
&console_observer);
}
if (LimitSelectURLCalls()) {
// The limit for `selectURL()` has now been reached for "a.test". Make one
// more call, which will return the default URL due to insufficient site
// pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(shell(), "a.test",
/*num_urls=*/4);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInMainFrame("a.test", /*num_urls=*/4,
&console_observer);
}
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
input4_call_limit + 2);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
input4_call_limit + 1);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
1);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
input4_call_limit + 2);
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageSelectURLLimitBrowserTest,
SelectURL_IframesSharingCommonSite_SameEntropy_SiteLimitReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// This test relies on the assumption that `kSelectURLOverallBitBudget` is set
// to be greater than or equal to `kSelectURLSiteBitBudget`.
EXPECT_GE(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget);
// Here each call to `selectURL()` will have 8 input URLs, and hence
// 3 = log2(8) bits of entropy.
int call_limit = kSelectURLSiteBitBudget / 3;
GURL iframe_url = https_server()->GetURL("b.test", kSimplePagePath);
for (int i = 0; i < call_limit; i++) {
// Create a new iframe.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(iframe_node, /*num_urls=*/8,
&console_observer);
}
// Create a new iframe.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
if (LimitSelectURLCalls()) {
EXPECT_TRUE(ExecJs(iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// The limit for `selectURL()` has now been reached for "b.test". Make one
// more call, which will return the default URL due to insufficient site
// pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(iframe_node, "b.test",
/*num_urls=*/8);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL("b.test", "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInIframe(iframe_node, /*num_urls=*/8,
&console_observer);
}
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
call_limit + 1);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
call_limit);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
1);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
call_limit + 1);
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageSelectURLLimitBrowserTest,
SelectURL_IframesSharingCommonSite_DifferentEntropy_SiteLimitReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL iframe_url = https_server()->GetURL("b.test", kSimplePagePath);
// Create a new iframe.
FrameTreeNode* first_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(first_iframe_node, /*num_urls=*/8,
&console_observer);
// This test relies on the assumptions that `kSelectURLOverallBitBudget` is
// set to be greater than or equal to `kSelectURLSiteBitBudget` and that the
// latter is at least 3.
EXPECT_GE(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget);
EXPECT_GE(kSelectURLSiteBitBudget, 3);
// Here the first call to `selectURL()` will have 8 input URLs, and hence
// 3 = log2(8) bits of entropy, and the subsequent calls will each have 4
// input URLs, and hence 2 = log2(4) bits of entropy.
int input4_call_limit = (kSelectURLSiteBitBudget - 3) / 2;
for (int i = 0; i < input4_call_limit; i++) {
// Create a new iframe.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(iframe_node, /*num_urls=*/4,
&console_observer);
}
// Create a new iframe.
FrameTreeNode* last_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
if (LimitSelectURLCalls()) {
EXPECT_TRUE(ExecJs(last_iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// The limit for `selectURL()` has now been reached for "b.test". Make one
// more call, which will return the default URL due to insufficient site
// pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(last_iframe_node,
"b.test",
/*num_urls=*/4);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL("b.test", "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInIframe(last_iframe_node, /*num_urls=*/4,
&console_observer);
}
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
input4_call_limit + 2);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
input4_call_limit + 1);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
1);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
input4_call_limit + 2);
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageSelectURLLimitBrowserTest,
SelectURL_IframesDifferentSite_SameEntropy_OverallLimitNotReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// This test relies on the assumption that `kSelectURLOverallBitBudget` is set
// to be strictly greater than `kSelectURLSiteBitBudget`, enough for at
// least one 8-URL call to `selectURL()` beyond the per-site limit.
EXPECT_GE(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget + 3);
// Here each call to `selectURL()` will have 8 input URLs, and hence
// 3 = log2(8) bits of entropy.
int per_site_call_limit = kSelectURLSiteBitBudget / 3;
GURL iframe_url1 = https_server()->GetURL("b.test", kSimplePagePath);
for (int i = 0; i < per_site_call_limit; i++) {
// Create a new iframe.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url1);
RunSuccessfulSelectURLInIframe(iframe_node, /*num_urls=*/8,
&console_observer);
}
// Create a new iframe.
FrameTreeNode* penultimate_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url1);
if (LimitSelectURLCalls()) {
EXPECT_TRUE(ExecJs(penultimate_iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// The limit for `selectURL()` has now been reached for "b.test". Make one
// more call, which will return the default URL due to insufficient site
// pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(
penultimate_iframe_node, "b.test",
/*num_urls=*/4);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL("b.test", "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInIframe(penultimate_iframe_node,
/*num_urls=*/4, &console_observer);
}
// Create a new iframe with a different site.
GURL iframe_url2 = https_server()->GetURL("c.test", kSimplePagePath);
FrameTreeNode* last_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url2);
// If enabled, the limit for `selectURL()` has now been reached for "b.test",
// but not for "c.test". Make one more call, which will be successful
// regardless of whether the limit is enabled.
RunSuccessfulSelectURLInIframe(last_iframe_node, /*num_urls=*/8,
&console_observer);
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(kTimingSelectUrlExecutedInWorkletHistogram,
per_site_call_limit + 2);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
per_site_call_limit + 1);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
1);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
per_site_call_limit + 2);
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageSelectURLLimitBrowserTest,
SelectURL_IframesDifferentSite_DifferentEntropy_OverallLimitReached) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// This test relies on the assumptions that `kSelectURLOverallBitBudget` is
// set to be strictly greater than `kSelectURLSiteBitBudget` and that the
// latter is at least 3.
EXPECT_GT(kSelectURLOverallBitBudget, kSelectURLSiteBitBudget);
EXPECT_GE(kSelectURLSiteBitBudget, 3);
int num_site_limit = kSelectURLOverallBitBudget / kSelectURLSiteBitBudget;
// We will run out of chars if we have too many sites.
EXPECT_LT(num_site_limit, 25);
// For each site, the first call to `selectURL()` will have 8 input URLs,
// and hence 3 = log2(8) bits of entropy, whereas the subsequent calls for
// that site will have 2 input URLs, and hence 1 = log2(2) bit of entropy.
int per_site_input2_call_limit = kSelectURLSiteBitBudget - 3;
for (int i = 0; i < num_site_limit; i++) {
std::string iframe_host = base::StrCat({std::string(1, 'b' + i), ".test"});
GURL iframe_url = https_server()->GetURL(iframe_host, kSimplePagePath);
// Create a new iframe.
FrameTreeNode* first_loop_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(first_loop_iframe_node,
/*num_urls=*/8, &console_observer);
for (int j = 0; j < per_site_input2_call_limit; j++) {
// Create a new iframe.
FrameTreeNode* loop_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(loop_iframe_node,
/*num_urls=*/2, &console_observer);
}
// Create a new iframe.
FrameTreeNode* last_loop_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
if (LimitSelectURLCalls()) {
EXPECT_TRUE(ExecJs(last_loop_iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// The limit for `selectURL()` has now been reached for `iframe_host`.
// Make one more call, which will return the default URL due to
// insufficient site pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(
last_loop_iframe_node, iframe_host,
/*num_urls=*/2);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL(iframe_host, "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInIframe(last_loop_iframe_node,
/*num_urls=*/2, &console_observer);
}
}
std::string iframe_host =
base::StrCat({std::string(1, 'b' + num_site_limit), ".test"});
GURL iframe_url = https_server()->GetURL(iframe_host, kSimplePagePath);
int overall_budget_remaining =
kSelectURLOverallBitBudget % kSelectURLSiteBitBudget;
for (int j = 0; j < overall_budget_remaining; j++) {
// Create a new iframe.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
RunSuccessfulSelectURLInIframe(iframe_node,
/*num_urls=*/2, &console_observer);
}
// Create a new iframe.
FrameTreeNode* final_iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), iframe_url);
if (LimitSelectURLCalls()) {
EXPECT_TRUE(ExecJs(final_iframe_node, R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
// The overall pageload limit for `selectURL()` has now been reached. Make
// one more call, which will return the default URL due to insufficient
// overall pageload budget.
std::optional<std::pair<GURL, double>> result_pair =
RunSelectURLExtractingMappedURLAndBudgetToCharge(final_iframe_node,
iframe_host,
/*num_urls=*/2);
ASSERT_TRUE(result_pair.has_value());
GURL expected_mapped_url =
https_server()->GetURL(iframe_host, "/fenced_frames/title0.html");
EXPECT_EQ(result_pair->first, expected_mapped_url);
EXPECT_DOUBLE_EQ(result_pair->second, 0.0);
EXPECT_EQ("Insufficient budget for selectURL().",
base::UTF16ToUTF8(console_observer.messages().back().message));
} else {
// The `selectURL()` limit is disabled. The call will run successfully.
RunSuccessfulSelectURLInIframe(final_iframe_node,
/*num_urls=*/2, &console_observer);
}
WaitForHistograms({kTimingSelectUrlExecutedInWorkletHistogram,
kSelectUrlBudgetStatusHistogram});
histogram_tester_.ExpectTotalCount(
kTimingSelectUrlExecutedInWorkletHistogram,
num_site_limit * (2 + per_site_input2_call_limit) +
overall_budget_remaining + 1);
if (LimitSelectURLCalls()) {
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
num_site_limit * (1 + per_site_input2_call_limit) +
overall_budget_remaining);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientSitePageloadBudget,
overall_budget_remaining ? num_site_limit : num_site_limit - 1);
histogram_tester_.ExpectBucketCount(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::
kInsufficientOverallPageloadBudget,
overall_budget_remaining ? 1 : 2);
} else {
histogram_tester_.ExpectUniqueSample(
kSelectUrlBudgetStatusHistogram,
blink::SharedStorageSelectUrlBudgetStatus::kSufficientBudget,
num_site_limit * (2 + per_site_input2_call_limit) +
overall_budget_remaining + 1);
}
}
class SharedStorageContextBrowserTest
: public SharedStorageFencedFrameInteractionBrowserTestBase {
public:
SharedStorageContextBrowserTest() {
fenced_frame_api_change_feature_.InitAndEnableFeature(
blink::features::kFencedFramesAPIChanges);
}
~SharedStorageContextBrowserTest() override = default;
void GenerateFencedFrameConfig(std::string hostname,
bool keep_alive_after_operation = true) {
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.keepWorklet = $1;",
keep_alive_after_operation)));
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.worklet.addModule('shared_storage/simple_module.js');
)"));
TestSelectURLFencedFrameConfigObserver config_observer(
GetStoragePartition());
GURL fenced_frame_url = https_server()->GetURL(hostname, kFencedFramePath);
// There is 1 more "worklet operation": `selectURL()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EvalJsResult result = EvalJs(shell(), JsReplace(R"(
(async function() {
window.fencedFrameConfig = await sharedStorage.selectURL(
'test-url-selection-operation',
[
{
url: $1,
}
],
{
data: {'mockResult': 0},
resolveToConfig: true,
keepAlive: keepWorklet
}
);
if (!(fencedFrameConfig instanceof FencedFrameConfig)) {
throw new Error('selectURL() did not return a FencedFrameConfig.');
}
return window.fencedFrameConfig;
})()
)",
fenced_frame_url.spec()));
EXPECT_TRUE(result.error.empty());
const std::optional<GURL>& observed_urn_uuid = config_observer.GetUrnUuid();
ASSERT_TRUE(observed_urn_uuid.has_value());
EXPECT_TRUE(blink::IsValidUrnUuidURL(observed_urn_uuid.value()));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
ASSERT_TRUE(config_observer.ConfigObserved());
const std::optional<FencedFrameConfig>& fenced_frame_config =
config_observer.GetConfig();
EXPECT_TRUE(fenced_frame_config.has_value());
EXPECT_EQ(fenced_frame_config->urn_uuid(), observed_urn_uuid.value());
}
private:
base::test::ScopedFeatureList fenced_frame_api_change_feature_;
};
// Tests that `blink::FencedFrameConfig::context` can be set and then accessed
// via `sharedStorage.context`. The context must be set prior to fenced frame
// navigation to the config.
IN_PROC_BROWSER_TEST_F(SharedStorageContextBrowserTest,
EmbedderContextSetBeforeNavigation_Defined) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Generate a config using `selectURL()`.
GenerateFencedFrameConfig("b.test");
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
// Set the context in the config.
const std::string kEmbedderContext = "some context";
EXPECT_TRUE(
ExecJs(shell(),
JsReplace("window.fencedFrameConfig.setSharedStorageContext($1);",
kEmbedderContext)));
// Create and navigate a fenced frame to the config.
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(FencedFrameNavigationTarget("fencedFrameConfig"));
ASSERT_TRUE(fenced_frame_root_node);
// Try to retrieve the context from the root fenced frame's worklet.
GURL script_url;
ExecuteScriptInWorklet(fenced_frame_root_node, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/2u);
// The root fenced frame will have access to the context.
EXPECT_EQ(kEmbedderContext,
base::UTF16ToUTF8(console_observer.messages().back().message));
// Create and navigate to a nested iframe that is same-origin to the root
// fenced frame.
GURL same_origin_url = https_server()->GetURL("b.test", kFencedFramePath);
FrameTreeNode* same_origin_nested_iframe =
CreateIFrame(fenced_frame_root_node, same_origin_url);
ASSERT_TRUE(same_origin_nested_iframe);
// Try to retrieve the context from the nested same-origin iframe's worklet.
ExecuteScriptInWorklet(same_origin_nested_iframe, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/3u);
// A same-origin child of the fenced frame will have access to the context.
EXPECT_EQ(kEmbedderContext,
base::UTF16ToUTF8(console_observer.messages().back().message));
// Create and navigate to a nested iframe that is cross-origin to the root
// fenced frame.
GURL cross_origin_url = https_server()->GetURL("c.test", kFencedFramePath);
FrameTreeNode* cross_origin_nested_iframe =
CreateIFrame(fenced_frame_root_node, cross_origin_url);
ASSERT_TRUE(cross_origin_nested_iframe);
// Try to retrieve the context from the nested cross-origin iframe's worklet.
ExecuteScriptInWorklet(cross_origin_nested_iframe, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/4u);
// A cross-origin child of the fenced frame will not have access to the
// context.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages().back().message));
}
// Tests that `blink::FencedFrameConfig::context`, when not set and then
// accessed via `sharedStorage.context`, will be undefined.
IN_PROC_BROWSER_TEST_F(SharedStorageContextBrowserTest,
EmbedderContextSetAfterNavigation_Undefined) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Generate a config using `selectURL()`.
GenerateFencedFrameConfig("b.test");
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
// Create and navigate a fenced frame to the config.
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(FencedFrameNavigationTarget("fencedFrameConfig"));
ASSERT_TRUE(fenced_frame_root_node);
// Set the context in the config. Since the fenced frame has already been
// navigated to the config and we are not going to navigate to it again, this
// context will not be propagated to the browser process and so won't be
// accessible via `sharedStorage.context`.
const std::string kEmbedderContext = "some context";
EXPECT_TRUE(
ExecJs(shell(),
JsReplace("window.fencedFrameConfig.setSharedStorageContext($1);",
kEmbedderContext)));
// Try to retrieve the context from the root fenced frame's worklet.
GURL script_url;
ExecuteScriptInWorklet(fenced_frame_root_node, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/2u);
// The root fenced frame will see that the context is undefined because it was
// not set before fenced frame navigation.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages().back().message));
}
// Tests that `blink::FencedFrameConfig::context`, when set after a first
// navigation to the config and before a second fenced frame navigation to the
// same config, is updated, as seen via `sharedStorage.context`.
IN_PROC_BROWSER_TEST_F(SharedStorageContextBrowserTest,
EmbedderContextNavigateTwice_ContextUpdated) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Generate a config using `selectURL()`.
GenerateFencedFrameConfig("b.test");
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
// Set the context in the config.
const std::string kEmbedderContext = "some context";
EXPECT_TRUE(
ExecJs(shell(),
JsReplace("window.fencedFrameConfig.setSharedStorageContext($1);",
kEmbedderContext)));
// Create and navigate a fenced frame to the config.
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(FencedFrameNavigationTarget("fencedFrameConfig"));
ASSERT_TRUE(fenced_frame_root_node);
// Try to retrieve the context from the root fenced frame's worklet.
GURL script_url;
ExecuteScriptInWorklet(fenced_frame_root_node, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/2u);
// The root fenced frame will have access to the context.
EXPECT_EQ(kEmbedderContext,
base::UTF16ToUTF8(console_observer.messages().back().message));
// Set the context in the config.
const std::string kNewEmbedderContext = "some different context";
EXPECT_TRUE(
ExecJs(shell(),
JsReplace("window.fencedFrameConfig.setSharedStorageContext($1);",
kNewEmbedderContext)));
// Navigate the fenced frame again to the updated config.
NavigateExistingFencedFrame(fenced_frame_root_node,
FencedFrameNavigationTarget("fencedFrameConfig"));
// Try to retrieve the context from the root fenced frame's worklet.
ExecuteScriptInWorklet(fenced_frame_root_node, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/2u);
// The root fenced frame will have access to the updated context.
EXPECT_EQ(kNewEmbedderContext,
base::UTF16ToUTF8(console_observer.messages().back().message));
}
// Tests that `blink::FencedFrameConfig::context` can be set and then accessed
// via `sharedStorage.context`, but that any context string exceeding the length
// limit is truncated.
IN_PROC_BROWSER_TEST_F(SharedStorageContextBrowserTest,
EmbedderContextExceedsLengthLimit_Truncated) {
GURL main_url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Generate a config using `selectURL()`.
GenerateFencedFrameConfig("b.test");
EXPECT_EQ("Finish executing 'test-url-selection-operation'",
base::UTF16ToUTF8(console_observer.messages().back().message));
// Set the context in the config.
const std::string kLongEmbedderContext(
blink::kFencedFrameConfigSharedStorageContextMaxLength, 'x');
EXPECT_TRUE(
ExecJs(shell(),
JsReplace("window.fencedFrameConfig.setSharedStorageContext($1);",
kLongEmbedderContext + 'X')));
// Create and navigate a fenced frame to the config.
FrameTreeNode* fenced_frame_root_node =
CreateFencedFrame(FencedFrameNavigationTarget("fencedFrameConfig"));
ASSERT_TRUE(fenced_frame_root_node);
// Try to retrieve the context from the root fenced frame's worklet.
GURL script_url;
ExecuteScriptInWorklet(fenced_frame_root_node, R"(
console.log(sharedStorage.context);
)",
&script_url, /*expected_total_host_count=*/2u);
// The root fenced frame will have access to the context, which will be
// truncated.
EXPECT_EQ(kLongEmbedderContext,
base::UTF16ToUTF8(console_observer.messages().back().message));
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
StringRoundTrip_SetThenGet_UnpairedSurrogatesArePreserved) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/round_trip.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
std::vector<std::tuple<std::vector<uint16_t>, bool>> string_test_cases = {
{std::vector<uint16_t>({0x0068}), true}, // letter 'h'
{std::vector<uint16_t>({0xd800}), false}, // lone high surrogate
{std::vector<uint16_t>({0xdc00}), false}, // lone low surrogate
{std::vector<uint16_t>({0xd800, 0xdc00}), true}, // surrogate pair
};
for (size_t i = 0; i < string_test_cases.size(); ++i) {
const std::vector<uint16_t>& char_code_array =
std::get<0>(string_test_cases[i]);
std::u16string test_string =
std::u16string(char_code_array.begin(), char_code_array.end());
base::Value::List char_code_values;
for (uint16_t char_code : char_code_array) {
char_code_values.Append(static_cast<int>(char_code));
}
const bool expected_valid = std::get<1>(string_test_cases[i]);
// Check validity of UTF-16.
std::string base_output;
EXPECT_EQ(expected_valid,
base::UTF16ToUTF8(test_string.c_str(), test_string.size(),
&base_output));
// The dummy assignment is necessary, because otherwise under the hood,
// `ExecJs` makes a call that tries to evaluate the most recent script
// result as a `base::Value`, and `char_code_values` causes that to fail.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(window.charCodeArray = $1;
window.dummyAssignment = 0;)",
std::move(char_code_values))));
// We will wait for 1 "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('set-get-operation',
{data: {'key': 'asValue',
'valueCharCodeArray' : charCodeArray},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(i + 1, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages().back().message),
testing::HasSubstr("was retrieved: true"));
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
StringRoundTrip_SetThenKeys_UnpairedSurrogatesArePreserved) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/round_trip.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
std::vector<std::tuple<std::vector<uint16_t>, bool>> string_test_cases = {
{std::vector<uint16_t>({0x0068}), true}, // letter 'h'
{std::vector<uint16_t>({0xd800}), false}, // lone high surrogate
{std::vector<uint16_t>({0xdc00}), false}, // lone low surrogate
{std::vector<uint16_t>({0xd800, 0xdc00}), true}, // surrogate pair
};
for (size_t i = 0; i < string_test_cases.size(); ++i) {
const std::vector<uint16_t>& char_code_array =
std::get<0>(string_test_cases[i]);
std::u16string test_string =
std::u16string(char_code_array.begin(), char_code_array.end());
base::Value::List char_code_values;
for (uint16_t char_code : char_code_array) {
char_code_values.Append(static_cast<int>(char_code));
}
const bool expected_valid = std::get<1>(string_test_cases[i]);
// Check validity of UTF-16.
std::string base_output;
EXPECT_EQ(expected_valid,
base::UTF16ToUTF8(test_string.c_str(), test_string.size(),
&base_output));
// The dummy assignment is necessary, because otherwise under the hood,
// `ExecJs` makes a call that tries to evaluate the most recent script
// result as a `base::Value`, and `char_code_values` causes that to fail.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(window.charCodeArray = $1;
window.dummyAssignment = 0;)",
std::move(char_code_values))));
// We will wait for 1 "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('set-keys-operation',
{data: {'keyCharCodeArray' : charCodeArray,
'value': 'asKey'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(i + 1, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages().back().message),
testing::HasSubstr("was retrieved: true"));
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
StringRoundTrip_AppendThenDelete_UnpairedSurrogatesArePreserved) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/round_trip.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
std::vector<std::tuple<std::vector<uint16_t>, bool>> string_test_cases = {
{std::vector<uint16_t>({0x0068}), true}, // letter 'h'
{std::vector<uint16_t>({0xd800}), false}, // lone high surrogate
{std::vector<uint16_t>({0xdc00}), false}, // lone low surrogate
{std::vector<uint16_t>({0xd800, 0xdc00}), true}, // surrogate pair
};
for (size_t i = 0; i < string_test_cases.size(); ++i) {
const std::vector<uint16_t>& char_code_array =
std::get<0>(string_test_cases[i]);
std::u16string test_string =
std::u16string(char_code_array.begin(), char_code_array.end());
base::Value::List char_code_values;
for (uint16_t char_code : char_code_array) {
char_code_values.Append(static_cast<int>(char_code));
}
const bool expected_valid = std::get<1>(string_test_cases[i]);
// Check validity of UTF-16.
std::string base_output;
EXPECT_EQ(expected_valid,
base::UTF16ToUTF8(test_string.c_str(), test_string.size(),
&base_output));
// The dummy assignment is necessary, because otherwise under the hood,
// `ExecJs` makes a call that tries to evaluate the most recent script
// result as a `base::Value`, and `char_code_values` causes that to fail.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(window.charCodeArray = $1;
window.dummyAssignment = 0;)",
std::move(char_code_values))));
// We will wait for 1 "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('append-delete-operation',
{data: {'keyCharCodeArray' : charCodeArray,
'value': 'asKey'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(i + 1, console_observer.messages().size());
EXPECT_EQ(u"delete success: true",
console_observer.messages().back().message);
}
}
IN_PROC_BROWSER_TEST_P(
SharedStorageBrowserTest,
StringRoundTrip_AppendThenEntries_UnpairedSurrogatesArePreserved) {
GURL url = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/round_trip.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
std::vector<std::tuple<std::vector<uint16_t>, bool>> string_test_cases = {
{std::vector<uint16_t>({0x0068}), true}, // letter 'h'
{std::vector<uint16_t>({0xd800}), false}, // lone high surrogate
{std::vector<uint16_t>({0xdc00}), false}, // lone low surrogate
{std::vector<uint16_t>({0xd800, 0xdc00}), true}, // surrogate pair
};
for (size_t i = 0; i < string_test_cases.size(); ++i) {
const std::vector<uint16_t>& char_code_array =
std::get<0>(string_test_cases[i]);
std::u16string test_string =
std::u16string(char_code_array.begin(), char_code_array.end());
base::Value::List char_code_values;
for (uint16_t char_code : char_code_array) {
char_code_values.Append(static_cast<int>(char_code));
}
const bool expected_valid = std::get<1>(string_test_cases[i]);
// Check validity of UTF-16.
std::string base_output;
EXPECT_EQ(expected_valid,
base::UTF16ToUTF8(test_string.c_str(), test_string.size(),
&base_output));
// The dummy assignment is necessary, because otherwise under the hood,
// `ExecJs` makes a call that tries to evaluate the most recent script
// result as a `base::Value`, and `char_code_values` causes that to fail.
EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(window.charCodeArray = $1;
window.dummyAssignment = 0;)",
std::move(char_code_values))));
// We will wait for 1 "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('append-entries-operation',
{data: {'key': 'asValue',
'valueCharCodeArray': charCodeArray},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(i + 1, console_observer.messages().size());
EXPECT_THAT(base::UTF16ToUTF8(console_observer.messages().back().message),
testing::HasSubstr("was retrieved: true"));
}
}
class SharedStorageHeaderObserverBrowserTest
: public SharedStorageBrowserTestBase {
public:
using Operation = network::mojom::SharedStorageOperation;
using OperationResult = storage::SharedStorageManager::OperationResult;
SharedStorageHeaderObserverBrowserTest() {
shared_storage_m118_feature_.InitAndEnableFeature(
blink::features::kSharedStorageAPIM118);
}
void FinishSetup() override {
https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
auto observer = std::make_unique<TestSharedStorageHeaderObserver>(
GetStoragePartition());
observer_ = observer->GetMutableWeakPtr();
static_cast<StoragePartitionImpl*>(GetStoragePartition())
->OverrideSharedStorageHeaderObserverForTesting(std::move(observer));
}
uint16_t port() { return https_server()->port(); }
bool NavigateToURLWithResponse(
Shell* window,
const GURL& url,
net::test_server::ControllableHttpResponse& response,
net::HttpStatusCode http_status,
const std::string& content_type = std::string("text/html"),
const std::string& content = std::string(),
const std::vector<std::string>& cookies = {},
const std::vector<std::string>& extra_headers = {}) {
auto* web_contents = window->web_contents();
DCHECK(web_contents);
// Prepare for the navigation.
WaitForLoadStop(web_contents);
TestNavigationObserver same_tab_observer(
web_contents,
/*expected_number_of_navigations=*/1,
MessageLoopRunner::QuitMode::IMMEDIATE,
/*ignore_uncommitted_navigations=*/false);
if (!blink::IsRendererDebugURL(url)) {
same_tab_observer.set_expected_initial_url(url);
}
// This mimics behavior of Shell::LoadURL...
NavigationController::LoadURLParams params(url);
params.transition_type = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
web_contents->GetController().LoadURLWithParams(params);
web_contents->GetOutermostWebContents()->Focus();
response.WaitForRequest();
response.Send(http_status, content_type, content, cookies, extra_headers);
response.Done();
// Wait until the expected number of navigations finish.
same_tab_observer.Wait();
if (!IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL)) {
return false;
}
bool is_same_url = web_contents->GetLastCommittedURL() == url;
if (!is_same_url) {
DLOG(WARNING) << "Expected URL " << url << " but observed "
<< web_contents->GetLastCommittedURL();
}
return is_same_url;
}
std::string ReplacePortInString(std::string str) {
const std::string kToReplace("{{port}}");
size_t index = str.find(kToReplace);
while (index != std::string::npos) {
str = str.replace(index, kToReplace.size(), base::NumberToString(port()));
index = str.find(kToReplace);
}
return str;
}
void SetUpResponsesAndNavigateMainPage(
std::string main_hostname,
std::string subresource_or_subframe_hostname,
std::optional<std::string> shared_storage_permissions = std::nullopt,
bool is_image = false,
std::vector<std::string> redirect_hostnames = {}) {
subresource_or_subframe_content_type_ =
is_image ? "image/png" : "text/plain;charset=UTF-8";
const char* subresource_or_subframe_path =
is_image ? kPngPath : kTitle1Path;
subresource_or_subframe_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), subresource_or_subframe_path);
std::unique_ptr<net::test_server::ControllableHttpResponse> main_response;
if (shared_storage_permissions.has_value()) {
main_response =
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), kSimplePagePath);
}
DCHECK_LT(redirect_hostnames.size(), 4u)
<< "You need to add more paths to "
"`SetUpResponsesAndNavigateMainPage()`. Currently there are enough "
"for up to 3 redirects.";
std::vector<std::string> paths({kTitle2Path, kTitle3Path, kTitle4Path});
for (size_t i = 0; i < redirect_hostnames.size(); i++) {
auto response =
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), paths[i]);
redirected_responses_.push_back(std::move(response));
}
ASSERT_TRUE(https_server()->Start());
main_url_ =
https_server()->GetURL(std::move(main_hostname), kSimplePagePath);
subresource_or_subframe_url_ =
https_server()->GetURL(std::move(subresource_or_subframe_hostname),
subresource_or_subframe_path);
subresource_or_subframe_origin_ =
url::Origin::Create(subresource_or_subframe_url_);
for (size_t i = 0; i < redirect_hostnames.size(); i++) {
redirect_urls_.emplace_back(
https_server()->GetURL(redirect_hostnames[i], paths[i]));
redirect_origins_.emplace_back(
url::Origin::Create(redirect_urls_.back()));
}
if (shared_storage_permissions.has_value()) {
EXPECT_TRUE(NavigateToURLWithResponse(
shell(), main_url_, *main_response,
/*http_status=*/net::HTTP_OK,
/*content_type=*/"text/plain;charset=UTF-8",
/*content=*/{}, /*cookies=*/{}, /*extra_headers=*/
{"Permissions-Policy: shared-storage=" +
ReplacePortInString(
std::move(shared_storage_permissions.value()))}));
} else {
EXPECT_TRUE(NavigateToURL(shell(), main_url_));
}
}
void WaitForRequestAndSendResponse(
net::test_server::ControllableHttpResponse& response,
bool expect_writable_header,
net::HttpStatusCode http_status,
const std::string& content_type,
const std::vector<std::string>& extra_headers) {
response.WaitForRequest();
if (expect_writable_header) {
ASSERT_TRUE(base::Contains(response.http_request()->headers,
"Sec-Shared-Storage-Writable"));
} else {
EXPECT_FALSE(base::Contains(response.http_request()->headers,
"Sec-Shared-Storage-Writable"));
}
EXPECT_EQ(response.http_request()->content, "");
response.Send(http_status, content_type,
/*content=*/{}, /*cookies=*/{}, extra_headers);
response.Done();
}
void WaitForSubresourceOrSubframeRequestAndSendResponse(
bool expect_writable_header,
net::HttpStatusCode http_status,
const std::vector<std::string>& extra_headers) {
WaitForRequestAndSendResponse(
*subresource_or_subframe_response_, expect_writable_header, http_status,
subresource_or_subframe_content_type_, extra_headers);
}
void WaitForRedirectRequestAndSendResponse(
bool expect_writable_header,
net::HttpStatusCode http_status,
const std::vector<std::string>& extra_headers,
size_t redirect_index = 0) {
ASSERT_LT(redirect_index, redirected_responses_.size());
WaitForRequestAndSendResponse(
*redirected_responses_[redirect_index], expect_writable_header,
http_status, subresource_or_subframe_content_type_, extra_headers);
}
void FetchWithSharedStorageWritable(const ToRenderFrameHost& execution_target,
const GURL& url) {
EXPECT_TRUE(ExecJs(execution_target,
JsReplace(R"(
fetch($1, {sharedStorageWritable: true});
)",
url.spec()),
EvalJsOptions::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
}
void StartServerAndLoadMainURLWithSameOriginSubresourceOrSubframe(
std::string main_hostname,
std::string main_path) {
ASSERT_TRUE(https_server()->Start());
main_url_ =
https_server()->GetURL(std::move(main_hostname), std::move(main_path));
subresource_or_subframe_origin_ = url::Origin::Create(main_url_);
EXPECT_TRUE(NavigateToURL(shell(), main_url_));
}
void CreateSharedStorageWritableImage(
const ToRenderFrameHost& execution_target,
const GURL& url) {
EXPECT_TRUE(ExecJs(execution_target, JsReplace(R"(
let img = document.createElement('img');
img.src = $1;
img.sharedStorageWritable = true;
document.body.appendChild(img);
)",
url.spec())));
}
void CreateSharedStorageWritableIframe(
const ToRenderFrameHost& execution_target,
const GURL& url) {
EXPECT_TRUE(ExecJs(execution_target, JsReplace(R"(
let frame = document.createElement('iframe');
frame.sharedStorageWritable = true;
frame.src = $1;
document.body.appendChild(frame);
)",
url.spec())));
}
protected:
base::WeakPtr<TestSharedStorageHeaderObserver> observer_;
std::unique_ptr<net::test_server::ControllableHttpResponse>
subresource_or_subframe_response_;
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
redirected_responses_;
GURL main_url_;
GURL subresource_or_subframe_url_;
std::vector<GURL> redirect_urls_;
url::Origin subresource_or_subframe_origin_;
std::vector<url::Origin> redirect_origins_;
std::string subresource_or_subframe_content_type_;
private:
base::test::ScopedFeatureList shared_storage_m118_feature_;
};
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsDefault_ClearSetAppend) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"()");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsAll_ClearSetAppend) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"*");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsSelf_ClearSetAppend) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"self");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_PermissionsDefault_ClearSetAppend) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
// Create an iframe that's same-origin to the fetch URL.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"()");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_PermissionsAll_ClearSetAppend) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"*");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
// Create an iframe that's same-origin to the fetch URL.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_PermissionsSelf) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"self");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteInitial) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *"});
// There won't be additional operations invoked.
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
// Nothing was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// The entry was set in c.test's shared storage.
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[3].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteBoth) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in c.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InititalAllowed_FinalDenied) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// No new operations are invoked.
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InititalAllowed_IntermediateDenied_FinalAllowed_WriteInitialAndFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://d.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test", "d.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.front().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_TEMPORARY_REDIRECT,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: append;key=wont;value=set"},
/*redirect_index=*/0);
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"},
/*redirect_index=*/1);
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create an iframe that's same-origin to the first redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.front());
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
// c.test does not have permission to use shared storage.
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the second redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node3, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in d.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Fetch_CrossOrigin_Redirect_InitialDenied_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
// No operations are invoked.
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
redirect_origins_.back());
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Create an iframe that's same-origin to the original fetch URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
EvalJsResult result = EvalJs(iframe_node1, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
// one key was set in c.test's shared storage.
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsDefault_VerifyDelete) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(sharedStorage.set('hello', 'world');)"));
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/getter_module.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'hello'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("sharedStorage.length(): 1",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("sharedStorage.get('hello'): world",
base::UTF16ToUTF8(console_observer.messages()[1].message));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: \"delete\";key=hello"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
DeleteOperation(subresource_or_subframe_origin_, "hello",
OperationResult::kSuccess)));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'hello'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("sharedStorage.length(): 0",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("sharedStorage.get('hello'): undefined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsDefault_VerifyClear) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), R"(sharedStorage.set('hello', 'world');)"));
GURL script_url =
https_server()->GetURL("a.test", "/shared_storage/getter_module.js");
EXPECT_TRUE(ExecJs(
shell(), JsReplace("sharedStorage.worklet.addModule($1)", script_url)));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'hello'},
keepAlive: true});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("sharedStorage.length(): 1",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("sharedStorage.get('hello'): world",
base::UTF16ToUTF8(console_observer.messages()[1].message));
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: \"clear\""});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(ClearOperation(
subresource_or_subframe_origin_, OperationResult::kSuccess)));
// There is 1 more "worklet operation": `run()`.
test_worklet_host_manager()
.GetAttachedWorkletHost()
->SetExpectedWorkletResponsesCount(1);
EXPECT_TRUE(ExecJs(shell(), R"(
sharedStorage.run('get-operation', {data: {'key': 'hello'}});
)"));
test_worklet_host_manager()
.GetAttachedWorkletHost()
->WaitForWorkletResponses();
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("sharedStorage.length(): 0",
base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("sharedStorage.get('hello'): undefined",
base::UTF16ToUTF8(console_observer.messages()[3].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Fetch_SameOrigin_PermissionsDefault_MultipleSet_Bytes) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: set;key=:aGVsbG8=:;value=:d29ybGQ=:, "
"set;value=:ZnJpZW5k:;key=:aGVsbG8=:;ignore_if_present=?0, "
"set;ignore_if_present;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
SetOperation(subresource_or_subframe_origin_, "hello",
"world", std::nullopt, OperationResult::kSet),
SetOperation(subresource_or_subframe_origin_, "hello",
"friend", false, OperationResult::kSet),
SetOperation(subresource_or_subframe_origin_, "hello",
"there", true, OperationResult::kIgnored)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("friend",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
NetworkServiceRestarts_HeaderObserverContinuesWorking) {
subresource_or_subframe_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), kTitle1Path);
ASSERT_TRUE(https_server()->Start());
if (IsInProcessNetworkService()) {
return;
}
main_url_ = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url_));
ASSERT_TRUE(observer_);
SimulateNetworkServiceCrash();
static_cast<StoragePartitionImpl*>(GetStoragePartition())
->FlushNetworkInterfaceForTesting();
// We should still have an `observer_`.
ASSERT_TRUE(observer_);
// We need to reinitialize and renavigate to `main_url_` after network service
// restart, if we want to prevent the shared storage operations below from
// being deferred then dropped due to switching to a new
// `NavigationOrDocumentHandle` for the main frame which hasn't yet seen a
// commit.
main_url_ = https_server()->GetURL("a.test", kSimplePagePath);
EXPECT_TRUE(NavigateToURL(shell(), main_url_));
// We will still have an `observer_` after renavigating.
ASSERT_TRUE(observer_);
// We also need to reinitialize `subresource_or_subframe_url_` after network
// service restart. Fetching with `sharedStorageWritable` works as expected.
subresource_or_subframe_url_ = https_server()->GetURL("a.test", kTitle1Path);
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
subresource_or_subframe_origin_ =
url::Origin::Create(subresource_or_subframe_url_);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
InvalidHeader_NoOperationsInvoked) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, invalid?item"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[0].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
ParsableUnrecognizedItemSkipped_RecognizedOperationsInvoked) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear, unrecognized;unknown_param=1,"
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
ExtraParametersIgnored) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: clear;unknown_param=1,"
"set;another_unknown=willIgnore;key=\"hello\";value=\"world\", "
"append;key=extra;key=hello;value=there;ignore_if_present;pi=3.14,"
"delete;value=ignored;key=toDelete;ignore_if_present=?0"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(4);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", std::nullopt, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(subresource_or_subframe_origin_, "toDelete",
OperationResult::kSuccess)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
KeyOrValueLengthInvalid_ItemSkipped) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test");
FetchWithSharedStorageWritable(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{base::StrCat({"Shared-Storage-Write: clear, ",
"set;key=\"\";value=v,append;key=\"\";value=v,",
"delete;key=\"\",clear"})});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, false, false, false, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[0].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_SameOrigin_PermissionsDefault) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/std::nullopt,
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_SameOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"()",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_SameOrigin_PermissionsAll) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"*",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_SameOrigin_PermissionsSelf) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"self",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_PermissionsDefault) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/std::nullopt,
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: set;key=a;value=b"});
// Create iframe that's same-origin to the image URL.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node, R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"()",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_PermissionsAll) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"*",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// Create iframe that's same-origin to the image URL.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node, R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_PermissionsSelf) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"self",
/*is_image=*/true);
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteInitial) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *"});
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
// There won't be additional operations invoked from the redirect, just the
// original 3.
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
// Nothing was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// The entry was set in c.test's shared storage.
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[3].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteBoth) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in c.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InititalAllowed_FinalDenied) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// There won't be additional operations invoked from the redirect, just the
// original 3.
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InititalAllowed_IntermediateDenied_FinalAllowed_WriteInitialAndFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://d.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test", "d.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.front().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_TEMPORARY_REDIRECT,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: append;key=wont;value=set"},
/*redirect_index=*/0);
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"},
/*redirect_index=*/1);
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create an iframe that's same-origin to the first redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.front());
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
// c.test does not have permission to use shared storage.
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the second redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node3, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in d.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_CrossOrigin_Redirect_InitialDenied_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://c.test:{{port}}\")",
/*is_image=*/true,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableImage(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
// No operations are invoked.
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
redirect_origins_.back());
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Create an iframe that's same-origin to the original image URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
EvalJsResult result = EvalJs(iframe_node1, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
// one key was set in c.test's shared storage.
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Image_ContentAttributeIncluded_Set_2ndImageCached_NotSet) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
StartServerAndLoadMainURLWithSameOriginSubresourceOrSubframe(
/*main_hostname=*/"a.test",
/*main_path=*/
"/shared_storage/page-with-shared-storage-writable-image.html");
// Wait for the image onload to fire.
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Image Loaded",
base::UTF16ToUTF8(console_observer.messages()[0].message));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(), testing::ElementsAre(AppendOperation(
subresource_or_subframe_origin_, "a",
"b", OperationResult::kSet)));
EXPECT_EQ(
true,
EvalJs(
shell(),
JsReplace(
R"(
new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = $1;
img.onload = () => resolve(true);
img.sharedStorageWritable = true;
document.body.appendChild(img);
})
)",
https_server()
->GetURL("a.test",
"/shared_storage/shared-storage-writable-pixel.png")
.spec())));
// Create an iframe that's same-origin in order to run a second worklet.
FrameTreeNode* iframe_node =
CreateIFrame(PrimaryFrameTreeNodeRoot(), main_url_);
ExecuteScriptInWorklet(iframe_node, R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// The value 'b' for the key 'a' is unchanged (nothing is appended to it).
EXPECT_EQ(6u, console_observer.messages().size());
EXPECT_EQ("Image Loaded",
base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[4].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[5].message));
// No new operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(), testing::ElementsAre(AppendOperation(
subresource_or_subframe_origin_, "a",
"b", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Image_ContentAttributeNotIncluded_NotSet) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
StartServerAndLoadMainURLWithSameOriginSubresourceOrSubframe(
/*main_hostname=*/"a.test",
/*main_path=*/
"/shared_storage/page-with-non-shared-storage-writable-image.html");
// Wait for the image onload to fire.
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Image Loaded",
base::UTF16ToUTF8(console_observer.messages()[0].message));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[2].message));
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_SameOrigin_PermissionsDefault) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/std::nullopt,
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_SameOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"()",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_SameOrigin_PermissionsAll) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"*",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_SameOrigin_PermissionsSelf) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"a.test",
/*shared_storage_permissions=*/"self",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_PermissionsDefault) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/std::nullopt,
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Shared-Storage-Write: set;key=a;value=b"});
// Create another iframe that's same-origin to the first iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_PermissionsNone) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"()",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_PermissionsAll) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"*",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// Create another iframe that's same-origin to the first iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(),
https_server()->GetURL("b.test", kTitle2Path));
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_PermissionsSelf) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/"self",
/*is_image=*/false);
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: set;key=a;value=b"});
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteInitial) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *"});
// Create another iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
// There won't be additional operations invoked from the redirect, just the
// original 3.
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create another iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
// Nothing was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node3, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// The entry was set in c.test's shared storage.
EXPECT_EQ(4u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[2].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[3].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InititalAllowed_FinalAllowed_WriteBoth) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create another iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node3, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in c.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InititalAllowed_FinalDenied) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=wont;key=set"});
// There won't be additional operations invoked from the redirect, just the
// original 3.
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
// Create an iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node1 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node1, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
// Create another iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InititalAllowed_IntermediateDenied_FinalAllowed_WriteInitialAndFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://b.test:{{port}}\" \"https://d.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test", "d.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.front().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
observer_->WaitForOperations(3);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet)));
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_TEMPORARY_REDIRECT,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: append;key=wont;value=set"},
/*redirect_index=*/0);
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"},
/*redirect_index=*/1);
// There will now have been a total of 5 operations (3 previous, 2 current).
ASSERT_TRUE(observer_);
observer_->WaitForOperations(5);
EXPECT_EQ(observer_->header_results().size(), 2u);
EXPECT_EQ(observer_->header_results().back().first, redirect_origins_.back());
EXPECT_THAT(observer_->header_results().back().second,
testing::ElementsAre(true, true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(
ClearOperation(subresource_or_subframe_origin_,
OperationResult::kSuccess),
SetOperation(subresource_or_subframe_origin_, "hello",
"world", true, OperationResult::kSet),
AppendOperation(subresource_or_subframe_origin_, "hello",
"there", OperationResult::kSet),
DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
// Create an iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
WebContentsConsoleObserver console_observer(shell()->web_contents());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node2, R"(
console.log(await sharedStorage.get('hello'));
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("worldthere",
base::UTF16ToUTF8(console_observer.messages()[0].message));
// Only one entry was set in b.test's shared storage.
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
// Create an iframe that's same-origin to the first redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.front());
EvalJsResult result = EvalJs(iframe_node3, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
// c.test does not have permission to use shared storage.
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the second redirect URL.
FrameTreeNode* iframe_node4 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
ExecuteScriptInWorklet(iframe_node4, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url, /*expected_total_host_count=*/2u);
// One entry was set in d.test's shared storage.
EXPECT_EQ(5u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[3].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[4].message));
}
IN_PROC_BROWSER_TEST_F(
SharedStorageHeaderObserverBrowserTest,
Iframe_CrossOrigin_Redirect_InitialDenied_FinalAllowed_WriteFinal) {
SetUpResponsesAndNavigateMainPage(
/*main_hostname=*/"a.test",
/*subresource_or_subframe_hostname=*/"b.test",
/*shared_storage_permissions=*/
"(self \"https://c.test:{{port}}\")",
/*is_image=*/false,
/*redirect_hostnames=*/std::vector<std::string>({"c.test"}));
CreateSharedStorageWritableIframe(shell(), subresource_or_subframe_url_);
WaitForSubresourceOrSubframeRequestAndSendResponse(
/*expect_writable_header=*/false,
/*http_status=*/net::HTTP_FOUND,
/*extra_headers=*/
{base::StrCat({"Location: ", redirect_urls_.back().spec()}),
"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: clear, "
"set;key=\"hello\";value=\"world\";ignore_if_present, "
"append;key=hello;value=there"});
ASSERT_TRUE(observer_);
// No operations are invoked.
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
WaitForRedirectRequestAndSendResponse(
/*expect_writable_header=*/true,
/*http_status=*/net::HTTP_OK,
/*extra_headers=*/
{"Access-Control-Allow-Origin: *",
"Shared-Storage-Write: delete;key=a, set;value=will;key=set"});
observer_->WaitForOperations(2);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
redirect_origins_.back());
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true, true));
EXPECT_THAT(
observer_->operations(),
testing::ElementsAre(DeleteOperation(redirect_origins_.back(), "a",
OperationResult::kSuccess),
SetOperation(redirect_origins_.back(), "set", "will",
std::nullopt, OperationResult::kSet)));
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Create an iframe that's same-origin to the original iframe URL.
FrameTreeNode* iframe_node2 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), subresource_or_subframe_url_);
EvalJsResult result = EvalJs(iframe_node2, R"(
sharedStorage.worklet.addModule('/shared_storage/simple_module.js');
)");
EXPECT_THAT(result.error,
testing::HasSubstr("The \"shared-storage\" Permissions Policy "
"denied the method on window.sharedStorage."));
// Create an iframe that's same-origin to the redirect URL.
FrameTreeNode* iframe_node3 =
CreateIFrame(PrimaryFrameTreeNodeRoot(), redirect_urls_.back());
GURL out_script_url;
ExecuteScriptInWorklet(iframe_node3, R"(
console.log(await sharedStorage.get('set'));
console.log(await sharedStorage.length());
)",
&out_script_url);
// one key was set in c.test's shared storage.
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("will", base::UTF16ToUTF8(console_observer.messages()[0].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[1].message));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_ContentAttributeIncluded_Set) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
StartServerAndLoadMainURLWithSameOriginSubresourceOrSubframe(
/*main_hostname=*/"a.test",
/*main_path=*/
"/shared_storage/page-with-shared-storage-writable-iframe.html");
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Iframe Loaded",
base::UTF16ToUTF8(console_observer.messages()[0].message));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("b", base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("1", base::UTF16ToUTF8(console_observer.messages()[2].message));
ASSERT_TRUE(observer_);
observer_->WaitForOperations(1);
EXPECT_EQ(observer_->header_results().size(), 1u);
EXPECT_EQ(observer_->header_results().front().first,
subresource_or_subframe_origin_);
EXPECT_THAT(observer_->header_results().front().second,
testing::ElementsAre(true));
EXPECT_THAT(observer_->operations(),
testing::ElementsAre(SetOperation(subresource_or_subframe_origin_,
"a", "b", std::nullopt,
OperationResult::kSet)));
}
IN_PROC_BROWSER_TEST_F(SharedStorageHeaderObserverBrowserTest,
Iframe_ContentAttributeNotIncluded_NotSet) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
StartServerAndLoadMainURLWithSameOriginSubresourceOrSubframe(
/*main_hostname=*/"a.test",
/*main_path=*/
"/shared_storage/page-with-non-shared-storage-writable-iframe.html");
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ(1u, console_observer.messages().size());
EXPECT_EQ("Iframe Loaded",
base::UTF16ToUTF8(console_observer.messages()[0].message));
GURL out_script_url;
ExecuteScriptInWorklet(shell(), R"(
console.log(await sharedStorage.get('a'));
console.log(await sharedStorage.length());
)",
&out_script_url);
EXPECT_EQ(3u, console_observer.messages().size());
EXPECT_EQ("undefined",
base::UTF16ToUTF8(console_observer.messages()[1].message));
EXPECT_EQ("0", base::UTF16ToUTF8(console_observer.messages()[2].message));
// No operations are invoked.
ASSERT_TRUE(observer_);
EXPECT_TRUE(observer_->header_results().empty());
EXPECT_TRUE(observer_->operations().empty());
}
class SharedStorageOriginTrialBrowserTest : public ContentBrowserTest {
public:
SharedStorageOriginTrialBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kPrivacySandboxAdsAPIs,
blink::features::kSharedStorageAPI},
/*disabled_features=*/{features::kPrivacySandboxAdsAPIsM1Override});
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
// We use a URLLoaderInterceptor, rather than the EmbeddedTestServer, since
// the origin trial token in the response is associated with a fixed
// origin, whereas EmbeddedTestServer serves content on a random port.
url_loader_interceptor_ =
std::make_unique<URLLoaderInterceptor>(base::BindLambdaForTesting(
[](URLLoaderInterceptor::RequestParams* params) -> bool {
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath test_data_dir;
CHECK(base::PathService::Get(DIR_TEST_DATA, &test_data_dir));
std::string relative_path =
params->url_request.url.path().substr(1);
base::FilePath file_path =
test_data_dir.AppendASCII(relative_path);
URLLoaderInterceptor::WriteResponse(file_path,
params->client.get());
return true;
}));
}
void TearDownOnMainThread() override {
// Resetting `URLLoaderInterceptor` needs a single-threaded context. Thus,
// reset it here instead of during destruction of `this`.
url_loader_interceptor_.reset();
}
private:
std::unique_ptr<URLLoaderInterceptor> url_loader_interceptor_;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SharedStorageOriginTrialBrowserTest,
OriginTrialEnabled_SharedStorageClassExposedInWorklet) {
EXPECT_TRUE(
NavigateToURL(shell(), GURL("https://example.test/attribution_reporting/"
"page_with_ads_apis_ot.html")));
EXPECT_TRUE(ExecJs(shell(), R"(
// Try accessing the `SharedStorage` interface which is gated by
// [RuntimeEnabled=SharedStorageAPI] which has an associated Origin Trial
// feature. If the OT features are correctly propagated to the worklet
// environment, the module script should execute successfully.
let module_content = `
SharedStorage;
`;
let blob = new Blob([module_content], {type: 'text/javascript'});
sharedStorage.worklet.addModule(URL.createObjectURL(blob));
)"));
}
} // namespace content