blob: 7f11270c946afce4472369a20c0f5384aae1f4f1 [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 "content/services/auction_worklet/context_recycler.h"
#include <stdint.h>
#include <limits>
#include <string>
#include <vector>
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/bidder_lazy_filler.h"
#include "content/services/auction_worklet/for_debugging_only_bindings.h"
#include "content/services/auction_worklet/private_aggregation_bindings.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h"
#include "content/services/auction_worklet/register_ad_beacon_bindings.h"
#include "content/services/auction_worklet/report_bindings.h"
#include "content/services/auction_worklet/set_bid_bindings.h"
#include "content/services/auction_worklet/set_priority_bindings.h"
#include "content/services/auction_worklet/worklet_test_util.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/interest_group/ad_auction_currencies.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom-shared.h"
#include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom.h"
#include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom-forward.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-primitive.h"
using testing::ElementsAre;
using testing::Pair;
namespace auction_worklet {
class ContextRecyclerTest : public testing::Test {
public:
ContextRecyclerTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
helper_(AuctionV8Helper::Create(
base::SingleThreadTaskRunner::GetCurrentDefault())) {
// Here since we're using the same thread for everything, we need to spin
// the event loop to let AuctionV8Helper finish initializing "off-thread";
// normally PostTask semantics will ensure that anything that uses it on its
// thread would happen after such initialization.
base::RunLoop().RunUntilIdle();
v8_scope_ =
std::make_unique<AuctionV8Helper::FullIsolateScope>(helper_.get());
}
~ContextRecyclerTest() override = default;
v8::Local<v8::UnboundScript> Compile(const std::string& code) {
v8::Local<v8::UnboundScript> script;
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
EXPECT_TRUE(helper_
->Compile(code, GURL("https://example.org/script.js"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value()) << error_msg.value();
return script;
}
// Runs a function with zero or 1 arguments.
v8::MaybeLocal<v8::Value> Run(
ContextRecyclerScope& scope,
v8::Local<v8::UnboundScript> script,
const std::string& function_name,
std::vector<std::string>& error_msgs,
v8::Local<v8::Value> maybe_arg = v8::Local<v8::Value>()) {
std::vector<v8::Local<v8::Value>> args;
if (!maybe_arg.IsEmpty())
args.push_back(maybe_arg);
if (!helper_->RunScript(scope.GetContext(), script,
/*debug_id=*/nullptr,
/*script_timeout=*/absl::nullopt, error_msgs)) {
return {};
}
return helper_->CallFunction(scope.GetContext(),
/*debug_id=*/nullptr,
helper_->FormatScriptName(script),
function_name, args,
/*script_timeout=*/absl::nullopt, error_msgs);
}
// Runs a function with a list of arguments.
v8::MaybeLocal<v8::Value> Run(ContextRecyclerScope& scope,
v8::Local<v8::UnboundScript> script,
const std::string& function_name,
std::vector<std::string>& error_msgs,
std::vector<v8::Local<v8::Value>> args) {
if (!helper_->RunScript(scope.GetContext(), script,
/*debug_id=*/nullptr,
/*script_timeout=*/absl::nullopt, error_msgs)) {
return {};
}
return helper_->CallFunction(scope.GetContext(),
/*debug_id=*/nullptr,
helper_->FormatScriptName(script),
function_name, args,
/*script_timeout=*/absl::nullopt, error_msgs);
}
protected:
base::test::TaskEnvironment task_environment_;
scoped_refptr<AuctionV8Helper> helper_;
std::unique_ptr<AuctionV8Helper::FullIsolateScope> v8_scope_;
};
// Test with no binding objects, just context creation.
TEST_F(ContextRecyclerTest, Basic) {
v8::Local<v8::UnboundScript> script = Compile("function test() { return 1;}");
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
v8::MaybeLocal<v8::Value> maybe_result =
Run(scope, script, "test", error_msgs);
v8::Local<v8::Value> result;
ASSERT_TRUE(maybe_result.ToLocal(&result));
int int_result = 0;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(1, int_result);
EXPECT_THAT(error_msgs, ElementsAre());
}
// Exercise ForDebuggingOnlyBindings, and make sure they reset properly.
TEST_F(ContextRecyclerTest, ForDebuggingOnlyBindings) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
const char kScript[] = R"(
function test(suffix) {
forDebuggingOnly.reportAdAuctionLoss('https://example.com/loss' + suffix);
forDebuggingOnly.reportAdAuctionWin('https://example.com/win' + suffix);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddForDebuggingOnlyBindings();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 1));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_EQ(
GURL("https://example.com/loss1"),
context_recycler.for_debugging_only_bindings()->TakeLossReportUrl());
EXPECT_EQ(
GURL("https://example.com/win1"),
context_recycler.for_debugging_only_bindings()->TakeWinReportUrl());
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 3));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_EQ(
GURL("https://example.com/loss3"),
context_recycler.for_debugging_only_bindings()->TakeLossReportUrl());
EXPECT_EQ(
GURL("https://example.com/win3"),
context_recycler.for_debugging_only_bindings()->TakeWinReportUrl());
}
}
// Exercise RegisterAdBeaconBindings, and make sure they reset properly.
TEST_F(ContextRecyclerTest, RegisterAdBeaconBindings) {
const char kScript[] = R"(
function test(num) {
let obj = {};
for (let i = num; i < num * 2; ++i) {
obj['f' + i] = 'https://example/com/' + i;
}
registerAdBeacon(obj);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddRegisterAdBeaconBindings();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 1));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(
context_recycler.register_ad_beacon_bindings()->TakeAdBeaconMap(),
ElementsAre(Pair("f1", GURL("https://example/com/1"))));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 2));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(
context_recycler.register_ad_beacon_bindings()->TakeAdBeaconMap(),
ElementsAre(Pair("f2", GURL("https://example/com/2")),
Pair("f3", GURL("https://example/com/3"))));
}
}
// Exercise ReportBindings, and make sure they reset properly.
TEST_F(ContextRecyclerTest, ReportBindings) {
const char kScript[] = R"(
function test(url) {
sendReportTo(url);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddReportBindings();
}
{
// Make sure an exception doesn't stick around between executions.
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), std::string("not-a-url")));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:3 Uncaught TypeError: "
"sendReportTo must be passed a valid HTTPS url."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(),
std::string("https://example.com/a")));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.report_bindings()->report_url().has_value());
EXPECT_EQ("https://example.com/a",
context_recycler.report_bindings()->report_url()->spec());
}
// Should already be cleared between executions.
EXPECT_FALSE(context_recycler.report_bindings()->report_url().has_value());
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(),
std::string("https://example.org/b")));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.report_bindings()->report_url().has_value());
EXPECT_EQ("https://example.org/b",
context_recycler.report_bindings()->report_url()->spec());
}
}
// Exercise SetBidBindings, and make sure they reset properly.
TEST_F(ContextRecyclerTest, SetBidBindings) {
const char kScript[] = R"(
function test(bid) {
setBid(bid);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddSetBidBindings();
}
base::RepeatingCallback<bool(const GURL&)> matches_ad1 = base::BindRepeating(
[](const GURL& url) { return url == GURL("https://example.com/ad1"); });
base::RepeatingCallback<bool(const GURL&)> ignore_arg_return_false =
base::BindRepeating([](const GURL& ignored) { return false; });
{
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad1"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/ignore_arg_return_false,
/*is_component_ad_excluded=*/ignore_arg_return_false);
task_environment_.FastForwardBy(base::Milliseconds(500));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad1"));
bid_dict.Set("bid", 10.0);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.set_bid_bindings()->has_bid());
mojom::BidderWorkletBidPtr bid =
context_recycler.set_bid_bindings()->TakeBid();
EXPECT_EQ("https://example.com/ad1", bid->ad_descriptor.url);
EXPECT_EQ(10.0, bid->bid);
EXPECT_EQ(base::Milliseconds(500), bid->bid_duration);
EXPECT_EQ(mojom::RejectReason::kNotAvailable,
context_recycler.set_bid_bindings()->reject_reason());
}
{
// Different ad objects get taken into account.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/notad1"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/ignore_arg_return_false,
/*is_component_ad_excluded=*/ignore_arg_return_false);
task_environment_.FastForwardBy(base::Milliseconds(500));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad1"));
bid_dict.Set("bid", 10.0);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:3 Uncaught TypeError: "
"bid render URL 'https://example.com/ad1' isn't one of "
"the registered creative URLs."));
EXPECT_FALSE(context_recycler.set_bid_bindings()->has_bid());
EXPECT_EQ(mojom::RejectReason::kNotAvailable,
context_recycler.set_bid_bindings()->reject_reason());
}
{
// Some components, and in a nested auction, w/o permission.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad3"),
absl::nullopt);
params->ad_components.emplace();
params->ad_components.value().emplace_back(
GURL("https://example.com/portion1"), absl::nullopt);
params->ad_components.value().emplace_back(
GURL("https://example.com/portion2"), absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/true, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/ignore_arg_return_false,
/*is_component_ad_excluded=*/ignore_arg_return_false);
task_environment_.FastForwardBy(base::Milliseconds(100));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad1"));
bid_dict.Set("bid", 10.0);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:3 Uncaught TypeError: bid does not "
"have allowComponentAuction set to true. Bid dropped from "
"component auction."));
EXPECT_FALSE(context_recycler.set_bid_bindings()->has_bid());
}
{
// Some components, and in a nested auction, w/permission.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad5"),
absl::nullopt);
params->ad_components.emplace();
params->ad_components.value().emplace_back(
GURL("https://example.com/portion3"), absl::nullopt);
params->ad_components.value().emplace_back(
GURL("https://example.com/portion4"), absl::nullopt);
params->ad_components.value().emplace_back(
GURL("https://example.com/portion5"), absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/true, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/ignore_arg_return_false,
/*is_component_ad_excluded=*/ignore_arg_return_false);
task_environment_.FastForwardBy(base::Milliseconds(200));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad5"));
bid_dict.Set("bid", 15.0);
bid_dict.Set("allowComponentAuction", true);
std::vector<v8::Local<v8::Value>> components;
components.push_back(gin::ConvertToV8(
helper_->isolate(), std::string("https://example.com/portion3")));
components.push_back(gin::ConvertToV8(
helper_->isolate(), std::string("https://example.com/portion5")));
bid_dict.Set("adComponents", components);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.set_bid_bindings()->has_bid());
mojom::BidderWorkletBidPtr bid =
context_recycler.set_bid_bindings()->TakeBid();
EXPECT_EQ("https://example.com/ad5", bid->ad_descriptor.url);
EXPECT_EQ(15.0, bid->bid);
EXPECT_EQ(base::Milliseconds(200), bid->bid_duration);
ASSERT_TRUE(bid->ad_component_descriptors.has_value());
EXPECT_THAT(
bid->ad_component_descriptors.value(),
ElementsAre(blink::AdDescriptor(GURL("https://example.com/portion3")),
blink::AdDescriptor(GURL("https://example.com/portion5"))));
}
{
// Wrong components.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad5"),
absl::nullopt);
params->ad_components.emplace();
params->ad_components.value().emplace_back(
GURL("https://example.com/portion6"), absl::nullopt);
params->ad_components.value().emplace_back(
GURL("https://example.com/portion7"), absl::nullopt);
params->ad_components.value().emplace_back(
GURL("https://example.com/portion8"), absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/ignore_arg_return_false,
/*is_component_ad_excluded=*/ignore_arg_return_false);
task_environment_.FastForwardBy(base::Milliseconds(200));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad5"));
bid_dict.Set("bid", 15.0);
std::vector<v8::Local<v8::Value>> components;
components.push_back(gin::ConvertToV8(
helper_->isolate(), std::string("https://example.com/portion3")));
components.push_back(gin::ConvertToV8(
helper_->isolate(), std::string("https://example.com/portion5")));
bid_dict.Set("adComponents", components);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:3 Uncaught TypeError: bid "
"adComponents "
"URL 'https://example.com/portion3' isn't one of the registered "
"creative URLs."));
EXPECT_FALSE(context_recycler.set_bid_bindings()->has_bid());
}
{
// use ad filter function - ads excluded.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad1"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/matches_ad1,
/*is_component_ad_excluded=*/matches_ad1);
task_environment_.FastForwardBy(base::Milliseconds(500));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad1"));
bid_dict.Set("bid", 10.0);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre("https://example.org/script.js:3 "
"Uncaught TypeError: bid render URL "
"'https://example.com/ad1' isn't one "
"of the registered creative URLs."));
EXPECT_FALSE(context_recycler.set_bid_bindings()->has_bid());
}
{
// use ad filter function - ads permitted.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad2"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
/*per_buyer_currency=*/absl::nullopt,
/*is_ad_excluded=*/matches_ad1,
/*is_component_ad_excluded=*/matches_ad1);
task_environment_.FastForwardBy(base::Milliseconds(500));
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad2"));
bid_dict.Set("bid", 10.0);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.set_bid_bindings()->has_bid());
mojom::BidderWorkletBidPtr bid =
context_recycler.set_bid_bindings()->TakeBid();
EXPECT_EQ("https://example.com/ad2", bid->ad_descriptor.url);
EXPECT_EQ(10.0, bid->bid);
EXPECT_EQ(base::Milliseconds(500), bid->bid_duration);
}
{
// Bid currency --- expect USD.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad2"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
blink::AdCurrency::From("USD"),
/*is_ad_excluded=*/matches_ad1,
/*is_component_ad_excluded=*/matches_ad1);
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad2"));
bid_dict.Set("bid", 10.0);
bid_dict.Set("bidCurrency", std::string("USD"));
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.set_bid_bindings()->has_bid());
mojom::BidderWorkletBidPtr bid =
context_recycler.set_bid_bindings()->TakeBid();
EXPECT_EQ("https://example.com/ad2", bid->ad_descriptor.url);
EXPECT_EQ(10.0, bid->bid);
ASSERT_TRUE(bid->bid_currency.has_value());
EXPECT_EQ("USD", bid->bid_currency->currency_code());
EXPECT_EQ(mojom::RejectReason::kNotAvailable,
context_recycler.set_bid_bindings()->reject_reason());
}
{
// Bid currency --- expect CAD.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad2"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
blink::AdCurrency::From("CAD"),
/*is_ad_excluded=*/matches_ad1,
/*is_component_ad_excluded=*/matches_ad1);
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad2"));
bid_dict.Set("bid", 10.0);
bid_dict.Set("bidCurrency", std::string("USD"));
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:3 Uncaught TypeError: bidCurrency "
"mismatch; returned 'USD', expected 'CAD'."));
EXPECT_FALSE(context_recycler.set_bid_bindings()->has_bid());
EXPECT_EQ(mojom::RejectReason::kWrongGenerateBidCurrency,
context_recycler.set_bid_bindings()->reject_reason());
}
{
// Make sure the reject reason doesn't latch.
ContextRecyclerScope scope(context_recycler);
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
params->ads.emplace();
params->ads.value().emplace_back(GURL("https://example.com/ad2"),
absl::nullopt);
context_recycler.set_bid_bindings()->ReInitialize(
base::TimeTicks::Now(),
/*has_top_level_seller_origin=*/false, params.get(),
blink::AdCurrency::From("CAD"),
/*is_ad_excluded=*/matches_ad1,
/*is_component_ad_excluded=*/matches_ad1);
gin::Dictionary bid_dict = gin::Dictionary::CreateEmpty(helper_->isolate());
bid_dict.Set("render", std::string("https://example.com/ad2"));
bid_dict.Set("bid", 10.0);
bid_dict.Set("bidCurrency", std::string("CAD"));
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), bid_dict));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(context_recycler.set_bid_bindings()->has_bid());
mojom::BidderWorkletBidPtr bid =
context_recycler.set_bid_bindings()->TakeBid();
EXPECT_EQ("https://example.com/ad2", bid->ad_descriptor.url);
EXPECT_EQ(10.0, bid->bid);
ASSERT_TRUE(bid->bid_currency.has_value());
EXPECT_EQ("CAD", bid->bid_currency->currency_code());
EXPECT_EQ(mojom::RejectReason::kNotAvailable,
context_recycler.set_bid_bindings()->reject_reason());
}
}
// Exercise SetPriorityBindings, and make sure they reset properly.
TEST_F(ContextRecyclerTest, SetPriorityBindings) {
const char kScript[] = R"(
function test(priority) {
setPriority(priority);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddSetPriorityBindings();
}
{
// Make sure an exception doesn't stick around between executions.
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), std::string("not-a-priority")));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:3 Uncaught TypeError: "
"setPriority requires 1 double parameter."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 5.0));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(
context_recycler.set_priority_bindings()->set_priority().has_value());
EXPECT_EQ(5.0,
context_recycler.set_priority_bindings()->set_priority().value());
}
// Should already be cleared between executions.
EXPECT_FALSE(
context_recycler.set_priority_bindings()->set_priority().has_value());
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), 10.0));
EXPECT_THAT(error_msgs, ElementsAre());
ASSERT_TRUE(
context_recycler.set_priority_bindings()->set_priority().has_value());
EXPECT_EQ(10.0,
context_recycler.set_priority_bindings()->set_priority().value());
}
}
TEST_F(ContextRecyclerTest, BidderLazyFiller) {
// Test to make sure lifetime managing/avoiding UaF is done right.
// Actual argument passing is covered thoroughly in bidder worklet unit tests.
const char kScript[] = R"(
function test(obj) {
if (!globalThis.stash) {
// On first run
globalThis.stash = obj;
} else {
return JSON.stringify(globalThis.stash);
}
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddInterestGroupLazyFiller();
context_recycler.AddBiddingBrowserSignalsLazyFiller();
}
{
base::Time now = base::Time::Now();
mojom::BidderWorkletNonSharedParamsPtr ig_params =
mojom::BidderWorkletNonSharedParams::New();
ig_params->user_bidding_signals.emplace("{\"j\": 1}");
ig_params->trusted_bidding_signals_keys.emplace();
ig_params->trusted_bidding_signals_keys->push_back("a");
ig_params->trusted_bidding_signals_keys->push_back("b");
ig_params->priority_vector.emplace();
ig_params->priority_vector->insert(
std::pair<std::string, double>("a", 42.0));
mojom::BiddingBrowserSignalsPtr bs_params =
mojom::BiddingBrowserSignals::New();
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(1), "[\"a\"]"));
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(2), "[\"b\"]"));
ContextRecyclerScope scope(context_recycler);
context_recycler.interest_group_lazy_filler()->ReInitialize(
ig_params.get());
context_recycler.bidding_browser_signals_lazy_filler()->ReInitialize(
bs_params.get(), now);
v8::Local<v8::Object> arg(v8::Object::New(helper_->isolate()));
context_recycler.interest_group_lazy_filler()->FillInObject(arg);
context_recycler.bidding_browser_signals_lazy_filler()->FillInObject(arg);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs, arg);
EXPECT_THAT(error_msgs, ElementsAre());
}
{
base::Time now = base::Time::Now();
mojom::BidderWorkletNonSharedParamsPtr ig_params =
mojom::BidderWorkletNonSharedParams::New();
ig_params->user_bidding_signals.emplace("{\"k\": 2}");
ig_params->trusted_bidding_signals_keys.emplace();
ig_params->trusted_bidding_signals_keys->push_back("c");
ig_params->trusted_bidding_signals_keys->push_back("d");
ig_params->priority_vector.emplace();
ig_params->priority_vector->insert(
std::pair<std::string, double>("e", 12.0));
mojom::BiddingBrowserSignalsPtr bs_params =
mojom::BiddingBrowserSignals::New();
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(3), "[\"c\"]"));
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(4), "[\"d\"]"));
ContextRecyclerScope scope(context_recycler);
context_recycler.interest_group_lazy_filler()->ReInitialize(
ig_params.get());
context_recycler.bidding_browser_signals_lazy_filler()->ReInitialize(
bs_params.get(), now);
v8::Local<v8::Object> arg(v8::Object::New(helper_->isolate()));
context_recycler.interest_group_lazy_filler()->FillInObject(arg);
context_recycler.bidding_browser_signals_lazy_filler()->FillInObject(arg);
std::vector<std::string> error_msgs;
v8::MaybeLocal<v8::Value> maybe_result =
Run(scope, script, "test", error_msgs, arg);
EXPECT_THAT(error_msgs, ElementsAre());
v8::Local<v8::Value> result;
ASSERT_TRUE(maybe_result.ToLocal(&result));
std::string str_result;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &str_result));
EXPECT_EQ(
"{\"userBiddingSignals\":{\"k\":2},"
"\"trustedBiddingSignalsKeys\":[\"c\",\"d\"],"
"\"priorityVector\":{\"e\":12},"
"\"prevWins\":[[240,[\"d\"]],[180,[\"c\"]]]}",
str_result);
}
}
TEST_F(ContextRecyclerTest, BidderLazyFiller2) {
// Test to make sure that stale objects with fields added that are no longer
// there handle it gracefully.
const char kScript[] = R"(
function test(obj) {
if (!globalThis.stash) {
// On first run
globalThis.stash = obj;
} else {
return JSON.stringify(globalThis.stash);
}
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddInterestGroupLazyFiller();
context_recycler.AddBiddingBrowserSignalsLazyFiller();
}
{
base::Time now = base::Time::Now();
mojom::BidderWorkletNonSharedParamsPtr ig_params =
mojom::BidderWorkletNonSharedParams::New();
ig_params->user_bidding_signals.emplace("{\"j\": 1}");
ig_params->trusted_bidding_signals_keys.emplace();
ig_params->trusted_bidding_signals_keys->push_back("a");
ig_params->trusted_bidding_signals_keys->push_back("b");
ig_params->priority_vector.emplace();
ig_params->priority_vector->insert(
std::pair<std::string, double>("a", 42.0));
mojom::BiddingBrowserSignalsPtr bs_params =
mojom::BiddingBrowserSignals::New();
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(1), "[\"a\"]"));
bs_params->prev_wins.push_back(
mojom::PreviousWin::New(now - base::Minutes(2), "[\"b\"]"));
ContextRecyclerScope scope(context_recycler);
context_recycler.interest_group_lazy_filler()->ReInitialize(
ig_params.get());
context_recycler.bidding_browser_signals_lazy_filler()->ReInitialize(
bs_params.get(), now);
v8::Local<v8::Object> arg(v8::Object::New(helper_->isolate()));
context_recycler.interest_group_lazy_filler()->FillInObject(arg);
context_recycler.bidding_browser_signals_lazy_filler()->FillInObject(arg);
std::vector<std::string> error_msgs;
Run(scope, script, "test", error_msgs, arg);
EXPECT_THAT(error_msgs, ElementsAre());
}
{
// Now cover the data for the fields not actually being there.
base::Time now = base::Time::Now();
mojom::BidderWorkletNonSharedParamsPtr ig_params =
mojom::BidderWorkletNonSharedParams::New();
mojom::BiddingBrowserSignalsPtr bs_params =
mojom::BiddingBrowserSignals::New();
ContextRecyclerScope scope(context_recycler);
context_recycler.interest_group_lazy_filler()->ReInitialize(
ig_params.get());
context_recycler.bidding_browser_signals_lazy_filler()->ReInitialize(
bs_params.get(), now);
v8::Local<v8::Object> arg(v8::Object::New(helper_->isolate()));
context_recycler.interest_group_lazy_filler()->FillInObject(arg);
context_recycler.bidding_browser_signals_lazy_filler()->FillInObject(arg);
std::vector<std::string> error_msgs;
v8::MaybeLocal<v8::Value> maybe_result =
Run(scope, script, "test", error_msgs, arg);
EXPECT_THAT(error_msgs, ElementsAre());
v8::Local<v8::Value> result;
ASSERT_TRUE(maybe_result.ToLocal(&result));
std::string str_result;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &str_result));
EXPECT_EQ(
"{\"userBiddingSignals\":null,"
"\"trustedBiddingSignalsKeys\":null,"
"\"priorityVector\":null,"
"\"prevWins\":[]}",
str_result);
}
}
TEST_F(ContextRecyclerTest, SharedStorageMethods) {
using RequestType =
auction_worklet::TestAuctionSharedStorageHost::RequestType;
using Request = auction_worklet::TestAuctionSharedStorageHost::Request;
const char kScript[] = R"(
function testSet(...args) {
sharedStorage.set(...args);
}
function testAppend(...args) {
sharedStorage.append(...args);
}
function testDelete(...args) {
sharedStorage.delete(...args);
}
function testClear(...args) {
sharedStorage.clear(...args);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddSharedStorageBindings(
&test_shared_storage_host,
/*shared_storage_permissions_policy_allowed=*/true);
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testSet", error_msgs,
/*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a")),
gin::ConvertToV8(helper_->isolate(), std::string("b"))}));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(test_shared_storage_host.observed_requests(),
ElementsAre(Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = false}));
test_shared_storage_host.ClearObservedRequests();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary options_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
options_dict.Set("ignoreIfPresent", true);
Run(scope, script, "testSet", error_msgs,
/*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a")),
gin::ConvertToV8(helper_->isolate(), std::string("b")),
gin::ConvertToV8(helper_->isolate(), options_dict)}));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(test_shared_storage_host.observed_requests(),
ElementsAre(Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = true}));
test_shared_storage_host.ClearObservedRequests();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testAppend", error_msgs,
/*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a")),
gin::ConvertToV8(helper_->isolate(), std::string("b"))}));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(test_shared_storage_host.observed_requests(),
ElementsAre(Request{.type = RequestType::kAppend,
.key = u"a",
.value = u"b",
.ignore_if_present = false}));
test_shared_storage_host.ClearObservedRequests();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testDelete", error_msgs,
/*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a"))}));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(test_shared_storage_host.observed_requests(),
ElementsAre(Request{.type = RequestType::kDelete,
.key = u"a",
.value = std::u16string(),
.ignore_if_present = false}));
test_shared_storage_host.ClearObservedRequests();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testClear", error_msgs,
/*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a"))}));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_THAT(test_shared_storage_host.observed_requests(),
ElementsAre(Request{.type = RequestType::kClear,
.key = std::u16string(),
.value = std::u16string(),
.ignore_if_present = false}));
test_shared_storage_host.ClearObservedRequests();
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testSet", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:3 Uncaught TypeError: Missing or "
"invalid \"key\" argument in sharedStorage.set()."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testSet", error_msgs, /*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a"))}));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:3 Uncaught TypeError: Missing or "
"invalid \"value\" argument in sharedStorage.set()."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testSet", error_msgs, /*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a")),
gin::ConvertToV8(helper_->isolate(), std::string("b")),
gin::ConvertToV8(helper_->isolate(), true)}));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:3 Uncaught TypeError: "
"Invalid \"options\" argument in sharedStorage.set()."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testAppend", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:7 Uncaught TypeError: Missing or "
"invalid \"key\" argument in sharedStorage.append()."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testAppend", error_msgs, /*args=*/
std::vector<v8::Local<v8::Value>>(
{gin::ConvertToV8(helper_->isolate(), std::string("a"))}));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:7 Uncaught TypeError: Missing or "
"invalid \"value\" argument in sharedStorage.append()."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testDelete", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:11 Uncaught TypeError: Missing or "
"invalid \"key\" argument in sharedStorage.delete()."));
}
}
TEST_F(ContextRecyclerTest, SharedStorageMethodsPermissionsPolicyDisabled) {
const char kScript[] = R"(
function testSet(...args) {
sharedStorage.set(...args);
}
function testAppend(...args) {
sharedStorage.append(...args);
}
function testDelete(...args) {
sharedStorage.delete(...args);
}
function testClear(...args) {
sharedStorage.clear(...args);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddSharedStorageBindings(
nullptr,
/*shared_storage_permissions_policy_allowed=*/false);
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testSet", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:3 Uncaught "
"TypeError: The \"shared-storage\" Permissions "
"Policy denied the method on sharedStorage."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testAppend", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:7 Uncaught "
"TypeError: The \"shared-storage\" Permissions "
"Policy denied the method on sharedStorage."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testDelete", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:11 Uncaught "
"TypeError: The \"shared-storage\" Permissions "
"Policy denied the method on sharedStorage."));
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "testClear", error_msgs,
/*args=*/std::vector<v8::Local<v8::Value>>());
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:15 Uncaught "
"TypeError: The \"shared-storage\" Permissions "
"Policy denied the method on sharedStorage."));
}
}
class ContextRecyclerPrivateAggregationEnabledTest
: public ContextRecyclerTest {
public:
ContextRecyclerPrivateAggregationEnabledTest() {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kPrivateAggregationApi);
}
// Wraps a debug_key into the appropriate dictionary. Templated to allow both
// integers and strings.
template <typename T>
v8::Local<v8::Value> WrapDebugKey(T debug_key) {
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("debug_key", debug_key);
return gin::ConvertToV8(helper_->isolate(), dict);
}
// Expects that pa_requests has one request, and the request has the given
// bucket, value and debug_key (or none, if absl::nullopt). Also expects that
// debug mode is enabled if debug_key is not absl::nullopt.
void ExpectOneHistogramRequestEqualTo(
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>
pa_requests,
absl::uint128 bucket,
int value,
absl::optional<blink::mojom::DebugKeyPtr> debug_key = absl::nullopt) {
blink::mojom::AggregatableReportHistogramContribution expected_contribution(
bucket, value);
blink::mojom::DebugModeDetailsPtr debug_mode_details;
if (debug_key.has_value()) {
debug_mode_details = blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/std::move(debug_key.value()));
} else {
debug_mode_details = blink::mojom::DebugModeDetails::New();
}
auction_worklet::mojom::PrivateAggregationRequest expected_request(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(expected_contribution.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
std::move(debug_mode_details));
ASSERT_EQ(pa_requests.size(), 1u);
EXPECT_EQ(pa_requests[0], expected_request.Clone());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Exercise `sendHistogramReport()` of PrivateAggregationBindings, and make sure
// they reset properly.
TEST_F(ContextRecyclerPrivateAggregationEnabledTest,
PrivateAggregationBindingsSendHistogramReport) {
using PrivateAggregationRequests =
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>;
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.sendHistogramReport(args);
}
function doNothing() {}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
// Basic test
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45);
}
// Large bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("18446744073709551616"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/absl::MakeUint128(/*high=*/1, /*low=*/0), /*value=*/45);
}
// Maximum bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("340282366920938463463374607431768211455"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/absl::Uint128Max(), /*value=*/45);
}
// Zero bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("0"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/0, /*value=*/45);
}
// Zero value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 0);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/0);
}
// Multiple requests
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
{
gin::Dictionary dict_1 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_1.Set("bucket", std::string("123"));
dict_1.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_1));
EXPECT_THAT(error_msgs, ElementsAre());
}
{
gin::Dictionary dict_2 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_2.Set("bucket", std::string("678"));
dict_2.Set("value", 90);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_2));
EXPECT_THAT(error_msgs, ElementsAre());
}
blink::mojom::AggregatableReportHistogramContribution
expected_contribution_1(/*bucket=*/123, /*value=*/45);
auction_worklet::mojom::PrivateAggregationRequest expected_request_1(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(expected_contribution_1.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
blink::mojom::AggregatableReportHistogramContribution
expected_contribution_2(/*bucket=*/678, /*value=*/90);
auction_worklet::mojom::PrivateAggregationRequest expected_request_2(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(expected_contribution_2.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
PrivateAggregationRequests pa_requests =
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests();
ASSERT_EQ(pa_requests.size(), 2u);
EXPECT_EQ(pa_requests[0], expected_request_1.Clone());
EXPECT_EQ(pa_requests[1], expected_request_2.Clone());
}
// Non-integer value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 4.5);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught TypeError: "
"Value must be an integer Number."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Too large bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("340282366920938463463374607431768211456"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught TypeError: "
"BigInt is too large."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Non-BigInt bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", 123);
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught TypeError: "
"bucket must be a BigInt."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Negative bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("-1"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught TypeError: "
"BigInt must be non-negative."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Negative value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", -1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught TypeError: "
"Value must be non-negative."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:8 Uncaught TypeError: "
"Invalid or missing bucket in sendHistogramReport argument."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:8 Uncaught TypeError: "
"Invalid or missing value in sendHistogramReport argument."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// API not called
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
Run(scope, script, "doNothing", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
}
// Exercise `enableDebugMode()` of PrivateAggregationBindings, and make sure
// they reset properly.
TEST_F(ContextRecyclerPrivateAggregationEnabledTest,
PrivateAggregationBindingsEnableDebugMode) {
using PrivateAggregationRequests =
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>;
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.sendHistogramReport(args);
}
function enableDebugMode(arg) {
if (arg === undefined) {
privateAggregation.enableDebugMode();
return;
}
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof arg.debug_key === "string") {
arg.debug_key = BigInt(arg.debug_key);
}
privateAggregation.enableDebugMode(arg);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
// Debug mode enabled with no debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs);
EXPECT_THAT(error_msgs, ElementsAre());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45, /*debug_key=*/nullptr);
}
// Debug mode enabled with debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("1234")));
EXPECT_THAT(error_msgs, ElementsAre());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45,
/*debug_key=*/blink::mojom::DebugKey::New(1234u));
}
// Debug mode enabled with large debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("18446744073709551615")));
EXPECT_THAT(error_msgs, ElementsAre());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45, /*debug_key=*/
blink::mojom::DebugKey::New(std::numeric_limits<uint64_t>::max()));
}
// Negative debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("-1")));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:21 Uncaught TypeError: "
"BigInt must be non-negative."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Too large debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("18446744073709551616")));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:21 Uncaught "
"TypeError: BigInt is too large."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Non-BigInt debug key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs, WrapDebugKey(1234));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:21 Uncaught TypeError: "
"debug_key must be a BigInt."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid enableDebugMode argument
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
// The debug key is not wrapped in a dictionary.
Run(scope, script, "enableDebugMode", error_msgs,
gin::ConvertToV8(helper_->isolate(), 1234));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:21 Uncaught TypeError: "
"Invalid argument in enableDebugMode."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// enableDebugMode called twice: second call fails, first continues to apply
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("1234")));
EXPECT_THAT(error_msgs, ElementsAre());
Run(scope, script, "enableDebugMode", error_msgs);
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"enableDebugMode may be called at most once."));
error_msgs.clear();
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45,
/*debug_key=*/blink::mojom::DebugKey::New(1234u));
}
// enableDebugMode called after report requested: debug details still applied
// Note that Shared Storage worklets have different behavior in this case.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("1234")));
EXPECT_THAT(error_msgs, ElementsAre());
ExpectOneHistogramRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
/*bucket=*/123, /*value=*/45,
/*debug_key=*/blink::mojom::DebugKey::New(1234u));
}
// Multiple debug mode reports
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
Run(scope, script, "enableDebugMode", error_msgs,
WrapDebugKey(std::string("1234")));
EXPECT_THAT(error_msgs, ElementsAre());
{
gin::Dictionary dict_1 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_1.Set("bucket", std::string("123"));
dict_1.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_1));
EXPECT_THAT(error_msgs, ElementsAre());
}
{
gin::Dictionary dict_2 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_2.Set("bucket", std::string("678"));
dict_2.Set("value", 90);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_2));
EXPECT_THAT(error_msgs, ElementsAre());
}
blink::mojom::AggregatableReportHistogramContribution
expected_contribution_1(/*bucket=*/123, /*value=*/45);
auction_worklet::mojom::PrivateAggregationRequest expected_request_1(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(expected_contribution_1.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/blink::mojom::DebugKey::New(1234u)));
blink::mojom::AggregatableReportHistogramContribution
expected_contribution_2(/*bucket=*/678, /*value=*/90);
auction_worklet::mojom::PrivateAggregationRequest expected_request_2(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(expected_contribution_2.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/blink::mojom::DebugKey::New(1234u)));
PrivateAggregationRequests pa_requests =
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests();
ASSERT_EQ(pa_requests.size(), 2u);
EXPECT_EQ(pa_requests[0], expected_request_1.Clone());
EXPECT_EQ(pa_requests[1], expected_request_2.Clone());
}
}
class ContextRecyclerPrivateAggregationExtensionsEnabledTest
: public ContextRecyclerTest {
public:
ContextRecyclerPrivateAggregationExtensionsEnabledTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kPrivateAggregationApi,
{{"fledge_extensions_enabled", "true"}});
}
// Creates a PrivateAggregationRequest with ForEvent contribution.
auction_worklet::mojom::PrivateAggregationRequestPtr CreateForEventRequest(
absl::uint128 bucket,
int value,
const std::string& event_type) {
auction_worklet::mojom::AggregatableReportForEventContribution contribution(
auction_worklet::mojom::ForEventSignalBucket::NewIdBucket(bucket),
auction_worklet::mojom::ForEventSignalValue::NewIntValue(value),
std::move(event_type));
return auction_worklet::mojom::PrivateAggregationRequest::New(
auction_worklet::mojom::AggregatableReportContribution::
NewForEventContribution(contribution.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
}
// Expects given `pa_requests` to have one item, which equals to the
// PrivateAggregationRequests created from given ForEvent contribution
// `expected_contribution`.
void ExpectOneForEventRequestEqualTo(
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>
pa_requests,
auction_worklet::mojom::AggregatableReportForEventContributionPtr
expected_contribution) {
auction_worklet::mojom::PrivateAggregationRequest expected_request(
auction_worklet::mojom::AggregatableReportContribution::
NewForEventContribution(expected_contribution.Clone()),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
ASSERT_EQ(pa_requests.size(), 1u);
EXPECT_EQ(pa_requests[0], expected_request.Clone());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Exercise `reportContributionsForEvent()` of PrivateAggregationBindings, and
// make sure they reset properly.
TEST_F(ContextRecyclerPrivateAggregationExtensionsEnabledTest,
PrivateAggregationForEventBindings) {
using PrivateAggregationRequests =
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>;
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === 'string') {
args.bucket = BigInt(args.bucket);
} else if (
typeof args.bucket === 'object' &&
typeof args.bucket.offset === 'string') {
args.bucket.offset = BigInt(args.bucket.offset);
}
privateAggregation.reportContributionForEvent('reserved.win', args);
}
function testDifferentEventTypes(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.reportContributionForEvent('reserved.win', args);
// Add 1 to value, to let reserved.loss request gets different
// contribution from reserved.win request.
args.value += 1;
privateAggregation.reportContributionForEvent('reserved.loss', args);
args.value += 1;
privateAggregation.reportContributionForEvent('reserved.always', args);
args.value += 1;
// Arbitrary unreserved event type.
privateAggregation.reportContributionForEvent('click', args);
}
function testMissingEventType(args) {
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.reportContributionForEvent(args);
}
function testMissingContribution() {
privateAggregation.reportContributionForEvent('reserved.win');
}
function testWrongArgumentsOrder(args) {
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.reportContributionForEvent(args, 'reserved.win');
}
function testInvalidReservedEventType(args) {
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.reportContributionForEvent("reserved.something", args);
}
function doNothing() {}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
// Basic test
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "testDifferentEventTypes", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auto pa_requests = context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests();
ASSERT_EQ(pa_requests.size(), 4u);
EXPECT_EQ(pa_requests[0],
CreateForEventRequest(/*bucket=*/123, /*value=*/45,
/*event_type=*/kReservedWin));
EXPECT_EQ(pa_requests[1],
CreateForEventRequest(/*bucket=*/123, /*value=*/46,
/*event_type=*/kReservedLoss));
EXPECT_EQ(pa_requests[2],
CreateForEventRequest(/*bucket=*/123, /*value=*/47,
/*event_type=*/kReservedAlways));
EXPECT_EQ(pa_requests[3],
CreateForEventRequest(/*bucket=*/123, /*value=*/48,
/*event_type=*/"click"));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing event_type (the first argument) to reportContributionForEvent()
// API.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "testMissingEventType", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:37 Uncaught TypeError: "
"reportContributionForEvent requires 2 parameters, with first "
"parameter being a string and second parameter being an object."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing contribution (the second argument) to reportContributionForEvent()
// API.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
Run(scope, script, "testMissingContribution", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:41 Uncaught TypeError: "
"reportContributionForEvent requires 2 parameters, with first "
"parameter being a string and second parameter being an object."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// The two arguments to reportContributionForEvent() API are in wrong order.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "testWrongArgumentsOrder", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre(
"https://example.org/script.js:48 Uncaught TypeError: "
"reportContributionForEvent requires 2 parameters, with first "
"parameter being a string and second parameter being an object."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid reserved event type.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "testInvalidReservedEventType", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
// Don't throw an error if an invalid reserved event type is provided, to
// provide forward compatibility with new reserved event types added later.
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Large bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("18446744073709551616"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewIdBucket(absl::MakeUint128(/*high=*/1, /*low=*/0)),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(45),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Maximum bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("340282366920938463463374607431768211455"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewIdBucket(absl::Uint128Max()),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(45),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Zero bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("0"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewIdBucket(0),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(45),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Zero value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 0);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewIdBucket(123),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(0),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Multiple requests
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
{
gin::Dictionary dict_1 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_1.Set("bucket", std::string("123"));
dict_1.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_1));
EXPECT_THAT(error_msgs, ElementsAre());
}
{
gin::Dictionary dict_2 = gin::Dictionary::CreateEmpty(helper_->isolate());
dict_2.Set("bucket", std::string("678"));
dict_2.Set("value", 90);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict_2));
EXPECT_THAT(error_msgs, ElementsAre());
}
PrivateAggregationRequests pa_requests =
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests();
ASSERT_EQ(pa_requests.size(), 2u);
EXPECT_EQ(pa_requests[0],
CreateForEventRequest(/*bucket=*/123, /*value=*/45,
/*event_type=*/kReservedWin));
EXPECT_EQ(pa_requests[1],
CreateForEventRequest(/*bucket=*/678, /*value=*/90,
/*event_type=*/kReservedWin));
}
// Too large bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("340282366920938463463374607431768211456"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"BigInt is too large."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Dictionary bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("bid-reject-reason"));
bucket_dict.Set("scale", 2);
bucket_dict.Set("offset", std::string("-255"));
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
auto signal_bucket = auction_worklet::mojom::SignalBucket::New(
/*base_value=*/auction_worklet::mojom::BaseValue::kBidRejectReason,
/*scale=*/2,
/*offset=*/
auction_worklet::mojom::BucketOffset::New(/*value=*/255,
/*is_negative=*/true));
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewSignalBucket(std::move(signal_bucket)),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(1),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Dictionary bucket. Scale and offset are optional.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("bid-reject-reason"));
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
auction_worklet::mojom::SignalBucket signal_bucket;
signal_bucket.base_value =
auction_worklet::mojom::BaseValue::kBidRejectReason;
signal_bucket.offset = nullptr;
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewSignalBucket(signal_bucket.Clone()),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewIntValue(1),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Invalid bucket dictionary, which has no "baseValue" key.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("scale", 1);
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid bucket dictionary, whose baseValue is invalid.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("notValidBaseValue"));
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid bucket dictionary, whose scale is not a Number.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("winning-bid"));
bucket_dict.Set("scale", std::string("255"));
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid bucket dictionary, whose scale is NaN.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("winningBid"));
bucket_dict.Set("scale", std::numeric_limits<double>::quiet_NaN());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid bucket dictionary, whose scale is infinity.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("winningBid"));
bucket_dict.Set("scale", std::numeric_limits<double>::infinity());
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Invalid bucket dictionary, whose offset is not a BigInt.
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary bucket_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
bucket_dict.Set("baseValue", std::string("winning-bid"));
bucket_dict.Set("offset", 255);
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", bucket_dict);
dict.Set("value", 1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid bucket dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Dictionary value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary value_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
value_dict.Set("baseValue", std::string("winning-bid"));
value_dict.Set("scale", 2);
value_dict.Set("offset", -5);
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("1596"));
dict.Set("value", value_dict);
auto signal_value = auction_worklet::mojom::SignalValue::New(
/*base_value=*/auction_worklet::mojom::BaseValue::kWinningBid,
/*scale=*/2,
/*offset=*/-5);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
auction_worklet::mojom::AggregatableReportForEventContribution
expected_contribution(
/*bucket=*/auction_worklet::mojom::ForEventSignalBucket::
NewIdBucket(1596),
/*value=*/
auction_worklet::mojom::ForEventSignalValue::NewSignalValue(
std::move(signal_value)),
/*event_type=*/kReservedWin);
ExpectOneForEventRequestEqualTo(
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests(),
expected_contribution.Clone());
}
// Invalid value dictionary, which has no base_value key
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary value_dict =
gin::Dictionary::CreateEmpty(helper_->isolate());
value_dict.Set("offset", 255);
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("1"));
dict.Set("value", value_dict);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught "
"TypeError: Invalid value dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Non BigInt or dictionary bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", 12.3);
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"Bucket must be a BigInt or a dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Non integer or dictionary value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 4.5);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"Value must be an integer or a dictionary."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Negative bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("-1"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"BigInt must be non-negative."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Negative value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", -1);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"Value must be non-negative."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing bucket
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"Invalid or missing bucket in reportContributionForEvent's "
"argument."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// Missing value
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", 123);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:12 Uncaught TypeError: "
"Invalid or missing value in reportContributionForEvent's "
"argument."));
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
// API not called
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
Run(scope, script, "doNothing", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(error_msgs, ElementsAre());
EXPECT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
}
class ContextRecyclerPrivateAggregationDisabledTest
: public ContextRecyclerTest {
public:
ContextRecyclerPrivateAggregationDisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kPrivateAggregationApi);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Exercise PrivateAggregationBindings, and make sure they reset properly.
TEST_F(ContextRecyclerPrivateAggregationDisabledTest,
PrivateAggregationBindings) {
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.sendHistogramReport(args);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught ReferenceError: "
"privateAggregation is not defined."));
ASSERT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
}
class ContextRecyclerPrivateAggregationDisabledForFledgeOnlyTest
: public ContextRecyclerTest {
public:
ContextRecyclerPrivateAggregationDisabledForFledgeOnlyTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kPrivateAggregationApi,
{{"enabled_in_fledge", "false"}});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Exercise PrivateAggregationBindings, and make sure they reset properly.
TEST_F(ContextRecyclerPrivateAggregationDisabledForFledgeOnlyTest,
PrivateAggregationBindings) {
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.sendHistogramReport(args);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:8 Uncaught ReferenceError: "
"privateAggregation is not defined."));
ASSERT_TRUE(context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests()
.empty());
}
}
class ContextRecyclerPrivateAggregationOnlyFledgeExtensionsDisabledTest
: public ContextRecyclerTest {
public:
ContextRecyclerPrivateAggregationOnlyFledgeExtensionsDisabledTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kPrivateAggregationApi,
{{"fledge_extensions_enabled", "false"}});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Make sure that `reportContributionForEvent()` isn't available, but the other
// `privateAggregation` functions are.
TEST_F(ContextRecyclerPrivateAggregationOnlyFledgeExtensionsDisabledTest,
PrivateAggregationForEventBindings) {
using PrivateAggregationRequests =
std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>;
const char kScript[] = R"(
function test(args) {
// Passing BigInts in directly is complicated so we construct them from
// strings.
if (typeof args.bucket === "string") {
args.bucket = BigInt(args.bucket);
}
privateAggregation.sendHistogramReport(args);
privateAggregation.reportContributionForEvent("example", args);
}
)";
v8::Local<v8::UnboundScript> script = Compile(kScript);
ASSERT_FALSE(script.IsEmpty());
ContextRecycler context_recycler(helper_.get());
{
ContextRecyclerScope scope(context_recycler); // Initialize context
context_recycler.AddPrivateAggregationBindings(
/*private_aggregation_permissions_policy_allowed=*/true);
}
{
ContextRecyclerScope scope(context_recycler);
std::vector<std::string> error_msgs;
gin::Dictionary dict = gin::Dictionary::CreateEmpty(helper_->isolate());
dict.Set("bucket", std::string("123"));
dict.Set("value", 45);
Run(scope, script, "test", error_msgs,
gin::ConvertToV8(helper_->isolate(), dict));
EXPECT_THAT(
error_msgs,
ElementsAre("https://example.org/script.js:9 Uncaught TypeError: "
"privateAggregation.reportContributionForEvent is not a "
"function."));
PrivateAggregationRequests pa_requests =
context_recycler.private_aggregation_bindings()
->TakePrivateAggregationRequests();
ASSERT_EQ(pa_requests.size(), 1u);
}
}
} // namespace auction_worklet