blob: 5257237c1328982027c228a7107d7d457ae9fa67 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/interest_group/additional_bids_util.h"
#include <stdint.h>
#include <array>
#include <limits>
#include <optional>
#include <string>
#include "base/base64.h"
#include "base/containers/flat_set.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/types/expected.h"
#include "base/types/optional_ref.h"
#include "base/uuid.h"
#include "base/values.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/interest_group/auction_metrics_recorder.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h"
#include "crypto/sha2.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "third_party/blink/public/common/interest_group/ad_display_size.h"
#include "third_party/boringssl/src/include/openssl/curve25519.h"
#include "url/gurl.h"
#include "url/origin.h"
using testing::UnorderedElementsAre;
namespace content {
namespace {
// In case one wants to generate some keys for test use, the following may be
// useful:
// template <int N>
// std::string SerializeKey(uint8_t key[N]) {
// std::string out;
// for (int row = 0; row < (N / 8); ++row) {
// for (int col = 0; col < 8; ++col) {
// base::StrAppend(
// &out, {base::StringPrintf("0x%02x",
// static_cast<unsigned>(
// key[row * 8 + col])),
// ", "});
// }
// base::StrAppend(&out, {"\n"});
// }
// return out;
// }
//
// TEST_F(AdditionalBidsUtilTest, GenerateKeyPair) {
// uint8_t public_key[32];
// uint8_t private_key[64];
// ED25519_keypair(public_key, private_key);
// std::cout << "public_key:\n";
// std::cout << SerializeKey<32>(public_key) << "\n";
// std::cout << base::Base64Encode(base::span(public_key));
// std::cout << "\n\n";
//
// std::cout << "private_key:\n";
// std::cout << SerializeKey<64>(private_key) << "\n";
// std::cout << base::Base64Encode(base::span(private_key));
// std::cout << "\n\n";
// }
// Some test data for key/signature fields. These are just random sequences
// of bytes of the right length, not proper cryptographic ones.
constexpr blink::InterestGroup::AdditionalBidKey kKey1{
0xF5, 0x30, 0x88, 0xE9, 0x9B, 0xC7, 0xB0, 0x2A, 0x8C, 0xBE, 0x11,
0x8D, 0xD3, 0xEC, 0xEF, 0xEB, 0xB5, 0x71, 0xDF, 0xF9, 0x7D, 0x67,
0xEF, 0xFF, 0x9A, 0xAD, 0xE1, 0x63, 0x86, 0xAD, 0x57, 0x5E};
constexpr char kKey1Base64[] = "9TCI6ZvHsCqMvhGN0+zv67Vx3/l9Z+//mq3hY4atV14=";
constexpr blink::InterestGroup::AdditionalBidKey kKey2{
0x79, 0x34, 0x0E, 0x99, 0xF6, 0x02, 0x98, 0xB2, 0xF6, 0x82, 0xAA,
0xDA, 0x3C, 0x95, 0xFA, 0x62, 0x3A, 0xF2, 0x53, 0xA8, 0x56, 0xEB,
0x21, 0xC4, 0xC2, 0x67, 0x6C, 0x5D, 0xE3, 0x4B, 0xDA, 0xA0};
constexpr char kKey2Base64[] = "eTQOmfYCmLL2gqraPJX6YjryU6hW6yHEwmdsXeNL2qA=";
constexpr auto kSig1 = std::to_array<uint8_t>(
{0x49, 0xD1, 0x27, 0x01, 0x29, 0x9E, 0xC8, 0x34, 0xE3, 0x12, 0x46,
0xA0, 0xFA, 0x17, 0x33, 0x1E, 0xD2, 0x7B, 0xC0, 0x63, 0x7D, 0x7F,
0x63, 0xF6, 0x12, 0x49, 0x39, 0x40, 0x80, 0x2F, 0x31, 0x93, 0x99,
0xD7, 0x93, 0x16, 0x58, 0x4D, 0x3B, 0xEC, 0x0F, 0x46, 0x07, 0x29,
0xE4, 0xE6, 0x13, 0x0D, 0xD7, 0xEA, 0x6D, 0x35, 0x60, 0xB8, 0x27,
0x9E, 0x86, 0xC7, 0xE0, 0x10, 0x63, 0xEA, 0x44, 0xE6});
constexpr char kSig1Base64[] =
"SdEnASmeyDTjEkag+hczHtJ7wGN9f2P2Ekk5QIAvMZOZ15MWWE077A9GBynk5hMN1+"
"ptNWC4J56Gx+AQY+pE5g==";
constexpr auto kSig2 = std::to_array<uint8_t>(
{0x91, 0x2C, 0xF4, 0x82, 0x8F, 0x62, 0x6B, 0x1F, 0x4A, 0x34, 0x1B,
0x8C, 0x4C, 0xB8, 0xD6, 0xA1, 0x41, 0xD0, 0xBD, 0xCC, 0x67, 0xBA,
0xCF, 0x08, 0xE4, 0x32, 0x09, 0x5D, 0x97, 0x06, 0x09, 0x41, 0xFA,
0xEA, 0x12, 0x8E, 0x49, 0x05, 0x73, 0xE2, 0xA4, 0x57, 0x7B, 0xA5,
0x3B, 0x00, 0xAE, 0x23, 0xAF, 0x61, 0xE9, 0x5F, 0xA4, 0x39, 0xBD,
0x07, 0x9B, 0xB7, 0x49, 0x31, 0x52, 0xDD, 0x69, 0xDD});
constexpr char kSig2Base64[] =
"kSz0go9iax9KNBuMTLjWoUHQvcxnus8I5DIJXZcGCUH66hKOSQVz4qRXe6U7AK4jr2HpX6Q5vQ"
"ebt0kxUt1p3Q==";
// Version that requires forgiving decoder to accept.
constexpr char kSig2Base64Sloppy[] =
" kSz0go9iax9KNBuMTLjWoUHQvcxnus8I5DIJXZcGCUH66hKOSQVz4qRXe6U7AK4jr2HpX6Q5v"
"Qebt0kxUt1p3Q";
constexpr char kPretendBid[] = "Hi, I am a JSON bid.";
std::string ComputeBidNonce(const base::Uuid& auction_nonce,
const base::Uuid& seller_nonce) {
return base::Base64Encode(crypto::SHA256HashString(base::StrCat(
{auction_nonce.AsLowercaseString(), seller_nonce.AsLowercaseString()})));
}
class AdditionalBidsUtilTest : public testing::Test {
protected:
base::Value::Dict MakeMinimalValid() {
base::Value::Dict ig_dict;
ig_dict.Set("name", "trainfans");
ig_dict.Set("biddingLogicURL", "https://rollingstock.test/logic.js");
ig_dict.Set("owner", "https://rollingstock.test/");
base::Value::Dict bid_dict;
bid_dict.Set("bid", 10.0);
bid_dict.Set("render", "https://en.wikipedia.test/wiki/Train");
base::Value::Dict additional_bid_dict;
additional_bid_dict.Set("auctionNonce", kAuctionNonce.AsLowercaseString());
additional_bid_dict.Set("seller", "https://seller.test");
additional_bid_dict.Set("topLevelSeller", "https://top-organizer.test");
additional_bid_dict.Set("interestGroup", std::move(ig_dict));
additional_bid_dict.Set("bid", std::move(bid_dict));
return additional_bid_dict;
}
base::Value::Dict MakeValidWithMultipleNegativeIGs() {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::Dict negative_igs_dict;
negative_igs_dict.Set("joiningOrigin", "https://depot.test");
base::Value::List negative_ig_names_list;
negative_ig_names_list.Append("negative_group");
negative_ig_names_list.Append("another negative group");
negative_igs_dict.Set("interestGroupNames",
std::move(negative_ig_names_list));
additional_bid_dict.Set("negativeInterestGroups",
std::move(negative_igs_dict));
return additional_bid_dict;
}
static base::Value::Dict MakeValidSignedBid() {
base::Value::Dict signed_dict;
signed_dict.Set("bid", kPretendBid);
base::Value::List sigs_list;
base::Value::Dict sig1;
sig1.Set("key", kKey1Base64);
sig1.Set("signature", kSig1Base64);
sigs_list.Append(std::move(sig1));
base::Value::Dict sig2;
sig2.Set("key", kKey2Base64);
sig2.Set("signature", kSig2Base64);
sigs_list.Append(std::move(sig2));
signed_dict.Set("signatures", std::move(sigs_list));
return signed_dict;
}
// Fills in the key only, we don't actually need the signature part any more.
SignedAdditionalBidSignature SignatureWithKey(
const blink::InterestGroup::AdditionalBidKey& key) {
SignedAdditionalBidSignature result;
result.key = key;
return result;
}
const base::Uuid kAuctionNonce{base::Uuid::GenerateRandomV4()};
const base::Uuid kSellerNonce{base::Uuid::GenerateRandomV4()};
const std::string kBidNonce{ComputeBidNonce(kAuctionNonce, kSellerNonce)};
const base::flat_set<url::Origin> kInterestGroupBuyers{
url::Origin::Create(GURL("https://buyer.test")),
url::Origin::Create(GURL("https://rollingstock.test")),
url::Origin::Create(GURL("https://trainstuff.test"))};
const url::Origin kSeller = url::Origin::Create(GURL("https://seller.test"));
const url::Origin kTopSeller =
url::Origin::Create(GURL("https://top-organizer.test"));
};
TEST_F(AdditionalBidsUtilTest, FailNotDict) {
base::Value input(5);
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' is not a "
"dictionary.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailNoNonce) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kFledgeSellerNonce);
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("auctionNonce");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect nonce.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailInvalidNonce) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kFledgeSellerNonce);
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("auctionNonce", "not-a-nonce");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect nonce.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailNoNonceWithSellerNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("auctionNonce");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to no auctionNonce or bidNonce in bid -- exactly one is required.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailInvalidNonceWithSellerNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("auctionNonce", "not-a-nonce");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
base::StringPrintf(
"Additional bid on auction with seller 'https://seller.test' "
"rejected due to auctionNonce from bid (not-a-nonce) not matching "
"the header auctionNonce (%s).",
kAuctionNonce.AsLowercaseString()),
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailBothAuctionNonceAndBidNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("auctionNonce", kAuctionNonce.AsLowercaseString());
additional_bid_dict.Set("bidNonce", kBidNonce);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to both auctionNonce and bidNonce in bid -- exactly one is "
"required.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailBidNoSellerNonceButNoAuctionNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("auctionNonce");
additional_bid_dict.Set("bidNonce", kBidNonce);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing auctionNonce on a bid returned without a seller nonce.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailBidSellerNonceButNoBidNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("auctionNonce", kAuctionNonce.AsLowercaseString());
base::Value input(std::move(additional_bid_dict));
auto result =
DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/kSellerNonce.AsLowercaseString(),
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing bidNonce on a bid returned with a seller nonce.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailInvalidBidNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("auctionNonce");
// Set bidNonce to base64(sha256("incorrect")).
constexpr char kIncorrectNonce[] =
"ID01Nr1irTOscLfqPU9eELbVLr0Mt1goQaBTrrtxhqM=";
additional_bid_dict.Set("bidNonce", kIncorrectNonce);
base::Value input(std::move(additional_bid_dict));
auto result =
DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/kSellerNonce.AsLowercaseString(),
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(base::StringPrintf(
"Additional bid on auction with seller "
"'https://seller.test' rejected due to bidNonce from bid (%s) "
"not matching its expectation (%s) as calculated from the "
"header auctionNonce (%s) and sellerNonce (%s).",
kIncorrectNonce, kBidNonce, kAuctionNonce.AsLowercaseString(),
kSellerNonce.AsLowercaseString()),
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingSeller) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("seller");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect seller.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailInvalidSeller) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("seller", "http://notseller.test");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect seller.",
result.error());
}
// Specifying topLevelSeller in a bid in a non-component auction is a problem.
TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller) {
base::Value input(MakeMinimalValid());
auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
/*top_level_seller=*/std::nullopt);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to specifying topLevelSeller in a non-component auction.",
result.error());
}
// Not specifying topLevelSeller in component auction bid is also a problem.
TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller2) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("topLevelSeller");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect topLevelSeller.",
result.error());
}
// An incorrect topLevelSeller in a bid in a component auction is also a
// problem.
TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller3) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("topLevelSeller", "https://wrong-organizer.test");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or incorrect topLevelSeller.",
result.error());
}
// Missing IG dictionary.
TEST_F(AdditionalBidsUtilTest, FailNoIGDictionary) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("interestGroup");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing interest group name.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupName) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.RemoveByDottedPath("interestGroup.name");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing interest group name.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupBiddingScript) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.RemoveByDottedPath("interestGroup.biddingLogicURL");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing interest group bidding URL.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupOwner) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.RemoveByDottedPath("interestGroup.owner");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing interest group owner.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailNonHttpsInterestGroupOwner) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("interestGroup.owner",
"http://rollingstock.test/");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to non-https interest group owner URL.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailDomainMismatchBetweenOwnerAndBiddingScript) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("interestGroup.owner",
"https://trainstuff.test/");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid origin of biddingLogicURL.",
result.error());
}
// The additional bid owner is missing from interestGroupBuyers.
TEST_F(AdditionalBidsUtilTest, AdditionalBidOwnerNotInInterestGroupBuyers) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value input(std::move(additional_bid_dict));
const base::flat_set<url::Origin> wrong_interest_group_buyers{
url::Origin::Create(GURL("https://wrongbuyer.test"))};
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
wrong_interest_group_buyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"because the additional bid's owner, 'https://rollingstock.test', "
"is not in interestGroupBuyers.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingBid) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("bid");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing bid info.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingBidCreative) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.RemoveByDottedPath("bid.render");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or invalid creative URL.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailMissingBidValue) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.RemoveByDottedPath("bid.bid");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or invalid bid value.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, FailInvalidBidValue) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.bid", 0.0);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or invalid bid value.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, MinimalValid) {
base::Value input(MakeMinimalValid());
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid_state);
ASSERT_TRUE(result->bid);
const InterestGroupAuction::BidState* bid_state = result->bid_state.get();
const InterestGroupAuction::Bid* bid = result->bid.get();
EXPECT_TRUE(bid_state->made_bid);
EXPECT_EQ("trainfans", bid_state->bidder->interest_group.name);
ASSERT_TRUE(bid_state->additional_bid_buyer.has_value());
EXPECT_EQ(bid_state->bidder->interest_group.owner,
bid_state->additional_bid_buyer);
EXPECT_EQ("https://rollingstock.test",
bid_state->bidder->interest_group.owner.Serialize());
ASSERT_TRUE(bid_state->bidder->interest_group.bidding_url.has_value());
EXPECT_EQ("https://rollingstock.test/logic.js",
bid_state->bidder->interest_group.bidding_url->spec());
ASSERT_TRUE(bid_state->bidder->interest_group.ads.has_value());
ASSERT_EQ(1u, bid_state->bidder->interest_group.ads->size());
EXPECT_EQ("https://en.wikipedia.test/wiki/Train",
bid_state->bidder->interest_group.ads.value()[0].render_url());
EXPECT_EQ(auction_worklet::mojom::BidRole::kBothKAnonModes, bid->bid_role);
EXPECT_EQ("null", bid->ad_metadata);
EXPECT_EQ(10.0, bid->bid);
EXPECT_EQ(std::nullopt, bid->bid_currency);
EXPECT_EQ(std::nullopt, bid->ad_cost);
EXPECT_EQ(blink::AdDescriptor(GURL("https://en.wikipedia.test/wiki/Train")),
bid->ad_descriptor);
EXPECT_EQ(0u, bid->selected_ad_components.size());
EXPECT_EQ(std::nullopt, bid->modeling_signals);
EXPECT_EQ(std::nullopt, bid->aggregate_win_signals);
EXPECT_EQ(&bid_state->bidder->interest_group, bid->interest_group);
EXPECT_EQ(&bid_state->bidder->interest_group.ads.value()[0], bid->bid_ad);
EXPECT_EQ(bid_state, bid->bid_state);
}
TEST_F(AdditionalBidsUtilTest, MinimalValidWithSellerNonce) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Remove("auctionNonce");
additional_bid_dict.Set("bidNonce", kBidNonce);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce,
/*seller_nonce=*/kSellerNonce.AsLowercaseString(), kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid_state);
ASSERT_TRUE(result->bid);
const InterestGroupAuction::BidState* bid_state = result->bid_state.get();
const InterestGroupAuction::Bid* bid = result->bid.get();
EXPECT_TRUE(bid_state->made_bid);
EXPECT_EQ("trainfans", bid_state->bidder->interest_group.name);
ASSERT_TRUE(bid_state->additional_bid_buyer.has_value());
EXPECT_EQ(bid_state->bidder->interest_group.owner,
bid_state->additional_bid_buyer);
EXPECT_EQ("https://rollingstock.test",
bid_state->bidder->interest_group.owner.Serialize());
ASSERT_TRUE(bid_state->bidder->interest_group.bidding_url.has_value());
EXPECT_EQ("https://rollingstock.test/logic.js",
bid_state->bidder->interest_group.bidding_url->spec());
ASSERT_TRUE(bid_state->bidder->interest_group.ads.has_value());
ASSERT_EQ(1u, bid_state->bidder->interest_group.ads->size());
EXPECT_EQ("https://en.wikipedia.test/wiki/Train",
bid_state->bidder->interest_group.ads.value()[0].render_url());
EXPECT_EQ(auction_worklet::mojom::BidRole::kBothKAnonModes, bid->bid_role);
EXPECT_EQ("null", bid->ad_metadata);
EXPECT_EQ(10.0, bid->bid);
EXPECT_EQ(std::nullopt, bid->bid_currency);
EXPECT_EQ(std::nullopt, bid->ad_cost);
EXPECT_EQ(blink::AdDescriptor(GURL("https://en.wikipedia.test/wiki/Train")),
bid->ad_descriptor);
EXPECT_EQ(0u, bid->selected_ad_components.size());
EXPECT_EQ(std::nullopt, bid->modeling_signals);
EXPECT_EQ(std::nullopt, bid->aggregate_win_signals);
EXPECT_EQ(&bid_state->bidder->interest_group, bid->interest_group);
EXPECT_EQ(&bid_state->bidder->interest_group.ads.value()[0], bid->bid_ad);
EXPECT_EQ(bid_state, bid->bid_state);
}
TEST_F(AdditionalBidsUtilTest, InvalidBidCurrencyType) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.bidCurrency", 5);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid bidCurrency.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, InvalidBidCurrencySyntax) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.bidCurrency", "Dollars");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid bidCurrency.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, ValidBidCurrency) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.bidCurrency", "USD");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_TRUE(result.has_value());
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->bid_currency);
EXPECT_EQ("USD", result->bid->bid_currency->currency_code());
}
TEST_F(AdditionalBidsUtilTest, InvalidAdCost) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.adCost", "big");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid adCost.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, ValidAdCost) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.adCost", 15.5);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->ad_cost);
EXPECT_EQ(15.5, *result->bid->ad_cost);
}
// We have a tradition of ignoring modeling signals if they're out of range,
// so this follows.
TEST_F(AdditionalBidsUtilTest, InvalidModelingSignals) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", 4096);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
EXPECT_FALSE(result->bid->modeling_signals);
}
TEST_F(AdditionalBidsUtilTest, InvalidModelingSignals2) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", -0.001);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
EXPECT_FALSE(result->bid->modeling_signals);
}
// Bad-type modeling signals still an error, however.
TEST_F(AdditionalBidsUtilTest, BadTypeModelingSignals) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", "string");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to non-numeric modelingSignals.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, ValidAggregateWinSignals) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::Dict aggregate_win_signals_dict;
aggregate_win_signals_dict.Set("test_string", "hello");
aggregate_win_signals_dict.Set("test_number", 1.0);
base::Value::List test_array;
test_array.Append(1);
test_array.Append(2);
test_array.Append(3);
aggregate_win_signals_dict.Set("test_array", std::move(test_array));
additional_bid_dict.SetByDottedPath("bid.aggregateWinSignals",
std::move(aggregate_win_signals_dict));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->aggregate_win_signals);
EXPECT_EQ(
*result->bid->aggregate_win_signals,
R"({"test_array":[1,2,3],"test_number":1.0,"test_string":"hello"})");
}
TEST_F(AdditionalBidsUtilTest, InvalidAggregateWinSignals) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::Dict aggregate_win_signals_dict;
// Create a deeply nested list that exceeds the maximum depth
// for JSON serialization.
const size_t kMaxDepth = 200;
base::Value::List deep_list;
for (size_t i = 0; i < kMaxDepth + 1; ++i) {
base::Value::List new_top_list;
new_top_list.Append(std::move(deep_list));
deep_list = std::move(new_top_list);
}
aggregate_win_signals_dict.Set("deeply_nested", std::move(deep_list));
additional_bid_dict.SetByDottedPath("bid.aggregateWinSignals",
std::move(aggregate_win_signals_dict));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
// Expect the decoding to fail due to exceeding max depth
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StrCat({"Additional bid on auction with seller '",
kSeller.Serialize(),
"' rejected due to invalid aggregateWinSignals."}));
}
TEST_F(AdditionalBidsUtilTest, ValidModelingSignals) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", 0);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->modeling_signals);
EXPECT_EQ(*result->bid->modeling_signals, 0);
}
TEST_F(AdditionalBidsUtilTest, ValidModelingSignals2) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", 2.5);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->modeling_signals);
EXPECT_EQ(*result->bid->modeling_signals, 2);
}
TEST_F(AdditionalBidsUtilTest, ValidModelingSignals3) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.modelingSignals", 4095.5);
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid->modeling_signals);
EXPECT_EQ(*result->bid->modeling_signals, 4095);
}
TEST_F(AdditionalBidsUtilTest, InvalidAdComponents) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.SetByDottedPath("bid.adComponents", "oops");
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid adComponents.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, InvalidAdComponentsEntry) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::List ad_components_list;
ad_components_list.Append(10);
additional_bid_dict.SetByDottedPath("bid.adComponents",
std::move(ad_components_list));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid entry in adComponents.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, TooManyAdComponents) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::List ad_components_list;
const size_t kMaxAdAuctionAdComponents = blink::MaxAdAuctionAdComponents();
for (size_t i = 0; i < kMaxAdAuctionAdComponents + 1; ++i) {
ad_components_list.Append("https://en.wikipedia.test/wiki/Locomotive");
}
additional_bid_dict.SetByDottedPath("bid.adComponents",
std::move(ad_components_list));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to too many ad component URLs.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, ValidAdComponents) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::List ad_components_list;
ad_components_list.Append("https://en.wikipedia.test/wiki/Locomotive");
ad_components_list.Append("https://en.wikipedia.test/wiki/High-speed_rail");
additional_bid_dict.SetByDottedPath("bid.adComponents",
std::move(ad_components_list));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid_state);
// Components should be both in the ad and the synthesized IG.
ASSERT_EQ(2u, result->bid->selected_ad_components.size());
EXPECT_EQ(
blink::AdDescriptor(GURL("https://en.wikipedia.test/wiki/Locomotive")),
result->bid->selected_ad_components[0].ad_descriptor);
EXPECT_EQ(blink::AdDescriptor(
GURL("https://en.wikipedia.test/wiki/High-speed_rail")),
result->bid->selected_ad_components[1].ad_descriptor);
ASSERT_TRUE(
result->bid_state->bidder->interest_group.ad_components.has_value());
ASSERT_EQ(2u,
result->bid_state->bidder->interest_group.ad_components->size());
EXPECT_EQ("https://en.wikipedia.test/wiki/Locomotive",
result->bid_state->bidder->interest_group.ad_components.value()[0]
.render_url());
EXPECT_EQ("https://en.wikipedia.test/wiki/High-speed_rail",
result->bid_state->bidder->interest_group.ad_components.value()[1]
.render_url());
EXPECT_EQ(&result->bid_state->bidder->interest_group.ad_components.value()[0],
result->bid->selected_ad_components[0].ad);
EXPECT_EQ(&result->bid_state->bidder->interest_group.ad_components.value()[1],
result->bid->selected_ad_components[1].ad);
}
TEST_F(AdditionalBidsUtilTest, ValidAdComponentsEmpty) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::List ad_components_list;
additional_bid_dict.SetByDottedPath("bid.adComponents",
std::move(ad_components_list));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->bid);
ASSERT_TRUE(result->bid_state);
EXPECT_EQ(0u, result->bid->selected_ad_components.size());
ASSERT_TRUE(
result->bid_state->bidder->interest_group.ad_components.has_value());
EXPECT_EQ(0u,
result->bid_state->bidder->interest_group.ad_components->size());
}
TEST_F(AdditionalBidsUtilTest, ValidAdMetadata) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
base::Value::Dict metadata_dict;
metadata_dict.Set("a", "hello");
metadata_dict.Set("b", 1.0);
additional_bid_dict.SetByDottedPath("bid.ad", std::move(metadata_dict));
base::Value input(std::move(additional_bid_dict));
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, input, kAuctionNonce, /*seller_nonce=*/std::nullopt,
kInterestGroupBuyers, kSeller,
base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_TRUE(result->bid);
EXPECT_EQ(R"({"a":"hello","b":1.0})", result->bid->ad_metadata);
}
TEST_F(AdditionalBidsUtilTest, ValidSingleNegativeIG) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("negativeInterestGroup", "not_if_here");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_FALSE(result->negative_target_joining_origin.has_value());
ASSERT_EQ(1u, result->negative_target_interest_group_names.size());
EXPECT_EQ("not_if_here", result->negative_target_interest_group_names[0]);
}
TEST_F(AdditionalBidsUtilTest, InvalidSingleNegativeIG) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("negativeInterestGroup", false);
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to non-string 'negativeInterestGroup'.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, InvalidBothKindsOfNegativeIG) {
base::Value::Dict additional_bid_dict = MakeMinimalValid();
additional_bid_dict.Set("negativeInterestGroup", "not_if_here");
additional_bid_dict.Set("negativeInterestGroups", "boo");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to specifying both 'negativeInterestGroup' and "
"'negativeInterestGroups'.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, ValidMultipleNegativeIG) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_TRUE(result->negative_target_joining_origin.has_value());
EXPECT_EQ("https://depot.test",
result->negative_target_joining_origin->Serialize());
ASSERT_EQ(2u, result->negative_target_interest_group_names.size());
EXPECT_EQ("negative_group", result->negative_target_interest_group_names[0]);
EXPECT_EQ("another negative group",
result->negative_target_interest_group_names[1]);
}
// Non-string joining origin.
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.SetByDottedPath("negativeInterestGroups.joiningOrigin",
10);
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid or missing 'joiningOrigin'.",
result.error());
}
// Non-HTTPS joining origin.
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG2) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.SetByDottedPath("negativeInterestGroups.joiningOrigin",
"http://example.org");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid or missing 'joiningOrigin'.",
result.error());
}
// Missing joining origin.
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG3) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.RemoveByDottedPath(
"negativeInterestGroups.joiningOrigin");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to invalid or missing 'joiningOrigin'.",
result.error());
}
// Missing interestGroupNames.
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG4) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.RemoveByDottedPath(
"negativeInterestGroups.interestGroupNames");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or invalid 'interestGroupNames' within "
"'negativeInterestGroups'.",
result.error());
}
// interestGroupNames not a list.
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG5) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.SetByDottedPath(
"negativeInterestGroups.interestGroupNames", "hi");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to missing or invalid 'interestGroupNames' within "
"'negativeInterestGroups'.",
result.error());
}
// Non-string entry in interestGroupNames
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG6) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict
.FindListByDottedPath("negativeInterestGroups.interestGroupNames")
->Append(50);
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to non-string 'interestGroupNames' entry.",
result.error());
}
// Wrong type for negativeInterestGroups
TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG7) {
base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs();
additional_bid_dict.Set("negativeInterestGroups", "boo");
auto result = DecodeAdditionalBid(
/*auction=*/nullptr, base::Value(std::move(additional_bid_dict)),
kAuctionNonce, /*seller_nonce=*/std::nullopt, kInterestGroupBuyers,
kSeller, base::optional_ref<const url::Origin>(kTopSeller));
EXPECT_FALSE(result.has_value());
EXPECT_EQ(
"Additional bid on auction with seller 'https://seller.test' rejected "
"due to non-dictionary 'negativeInterestGroups'.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeBasicSignedBid) {
for (bool require_forgiving_base64 : {false, true}) {
SCOPED_TRACE(require_forgiving_base64);
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
if (require_forgiving_base64) {
*((*signed_bid_dict.FindList("signatures"))[1].GetDict().FindString(
"signature")) = kSig2Base64Sloppy;
}
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_EQ(kPretendBid, result->additional_bid_json);
ASSERT_EQ(2u, result->signatures.size());
EXPECT_EQ(kKey1, result->signatures[0].key);
EXPECT_EQ(kSig1, result->signatures[0].signature);
EXPECT_EQ(kKey2, result->signatures[1].key);
EXPECT_EQ(kSig2, result->signatures[1].signature);
}
}
TEST_F(AdditionalBidsUtilTest, SignedNotDict) {
auto result = DecodeSignedAdditionalBid(base::Value(10));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Signed additional bid not a dictionary.", result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingBid) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
signed_bid_dict.Remove("bid");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Signed additional bid missing string 'bid' field.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatures) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
signed_bid_dict.Remove("signatures");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Signed additional bid missing list 'signatures' field.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatures) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
signed_bid_dict.FindList("signatures")->Append(40);
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Signed additional bid 'signatures' list entry not a dictionary.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatureKey) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Remove("key");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Signed additional bid 'signatures' list entry missing 'key' string.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureKey) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("key", "$$$");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Field 'key' is not valid base64.", result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureKeyLength) {
const char kLength31[] = "r7J39NbxqA5AvGD57ENOYdOvxzHPwA6KoehNIFCjDw==";
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("key", kLength31);
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Field 'key' has unexpected length.", result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatureSig) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Remove("signature");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
"Signed additional bid 'signatures' list entry missing 'signature' "
"string.",
result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureSig) {
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("signature",
"$$$");
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Field 'signature' is not valid base64.", result.error());
}
TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureSigLength) {
const char kLength65[] =
"rq9Nm5seElZB7vH9u8o6Cjt4v72LkPKGVKVl6k4uOlmV8Y7n023fmOk47R2bPRNYx/"
"EzpBSXdJainpItZwK5DTI=";
base::Value::Dict signed_bid_dict = MakeValidSignedBid();
(*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("signature",
kLength65);
auto result =
DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict)));
ASSERT_FALSE(result.has_value());
EXPECT_EQ("Field 'signature' has unexpected length.", result.error());
}
TEST_F(AdditionalBidsUtilTest, VerifySignature) {
const int kKeys = 4;
struct KeyPairs {
blink::InterestGroup::AdditionalBidKey public_key;
std::array<uint8_t, 64> private_key;
};
std::array<KeyPairs, kKeys> key_pairs;
SignedAdditionalBid data;
data.additional_bid_json = "Greetings. I am JSON!";
for (int i = 0; i < kKeys; ++i) {
ED25519_keypair(key_pairs[i].public_key.data(),
key_pairs[i].private_key.data());
data.signatures.emplace_back(SignatureWithKey(key_pairs[i].public_key));
bool ok = ED25519_sign(
data.signatures[i].signature.data(),
reinterpret_cast<const uint8_t*>(data.additional_bid_json.data()),
data.additional_bid_json.size(), key_pairs[i].private_key.data());
CHECK(ok);
}
EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 1, 2, 3));
// Flip a bit in the [1] signature.
data.signatures[1].signature[3] ^= 0x02;
EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 2, 3));
// Flip a couple of bits in the [2] key.
data.signatures[2].key[7] ^= 0x41;
EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 3));
// Change the payload.
data.additional_bid_json += "Boo. Bad unverified data appended!";
EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre());
}
class AdditionalBidsUtilNegativeTargetingTest : public AdditionalBidsUtilTest {
public:
const url::Origin kBuyer = url::Origin::Create(GURL("https://buyer.test"));
const url::Origin kOtherBuyer =
url::Origin::Create(GURL("https://other.test"));
const url::Origin kJoin = url::Origin::Create(GURL("https://engines.test"));
const url::Origin kOtherJoin =
url::Origin::Create(GURL("https://wagons.test"));
static constexpr size_t kNumNegativeInterestGroups = 4;
AdditionalBidsUtilNegativeTargetingTest()
: source_id_(ukm::AssignNewSourceId()), recorder_(source_id_) {
negative_targeter_.AddInterestGroupInfo(kBuyer, "a", kJoin, kKey1);
negative_targeter_.AddInterestGroupInfo(kBuyer, "b", kJoin, kKey2);
negative_targeter_.AddInterestGroupInfo(kOtherBuyer, "c", kJoin, kKey1);
negative_targeter_.AddInterestGroupInfo(kBuyer, "z", kOtherJoin, kKey1);
}
void VerifyMetricValue(std::string metric_name, int64_t expected_value) {
std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> entries =
ukm_recorder_.GetEntries(
ukm::builders::AdsInterestGroup_AuctionLatency_V2::kEntryName,
{metric_name});
ASSERT_THAT(entries, testing::SizeIs(1));
ASSERT_EQ(entries.at(0).source_id, source_id_);
ASSERT_TRUE(entries.at(0).metrics.contains(metric_name))
<< "Missing expected metric, " << metric_name;
EXPECT_EQ(entries.at(0).metrics[metric_name], expected_value)
<< "Unexpected value for " << metric_name << " metric";
}
void VerifyMetrics(int64_t invalid_signatures,
int64_t joining_origin_mismatches) {
recorder_.OnAuctionEnd(AuctionResult::kSuccess);
VerifyMetricValue(
ukm::builders::AdsInterestGroup_AuctionLatency_V2::
kNumNegativeInterestGroupsIgnoredDueToInvalidSignatureName,
invalid_signatures);
VerifyMetricValue(
ukm::builders::AdsInterestGroup_AuctionLatency_V2::
kNumNegativeInterestGroupsIgnoredDueToJoiningOriginMismatchName,
joining_origin_mismatches);
}
protected:
base::test::SingleThreadTaskEnvironment task_environment_;
ukm::TestAutoSetUkmRecorder ukm_recorder_;
ukm::SourceId source_id_;
AuctionMetricsRecorder recorder_;
AdAuctionNegativeTargeter negative_targeter_;
};
TEST_F(AdditionalBidsUtilNegativeTargetingTest, GetNumNegativeInterestGroups) {
EXPECT_EQ(negative_targeter_.GetNumNegativeInterestGroups(),
kNumNegativeInterestGroups);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest, SuccessfullyNegativeTargets) {
// Negative targets a, key1 matches that.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"a"},
/*signatures=*/
{SignatureWithKey(kKey1), SignatureWithKey(kKey2)},
/*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out, UnorderedElementsAre());
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest, WrongBuyer) {
// Negative targets c, which isn't under kBuyer.
std::vector<std::string> errors_out;
EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"c"},
/*signatures=*/
{SignatureWithKey(kKey1)},
/*valid_signatures=*/{0}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out, UnorderedElementsAre());
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest,
SuccessfulDespiteUnusedBadSignature) {
// Negative targets a, key1 matches that, but one of the keys is wrong.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"a"},
/*signatures=*/
{SignatureWithKey(kKey1), SignatureWithKey(kKey2)},
/*valid_signatures=*/{0}, kSeller, recorder_, errors_out));
EXPECT_THAT(
errors_out,
UnorderedElementsAre("Warning: Some signatures on an additional bid "
"from 'https://buyer.test' on auction with seller "
"'https://seller.test' failed to verify."));
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest, NoMatchingKey) {
// Negative targets b, key does not match that.
std::vector<std::string> errors_out;
EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"b"},
/*signatures=*/
{SignatureWithKey(kKey1)},
/*valid_signatures=*/{0}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out,
UnorderedElementsAre(
"Warning: Ignoring negative targeting group 'b' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since its key does not "
"correspond to a valid signature."));
VerifyMetrics(
/*invalid_signatures=*/1,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest, SuccessfulDespiteMissingKey) {
// Negative targets a with invalid key, non-existent c and d,
// then b with valid key.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"a", "c", "d", "b"},
/*signatures=*/
{SignatureWithKey(kKey2)},
/*valid_signatures=*/{0}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out,
UnorderedElementsAre(
"Warning: Ignoring negative targeting group 'a' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since its key does not "
"correspond to a valid signature."));
VerifyMetrics(
/*invalid_signatures=*/1,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest,
SuccessfulSoDoesNotEvenSeeMissingKey) {
// Negative targets a with valid key, non-existent c and d,
// then b with invalid key. We don't get far enough to warn about b.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/std::nullopt,
/*negative_target_interest_group_names=*/{"a", "c", "d", "b"},
/*signatures=*/
{SignatureWithKey(kKey1)},
/*valid_signatures=*/{0}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out, UnorderedElementsAre());
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest, JoiningOriginMismatch) {
// Negative targets a, b with valid keys, but none of the joins match.
std::vector<std::string> errors_out;
EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/kOtherJoin,
/*negative_target_interest_group_names=*/{"a", "b"},
/*signatures=*/
{SignatureWithKey(kKey1), SignatureWithKey(kKey2)},
/*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out,
UnorderedElementsAre(
"Warning: Ignoring negative targeting group 'a' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since it does not have the "
"expected joining origin.",
"Warning: Ignoring negative targeting group 'b' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since it does not have the "
"expected joining origin."));
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/2);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest,
SuccessfulWithMultipleNegativeInterestGroups) {
// Negative targets a, b with valid keys; all of the joins match.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/kJoin,
/*negative_target_interest_group_names=*/{"a", "b"},
/*signatures=*/
{SignatureWithKey(kKey1), SignatureWithKey(kKey2)},
/*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out, UnorderedElementsAre());
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/0);
}
TEST_F(AdditionalBidsUtilNegativeTargetingTest,
SuccessfulDespiteTwoJoiningOriginMismatches) {
// Negative targets a, b,z with valid keys; only the join on z matches.
std::vector<std::string> errors_out;
EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting(
kBuyer,
/*negative_target_joining_origin=*/kOtherJoin,
/*negative_target_interest_group_names=*/{"a", "b", "z"},
/*signatures=*/
{SignatureWithKey(kKey1), SignatureWithKey(kKey2)},
/*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out));
EXPECT_THAT(errors_out,
UnorderedElementsAre(
"Warning: Ignoring negative targeting group 'a' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since it does not have the "
"expected joining origin.",
"Warning: Ignoring negative targeting group 'b' on an "
"additional bid from 'https://buyer.test' on auction with "
"seller 'https://seller.test' since it does not have the "
"expected joining origin."));
VerifyMetrics(
/*invalid_signatures=*/0,
/*joining_origin_mismatches=*/2);
}
} // namespace
} // namespace content